implementation of manage reporting inside employee profile

This commit is contained in:
Manish 2025-11-12 17:17:01 +05:30 committed by Vaibhav Surve
parent 0334a6b7a8
commit 5c386cb8ff
3 changed files with 224 additions and 31 deletions

View File

@ -21,6 +21,10 @@ class EmployeesScreenController extends GetxController {
/// Upload state tracking (if needed later) /// Upload state tracking (if needed later)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
<EmployeeModel>[].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -86,6 +90,52 @@ class EmployeesScreenController extends GetxController {
isLoadingEmployeeDetails.value = false; isLoadingEmployeeDetails.value = false;
} }
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId
Future<void> 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 /// 🔹 Clear all employee data
void clearEmployees() { void clearEmployees() {
employees.clear(); employees.clear();

View File

@ -38,6 +38,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchEmployeeDetails(widget.employeeId); controller.fetchEmployeeDetails(widget.employeeId);
controller.fetchReportingManagers(widget.employeeId);
}); });
} }
@ -193,6 +194,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
child: MyRefreshIndicator( child: MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
await controller.fetchEmployeeDetails(widget.employeeId); await controller.fetchEmployeeDetails(widget.employeeId);
await controller.fetchReportingManagers(employee.id);
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@ -298,10 +300,14 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
action: 0, action: 0,
), ),
hideMainSelector: true, hideMainSelector: true,
hideLoggedUserFromSelection: hideLoggedUserFromSelection: true,
true, loggedUserId:
controller.selectedEmployeeDetails.value?.id,
), ),
); );
// 🔄 Refresh reporting managers after editing
await controller.fetchReportingManagers(employee.id);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@ -326,6 +332,58 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> 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,
),
),
),
],
),
);
})
], ],
), ),
@ -500,4 +558,13 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
}), }),
); );
} }
String _getManagerNames(List<EmployeeModel> managers) {
if (managers.isEmpty) return '';
return managers
.map((m) =>
'${(m.firstName ?? '').trim()} ${(m.lastName ?? '').trim()}'.trim())
.where((name) => name.isNotEmpty)
.join(', ');
}
} }

View File

@ -13,14 +13,16 @@ class ManageReportingBottomSheet extends StatefulWidget {
final EmployeeModel? initialEmployee; final EmployeeModel? initialEmployee;
final bool hideMainSelector; final bool hideMainSelector;
final bool renderAsCard; final bool renderAsCard;
final bool hideLoggedUserFromSelection; // new final bool hideLoggedUserFromSelection;
final String? loggedUserId;
const ManageReportingBottomSheet({ const ManageReportingBottomSheet({
super.key, super.key,
this.initialEmployee, this.initialEmployee,
this.hideMainSelector = false, this.hideMainSelector = false,
this.renderAsCard = false, this.renderAsCard = false,
this.hideLoggedUserFromSelection = false, // default false this.hideLoggedUserFromSelection = false,
this.loggedUserId,
}); });
@override @override
@ -211,21 +213,76 @@ class _ManageReportingBottomSheetState
Future<void> _handleSubmit() async { Future<void> _handleSubmit() async {
if (_selectedEmployee == null) { if (_selectedEmployee == null) {
showAppSnackbar( showAppSnackbar(
title: 'Error', title: 'Error',
message: 'Please select the employee.', message: 'Please select the employee.',
type: SnackbarType.error); type: SnackbarType.error,
);
return; return;
} }
if (_selectedPrimary.isEmpty) { if (_selectedPrimary.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: 'Error', title: 'Error',
message: 'Please select at least one primary employee.', message: 'Please select at least one primary employee.',
type: SnackbarType.error); type: SnackbarType.error,
);
return; return;
} }
final employeeId = _selectedEmployee!.id;
// === BUILD PAYLOAD (updated logic) ===
// fetch current assignments so we can deactivate old ones
List<dynamic>? 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<Map<String, dynamic>> payload = []; final List<Map<String, dynamic>> 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) { for (final emp in _selectedPrimary) {
payload.add({ payload.add({
"reportToId": emp.id, "reportToId": emp.id,
@ -233,6 +290,8 @@ class _ManageReportingBottomSheetState
"isActive": true, "isActive": true,
}); });
} }
// 3) Add new secondary (active)
for (final emp in _selectedSecondary) { for (final emp in _selectedSecondary) {
payload.add({ payload.add({
"reportToId": emp.id, "reportToId": emp.id,
@ -242,11 +301,13 @@ class _ManageReportingBottomSheetState
} }
setState(() => _isSubmitting = true); 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( final success = await ApiService.manageOrganizationHierarchy(
employeeId: employeeId, employeeId: employeeId,
payload: payload, payload: payload,
@ -254,25 +315,36 @@ class _ManageReportingBottomSheetState
// hide loader // hide loader
if (Get.isDialogOpen == true) Get.back(); if (Get.isDialogOpen == true) Get.back();
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
if (success) { if (success) {
showAppSnackbar( showAppSnackbar(
title: 'Success', title: 'Success',
message: 'Reporting assigned successfully', message: 'Reporting assigned successfully',
type: SnackbarType.success); 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); await ApiService.getOrganizationHierarchyList(employeeId);
// Keep sheet open and reset reporter selections for next assignment // Keep sheet open and reset selections
_resetForm(); _resetForm();
} else { } else {
showAppSnackbar( showAppSnackbar(
title: 'Error', title: 'Error',
message: 'Failed to assign reporting. Please try again.', message: 'Failed to assign reporting. Please try again.',
type: SnackbarType.error); type: SnackbarType.error,
);
} }
} }
@ -354,11 +426,6 @@ class _ManageReportingBottomSheetState
backgroundColor: Colors.indigo.shade50, backgroundColor: Colors.indigo.shade50,
labelStyle: TextStyle(color: AppTheme.primaryColor), labelStyle: TextStyle(color: AppTheme.primaryColor),
deleteIcon: const Icon(Icons.close, size: 16), deleteIcon: const Icon(Icons.close, size: 16),
onDeleted: () => setState(() {
_selectedEmployee = null;
_selectEmployeeController.clear();
_resetReportersOnly();
}),
), ),
) )
else else
@ -460,12 +527,21 @@ class _ManageReportingBottomSheetState
spacing: 6, spacing: 6,
runSpacing: 6, runSpacing: 6,
children: selectedList.map((emp) { children: selectedList.map((emp) {
final isProfileEmployee = widget.initialEmployee != null &&
emp.id == widget.initialEmployee!.id;
return Chip( return Chip(
label: Text(emp.name, style: const TextStyle(fontSize: 12)), 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), labelStyle: TextStyle(color: AppTheme.primaryColor),
deleteIcon: const Icon(Icons.close, size: 16), // Only show delete icon / action for non-profile employees
onDeleted: () => selectedList.remove(emp), deleteIcon: isProfileEmployee
? null
: const Icon(Icons.close, size: 16),
onDeleted:
isProfileEmployee ? null : () => selectedList.remove(emp),
); );
}).toList(), }).toList(),
); );