marco.pms.mobileapp/lib/model/service_project/service_project_allocation_bottomsheet.dart
Vaibhav Surve 5c53a3f4be Refactor project structure and rename from 'marco' to 'on field work'
- Updated import paths across multiple files to reflect the new package name.
- Changed application name and identifiers in CMakeLists.txt, Runner.rc, and other configuration files.
- Modified web index.html and manifest.json to update the app title and name.
- Adjusted macOS and Windows project settings to align with the new application name.
- Ensured consistency in naming across all relevant files and directories.
2025-11-22 14:20:37 +05:30

469 lines
16 KiB
Dart

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<EmployeeModel> employees;
RoleEmployeeAllocation({required this.role, required this.employees});
}
class SimpleProjectAllocationBottomSheet extends StatefulWidget {
final String projectId;
final List<RoleEmployeeAllocation>? existingAllocations;
const SimpleProjectAllocationBottomSheet({
super.key,
required this.projectId,
this.existingAllocations,
});
@override
State<SimpleProjectAllocationBottomSheet> createState() =>
_SimpleProjectAllocationBottomSheetState();
}
class _SimpleProjectAllocationBottomSheetState
extends State<SimpleProjectAllocationBottomSheet> with UIMixin {
late ServiceProjectAllocationController controller;
final RxList<EmployeeModel> _selectedEmployees = <EmployeeModel>[].obs;
final RxList<RoleEmployeeAllocation> _addedAllocations =
<RoleEmployeeAllocation>[].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<ServiceProjectAllocationController>();
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<void> _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<String>(
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<String>(
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<List<EmployeeModel>>(
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<RoleEmployeeAllocation>.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 = <Map<String, dynamic>>[];
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<Map<String, dynamic>> _getRemovedEmployeesPayload() {
final removedPayload = <Map<String, dynamic>>[];
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<Map<String, dynamic>> _getAddedEmployeesPayload() {
final addedPayload = <Map<String, dynamic>>[];
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),
],
),
),
));
}
}