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'; class AddEmployeeBottomSheet extends StatefulWidget { final Map? employeeData; const AddEmployeeBottomSheet({super.key, this.employeeData}); @override State createState() => _AddEmployeeBottomSheetState(); } 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(); _orgFieldController = TextEditingController(); _joiningDateController = TextEditingController(); _genderController = TextEditingController(); _roleController = TextEditingController(); _controller = Get.put(AddEmployeeController(), tag: UniqueKey().toString()); if (widget.employeeData != null) { _controller.editingEmployeeData = widget.employeeData; _controller.prefillFields(); _hasApplicationAccess = widget.employeeData?['hasApplicationAccess'] ?? false; final email = widget.employeeData?['email']; if (email != null && email.toString().isNotEmpty) { _controller.basicValidator.getController('email')?.text = email.toString(); } final orgId = widget.employeeData?['organization_id']; if (orgId != null) { final org = _organizationController.organizations .firstWhereOrNull((o) => o.id == orgId); if (org != null) { WidgetsBinding.instance.addPostFrameCallback((_) { _organizationController.selectOrganization(org); _controller.selectedOrganizationId = org.id; _orgFieldController.text = org.name; }); } } // ✅ Prefill Joining date if (_controller.joiningDate != null) { _joiningDateController.text = DateFormat('dd MMM yyyy').format(_controller.joiningDate!); } // ✅ Prefill Gender if (_controller.selectedGender != null) { _genderController.text = _controller.selectedGender!.name.capitalizeFirst ?? ''; } // ✅ Prefill Role _controller.fetchRoles().then((_) { if (_controller.selectedRoleId != null) { final roleName = _controller.roles.firstWhereOrNull( (r) => r['id'] == _controller.selectedRoleId, )?['name']; if (roleName != null) { _roleController.text = roleName; } _controller.update(); } }); } else { _controller.fetchRoles(); } } @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', 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('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', controller: _joiningDateController, hint: 'Select Joining Date', onTap: () => _pickJoiningDate(context), ), MySpacing.height(16), _sectionLabel('Contact Details'), MySpacing.height(16), _buildPhoneInput(context), MySpacing.height(24), _sectionLabel('Other Details'), MySpacing.height(16), _buildDropdownField( label: 'Gender', controller: _genderController, hint: 'Select Gender', onTap: () => _showGenderPopup(context), ), MySpacing.height(16), _buildDropdownField( label: 'Role', controller: _roleController, hint: 'Select Role', onTap: () => _showRolePopup(context), ), ], ), ), ); }, ); } // UI Pieces Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelLarge(title, fontWeight: 600), MySpacing.height(4), Divider(thickness: 1, color: Colors.grey.shade200), ], ); Widget _requiredLabel(String text) { return Row( children: [ MyText.labelMedium(text), const SizedBox(width: 4), const Text('*', style: TextStyle(color: Colors.red)), ], ); } 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), ); } 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) => PopupMenuItem( value: gender, child: Text(gender.name.capitalizeFirst!), ), ) .toList(), ); if (selected != null) { _controller.onGenderSelected(selected); _genderController.text = selected.name.capitalizeFirst ?? ''; _controller.update(); } } void _showRolePopup(BuildContext context) async { final selected = await showMenu( context: context, position: _popupMenuPosition(context), 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(); } } RelativeRect _popupMenuPosition(BuildContext context) { final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0); } }