diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart index 33cc9b2..ea43d3c 100644 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -12,7 +12,8 @@ class DailyTaskPlanningController extends GetxController { RxList employees = [].obs; RxList selectedEmployees = [].obs; List allEmployeesCache = []; - List dailyTasks = []; + RxList dailyTasks = + [].obs; RxMap uploadingStates = {}.obs; MyFormValidator basicValidator = MyFormValidator(); @@ -97,7 +98,6 @@ class DailyTaskPlanningController extends GetxController { if (response == true) { logSafe("Task assigned successfully", level: LogLevel.info); await fetchBuildingInfra(buildingId, projectId, serviceId); - Get.back(); showAppSnackbar( title: "Success", @@ -129,18 +129,17 @@ class DailyTaskPlanningController extends GetxController { final infraData = infraResponse?['data'] as List?; if (infraData == null || infraData.isEmpty) { - dailyTasks = []; + dailyTasks.clear(); //reactive clear return; } - // Filter buildings with 0 planned & completed work final filteredBuildings = infraData.where((b) { final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0; final completed = (b['completedWork'] as num?)?.toDouble() ?? 0; return planned > 0 || completed > 0; }).toList(); - dailyTasks = filteredBuildings.map((buildingJson) { + final mapped = filteredBuildings.map((buildingJson) { final building = Building( id: buildingJson['id'], name: buildingJson['buildingName'], @@ -163,14 +162,19 @@ class DailyTaskPlanningController extends GetxController { ); }).toList(); + dailyTasks.assignAll(mapped); + buildingLoadingStates.clear(); buildingsWithDetails.clear(); } catch (e, stack) { - logSafe("Error fetching daily task data", - level: LogLevel.error, error: e, stackTrace: stack); + logSafe( + "Error fetching daily task data", + level: LogLevel.error, + error: e, + stackTrace: stack, + ); } finally { isFetchingTasks.value = false; - update(); // dailyTasks is non-reactive } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index b39cb40..29b41ff 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,10 +1,9 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; - static const String baseUrl = "https://api.onfieldwork.com/api"; - + // static const String baseUrl = "https://api.onfieldwork.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = @@ -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/helpers/services/app_initializer.dart b/lib/helpers/services/app_initializer.dart index 1196355..6b48339 100644 --- a/lib/helpers/services/app_initializer.dart +++ b/lib/helpers/services/app_initializer.dart @@ -38,11 +38,16 @@ Future initializeApp() async { } } +/// --------------------------------------------------------------------------- +/// 🔹 AUTH TOKEN HANDLER +/// --------------------------------------------------------------------------- Future _handleAuthTokens() async { final refreshToken = await LocalStorage.getRefreshToken(); + if (refreshToken?.isNotEmpty ?? false) { logSafe("🔁 Refresh token found. Attempting to refresh JWT..."); final success = await AuthService.refreshToken(); + if (!success) { logSafe("âš ī¸ Refresh token invalid or expired. User must login again."); } @@ -51,43 +56,67 @@ Future _handleAuthTokens() async { } } +/// --------------------------------------------------------------------------- +/// 🔹 UI SETUP +/// --------------------------------------------------------------------------- Future _setupUI() async { setPathUrlStrategy(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - logSafe("💡 UI setup completed with default system behavior."); + logSafe("💡 UI setup completed."); } +/// --------------------------------------------------------------------------- +/// 🔹 FIREBASE + GEMINI SETUP +/// --------------------------------------------------------------------------- Future _setupFirebase() async { + // Firebase Core await Firebase.initializeApp(); - logSafe("💡 Firebase initialized."); + logSafe("đŸ”Ĩ Firebase initialized."); + } +/// --------------------------------------------------------------------------- +/// 🔹 LOCAL STORAGE SETUP +/// --------------------------------------------------------------------------- Future _setupLocalStorage() async { if (!LocalStorage.isInitialized) { await LocalStorage.init(); - logSafe("💡 Local storage initialized."); + logSafe("💾 Local storage initialized."); } else { - logSafe("â„šī¸ Local storage already initialized, skipping."); + logSafe("â„šī¸ Local storage already initialized. Skipping."); } } +/// --------------------------------------------------------------------------- +/// 🔹 DEVICE INFO +/// --------------------------------------------------------------------------- Future _setupDeviceInfo() async { final deviceInfoService = DeviceInfoService(); await deviceInfoService.init(); - logSafe("📱 Device Info: ${deviceInfoService.deviceData}"); + + logSafe("📱 Device Info Loaded: ${deviceInfoService.deviceData}"); } +/// --------------------------------------------------------------------------- +/// 🔹 THEME SETUP +/// --------------------------------------------------------------------------- Future _setupTheme() async { await ThemeCustomizer.init(); - logSafe("💡 Theme customizer initialized."); + logSafe("🎨 Theme customizer initialized."); } +/// --------------------------------------------------------------------------- +/// 🔹 FIREBASE CLOUD MESSAGING (PUSH) +/// --------------------------------------------------------------------------- Future _setupFirebaseMessaging() async { await FirebaseNotificationService().initialize(); - logSafe("💡 Firebase Messaging initialized."); + logSafe("📨 Firebase Messaging initialized."); } +/// --------------------------------------------------------------------------- +/// 🔹 FINAL APP STYLE +/// --------------------------------------------------------------------------- void _finalizeAppStyle() { AppStyle.init(); - logSafe("💡 AppStyle initialized."); + logSafe("đŸŽ¯ AppStyle initialized."); } diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index cd16993..5a6c73b 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -24,9 +24,11 @@ class AssignTaskBottomSheet extends StatefulWidget { final String buildingName; final String floorName; final String workAreaName; + final String buildingId; const AssignTaskBottomSheet({ super.key, + required this.buildingId, required this.buildingName, required this.workLocation, required this.floorName, @@ -372,7 +374,7 @@ class _AssignTaskBottomSheetState extends State { } } - void _onAssignTaskPressed() { + Future _onAssignTaskPressed() async { final selectedTeam = controller.selectedEmployees; if (selectedTeam.isEmpty) { @@ -413,16 +415,20 @@ class _AssignTaskBottomSheetState extends State { return; } - controller.assignDailyTask( + final success = await controller.assignDailyTask( workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, taskTeam: selectedTeam.map((e) => e.id).toList(), assignmentDate: widget.assignmentDate, - buildingId: widget.buildingName, + buildingId: widget.buildingId, projectId: selectedProjectId!, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, ); + + if (success) { + Navigator.pop(context); + } } } 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/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index b14af24..dff0b48 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -77,6 +77,7 @@ class _DashboardScreenState extends State with UIMixin { ); } + Widget _sectionTitle(String title) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), @@ -558,51 +559,56 @@ class _DashboardScreenState extends State with UIMixin { // --------------------------------------------------------------------------- @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xfff5f6fa), - body: Layout( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _projectSelector(), - MySpacing.height(20), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _quickActions(), - MySpacing.height(20), - _dashboardModules(), - MySpacing.height(20), - _sectionTitle('Reports & Analytics'), - _cardWrapper( - child: ExpenseTypeReportChart(), - ), - _cardWrapper( - child: ExpenseByStatusWidget( - controller: dashboardController, +Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xfff5f6fa), + body: Layout( + child: Stack( + children: [ + // Main content + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _projectSelector(), + MySpacing.height(20), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _quickActions(), + MySpacing.height(20), + _dashboardModules(), + MySpacing.height(20), + _sectionTitle('Reports & Analytics'), + _cardWrapper( + child: ExpenseTypeReportChart(), ), - ), - _cardWrapper( - child: MonthlyExpenseDashboardChart(), - ), - MySpacing.height(20), - ], + _cardWrapper( + child: ExpenseByStatusWidget( + controller: dashboardController, + ), + ), + _cardWrapper( + child: MonthlyExpenseDashboardChart(), + ), + MySpacing.height(80), // give space under content + ], + ), ), ), - ), - ], + ], + ), ), - ), + ], ), - ); - } + ), + ); +} } - class _DashboardCardMeta { final IconData icon; final Color color; diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 4aabaf1..3136d5e 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -36,6 +36,10 @@ class _ManageReportingBottomSheetState final TextEditingController _primaryController = TextEditingController(); final TextEditingController _secondaryController = TextEditingController(); + final FocusNode _mainEmployeeFocus = FocusNode(); + final FocusNode _primaryFocus = FocusNode(); + final FocusNode _secondaryFocus = FocusNode(); + final RxList _filteredPrimary = [].obs; final RxList _filteredSecondary = [].obs; final RxList _selectedPrimary = [].obs; @@ -69,6 +73,10 @@ class _ManageReportingBottomSheetState @override void dispose() { + _mainEmployeeFocus.dispose(); + _primaryFocus.dispose(); + _secondaryFocus.dispose(); + _primaryController.dispose(); _secondaryController.dispose(); _selectEmployeeController.dispose(); @@ -368,6 +376,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Primary Reporting Manager*", controller: _primaryController, + focusNode: _primaryFocus, filteredList: _filteredPrimary, selectedList: _selectedPrimary, isPrimary: true, @@ -379,6 +388,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Secondary Reporting Manager", controller: _secondaryController, + focusNode: _secondaryFocus, filteredList: _filteredSecondary, selectedList: _selectedSecondary, isPrimary: false, @@ -390,8 +400,10 @@ class _ManageReportingBottomSheetState final safeWrappedContent = SafeArea( child: SingleChildScrollView( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewPadding.bottom + 20, - left: 16, right: 16, top: 8, + bottom: MediaQuery.of(context).viewPadding.bottom + 20, + left: 16, + right: 16, + top: 8, ), child: content, ), @@ -417,7 +429,7 @@ class _ManageReportingBottomSheetState isSubmitting: _isSubmitting, onCancel: _handleCancel, onSubmit: _handleSubmit, - child: safeWrappedContent, + child: safeWrappedContent, ); } @@ -449,6 +461,7 @@ class _ManageReportingBottomSheetState Widget _buildSearchSection({ required String label, required TextEditingController controller, + required FocusNode focusNode, required RxList filteredList, required RxList selectedList, required bool isPrimary, @@ -459,20 +472,10 @@ class _ManageReportingBottomSheetState MyText.bodyMedium(label, fontWeight: 600), MySpacing.height(8), - // Search field - TextField( + _searchBar( controller: controller, - decoration: InputDecoration( - hintText: "Type to search employees...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: focusNode, + hint: "Type to search employees...", ), // Dropdown suggestions @@ -567,19 +570,10 @@ class _ManageReportingBottomSheetState children: [ MyText.bodyMedium("Select Employee *", fontWeight: 600), MySpacing.height(8), - TextField( + _searchBar( controller: _selectEmployeeController, - decoration: InputDecoration( - hintText: "Type to search employee...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: _mainEmployeeFocus, + hint: "Type to search employee...", ), Obx(() { if (_filteredEmployees.isEmpty) return const SizedBox.shrink(); @@ -641,4 +635,41 @@ class _ManageReportingBottomSheetState ], ); } + + Widget _searchBar({ + required TextEditingController controller, + required FocusNode focusNode, + required String hint, + }) { + return GestureDetector( + onTap: () => focusNode.requestFocus(), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + color: Colors.white, + ), + child: Row( + children: [ + const Icon(Icons.search, size: 18, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + hintText: hint, + border: InputBorder.none, + isDense: true, + ), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ); + } } 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 a6a3c51..f5fc4b1 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; @@ -83,6 +85,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(); @@ -493,6 +510,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( diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index a184b6e..5b3eb19 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -472,6 +472,7 @@ class _DailyTaskPlanningScreenState extends State ), builder: (context) => AssignTaskBottomSheet( + buildingId: building.id, buildingName: building.name, floorName: