From 8576448a32510bfcd6323f4869667f78072711b7 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 27 Sep 2025 16:56:56 +0530 Subject: [PATCH] feat: Enhance AddEmployee functionality with email and organization selection support --- .../employee/add_employee_controller.dart | 149 ++-- lib/helpers/services/api_endpoints.dart | 2 +- lib/helpers/services/api_service.dart | 67 +- .../employees/add_employee_bottom_sheet.dart | 685 ++++++++++++------ 4 files changed, 570 insertions(+), 333 deletions(-) diff --git a/lib/controller/employee/add_employee_controller.dart b/lib/controller/employee/add_employee_controller.dart index 4d51351..44aa286 100644 --- a/lib/controller/employee/add_employee_controller.dart +++ b/lib/controller/employee/add_employee_controller.dart @@ -1,13 +1,13 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:marco/controller/my_controller.dart'; -import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:marco/helpers/services/app_logger.dart'; -import 'package:collection/collection.dart'; enum Gender { male, @@ -18,22 +18,26 @@ enum Gender { } class AddEmployeeController extends MyController { - Map? editingEmployeeData; // For edit mode + Map? editingEmployeeData; - List files = []; + // State final MyFormValidator basicValidator = MyFormValidator(); + final List files = []; + final List categories = []; + Gender? selectedGender; List> roles = []; String? selectedRoleId; - String selectedCountryCode = "+91"; + String selectedCountryCode = '+91'; bool showOnline = true; - final List categories = []; DateTime? joiningDate; + String? selectedOrganizationId; + RxString selectedOrganizationName = RxString(''); @override void onInit() { super.onInit(); - logSafe("Initializing AddEmployeeController..."); + logSafe('Initializing AddEmployeeController...'); _initializeFields(); fetchRoles(); @@ -45,29 +49,36 @@ class AddEmployeeController extends MyController { void _initializeFields() { basicValidator.addField( 'first_name', - label: "First Name", + label: 'First Name', required: true, controller: TextEditingController(), ); basicValidator.addField( 'phone_number', - label: "Phone Number", + label: 'Phone Number', required: true, controller: TextEditingController(), ); basicValidator.addField( 'last_name', - label: "Last Name", + label: 'Last Name', required: true, controller: TextEditingController(), ); - logSafe("Fields initialized for first_name, phone_number, last_name."); + // Email is optional in controller; UI enforces when application access is checked + basicValidator.addField( + 'email', + label: 'Email', + required: false, + controller: TextEditingController(), + ); + + logSafe('Fields initialized for first_name, phone_number, last_name, email.'); } - /// Prefill fields in edit mode - // In AddEmployeeController + // Prefill fields in edit mode void prefillFields() { - logSafe("Prefilling data for editing..."); + logSafe('Prefilling data for editing...'); basicValidator.getController('first_name')?.text = editingEmployeeData?['first_name'] ?? ''; basicValidator.getController('last_name')?.text = @@ -76,10 +87,12 @@ class AddEmployeeController extends MyController { editingEmployeeData?['phone_number'] ?? ''; selectedGender = editingEmployeeData?['gender'] != null - ? Gender.values - .firstWhereOrNull((g) => g.name == editingEmployeeData!['gender']) + ? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender']) : null; + basicValidator.getController('email')?.text = + editingEmployeeData?['email'] ?? ''; + selectedRoleId = editingEmployeeData?['job_role_id']; if (editingEmployeeData?['joining_date'] != null) { @@ -91,92 +104,102 @@ class AddEmployeeController extends MyController { void setJoiningDate(DateTime date) { joiningDate = date; - logSafe("Joining date selected: $date"); + logSafe('Joining date selected: $date'); update(); } void onGenderSelected(Gender? gender) { selectedGender = gender; - logSafe("Gender selected: ${gender?.name}"); + logSafe('Gender selected: ${gender?.name}'); update(); } Future fetchRoles() async { - logSafe("Fetching roles..."); + logSafe('Fetching roles...'); try { final result = await ApiService.getRoles(); if (result != null) { roles = List>.from(result); - logSafe("Roles fetched successfully."); + logSafe('Roles fetched successfully.'); update(); } else { - logSafe("Failed to fetch roles: null result", level: LogLevel.error); + logSafe('Failed to fetch roles: null result', level: LogLevel.error); } } catch (e, st) { - logSafe("Error fetching roles", - level: LogLevel.error, error: e, stackTrace: st); + logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st); } } void onRoleSelected(String? roleId) { selectedRoleId = roleId; - logSafe("Role selected: $roleId"); + logSafe('Role selected: $roleId'); update(); } - /// Create or update employee - Future?> createOrUpdateEmployee() async { + // Create or update employee + Future?> createOrUpdateEmployee({ + String? email, + bool hasApplicationAccess = false, + }) async { logSafe(editingEmployeeData != null - ? "Starting employee update..." - : "Starting employee creation..."); + ? 'Starting employee update...' + : 'Starting employee creation...'); if (selectedGender == null || selectedRoleId == null) { showAppSnackbar( - title: "Missing Fields", - message: "Please select both Gender and Role.", + title: 'Missing Fields', + message: 'Please select both Gender and Role.', type: SnackbarType.warning, ); return null; } - final firstName = basicValidator.getController("first_name")?.text.trim(); - final lastName = basicValidator.getController("last_name")?.text.trim(); - final phoneNumber = - basicValidator.getController("phone_number")?.text.trim(); + final firstName = basicValidator.getController('first_name')?.text.trim(); + final lastName = basicValidator.getController('last_name')?.text.trim(); + final phoneNumber = basicValidator.getController('phone_number')?.text.trim(); try { + // sanitize orgId before sending + final String? orgId = (selectedOrganizationId != null && + selectedOrganizationId!.trim().isNotEmpty) + ? selectedOrganizationId + : null; + final response = await ApiService.createEmployee( - id: editingEmployeeData?['id'], // Pass id if editing + id: editingEmployeeData?['id'], firstName: firstName!, lastName: lastName!, phoneNumber: phoneNumber!, gender: selectedGender!.name, jobRoleId: selectedRoleId!, - joiningDate: joiningDate?.toIso8601String() ?? "", + joiningDate: joiningDate?.toIso8601String() ?? '', + organizationId: orgId, + email: email, + hasApplicationAccess: hasApplicationAccess, ); - logSafe("Response: $response"); + logSafe('Response: $response'); if (response != null && response['success'] == true) { showAppSnackbar( - title: "Success", + title: 'Success', message: editingEmployeeData != null - ? "Employee updated successfully!" - : "Employee created successfully!", + ? 'Employee updated successfully!' + : 'Employee created successfully!', type: SnackbarType.success, ); return response; } else { - logSafe("Failed operation", level: LogLevel.error); + logSafe('Failed operation', level: LogLevel.error); } } catch (e, st) { - logSafe("Error creating/updating employee", + logSafe('Error creating/updating employee', level: LogLevel.error, error: e, stackTrace: st); } showAppSnackbar( - title: "Error", - message: "Failed to save employee.", + title: 'Error', + message: 'Failed to save employee.', type: SnackbarType.error, ); return null; @@ -192,9 +215,8 @@ class AddEmployeeController extends MyController { } showAppSnackbar( - title: "Permission Required", - message: - "Please allow Contacts permission from settings to pick a contact.", + title: 'Permission Required', + message: 'Please allow Contacts permission from settings to pick a contact.', type: SnackbarType.warning, ); return false; @@ -212,8 +234,8 @@ class AddEmployeeController extends MyController { await FlutterContacts.getContact(picked.id, withProperties: true); if (contact == null) { showAppSnackbar( - title: "Error", - message: "Failed to load contact details.", + title: 'Error', + message: 'Failed to load contact details.', type: SnackbarType.error, ); return; @@ -221,8 +243,8 @@ class AddEmployeeController extends MyController { if (contact.phones.isEmpty) { showAppSnackbar( - title: "No Phone Number", - message: "Selected contact has no phone number.", + title: 'No Phone Number', + message: 'Selected contact has no phone number.', type: SnackbarType.warning, ); return; @@ -236,8 +258,8 @@ class AddEmployeeController extends MyController { if (indiaPhones.isEmpty) { showAppSnackbar( - title: "No Indian Number", - message: "Selected contact has no Indian (+91) phone number.", + title: 'No Indian Number', + message: 'Selected contact has no Indian (+91) phone number.', type: SnackbarType.warning, ); return; @@ -250,19 +272,20 @@ class AddEmployeeController extends MyController { selectedPhone = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: Text("Choose an Indian number"), + title: const Text('Choose an Indian number'), content: Column( mainAxisSize: MainAxisSize.min, children: indiaPhones - .map((p) => ListTile( - title: Text(p.number), - onTap: () => Navigator.of(ctx).pop(p.number), - )) + .map( + (p) => ListTile( + title: Text(p.number), + onTap: () => Navigator.of(ctx).pop(p.number), + ), + ) .toList(), ), ), ); - if (selectedPhone == null) return; } @@ -275,11 +298,11 @@ class AddEmployeeController extends MyController { phoneWithoutCountryCode; update(); } catch (e, st) { - logSafe("Error fetching contacts", + logSafe('Error fetching contacts', level: LogLevel.error, error: e, stackTrace: st); showAppSnackbar( - title: "Error", - message: "Failed to fetch contacts.", + title: 'Error', + message: 'Failed to fetch contacts.', type: SnackbarType.error, ); } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 7f8b6c4..960c3a7 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -25,7 +25,7 @@ class ApiEndpoints { static const String getAllEmployees = "/employee/list"; static const String getEmployeesWithoutPermission = "/employee/basic"; static const String getRoles = "/roles/jobrole"; - static const String createEmployee = "/employee/manage-mobile"; + static const String createEmployee = "/employee/app/manage"; static const String getEmployeeInfo = "/employee/profile/get"; static const String assignEmployee = "/employee/profile/get"; static const String getAssignedProjects = "/project/assigned-projects"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 70ce6b2..03cbb46 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1976,39 +1976,46 @@ class ApiService { static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); - static Future?> createEmployee({ - String? id, - required String firstName, - required String lastName, - required String phoneNumber, - required String gender, - required String jobRoleId, - required String joiningDate, - }) async { - final body = { - if (id != null) "id": id, - "firstName": firstName, - "lastName": lastName, - "phoneNumber": phoneNumber, - "gender": gender, - "jobRoleId": jobRoleId, - "joiningDate": joiningDate, - }; +static Future?> createEmployee({ + String? id, + required String firstName, + required String lastName, + required String phoneNumber, + required String gender, + required String jobRoleId, + required String joiningDate, + String? email, + String? organizationId, + bool? hasApplicationAccess, +}) async { + final body = { + if (id != null) "id": id, + "firstName": firstName, + "lastName": lastName, + "phoneNumber": phoneNumber, + "gender": gender, + "jobRoleId": jobRoleId, + "joiningDate": joiningDate, + if (email != null && email.isNotEmpty) "email": email, + if (organizationId != null && organizationId.isNotEmpty) + "organizationId": organizationId, + if (hasApplicationAccess != null) "hasApplicationAccess": hasApplicationAccess, + }; - final response = await _postRequest( - ApiEndpoints.createEmployee, - body, - customTimeout: extendedTimeout, - ); + final response = await _postRequest( + ApiEndpoints.createEmployee, + body, + customTimeout: extendedTimeout, + ); - if (response == null) return null; + if (response == null) return null; - final json = jsonDecode(response.body); - return { - "success": response.statusCode == 200 && json['success'] == true, - "data": json, - }; - } + final json = jsonDecode(response.body); + return { + "success": response.statusCode == 200 && json['success'] == true, + "data": json, + }; +} static Future?> getEmployeeDetails( String employeeId) async { diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index a4cc09a..82277f0 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + import 'package:marco/controller/employee/add_employee_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart'; +import 'package:marco/controller/tenant/organization_selection_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_snackbar.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'; -import 'package:intl/intl.dart'; -import 'package:marco/helpers/widgets/my_snackbar.dart'; class AddEmployeeBottomSheet extends StatefulWidget { final Map? employeeData; - AddEmployeeBottomSheet({this.employeeData}); + const AddEmployeeBottomSheet({super.key, this.employeeData}); @override State createState() => _AddEmployeeBottomSheetState(); @@ -22,28 +24,88 @@ class AddEmployeeBottomSheet extends StatefulWidget { class _AddEmployeeBottomSheetState extends State with UIMixin { late final AddEmployeeController _controller; + final OrganizationController _organizationController = + Get.put(OrganizationController()); + + // Local UI state + bool _hasApplicationAccess = false; + + // Local read-only controllers to avoid recreating TextEditingController in build + late final TextEditingController _orgFieldController; + late final TextEditingController _joiningDateController; + late final TextEditingController _genderController; + late final TextEditingController _roleController; @override void initState() { super.initState(); + _controller = Get.put( AddEmployeeController(), - tag: UniqueKey().toString(), + // Unique tag to avoid clashes, but stable for this widget instance + tag: UniqueKey().toString(), ); + _orgFieldController = TextEditingController(text: ''); + _joiningDateController = TextEditingController(text: ''); + _genderController = TextEditingController(text: ''); + _roleController = TextEditingController(text: ''); + + // Prefill when editing if (widget.employeeData != null) { _controller.editingEmployeeData = widget.employeeData; _controller.prefillFields(); + + final orgId = widget.employeeData!['organizationId']; + if (orgId != null) { + _controller.selectedOrganizationId = orgId; + + final selectedOrg = _organizationController.organizations + .firstWhereOrNull((o) => o.id == orgId); + if (selectedOrg != null) { + _organizationController.selectOrganization(selectedOrg); + _orgFieldController.text = selectedOrg.name; + } + } + + if (_controller.joiningDate != null) { + _joiningDateController.text = + DateFormat('dd MMM yyyy').format(_controller.joiningDate!); + } + + if (_controller.selectedGender != null) { + _genderController.text = + _controller.selectedGender!.name.capitalizeFirst ?? ''; + } + + final roleName = _controller.roles.firstWhereOrNull( + (r) => r['id'] == _controller.selectedRoleId)?['name'] ?? + ''; + _roleController.text = roleName; + } else { + _orgFieldController.text = _organizationController.currentSelection; } } + @override + void dispose() { + _orgFieldController.dispose(); + _joiningDateController.dispose(); + _genderController.dispose(); + _roleController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return GetBuilder( init: _controller, builder: (_) { + // Keep org field in sync with controller selection + _orgFieldController.text = _organizationController.currentSelection; + return BaseBottomSheet( - title: widget.employeeData != null ? "Edit Employee" : "Add Employee", + title: widget.employeeData != null ? 'Edit Employee' : 'Add Employee', onCancel: () => Navigator.pop(context), onSubmit: _handleSubmit, child: Form( @@ -51,11 +113,11 @@ class _AddEmployeeBottomSheetState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionLabel("Personal Info"), + _sectionLabel('Personal Info'), MySpacing.height(16), _inputWithIcon( - label: "First Name", - hint: "e.g., John", + label: 'First Name', + hint: 'e.g., John', icon: Icons.person, controller: _controller.basicValidator.getController('first_name')!, @@ -64,8 +126,8 @@ class _AddEmployeeBottomSheetState extends State ), MySpacing.height(16), _inputWithIcon( - label: "Last Name", - hint: "e.g., Doe", + label: 'Last Name', + hint: 'e.g., Doe', icon: Icons.person_outline, controller: _controller.basicValidator.getController('last_name')!, @@ -73,37 +135,91 @@ class _AddEmployeeBottomSheetState extends State _controller.basicValidator.getValidation('last_name'), ), MySpacing.height(16), - _sectionLabel("Joining Details"), + _sectionLabel('Organization'), + MySpacing.height(8), + GestureDetector( + onTap: () => _showOrganizationPopup(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: _orgFieldController, + validator: (val) { + if (val == null || + val.trim().isEmpty || + val == 'All Organizations') { + return 'Organization is required'; + } + return null; + }, + decoration: + _inputDecoration('Select Organization').copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + MySpacing.height(24), + _sectionLabel('Application Access'), + Row( + children: [ + Checkbox( + value: _hasApplicationAccess, + onChanged: (val) { + setState(() => _hasApplicationAccess = val ?? false); + }, + fillColor: + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Colors.indigo; + } + return Colors.white; + }), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return BorderSide.none; + } + return const BorderSide( + color: Colors.black, + width: 2, + ); + }), + checkColor: Colors.white, + ), + MyText.bodyMedium( + 'Has Application Access', + fontWeight: 600, + ), + ], + ), + MySpacing.height(8), + _buildEmailField(), + MySpacing.height(12), + _sectionLabel('Joining Details'), MySpacing.height(16), _buildDatePickerField( - label: "Joining Date", - value: _controller.joiningDate != null - ? DateFormat("dd MMM yyyy") - .format(_controller.joiningDate!) - : "", - hint: "Select Joining Date", + label: 'Joining Date', + controller: _joiningDateController, + hint: 'Select Joining Date', onTap: () => _pickJoiningDate(context), ), MySpacing.height(16), - _sectionLabel("Contact Details"), + _sectionLabel('Contact Details'), MySpacing.height(16), _buildPhoneInput(context), MySpacing.height(24), - _sectionLabel("Other Details"), + _sectionLabel('Other Details'), MySpacing.height(16), _buildDropdownField( - label: "Gender", - value: _controller.selectedGender?.name.capitalizeFirst ?? '', - hint: "Select Gender", + label: 'Gender', + controller: _genderController, + 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", + label: 'Role', + controller: _roleController, + hint: 'Select Role', onTap: () => _showRolePopup(context), ), ], @@ -114,96 +230,7 @@ class _AddEmployeeBottomSheetState extends State ); } - Widget _requiredLabel(String text) { - return Row( - children: [ - MyText.labelMedium(text), - const SizedBox(width: 4), - const Text("*", style: TextStyle(color: Colors.red)), - ], - ); - } - - Widget _buildDatePickerField({ - required String label, - required String value, - required String hint, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _requiredLabel(label), - MySpacing.height(8), - GestureDetector( - onTap: onTap, - child: AbsorbPointer( - child: TextFormField( - readOnly: true, - controller: TextEditingController(text: value), - validator: (val) { - if (val == null || val.trim().isEmpty) { - return "$label is required"; - } - return null; - }, - decoration: _inputDecoration(hint).copyWith( - suffixIcon: const Icon(Icons.calendar_today), - ), - ), - ), - ), - ], - ); - } - - Future _pickJoiningDate(BuildContext context) async { - final picked = await showDatePicker( - context: context, - initialDate: _controller.joiningDate ?? DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime(2100), - ); - - if (picked != null) { - _controller.setJoiningDate(picked); - _controller.update(); - } - } - - Future _handleSubmit() async { - final isValid = - _controller.basicValidator.formKey.currentState?.validate() ?? false; - - if (!isValid || - _controller.joiningDate == null || - _controller.selectedGender == null || - _controller.selectedRoleId == null) { - showAppSnackbar( - title: "Missing Fields", - message: "Please complete all required fields.", - type: SnackbarType.warning, - ); - return; - } - - final result = await _controller.createOrUpdateEmployee(); - - if (result != null && result['success'] == true) { - final employeeController = Get.find(); - final projectId = employeeController.selectedProjectId; - - if (projectId == null) { - await employeeController.fetchAllEmployees(); - } else { - await employeeController.fetchEmployeesByProject(projectId); - } - - employeeController.update(['employee_screen_controller']); - - Navigator.pop(context, result['data']); - } - } + // UI Pieces Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -214,116 +241,12 @@ class _AddEmployeeBottomSheetState extends State ], ); - Widget _inputWithIcon({ - required String label, - required String hint, - required IconData icon, - required TextEditingController controller, - required String? Function(String?)? validator, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _requiredLabel(String text) { + return Row( children: [ - _requiredLabel(label), - MySpacing.height(8), - TextFormField( - controller: controller, - validator: (val) { - if (val == null || val.trim().isEmpty) { - return "$label is required"; - } - return validator?.call(val); - }, - decoration: _inputDecoration(hint).copyWith( - prefixIcon: Icon(icon, size: 20), - ), - ), - ], - ); - } - - Widget _buildPhoneInput(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _requiredLabel("Phone Number"), - MySpacing.height(8), - 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: const Text("+91"), - ), - 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"; - } - if (value.trim().length != 10) { - return "Phone Number must be exactly 10 digits"; - } - if (!RegExp(r'^\d{10}$').hasMatch(value.trim())) { - return "Enter a valid 10-digit number"; - } - return null; - }, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(10), - ], - decoration: _inputDecoration("e.g., 9876543210").copyWith( - suffixIcon: IconButton( - icon: const Icon(Icons.contacts), - onPressed: () => _controller.pickContact(context), - ), - ), - ), - ), - ], - ), - ], - ); - } - - Widget _buildDropdownField({ - required String label, - required String value, - required String hint, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _requiredLabel(label), - MySpacing.height(8), - GestureDetector( - onTap: onTap, - child: AbsorbPointer( - child: TextFormField( - readOnly: true, - controller: TextEditingController(text: value), - validator: (val) { - if (val == null || val.trim().isEmpty) { - return "$label is required"; - } - return null; - }, - decoration: _inputDecoration(hint).copyWith( - suffixIcon: const Icon(Icons.expand_more), - ), - ), - ), - ), + MyText.labelMedium(text), + const SizedBox(width: 4), + const Text('*', style: TextStyle(color: Colors.red)), ], ); } @@ -350,20 +273,298 @@ class _AddEmployeeBottomSheetState extends State ); } + 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: [ + _requiredLabel(label), + MySpacing.height(8), + TextFormField( + controller: controller, + validator: (val) { + if (val == null || val.trim().isEmpty) { + return '$label is required'; + } + return validator?.call(val); + }, + decoration: _inputDecoration(hint).copyWith( + prefixIcon: Icon(icon, size: 20), + ), + ), + ], + ); + } + + Widget _buildEmailField() { + final emailController = _controller.basicValidator.getController('email') ?? + TextEditingController(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + MyText.labelMedium('Email'), + const SizedBox(width: 4), + if (_hasApplicationAccess) + const Text('*', style: TextStyle(color: Colors.red)), + ], + ), + MySpacing.height(8), + TextFormField( + controller: emailController, + validator: (val) { + if (_hasApplicationAccess) { + if (val == null || val.trim().isEmpty) { + return 'Email is required for application users'; + } + final email = val.trim(); + if (!RegExp(r'^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,4}$') + .hasMatch(email)) { + return 'Enter a valid email address'; + } + } + return null; + }, + keyboardType: TextInputType.emailAddress, + decoration: _inputDecoration('e.g., john.doe@example.com').copyWith( + ), + ), + ], + ); + } + + Widget _buildDatePickerField({ + required String label, + required TextEditingController controller, + required String hint, + required VoidCallback onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _requiredLabel(label), + MySpacing.height(8), + GestureDetector( + onTap: onTap, + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: controller, + validator: (val) { + if (val == null || val.trim().isEmpty) { + return '$label is required'; + } + return null; + }, + decoration: _inputDecoration(hint).copyWith( + suffixIcon: const Icon(Icons.calendar_today), + ), + ), + ), + ), + ], + ); + } + + Widget _buildDropdownField({ + required String label, + required TextEditingController controller, + required String hint, + required VoidCallback onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _requiredLabel(label), + MySpacing.height(8), + GestureDetector( + onTap: onTap, + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: controller, + validator: (val) { + if (val == null || val.trim().isEmpty) { + return '$label is required'; + } + return null; + }, + decoration: _inputDecoration(hint).copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + ], + ); + } + + Widget _buildPhoneInput(BuildContext context) { + final phoneController = + _controller.basicValidator.getController('phone_number'); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _requiredLabel('Phone Number'), + MySpacing.height(8), + 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: const Text('+91'), + ), + MySpacing.width(12), + Expanded( + child: TextFormField( + controller: phoneController, + validator: (value) { + final v = value?.trim() ?? ''; + if (v.isEmpty) return 'Phone Number is required'; + if (v.length != 10) + return 'Phone Number must be exactly 10 digits'; + if (!RegExp(r'^\d{10}$').hasMatch(v)) { + return 'Enter a valid 10-digit number'; + } + return null; + }, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: _inputDecoration('e.g., 9876543210').copyWith( + suffixIcon: IconButton( + icon: const Icon(Icons.contacts), + onPressed: () => _controller.pickContact(context), + ), + ), + ), + ), + ], + ), + ], + ); + } + + // Actions + + Future _pickJoiningDate(BuildContext context) async { + final picked = await showDatePicker( + context: context, + initialDate: _controller.joiningDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + + if (picked != null) { + _controller.setJoiningDate(picked); + _joiningDateController.text = DateFormat('dd MMM yyyy').format(picked); + _controller.update(); + } + } + + Future _handleSubmit() async { + final isValid = + _controller.basicValidator.formKey.currentState?.validate() ?? false; + + if (!isValid || + _controller.joiningDate == null || + _controller.selectedGender == null || + _controller.selectedRoleId == null || + _organizationController.currentSelection.isEmpty || + _organizationController.currentSelection == 'All Organizations') { + showAppSnackbar( + title: 'Missing Fields', + message: 'Please complete all required fields.', + type: SnackbarType.warning, + ); + return; + } + + final result = await _controller.createOrUpdateEmployee( + email: _controller.basicValidator.getController('email')?.text.trim(), + hasApplicationAccess: _hasApplicationAccess, + ); + + if (result != null && result['success'] == true) { + final employeeController = Get.find(); + final projectId = employeeController.selectedProjectId; + + if (projectId == null) { + await employeeController.fetchAllEmployees(); + } else { + await employeeController.fetchEmployeesByProject(projectId); + } + + employeeController.update(['employee_screen_controller']); + if (mounted) Navigator.pop(context, result['data']); + } + } + + void _showOrganizationPopup(BuildContext context) async { + final orgs = _organizationController.organizations; + + if (orgs.isEmpty) { + showAppSnackbar( + title: 'No Organizations', + message: 'No organizations available to select.', + type: SnackbarType.warning, + ); + return; + } + + final selected = await showMenu( + context: context, + position: _popupMenuPosition(context), + items: orgs + .map( + (org) => PopupMenuItem( + value: org.id, + child: Text(org.name), + ), + ) + .toList(), + ); + + if (selected != null && selected.trim().isNotEmpty) { + final chosen = orgs.firstWhere((e) => e.id == selected); + _organizationController.selectOrganization(chosen); + _controller.selectedOrganizationId = chosen.id; + _orgFieldController.text = chosen.name; + _controller.update(); + } + } + void _showGenderPopup(BuildContext context) async { final selected = await showMenu( context: context, position: _popupMenuPosition(context), - items: Gender.values.map((gender) { - return PopupMenuItem( - value: gender, - child: Text(gender.name.capitalizeFirst!), - ); - }).toList(), + items: Gender.values + .map( + (gender) => PopupMenuItem( + value: gender, + child: Text(gender.name.capitalizeFirst!), + ), + ) + .toList(), ); if (selected != null) { _controller.onGenderSelected(selected); + _genderController.text = selected.name.capitalizeFirst ?? ''; _controller.update(); } } @@ -372,16 +573,22 @@ class _AddEmployeeBottomSheetState extends State final selected = await showMenu( context: context, position: _popupMenuPosition(context), - items: _controller.roles.map((role) { - return PopupMenuItem( - value: role['id'], - child: Text(role['name']), - ); - }).toList(), + items: _controller.roles + .map( + (role) => PopupMenuItem( + value: role['id'], + child: Text(role['name']), + ), + ) + .toList(), ); if (selected != null) { _controller.onRoleSelected(selected); + final roleName = _controller.roles + .firstWhereOrNull((r) => r['id'] == selected)?['name'] ?? + ''; + _roleController.text = roleName; _controller.update(); } }