diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index c690514..fa0c8d7 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -1,361 +1,103 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; + import 'package:marco/controller/dashboard/add_employee_controller.dart'; -import 'package:marco/helpers/theme/app_theme.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_button.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/controller/dashboard/employees_screen_controller.dart'; + class AddEmployeeBottomSheet extends StatefulWidget { @override - _AddEmployeeBottomSheetState createState() => _AddEmployeeBottomSheetState(); + State createState() => _AddEmployeeBottomSheetState(); } -class _AddEmployeeBottomSheetState extends State - with UIMixin { - final AddEmployeeController controller = Get.put(AddEmployeeController()); +class _AddEmployeeBottomSheetState extends State with UIMixin { + final AddEmployeeController _controller = Get.put(AddEmployeeController()); + + late TextEditingController genderController; + late TextEditingController roleController; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); + void initState() { + super.initState(); + genderController = TextEditingController(); + roleController = TextEditingController(); + } - return GetBuilder( - init: controller, - builder: (_) { - return SingleChildScrollView( - padding: MediaQuery.of(context).viewInsets, - child: Container( - padding: - const EdgeInsets.only(top: 12, left: 24, right: 24, bottom: 24), - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(24)), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 10, - spreadRadius: 1, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Drag handle bar - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.titleMedium( - "Add Employee", - fontWeight: 600, - fontSize: 18, - ), - ], - ), - MySpacing.height(24), + RelativeRect _popupMenuPosition(BuildContext context) { + final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0); + } - Form( - key: controller.basicValidator.formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildLabel("First Name"), - MySpacing.height(8), - _buildTextField( - hint: "eg: John", - controller: controller.basicValidator - .getController('first_name')!, - validator: controller.basicValidator - .getValidation('first_name'), - keyboardType: TextInputType.name, - ), - MySpacing.height(24), - - _buildLabel("Last Name"), - MySpacing.height(8), - _buildTextField( - hint: "eg: Doe", - controller: controller.basicValidator - .getController('last_name')!, - validator: controller.basicValidator - .getValidation('last_name'), - keyboardType: TextInputType.name, - ), - MySpacing.height(24), - - _buildLabel("Phone Number"), - MySpacing.height(8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 14), - decoration: BoxDecoration( - border: - Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - ), - child: PopupMenuButton>( - onSelected: (country) { - setState(() { - controller.selectedCountryCode = - country['code']!; - }); - }, - itemBuilder: (context) => [ - PopupMenuItem( - enabled: false, - padding: EdgeInsets.zero, - child: Container( - padding: EdgeInsets.zero, - height: 200, - width: 100, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surface, - borderRadius: - BorderRadius.circular(8), - ), - child: Scrollbar( - child: ListView( - padding: EdgeInsets.zero, - children: controller.countries - .map((country) { - return ListTile( - title: Text( - "${country['name']} (${country['code']})", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ), - onTap: () => Navigator.pop( - context, country), - hoverColor: Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 12), - ); - }).toList(), - ), - ), - ), - ), - ], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(controller.selectedCountryCode), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - children: [ - 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 "Phone number must contain digits only"; - } - - if (digitsOnly.length < minLength || - digitsOnly.length > maxLength) { - return "Number Must be between $minLength and $maxLength"; - } - - return null; - }, - keyboardType: TextInputType.phone, - decoration: - _inputDecoration("eg: 9876543210") - .copyWith( - suffixIcon: IconButton( - icon: Icon(Icons.contacts), - tooltip: "Pick from contacts", - onPressed: () async { - await controller - .pickContact(context); - }, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - MySpacing.height(24), - - _buildLabel("Select Gender"), - MySpacing.height(8), - DropdownButtonFormField( - value: controller.selectedGender, - dropdownColor: contentTheme.background, - isDense: true, - menuMaxHeight: 200, - decoration: _inputDecoration("Select Gender"), - icon: const Icon(Icons.expand_more, size: 20), - items: Gender.values - .map( - (gender) => DropdownMenuItem( - value: gender, - child: MyText.labelMedium( - gender.name.capitalizeFirst!), - ), - ) - .toList(), - onChanged: controller.onGenderSelected, - ), - MySpacing.height(24), - - _buildLabel("Select Role"), - MySpacing.height(8), - DropdownButtonFormField( - value: controller.selectedRoleId, - dropdownColor: contentTheme.background, - isDense: true, - decoration: _inputDecoration("Select Role"), - icon: const Icon(Icons.expand_more, size: 20), - items: controller.roles - .map( - (role) => DropdownMenuItem( - value: role['id'], - child: Text(role['name']), - ), - ) - .toList(), - onChanged: controller.onRoleSelected, - ), - MySpacing.height(24), - - // Buttons row - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - MyButton.text( - onPressed: () => Navigator.pop(context), - padding: MySpacing.xy(20, 16), - child: MyText.bodySmall("Cancel"), - ), - MySpacing.width(12), - MyButton( - onPressed: () async { - if (controller.basicValidator.validateForm()) { - final success = - await controller.createEmployees(); - if (success) { - // Call refresh logic here via callback or GetX - final employeeController = - Get.find(); - 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(); - } - } - }, - elevation: 0, - padding: MySpacing.xy(20, 16), - backgroundColor: Colors.blueAccent, - borderRadiusAll: AppStyle.buttonRadius.medium, - child: MyText.bodySmall("Save", - color: contentTheme.onPrimary), - ), - ], - ), - ], - ), - ), - ], - ), - ), + 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(), + ); + + if (selected != null) { + _controller.onGenderSelected(selected); + _controller.update(); + } + } + + void _showRolePopup(BuildContext context) async { + final selected = await showMenu( + context: context, + position: _popupMenuPosition(context), + items: _controller.roles.map((role) { + return PopupMenuItem( + value: role['id'], + child: Text(role['name']), + ); + }).toList(), + ); + + if (selected != null) { + _controller.onRoleSelected(selected); + _controller.update(); + } + } + + Widget _sectionLabel(String title) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelLarge(title, fontWeight: 600), + MySpacing.height(4), + Divider(thickness: 1, color: Colors.grey.shade200), + ], ); } - Widget _buildLabel(String text) => MyText.labelMedium(text); - - Widget _buildTextField({ + Widget _inputWithIcon({ + required String label, + required String hint, + required IconData icon, required TextEditingController controller, required String? Function(String?)? validator, - required String hint, - TextInputType keyboardType = TextInputType.text, }) { - return TextFormField( - controller: controller, - validator: validator, - keyboardType: keyboardType, - decoration: _inputDecoration(hint), + 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), + ), + ), + ], ); } @@ -363,12 +105,258 @@ class _AddEmployeeBottomSheetState extends State return InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), - border: outlineInputBorder, - enabledBorder: outlineInputBorder, - focusedBorder: focusedInputBorder, + 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: BorderSide(color: Colors.blueAccent, width: 1.5), + ), contentPadding: MySpacing.all(16), - isCollapsed: true, - floatingLabelBehavior: FloatingLabelBehavior.never, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GetBuilder( + init: _controller, + builder: (_) { + return SingleChildScrollView( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2))], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag Handle + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), + ), + MySpacing.height(12), + Text("Add Employee", style: MyTextStyle.titleLarge(fontWeight: 700)), + MySpacing.height(24), + 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), + MyText.labelMedium("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: PopupMenuButton>( + 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, + decoration: _inputDecoration("e.g., 9876543210").copyWith( + suffixIcon: IconButton( + icon: const Icon(Icons.contacts), + onPressed: () => _controller.pickContact(context), + ), + ), + ), + ), + ], + ), + MySpacing.height(24), + _sectionLabel("Other Details"), + MySpacing.height(16), + MyText.labelMedium("Gender"), + MySpacing.height(8), + GestureDetector( + onTap: () => _showGenderPopup(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: TextEditingController( + text: _controller.selectedGender?.name.capitalizeFirst, + ), + decoration: _inputDecoration("Select Gender").copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + MySpacing.height(16), + MyText.labelMedium("Role"), + MySpacing.height(8), + GestureDetector( + onTap: () => _showRolePopup(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: TextEditingController( + text: _controller.roles.firstWhereOrNull( + (role) => role['id'] == _controller.selectedRoleId, + )?['name'] ?? "", + ), + decoration: _inputDecoration("Select Role").copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, size: 18), + label: MyText.bodyMedium("Cancel", fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + if (_controller.basicValidator.validateForm()) { + final success = await _controller.createEmployees(); + if (success) { + final employeeController = Get.find(); + 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); + } + } + }, + icon: const Icon(Icons.check, size: 18), + label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, ); } }