diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index d3fdb92..0c549c3 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -10,7 +10,7 @@ class AddContactController extends GetxController { final RxList tags = [].obs; final RxString selectedCategory = ''.obs; - final RxString selectedBucket = ''.obs; + final RxList selectedBuckets = [].obs; final RxString selectedProject = ''.obs; final RxList enteredTags = [].obs; @@ -50,7 +50,7 @@ class AddContactController extends GetxController { void resetForm() { selectedCategory.value = ''; selectedProject.value = ''; - selectedBucket.value = ''; + selectedBuckets.clear(); enteredTags.clear(); filteredSuggestions.clear(); filteredOrgSuggestions.clear(); @@ -100,7 +100,21 @@ class AddContactController extends GetxController { isSubmitting.value = true; final categoryId = categoriesMap[selectedCategory.value]; - final bucketId = bucketsMap[selectedBucket.value]; + final bucketIds = selectedBuckets + .map((name) => bucketsMap[name]) + .whereType() + .toList(); + + if (bucketIds.isEmpty) { + showAppSnackbar( + title: "Missing Buckets", + message: "Please select at least one bucket.", + type: SnackbarType.warning, + ); + isSubmitting.value = false; + return; + } + final projectIds = selectedProjects .map((name) => projectsMap[name]) .whereType() @@ -126,10 +140,10 @@ class AddContactController extends GetxController { return; } - if (selectedBucket.value.trim().isEmpty || bucketId == null) { + if (selectedBuckets.isEmpty) { showAppSnackbar( title: "Missing Bucket", - message: "Please select a bucket.", + message: "Please select at least one bucket.", type: SnackbarType.warning, ); isSubmitting.value = false; @@ -151,7 +165,7 @@ class AddContactController extends GetxController { if (selectedCategory.value.isNotEmpty && categoryId != null) "contactCategoryId": categoryId, if (projectIds.isNotEmpty) "projectIds": projectIds, - "bucketIds": [bucketId], + "bucketIds": bucketIds, if (enteredTags.isNotEmpty) "tags": tagObjects, if (emails.isNotEmpty) "contactEmails": emails, if (phones.isNotEmpty) "contactPhones": phones, diff --git a/lib/controller/employee/add_employee_controller.dart b/lib/controller/employee/add_employee_controller.dart index 44aa286..ea5e398 100644 --- a/lib/controller/employee/add_employee_controller.dart +++ b/lib/controller/employee/add_employee_controller.dart @@ -73,7 +73,8 @@ class AddEmployeeController extends MyController { controller: TextEditingController(), ); - logSafe('Fields initialized for first_name, phone_number, last_name, email.'); + logSafe( + 'Fields initialized for first_name, phone_number, last_name, email.'); } // Prefill fields in edit mode @@ -87,7 +88,8 @@ 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 = @@ -121,12 +123,24 @@ class AddEmployeeController extends MyController { if (result != null) { roles = List>.from(result); logSafe('Roles fetched successfully.'); + + // ✅ If editing, and role already selected, update the role controller text here + if (editingEmployeeData != null && selectedRoleId != null) { + final selectedRole = roles.firstWhereOrNull( + (r) => r['id'] == selectedRoleId, + ); + if (selectedRole != null) { + update(); + } + } + update(); } else { 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); } } @@ -156,7 +170,8 @@ class AddEmployeeController extends MyController { 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 phoneNumber = + basicValidator.getController('phone_number')?.text.trim(); try { // sanitize orgId before sending @@ -216,7 +231,8 @@ class AddEmployeeController extends MyController { showAppSnackbar( title: 'Permission Required', - message: 'Please allow Contacts permission from settings to pick a contact.', + message: + 'Please allow Contacts permission from settings to pick a contact.', type: SnackbarType.warning, ); return false; diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 924b09d..358b913 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -95,7 +95,9 @@ class _AddContactBottomSheetState extends State { final name = controller.bucketsMap.entries .firstWhereOrNull((e) => e.value == bucketId) ?.key; - if (name != null) controller.selectedBucket.value = name; + if (name != null && !controller.selectedBuckets.contains(name)) { + controller.selectedBuckets.add(name); + } } } }); @@ -363,10 +365,127 @@ class _AddContactBottomSheetState extends State { ); } + Widget _bucketMultiSelectField() { + return _multiSelectField( + items: controller.buckets + .map((name) => FilterItem(id: name, name: name)) + .toList(), + fallback: "Choose Buckets", + selectedValues: controller.selectedBuckets, + ); + } + + Widget _multiSelectField({ + required List items, + required String fallback, + required RxList selectedValues, + }) { + if (items.isEmpty) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final selectedNames = items + .where((f) => selectedValues.contains(f.id)) + .map((f) => f.name) + .join(", "); + final displayText = + selectedNames.isNotEmpty ? selectedNames : fallback; + + return Builder( + builder: (context) { + return GestureDetector( + onTap: () async { + final RenderBox button = + context.findRenderObject() as RenderBox; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + + final position = button.localToGlobal(Offset.zero); + + await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy + button.size.height, + overlay.size.width - position.dx - button.size.width, + 0, + ), + items: items.map((f) { + return PopupMenuItem( + enabled: false, + child: StatefulBuilder( + builder: (context, setState) { + final isChecked = selectedValues.contains(f.id); + return CheckboxListTile( + dense: true, + value: isChecked, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: MyText(f.name), + checkColor: Colors.white, + side: const BorderSide( + color: Colors.black, width: 1.5), + fillColor: + MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.selected)) { + return Colors.indigo; + } + return Colors.white; + }, + ), + onChanged: (val) { + if (val == true) { + selectedValues.add(f.id); + } else { + selectedValues.remove(f.id); + } + setState(() {}); + }, + ); + }, + ), + ); + }).toList(), + ); + }, + child: Container( + padding: MySpacing.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyText( + displayText, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + }, + ); + }), + MySpacing.height(16), + ], + ); + } + void _handleSubmit() { bool valid = formKey.currentState?.validate() ?? false; - if (controller.selectedBucket.value.isEmpty) { + if (controller.selectedBuckets.isEmpty) { bucketError.value = "Bucket is required"; valid = false; } else { @@ -430,29 +549,14 @@ class _AddContactBottomSheetState extends State { MySpacing.height(16), _textField("Organization", orgCtrl, required: true), MySpacing.height(16), - _labelWithStar("Bucket", required: true), + _labelWithStar("Buckets", required: true), MySpacing.height(8), Stack( children: [ - _popupSelector(controller.selectedBucket, controller.buckets, - "Choose Bucket"), - Positioned( - left: 0, - right: 0, - top: 56, - child: Obx(() => bucketError.value.isEmpty - ? const SizedBox.shrink() - : Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Text(bucketError.value, - style: const TextStyle( - color: Colors.red, fontSize: 12)), - )), - ), + _bucketMultiSelectField(), ], ), - MySpacing.height(24), + MySpacing.height(12), Obx(() => GestureDetector( onTap: () => showAdvanced.toggle(), child: Row( @@ -562,3 +666,9 @@ class _AddContactBottomSheetState extends State { }); } } + +class FilterItem { + final String id; + final String name; + FilterItem({required this.id, required this.name}); +} diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index e28d1b9..4ad02f9 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -37,36 +37,39 @@ class _AddEmployeeBottomSheetState extends State void initState() { super.initState(); - _controller = Get.put( - AddEmployeeController(), - // Unique tag to avoid clashes, but stable for this widget instance - tag: UniqueKey().toString(), - ); + _joiningDateController = TextEditingController(); + _genderController = TextEditingController(); + _roleController = TextEditingController(); - _orgFieldController = TextEditingController(text: ''); - _joiningDateController = TextEditingController(text: ''); - _genderController = TextEditingController(text: ''); - _roleController = TextEditingController(text: ''); + _controller = Get.put(AddEmployeeController(), tag: UniqueKey().toString()); - // Prefill when editing if (widget.employeeData != null) { _controller.editingEmployeeData = widget.employeeData; _controller.prefillFields(); + // 👇 joining date & gender already handled in your code 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 {} + // ✅ Important part: fetch roles, then set roleController + _controller.fetchRoles().then((_) { + if (_controller.selectedRoleId != null) { + final roleName = _controller.roles.firstWhereOrNull( + (r) => r['id'] == _controller.selectedRoleId, + )?['name']; + if (roleName != null) { + _roleController.text = roleName; + } + } + }); + } else { + _controller.fetchRoles(); + } } @override @@ -422,7 +425,7 @@ class _AddEmployeeBottomSheetState extends State context: context, initialDate: _controller.joiningDate ?? DateTime.now(), firstDate: DateTime(2000), - lastDate: DateTime(2100), + lastDate: DateTime.now(), ); if (picked != null) { @@ -435,6 +438,15 @@ class _AddEmployeeBottomSheetState extends State Future _handleSubmit() async { final isValid = _controller.basicValidator.formKey.currentState?.validate() ?? false; + if (_controller.joiningDate != null && + _controller.joiningDate!.isAfter(DateTime.now())) { + showAppSnackbar( + title: 'Invalid Date', + message: 'Joining Date cannot be in the future.', + type: SnackbarType.warning, + ); + return; + } if (!isValid || _controller.joiningDate == null || diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 09a5438..c91ccee 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -204,14 +204,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { ], _gap(), - _buildTextFieldSection( - icon: Icons.confirmation_number_outlined, - title: "GST No.", - controller: controller.gstController, - hint: "Enter GST No.", - ), - _gap(), - _buildDropdownField( icon: Icons.payment, title: "Payment Mode", @@ -252,20 +244,11 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { ), _gap(), - _buildTextFieldSection( - icon: Icons.confirmation_number_outlined, - title: "Transaction ID", - controller: controller.transactionIdController, - hint: "Enter Transaction ID", - validator: (v) => (v != null && v.isNotEmpty) - ? Validators.transactionIdValidator(v) - : null, - ), - _gap(), - _buildTransactionDateField(), _gap(), + _buildTransactionIdField(), + _gap(), _buildLocationField(), _gap(), @@ -288,6 +271,39 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { ); } + Widget _buildTransactionIdField() { + final paymentMode = + controller.selectedPaymentMode.value?.name.toLowerCase() ?? ''; + final isRequired = paymentMode.isNotEmpty && + paymentMode != 'cash' && + paymentMode != 'cheque'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionTitle( + icon: Icons.confirmation_number_outlined, + title: "Transaction ID", + requiredField: isRequired, + ), + MySpacing.height(6), + CustomTextField( + controller: controller.transactionIdController, + hint: "Enter Transaction ID", + validator: (v) { + if (isRequired) { + if (v == null || v.isEmpty) { + return "Transaction ID is required for this payment mode"; + } + return Validators.transactionIdValidator(v); + } + return null; + }, + ), + ], + ); + } + Widget _gap([double h = 16]) => MySpacing.height(h); Widget _buildDropdownField({ @@ -326,8 +342,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { CustomTextField( controller: controller, hint: hint ?? "", - keyboardType: - keyboardType ?? TextInputType.text, + keyboardType: keyboardType ?? TextInputType.text, validator: validator, maxLines: maxLines, ),