From 55f36fac6deb3b85a966fcc643d7ab5d674bc616 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 10:37:48 +0530 Subject: [PATCH] implemented assign employee feature for infra project module --- lib/helpers/services/api_endpoints.dart | 19 +- lib/helpers/services/api_service.dart | 38 +++ .../assign_project_allocation_request.dart | 25 ++ .../assign_employee_infra_bottom_sheet.dart | 299 ++++++++++++++++++ .../infra_project_details_screen.dart | 30 ++ 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 lib/model/infra_project/assign_project_allocation_request.dart create mode 100644 lib/view/infraProject/assign_employee_infra_bottom_sheet.dart diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 7d94d8b..29b41ff 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -5,7 +5,6 @@ class ApiEndpoints { // static const String baseUrl = "https://mapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.onfieldwork.com/api"; - static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = "/Master/expenses-categories"; @@ -48,7 +47,8 @@ class ApiEndpoints { static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; static const String getTodaysAttendance = "/attendance/project/team"; - static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId"; + static const String getAttendanceForDashboard = + "/dashboard/get/attendance/employee/:projectId"; static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getRegularizationLogs = "/attendance/regularize"; @@ -142,7 +142,6 @@ class ApiEndpoints { static const String manageOrganizationHierarchy = "/organization/hierarchy/manage"; - // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; @@ -151,10 +150,14 @@ class ApiEndpoints { "/serviceproject/job/details"; static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String createServiceProjectJob = "/serviceproject/job/create"; - static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance"; - static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log"; - static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; - static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; + static const String serviceProjectUpateJobAttendance = + "/serviceproject/job/attendance"; + static const String serviceProjectUpateJobAttendanceLog = + "/serviceproject/job/attendance/log"; + static const String getServiceProjectUpateJobAllocationList = + "/serviceproject/get/allocation/list"; + static const String manageServiceProjectUpateJobAllocation = + "/serviceproject/manage/allocation"; static const String getTeamRoles = "/master/team-roles/list"; static const String getServiceProjectBranches = "/serviceproject/branch/list"; @@ -168,5 +171,5 @@ class ApiEndpoints { static const String getInfraProjectsList = "/project/list"; static const String getInfraProjectDetail = "/project/details"; static const String getInfraProjectTeamList = "/project/allocation"; - + static const String assignInfraProjectAllocation = "/project/allocation"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index f56341e..618410b 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -52,6 +52,8 @@ import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + class ApiService { static const bool enableLogs = true; @@ -2008,6 +2010,42 @@ class ApiService { label: "Comment Task", returnFullResponse: true); return parsed != null && parsed['success'] == true; } + + static Future assignEmployeesToProject({ + required List allocations, + }) async { + if (allocations.isEmpty) { + _log( + "No allocations provided for assignEmployeesToProject", + level: LogLevel.error, + ); + return null; + } + + final endpoint = ApiEndpoints.assignInfraProjectAllocation; + final payload = allocations.map((e) => e.toJson()).toList(); + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: payload, + ); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse( + response, + label: "AssignInfraProjectAllocation", + returnFullResponse: true, + ); + + if (parsedJson == null || parsedJson is! Map) { + return null; + } + + return ProjectAllocationResponse.fromJson(parsedJson); + } + static Future getInfraProjectTeamListApi({ required String projectId, String? serviceId, diff --git a/lib/model/infra_project/assign_project_allocation_request.dart b/lib/model/infra_project/assign_project_allocation_request.dart new file mode 100644 index 0000000..f64b826 --- /dev/null +++ b/lib/model/infra_project/assign_project_allocation_request.dart @@ -0,0 +1,25 @@ +class AssignProjectAllocationRequest { + final String employeeId; + final String projectId; + final String jobRoleId; + final String serviceId; + final bool status; + + AssignProjectAllocationRequest({ + required this.employeeId, + required this.projectId, + required this.jobRoleId, + required this.serviceId, + required this.status, + }); + + Map toJson() { + return { + "employeeId": employeeId, + "projectId": projectId, + "jobRoleId": jobRoleId, + "serviceId": serviceId, + "status": status, + }; + } +} diff --git a/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart new file mode 100644 index 0000000..fac4332 --- /dev/null +++ b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:on_field_work/controller/tenant/organization_selection_controller.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/widgets/tenant/organization_selector.dart'; +import 'package:on_field_work/model/attendance/organization_per_project_list_model.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/controller/tenant/service_controller.dart'; +import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; +import 'package:on_field_work/model/tenant/tenant_services_model.dart'; +import 'package:on_field_work/helpers/services/api_service.dart'; +import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + + +class JobRole { + final String id; + final String name; + + JobRole({required this.id, required this.name}); + + factory JobRole.fromJson(Map json) { + return JobRole( + id: json['id'].toString(), + name: json['name'] ?? '', + ); + } +} + +class AssignEmployeeBottomSheet extends StatefulWidget { + final String projectId; + + const AssignEmployeeBottomSheet({ + super.key, + required this.projectId, + }); + + @override + State createState() => + _AssignEmployeeBottomSheetState(); +} + +class _AssignEmployeeBottomSheetState extends State { + late final OrganizationController _organizationController; + late final ServiceController _serviceController; + + final RxList _selectedEmployees = [].obs; + + Organization? _selectedOrganization; + JobRole? _selectedRole; + + final RxBool _isLoadingRoles = false.obs; + final RxList _roles = [].obs; + + @override + void initState() { + super.initState(); + + _organizationController = Get.put( + OrganizationController(), + tag: 'assign_employee_org', + ); + + _serviceController = Get.put( + ServiceController(), + tag: 'assign_employee_service', + ); + + _organizationController.fetchOrganizations(widget.projectId); + _serviceController.fetchServices(widget.projectId); + + _fetchRoles(); + } + + Future _fetchRoles() async { + try { + _isLoadingRoles.value = true; + final res = await ApiService.getRoles(); + if (res != null) { + _roles.assignAll( + res.map((e) => JobRole.fromJson(e)).toList(), + ); + } + } finally { + _isLoadingRoles.value = false; + } + } + + @override + void dispose() { + Get.delete(tag: 'assign_employee_org'); + Get.delete(tag: 'assign_employee_service'); + super.dispose(); + } + + Future _openEmployeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: 'Select Employee(s)', + multipleSelection: true, + initiallySelected: _selectedEmployees.toList(), + ), + ); + + if (result != null && result is List) { + _selectedEmployees.assignAll(result); + } + } + + void _handleAssign() async { + if (_selectedEmployees.isEmpty || + _selectedRole == null || + _serviceController.selectedService == null) { + Get.snackbar('Error', 'Please complete all selections'); + return; + } + + final allocations = _selectedEmployees + .map( + (e) => AssignProjectAllocationRequest( + employeeId: e.id, + projectId: widget.projectId, + jobRoleId: _selectedRole!.id, + serviceId: _serviceController.selectedService!.id, + status: true, + ), + ) + .toList(); + + final res = await ApiService.assignEmployeesToProject( + allocations: allocations, + ); + + if (res?.success == true) { + Navigator.of(context).pop(true); // 🔥 triggers refresh + } else { + Get.snackbar('Error', res?.message ?? 'Assignment failed'); + } + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: 'Assign Employee', + submitText: 'Assign', + isSubmitting: false, + onCancel: () => Navigator.of(context).pop(), + onSubmit: _handleAssign, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //ORGANIZATION + MyText.bodySmall( + 'Organization', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + OrganizationSelector( + controller: _organizationController, + height: 44, + onSelectionChanged: (Organization? org) async { + _selectedOrganization = org; + _selectedEmployees.clear(); + _selectedRole = null; + _serviceController.clearSelection(); + }, + ), + + MySpacing.height(20), + + ///EMPLOYEES (SEARCH) + MyText.bodySmall( + 'Employees', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx( + () => InkWell( + onTap: _openEmployeeSelector, + child: _dropdownBox( + _selectedEmployees.isEmpty + ? 'Select employee(s)' + : '${_selectedEmployees.length} employee(s) selected', + icon: Icons.search, + ), + ), + ), + + MySpacing.height(20), + + ///SERVICE + MyText.bodySmall( + 'Service', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + ServiceSelector( + controller: _serviceController, + height: 44, + onSelectionChanged: (Service? service) async { + _selectedRole = null; + }, + ), + + MySpacing.height(20), + + /// JOB ROLE + MyText.bodySmall( + 'Job Role', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx(() { + if (_isLoadingRoles.value) { + return _skeleton(); + } + + return PopupMenuButton( + onSelected: (role) { + _selectedRole = role; + setState(() {}); + }, + itemBuilder: (context) { + if (_roles.isEmpty) { + return const [ + PopupMenuItem( + enabled: false, + child: Text('No roles found'), + ), + ]; + } + return _roles + .map( + (r) => PopupMenuItem( + value: r, + child: Text(r.name), + ), + ) + .toList(); + }, + child: _dropdownBox( + _selectedRole?.name ?? 'Select role', + ), + ); + }), + ], + ), + ); + } + + Widget _dropdownBox(String text, {IconData icon = Icons.arrow_drop_down}) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), + ), + ), + Icon(icon, color: Colors.grey), + ], + ), + ); + } + + Widget _skeleton() { + return Container( + height: 44, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + ); + } +} diff --git a/lib/view/infraProject/infra_project_details_screen.dart b/lib/view/infraProject/infra_project_details_screen.dart index c4af071..82cb2ad 100644 --- a/lib/view/infraProject/infra_project_details_screen.dart +++ b/lib/view/infraProject/infra_project_details_screen.dart @@ -18,6 +18,8 @@ import 'package:on_field_work/controller/infra_project/infra_project_screen_deta import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart'; import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/view/infraProject/assign_employee_infra_bottom_sheet.dart'; + class InfraProjectDetailsScreen extends StatefulWidget { final String projectId; @@ -77,6 +79,21 @@ class _InfraProjectDetailsScreenState extends State _tabController = TabController(length: _tabs.length, vsync: this); } + void _openAssignEmployeeBottomSheet() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AssignEmployeeBottomSheet( + projectId: widget.projectId, + ), + ); + if (result == true) { + controller.fetchProjectTeamList(); + Get.snackbar('Success', 'Employee assigned successfully'); + } + } + @override void dispose() { _tabController.dispose(); @@ -487,6 +504,19 @@ class _InfraProjectDetailsScreenState extends State projectName: widget.projectName, backgroundColor: appBarColor, ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + _openAssignEmployeeBottomSheet(); + }, + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.person_add), + label: MyText( + 'Assign Employee', + fontSize: 14, + color: Colors.white, + fontWeight: 500, + ), + ), body: Stack( children: [ Container(