implementation of manage reporting inside employee profile
This commit is contained in:
parent
c631f8b092
commit
1b883ac524
@ -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();
|
||||||
|
|||||||
@ -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(', ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -213,19 +215,74 @@ class _ManageReportingBottomSheetState
|
|||||||
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(),
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user