From cdba511d434fcf24cc4b0405ca8043d81af8c46d Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 12 Nov 2025 17:17:01 +0530 Subject: [PATCH] implementation of manage reporting inside employee profile --- .../employee/employees_screen_controller.dart | 50 +++++++ .../employees/employee_detail_screen.dart | 71 +++++++++- .../manage_reporting_bottom_sheet.dart | 134 ++++++++++++++---- 3 files changed, 224 insertions(+), 31 deletions(-) diff --git a/lib/controller/employee/employees_screen_controller.dart b/lib/controller/employee/employees_screen_controller.dart index 7d95421..a8d9f1a 100644 --- a/lib/controller/employee/employees_screen_controller.dart +++ b/lib/controller/employee/employees_screen_controller.dart @@ -21,6 +21,10 @@ class EmployeesScreenController extends GetxController { /// ✅ Upload state tracking (if needed later) RxMap uploadingStates = {}.obs; + RxList selectedEmployeePrimaryManagers = [].obs; + RxList selectedEmployeeSecondaryManagers = + [].obs; + @override void onInit() { super.onInit(); @@ -86,6 +90,52 @@ class EmployeesScreenController extends GetxController { isLoadingEmployeeDetails.value = false; } + /// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId + Future fetchReportingManagers(String? employeeId) async { + if (employeeId == null || employeeId.isEmpty) return; + + try { + // ✅ Always clear before new fetch (to avoid mixing old data) + selectedEmployeePrimaryManagers.clear(); + selectedEmployeeSecondaryManagers.clear(); + + // Fetch from existing API helper + final data = await ApiService.getOrganizationHierarchyList(employeeId); + + if (data == null || data.isEmpty) { + update(['employee_screen_controller']); + return; + } + + for (final item in data) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + + final emp = EmployeeModel.fromJson(reportTo); + final isPrimary = item['isPrimary'] == true; + + if (isPrimary) { + if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) { + selectedEmployeePrimaryManagers.add(emp); + } + } else { + if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) { + selectedEmployeeSecondaryManagers.add(emp); + } + } + } catch (_) { + // ignore malformed items + } + } + + update(['employee_screen_controller']); + } catch (e) { + logSafe("Error fetching reporting managers for $employeeId", + level: LogLevel.error, error: e); + } + } + /// 🔹 Clear all employee data void clearEmployees() { employees.clear(); diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 217de6c..0cb20c2 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -38,6 +38,7 @@ class _EmployeeDetailPageState extends State with UIMixin { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { controller.fetchEmployeeDetails(widget.employeeId); + controller.fetchReportingManagers(widget.employeeId); }); } @@ -207,6 +208,7 @@ class _EmployeeDetailPageState extends State with UIMixin { child: MyRefreshIndicator( onRefresh: () async { await controller.fetchEmployeeDetails(widget.employeeId); + await controller.fetchReportingManagers(employee.id); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -312,10 +314,14 @@ class _EmployeeDetailPageState extends State with UIMixin { action: 0, ), hideMainSelector: true, - hideLoggedUserFromSelection: - true, + hideLoggedUserFromSelection: true, + loggedUserId: + controller.selectedEmployeeDetails.value?.id, ), ); + + // 🔄 Refresh reporting managers after editing + await controller.fetchReportingManagers(employee.id); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), @@ -340,6 +346,58 @@ class _EmployeeDetailPageState extends State with UIMixin { ), ), ), + Obx(() { + final primary = + controller.selectedEmployeePrimaryManagers; + final secondary = + controller.selectedEmployeeSecondaryManagers; + + if (primary.isEmpty && secondary.isEmpty) { + return const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + 'No reporting managers assigned', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.only( + top: 8.0, left: 8, right: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'Primary → ${_getManagerNames(primary)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'Secondary → ${_getManagerNames(secondary)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + }) ], ), @@ -513,4 +571,13 @@ class _EmployeeDetailPageState extends State with UIMixin { }), ); } + + String _getManagerNames(List managers) { + if (managers.isEmpty) return '—'; + return managers + .map((m) => + '${(m.firstName ?? '').trim()} ${(m.lastName ?? '').trim()}'.trim()) + .where((name) => name.isNotEmpty) + .join(', '); + } } diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 3eb5dd4..ba035c6 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; - final bool hideLoggedUserFromSelection; // ✅ new + final bool hideLoggedUserFromSelection; + final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, - this.hideLoggedUserFromSelection = false, // default false + this.hideLoggedUserFromSelection = false, + this.loggedUserId, }); @override @@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState Future _handleSubmit() async { if (_selectedEmployee == null) { showAppSnackbar( - title: 'Error', - message: 'Please select the employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error, + ); return; } + if (_selectedPrimary.isEmpty) { showAppSnackbar( - title: 'Error', - message: 'Please select at least one primary employee.', - type: SnackbarType.error); + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error, + ); return; } + final employeeId = _selectedEmployee!.id; + + // === BUILD PAYLOAD (updated logic) === + + // fetch current assignments so we can deactivate old ones + List? currentAssignments; + try { + currentAssignments = + await ApiService.getOrganizationHierarchyList(employeeId); + } catch (_) { + currentAssignments = null; + } + + // helper sets of newly selected ids + final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); + final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); + final List> payload = []; + // 1) For current active assignments: if they are not in new selections -> add isActive:false + if (currentAssignments != null && currentAssignments.isNotEmpty) { + for (final item in currentAssignments) { + try { + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + final isPrimary = item['isPrimary'] == true; + final currentlyActive = + item['isActive'] == true || item['isActive'] == null; // be safe + + // if currently active and not included in new selection -> mark false + if (currentlyActive) { + if (isPrimary && !newPrimaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": true, + "isActive": false, + }); + } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { + payload.add({ + "reportToId": reportToId, + "isPrimary": false, + "isActive": false, + }); + } + } + } catch (_) { + // ignore malformed items (same behavior as before) + } + } + } + + // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, @@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState "isActive": true, }); } + + // 3) Add new secondary (active) for (final emp in _selectedSecondary) { payload.add({ "reportToId": emp.id, @@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState } setState(() => _isSubmitting = true); - // show loader - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); - final employeeId = _selectedEmployee!.id; + // show loader + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + final success = await ApiService.manageOrganizationHierarchy( employeeId: employeeId, payload: payload, @@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState // hide loader if (Get.isDialogOpen == true) Get.back(); - setState(() => _isSubmitting = false); if (success) { showAppSnackbar( - title: 'Success', - message: 'Reporting assigned successfully', - type: SnackbarType.success); + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success, + ); - // Optionally refresh the saved hierarchy (not necessary here) but we can call: + // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers + try { + final empId = employeeId; + final EmployeesScreenController controller = Get.find(); + await controller.fetchReportingManagers(empId); + await controller.fetchEmployeeDetails(empId); + } catch (_) { + // ignore if controller not found — not critical + } + + // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); - // Keep sheet open and reset reporter selections for next assignment + // Keep sheet open and reset selections _resetForm(); } else { showAppSnackbar( - title: 'Error', - message: 'Failed to assign reporting. Please try again.', - type: SnackbarType.error); + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error, + ); } } @@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState backgroundColor: Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => setState(() { - _selectedEmployee = null; - _selectEmployeeController.clear(); - _resetReportersOnly(); - }), ), ) else @@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState spacing: 6, runSpacing: 6, children: selectedList.map((emp) { + final isProfileEmployee = widget.initialEmployee != null && + emp.id == widget.initialEmployee!.id; + return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), - backgroundColor: Colors.indigo.shade50, + backgroundColor: isProfileEmployee + ? Colors.indigo.shade50 + : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => selectedList.remove(emp), + // Only show delete icon / action for non-profile employees + deleteIcon: isProfileEmployee + ? null + : const Icon(Icons.close, size: 16), + onDeleted: + isProfileEmployee ? null : () => selectedList.remove(emp), ); }).toList(), );