marco.pms.mobileapp/lib/model/employees/add_employee_bottom_sheet.dart

332 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AddEmployeeBottomSheet extends StatefulWidget {
@override
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
}
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin {
final AddEmployeeController _controller = Get.put(AddEmployeeController());
@override
Widget build(BuildContext context) {
return GetBuilder<AddEmployeeController>(
init: _controller,
builder: (_) {
return BaseBottomSheet(
title: "Add Employee",
onCancel: () => Navigator.pop(context),
onSubmit: _handleSubmit,
child: Form(
key: _controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionLabel("Personal Info"),
MySpacing.height(16),
_inputWithIcon(
label: "First Name",
hint: "e.g., John",
icon: Icons.person,
controller:
_controller.basicValidator.getController('first_name')!,
validator:
_controller.basicValidator.getValidation('first_name'),
),
MySpacing.height(16),
_inputWithIcon(
label: "Last Name",
hint: "e.g., Doe",
icon: Icons.person_outline,
controller:
_controller.basicValidator.getController('last_name')!,
validator:
_controller.basicValidator.getValidation('last_name'),
),
MySpacing.height(16),
_sectionLabel("Contact Details"),
MySpacing.height(16),
_buildPhoneInput(context),
MySpacing.height(24),
_sectionLabel("Other Details"),
MySpacing.height(16),
_buildDropdownField(
label: "Gender",
value: _controller.selectedGender?.name.capitalizeFirst ?? '',
hint: "Select Gender",
onTap: () => _showGenderPopup(context),
),
MySpacing.height(16),
_buildDropdownField(
label: "Role",
value: _controller.roles.firstWhereOrNull((role) =>
role['id'] == _controller.selectedRoleId)?['name'] ??
"",
hint: "Select Role",
onTap: () => _showRolePopup(context),
),
],
),
),
);
},
);
}
// Submit logic
Future<void> _handleSubmit() async {
final result = await _controller.createEmployees();
if (result != null && result['success'] == true) {
final employeeData = result['data']; // ✅ Safe now
final employeeController = Get.find<EmployeesScreenController>();
final projectId = employeeController.selectedProjectId;
if (projectId == null) {
await employeeController.fetchAllEmployees();
} else {
await employeeController.fetchEmployeesByProject(projectId);
}
employeeController.update(['employee_screen_controller']);
_controller.basicValidator.getController("first_name")?.clear();
_controller.basicValidator.getController("last_name")?.clear();
_controller.basicValidator.getController("phone_number")?.clear();
_controller.selectedGender = null;
_controller.selectedRoleId = null;
_controller.update();
Navigator.pop(context, employeeData);
}
}
// Section label widget
Widget _sectionLabel(String title) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelLarge(title, fontWeight: 600),
MySpacing.height(4),
Divider(thickness: 1, color: Colors.grey.shade200),
],
);
// Input field with icon
Widget _inputWithIcon({
required String label,
required String hint,
required IconData icon,
required TextEditingController controller,
required String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
TextFormField(
controller: controller,
validator: validator,
decoration: _inputDecoration(hint).copyWith(
prefixIcon: Icon(icon, size: 20),
),
),
],
);
}
// Phone input with country code selector
Widget _buildPhoneInput(BuildContext context) {
return Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade100,
),
child: PopupMenuButton<Map<String, String>>(
onSelected: (country) {
_controller.selectedCountryCode = country['code']!;
_controller.update();
},
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
padding: EdgeInsets.zero,
child: SizedBox(
height: 200,
width: 100,
child: ListView(
children: _controller.countries.map((country) {
return ListTile(
dense: true,
title: Text("${country['name']} (${country['code']})"),
onTap: () => Navigator.pop(context, country),
);
}).toList(),
),
),
),
],
child: Row(
children: [
Text(_controller.selectedCountryCode),
const Icon(Icons.arrow_drop_down),
],
),
),
),
MySpacing.width(12),
Expanded(
child: TextFormField(
controller:
_controller.basicValidator.getController('phone_number'),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Phone number is required";
}
final digitsOnly = value.trim();
final minLength = _controller
.minDigitsPerCountry[_controller.selectedCountryCode] ??
7;
final maxLength = _controller
.maxDigitsPerCountry[_controller.selectedCountryCode] ??
15;
if (!RegExp(r'^[0-9]+$').hasMatch(digitsOnly)) {
return "Only digits allowed";
}
if (digitsOnly.length < minLength ||
digitsOnly.length > maxLength) {
return "Between $minLength$maxLength digits";
}
return null;
},
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(15),
],
decoration: _inputDecoration("e.g., 9876543210").copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () => _controller.pickContact(context),
),
),
),
),
],
);
}
// Gender/Role field (read-only dropdown)
Widget _buildDropdownField({
required String label,
required String value,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(text: value),
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
],
);
}
// Common input decoration
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
),
contentPadding: MySpacing.all(16),
);
}
// Gender popup menu
void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>(
context: context,
position: _popupMenuPosition(context),
items: Gender.values.map((gender) {
return PopupMenuItem<Gender>(
value: gender,
child: Text(gender.name.capitalizeFirst!),
);
}).toList(),
);
if (selected != null) {
_controller.onGenderSelected(selected);
_controller.update();
}
}
// Role popup menu
void _showRolePopup(BuildContext context) async {
final selected = await showMenu<String>(
context: context,
position: _popupMenuPosition(context),
items: _controller.roles.map((role) {
return PopupMenuItem<String>(
value: role['id'],
child: Text(role['name']),
);
}).toList(),
);
if (selected != null) {
_controller.onRoleSelected(selected);
_controller.update();
}
}
RelativeRect _popupMenuPosition(BuildContext context) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
}
}