import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/controller/service_project/service_project_allocation_controller.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart'; import 'package:on_field_work/model/service_project/job_allocation_model.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; class RoleEmployeeAllocation { final TeamRole role; final List employees; RoleEmployeeAllocation({required this.role, required this.employees}); } class SimpleProjectAllocationBottomSheet extends StatefulWidget { final String projectId; final List? existingAllocations; const SimpleProjectAllocationBottomSheet({ super.key, required this.projectId, this.existingAllocations, }); @override State createState() => _SimpleProjectAllocationBottomSheetState(); } class _SimpleProjectAllocationBottomSheetState extends State with UIMixin { late ServiceProjectAllocationController controller; final RxList _selectedEmployees = [].obs; final RxList _addedAllocations = [].obs; final TextEditingController _employeeTextCtrl = TextEditingController(); final _roleDropdownKey = GlobalKey(); final RxBool _hasChanges = false.obs; bool _checkIfChanged() { final existingMap = { for (var alloc in widget.existingAllocations ?? []) alloc.role.id: alloc.employees.map((e) => e.id).toSet() }; final currentMap = { for (var alloc in _addedAllocations) alloc.role.id: alloc.employees.map((e) => e.id).toSet() }; // Compare existing and current allocations if (existingMap.length != currentMap.length) return true; for (var roleId in existingMap.keys) { if (!currentMap.containsKey(roleId)) return true; if (existingMap[roleId]!.difference(currentMap[roleId]!).isNotEmpty || currentMap[roleId]!.difference(existingMap[roleId]!).isNotEmpty) { return true; } } return false; } @override void initState() { super.initState(); controller = Get.put(ServiceProjectAllocationController()); controller.projectId.value = widget.projectId; controller.fetchRoles(); // Prepopulate existing allocations if (widget.existingAllocations != null) { _addedAllocations.assignAll(widget.existingAllocations!); } } @override void dispose() { _employeeTextCtrl.dispose(); Get.delete(); super.dispose(); } InputDecoration _inputDecoration(String hint) => InputDecoration( hintText: hint, filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade400)), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ); Widget _roleDropdown() { return GestureDetector( key: _roleDropdownKey, onTap: _showRoleMenu, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade400), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.08), offset: const Offset(0, 1), blurRadius: 2, ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Obx(() => MyText.bodyMedium( controller.selectedRole.value?.name ?? "Select Role")), const Icon(Icons.arrow_drop_down_rounded, size: 30), ], ), ), ); } Future _showRoleMenu() async { if (_roleDropdownKey.currentContext == null) return; final RenderBox button = _roleDropdownKey.currentContext!.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero, ancestor: overlay); final selectedRoleId = await showMenu( context: context, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, overlay.size.width - position.dx - button.size.width, 0, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), items: controller.roles .map( (role) => PopupMenuItem( value: role.id, child: MyText.bodyMedium(role.name), ), ) .toList(), ); if (selectedRoleId != null) { final role = controller.roles.firstWhere((r) => r.id == selectedRoleId); controller.selectedRole.value = role; controller.fetchEmployeesByRole(role.id); _selectedEmployees.clear(); _employeeTextCtrl.clear(); } } Widget _employeeSelector() { return GestureDetector( onTap: () async { final selected = await showModalBottomSheet>( context: context, isScrollControlled: true, builder: (_) => EmployeeSelectionBottomSheet( multipleSelection: true, initiallySelected: _selectedEmployees, title: "Select Employees", ), ); if (selected != null) { _selectedEmployees.assignAll(selected); _employeeTextCtrl.text = _selectedEmployees .map((e) => "${e.firstName} ${e.lastName}") .join(", "); } }, child: AbsorbPointer( child: TextFormField( controller: _employeeTextCtrl, decoration: _inputDecoration("Select Employees").copyWith( suffixIcon: const Icon(Icons.search), ), validator: (_) => _selectedEmployees.isEmpty ? "Please select employees" : null, ), ), ); } void _handleAdd() { final selectedRole = controller.selectedRole.value; if (selectedRole == null || _selectedEmployees.isEmpty) { showAppSnackbar( title: "Error", message: "Please select role and employees", type: SnackbarType.error, ); return; } final updatedAllocations = List.from(_addedAllocations); final existingIndex = updatedAllocations.indexWhere((a) => a.role.id == selectedRole.id); if (existingIndex >= 0) { final existingAlloc = updatedAllocations[existingIndex]; final mergedEmployees = [ ...existingAlloc.employees, ..._selectedEmployees.where( (emp) => !existingAlloc.employees.any((e) => e.id == emp.id), ), ]; updatedAllocations[existingIndex] = RoleEmployeeAllocation( role: existingAlloc.role, employees: mergedEmployees); } else { updatedAllocations.add( RoleEmployeeAllocation( role: selectedRole, employees: List.from(_selectedEmployees), ), ); } _addedAllocations.assignAll(updatedAllocations); _hasChanges.value = _checkIfChanged(); controller.selectedRole.value = null; _selectedEmployees.clear(); _employeeTextCtrl.clear(); } void _handleSubmit() async { if (_addedAllocations.isEmpty) { showAppSnackbar( title: "Error", message: "Please add at least one allocation", type: SnackbarType.error, ); return; } final payload = >[]; payload.addAll(_getRemovedEmployeesPayload()); payload.addAll(_getAddedEmployeesPayload()); final success = await ApiService.manageServiceProjectAllocation(payload: payload); if (success) { Get.back(result: true); } else { showAppSnackbar( title: "Error", message: "Failed to save allocation", type: SnackbarType.error, ); } } List> _getRemovedEmployeesPayload() { final removedPayload = >[]; final existingMap = { for (var alloc in widget.existingAllocations ?? []) alloc.role.id: alloc.employees.map((e) => e.id).toList() }; final currentMap = { for (var alloc in _addedAllocations) alloc.role.id: alloc.employees.map((e) => e.id).toList() }; existingMap.forEach((roleId, existingEmpIds) { final currentEmpIds = currentMap[roleId] ?? []; final removedEmpIds = existingEmpIds.where((id) => !currentEmpIds.contains(id)).toList(); for (var empId in removedEmpIds) { removedPayload.add({ "projectId": widget.projectId, "employeeId": empId.toString(), "teamRoleId": roleId, "isActive": false, }); } }); return removedPayload; } List> _getAddedEmployeesPayload() { final addedPayload = >[]; final existingMap = { for (var alloc in widget.existingAllocations ?? []) alloc.role.id: alloc.employees.map((e) => e.id).toList() }; for (var alloc in _addedAllocations) { final currentEmpIds = alloc.employees.map((e) => e.id).toList(); final existingEmpIds = existingMap[alloc.role.id] ?? []; final newEmpIds = currentEmpIds.where((id) => !existingEmpIds.contains(id)).toList(); for (var empId in newEmpIds) { addedPayload.add({ "projectId": widget.projectId, "employeeId": empId.toString(), "teamRoleId": alloc.role.id, "isActive": true, }); } } return addedPayload; } Widget _addedAllocationList() { return Obx(() { if (_addedAllocations.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium("Added Allocations"), const Divider(height: 22, thickness: 1.2), ..._addedAllocations.map((alloc) => Padding( padding: const EdgeInsets.symmetric(vertical: 9), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( "Role: ${alloc.role.name}", fontWeight: 700, ), const SizedBox(height: 7), Wrap( spacing: 7, runSpacing: 5, children: alloc.employees .map((e) => Chip( label: MyText.bodyMedium( "${e.firstName} ${e.lastName}"), onDeleted: () { final updatedEmployees = alloc.employees .where((emp) => emp.id != e.id) .toList(); if (updatedEmployees.isEmpty) { _addedAllocations.removeWhere( (a) => a.role.id == alloc.role.id); } else { final updatedAlloc = RoleEmployeeAllocation( role: alloc.role, employees: updatedEmployees); final index = _addedAllocations.indexWhere( (a) => a.role.id == alloc.role.id); _addedAllocations[index] = updatedAlloc; } _addedAllocations.refresh(); _hasChanges.value = _checkIfChanged(); }, backgroundColor: Colors.grey.shade200, )) .toList(), ), ], ), )), ], ); }); } @override Widget build(BuildContext context) { return Obx(() => BaseBottomSheet( title: "Allocate Employees", onCancel: () => Get.back(), onSubmit: _hasChanges.value ? _handleSubmit : () { showAppSnackbar( title: "No changes", message: "You haven't made any changes to submit.", type: SnackbarType.info, ); }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium("Select Role"), MySpacing.height(8), _roleDropdown(), MySpacing.height(12), MyText.labelMedium("Select Employees"), MySpacing.height(8), _employeeSelector(), MySpacing.height(8), Obx(() => Wrap( spacing: 7, runSpacing: 5, children: _selectedEmployees .map((emp) => Chip( label: MyText.bodyMedium( "${emp.firstName} ${emp.lastName}"), onDeleted: () { _selectedEmployees.remove(emp); _employeeTextCtrl.text = _selectedEmployees .map( (e) => "${e.firstName} ${e.lastName}") .join(", "); }, )) .toList(), )), MySpacing.height(12), SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Icons.add_circle_outline), label: MyText.bodyMedium( "Add Allocation", fontWeight: 600, color: Colors.white, ), onPressed: _handleAdd, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), backgroundColor: contentTheme.primary, textStyle: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, ), ), ), ), MySpacing.height(12), _addedAllocationList(), MySpacing.height(12), ], ), ), )); } }