From 49326e99291605e615752ce939c3c331d360f63f Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 19 Nov 2025 12:00:26 +0530 Subject: [PATCH] feat: enhance employee selection and validation in service project job form --- .../multiple_select_bottomsheet.dart | 85 ++++++++++------- .../add_service_project_job_bottom_sheet.dart | 92 +++++++++---------- 2 files changed, 91 insertions(+), 86 deletions(-) diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 34b62bc..336bb1a 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -58,11 +58,10 @@ class _EmployeeSelectionBottomSheetState } else { _selectedEmployees.add(emp); } - _selectedEmployees.refresh(); } else { _selectedEmployees.assignAll([emp]); - _selectedEmployees.refresh(); } + _selectedEmployees.refresh(); // important for Obx rebuild } void _handleSubmit() { @@ -104,41 +103,57 @@ class _EmployeeSelectionBottomSheetState ); Widget _employeeList() => Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: _searchResults.length, - itemBuilder: (context, index) { - final emp = _searchResults[index]; - return Obx(() { - // wrap each tile - final isSelected = _selectedEmployees.contains(emp); - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Text( - (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') - .toUpperCase(), - style: const TextStyle(color: Colors.white), + child: Obx(() { + if (_isSearching.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (_searchResults.isEmpty) { + return const Center(child: Text("No employees found")); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final emp = _searchResults[index]; + + return Obx(() { + final isSelected = _selectedEmployees.contains(emp); + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), + ), ), - ), - title: Text('${emp.firstName} ${emp.lastName}'), - subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleEmployee(emp), - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) { + FocusScope.of(context).unfocus(); // hide keyboard + _toggleEmployee(emp); + }, + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), ), - ), - onTap: () => _toggleEmployee(emp), - contentPadding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - ); - }); - }, - ), + onTap: () { + FocusScope.of(context).unfocus(); + _toggleEmployee(emp); + }, + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); + }); + }, + ); + }), ); @override diff --git a/lib/model/service_project/add_service_project_job_bottom_sheet.dart b/lib/model/service_project/add_service_project_job_bottom_sheet.dart index 64fbbfd..ffb1fcd 100644 --- a/lib/model/service_project/add_service_project_job_bottom_sheet.dart +++ b/lib/model/service_project/add_service_project_job_bottom_sheet.dart @@ -24,7 +24,6 @@ class _AddServiceProjectJobBottomSheetState final formKey = GlobalKey(); final controller = Get.put(AddServiceProjectJobController()); - final TextEditingController _searchController = TextEditingController(); late RxList _selectedEmployees; @override @@ -36,7 +35,6 @@ class _AddServiceProjectJobBottomSheetState @override void dispose() { - _searchController.dispose(); Get.delete(); super.dispose(); } @@ -91,6 +89,7 @@ class _AddServiceProjectJobBottomSheetState ), ], ); + Widget _employeeSelector() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -109,27 +108,42 @@ class _AddServiceProjectJobBottomSheetState ); if (selectedEmployees != null) { - setState(() { - _selectedEmployees.assignAll(selectedEmployees); - }); + _selectedEmployees.assignAll(selectedEmployees); + controller.selectedAssignees.assignAll(selectedEmployees); } }, child: AbsorbPointer( - child: TextFormField( - decoration: _inputDecoration("Select Employees"), - controller: TextEditingController( - text: _selectedEmployees.isEmpty - ? "" - : _selectedEmployees - .map((e) => "${e.firstName} ${e.lastName}") - .join(", "), - ), - validator: (v) => _selectedEmployees.isEmpty - ? "Please select employees" - : null, - ), + child: Obx(() => TextFormField( + decoration: _inputDecoration("Select Employees"), + controller: TextEditingController( + text: _selectedEmployees.isEmpty + ? "" + : _selectedEmployees + .map((e) => "${e.firstName} ${e.lastName}") + .join(", "), + ), + validator: (v) => _selectedEmployees.isEmpty + ? "Please select employees" + : null, + )), ), ), + MySpacing.height(8), + Obx(() => Wrap( + spacing: 8, + runSpacing: 4, + children: _selectedEmployees + .map( + (emp) => Chip( + label: Text('${emp.firstName} ${emp.lastName}'), + onDeleted: () { + _selectedEmployees.remove(emp); + controller.selectedAssignees.remove(emp); + }, + ), + ) + .toList(), + )), ], ); @@ -138,9 +152,9 @@ class _AddServiceProjectJobBottomSheetState children: [ SizedBox( height: 48, - child: TextField( + child: TextFormField( controller: controller.tagCtrl, - onSubmitted: (v) { + onFieldSubmitted: (v) { final value = v.trim(); if (value.isNotEmpty && !controller.enteredTags.contains(value)) { @@ -149,6 +163,9 @@ class _AddServiceProjectJobBottomSheetState controller.tagCtrl.clear(); }, decoration: _inputDecoration("Start typing to add tags"), + validator: (v) => controller.enteredTags.isEmpty + ? "Please add at least one tag" + : null, ), ), MySpacing.height(8), @@ -156,25 +173,14 @@ class _AddServiceProjectJobBottomSheetState spacing: 8, children: controller.enteredTags .map((tag) => Chip( - label: Text(tag), - onDeleted: () => controller.enteredTags.remove(tag))) + label: Text(tag), + onDeleted: () => controller.enteredTags.remove(tag), + )) .toList(), )), ], ); - - - void _toggleEmployee(EmployeeModel emp) { - final contains = _selectedEmployees.contains(emp); - if (contains) { - _selectedEmployees.remove(emp); - } else { - _selectedEmployees.add(emp); - } - controller.toggleAssignee(emp); - } - void _handleSubmit() { if (!(formKey.currentState?.validate() ?? false)) return; controller.titleCtrl.text = controller.titleCtrl.text.trim(); @@ -195,25 +201,9 @@ class _AddServiceProjectJobBottomSheetState children: [ _textField("Title", controller.titleCtrl, required: true), MySpacing.height(16), - Obx(() { - if (_searchController.text.isNotEmpty) - return const SizedBox.shrink(); - return Wrap( - spacing: 8, - runSpacing: 4, - children: _selectedEmployees - .map( - (emp) => Chip( - label: Text('${emp.firstName} ${emp.lastName}'), - onDeleted: () => _toggleEmployee(emp), - ), - ) - .toList(), - ); - }), _employeeSelector(), MySpacing.height(16), - MyText.labelMedium("Tags (Optional)"), + _labelWithStar("Tags", required: true), MySpacing.height(8), _tagInput(), MySpacing.height(16),