feat: enhance employee selection and validation in service project job form

This commit is contained in:
Vaibhav Surve 2025-11-19 12:00:26 +05:30
parent 0e575c393d
commit 49326e9929
2 changed files with 91 additions and 86 deletions

View File

@ -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<Color>(
(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<Color>(
(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

View File

@ -24,7 +24,6 @@ class _AddServiceProjectJobBottomSheetState
final formKey = GlobalKey<FormState>();
final controller = Get.put(AddServiceProjectJobController());
final TextEditingController _searchController = TextEditingController();
late RxList<EmployeeModel> _selectedEmployees;
@override
@ -36,7 +35,6 @@ class _AddServiceProjectJobBottomSheetState
@override
void dispose() {
_searchController.dispose();
Get.delete<AddServiceProjectJobController>();
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),