From 88a280eea243f17e5122206584b8b2a6b6818e7a Mon Sep 17 00:00:00 2001 From: Michael Abebe Date: Tue, 2 Jun 2026 13:43:44 +0300 Subject: [PATCH] WIP --- .../backoffice/backoffice.controller.ts | 14 + .../modules/backoffice/backoffice.service.ts | 88 +++ .../src/seed/edr-freight.seed.ts | 2 + apps/edr-freight-web/backoffice/src/App.tsx | 2 +- .../user-management/EmployeesPage.tsx | 2 +- .../user-management/PositionTypesPage.tsx | 694 ++++++++++++++++-- .../user-management/UserManagementPage.tsx | 54 +- .../dashboard/user-management/UsersPage.tsx | 44 +- 8 files changed, 851 insertions(+), 49 deletions(-) diff --git a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts index 41e303985..395ff0386 100644 --- a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts +++ b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts @@ -5,6 +5,7 @@ import { Param, ParseUUIDPipe, Post, + Query, Put, } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; @@ -27,6 +28,19 @@ export class BackofficeController { return this.backofficeService.createOrganizationUser(organizationId, dto); } + @Get("organizations/:orgId/employees") + @ApiOperation({ summary: "Get deduplicated organization employees for backoffice" }) + getOrganizationEmployees( + @Param("orgId", ParseUUIDPipe) organizationId: string, + @Query("skip") skip?: string, + @Query("take") take?: string, + ) { + return this.backofficeService.getOrganizationEmployees(organizationId, { + skip, + take, + }); + } + @Get("organizations/:orgId/employee-users/:userId/roles") @ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" }) getEmployeeUserRoles( diff --git a/apps/edr-freight-api/src/modules/backoffice/backoffice.service.ts b/apps/edr-freight-api/src/modules/backoffice/backoffice.service.ts index 7c3e05154..7c7805b28 100644 --- a/apps/edr-freight-api/src/modules/backoffice/backoffice.service.ts +++ b/apps/edr-freight-api/src/modules/backoffice/backoffice.service.ts @@ -27,6 +27,8 @@ const EDR_ORG_MANAGER_ROLE_KEY = "edr_org_manager"; @Injectable() export class BackofficeService { constructor( + @InjectRepository(Employee) + private readonly employeeRepository: Repository, @InjectRepository(Organization) private readonly organizationRepository: Repository, @InjectRepository(Role) @@ -214,6 +216,45 @@ export class BackofficeService { })); } + async getOrganizationEmployees( + organizationId: string, + query: { skip?: string; take?: string }, + ) { + const organizationExists = await this.organizationRepository.exists({ + where: { id: organizationId }, + }); + + if (!organizationExists) { + throw new NotFoundException("organization_not_found"); + } + + const take = Number.parseInt(query.take ?? "1000", 10); + const skip = Number.parseInt(query.skip ?? "0", 10); + + const employees = await this.employeeRepository.find({ + where: { + organizationId, + isCurrent: true, + }, + relations: { + user: true, + employeePositions: { + position: true, + }, + }, + order: { + createdAt: "DESC", + }, + }); + + const deduplicated = this.mergeEmployeesByUser(employees); + + return { + count: deduplicated.length, + items: deduplicated.slice(skip, skip + take), + }; + } + async replaceEmployeeUserRoles( organizationId: string, userId: string, @@ -282,6 +323,53 @@ export class BackofficeService { } } + private mergeEmployeesByUser(employees: Employee[]) { + const employeesByUserId = new Map(); + + for (const employee of employees) { + const userId = employee.userId; + const employeeId = employee.id; + + if (!userId) { + if (employeeId) { + employeesByUserId.set(employeeId, employee); + } + continue; + } + + const existing = employeesByUserId.get(userId); + + if (!existing) { + employeesByUserId.set(userId, employee); + continue; + } + + const existingPositions = existing.employeePositions ?? []; + const nextPositions = employee.employeePositions ?? []; + const mergedEmployeePositions = Array.from( + new Map( + [...existingPositions, ...nextPositions].map((employeePosition) => [ + employeePosition.id, + employeePosition, + ]), + ).values(), + ); + + employeesByUserId.set(userId, { + ...existing, + ...employee, + id: existing.id, + user: existing.user ?? employee.user, + userId, + name: existing.name ?? employee.name, + status: existing.status ?? employee.status, + employeePositions: mergedEmployeePositions, + }); + } + + return [...employeesByUserId.values()]; + } + private async ensureOrganizationAdminAccess( manager: EntityManager, organizationId: string, diff --git a/apps/edr-freight-api/src/seed/edr-freight.seed.ts b/apps/edr-freight-api/src/seed/edr-freight.seed.ts index 7fc9b9697..dda476841 100644 --- a/apps/edr-freight-api/src/seed/edr-freight.seed.ts +++ b/apps/edr-freight-api/src/seed/edr-freight.seed.ts @@ -15,6 +15,7 @@ const IAM_PERMISSION_KEYS = { deletePositionPermission: "can:delete:position_permission", deleteUnit: "can:delete:unit", deleteUserRole: "can:delete:user_role", + findAllOrganization: "can:find_all:organization", manageOrganizationAdmin: "manage:organizationAdmin", manageUnitAdmin: "manage:unitAdmin", updateUnit: "can:update:unit", @@ -217,6 +218,7 @@ export const EDR_FREIGHT_ROLES: FreightSeedRole[] = [ IAM_PERMISSION_KEYS.createPositionPermission, IAM_PERMISSION_KEYS.deletePositionPermission, IAM_PERMISSION_KEYS.viewPositionPermission, + IAM_PERMISSION_KEYS.findAllOrganization, ], }, { diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 50ae8103b..8fc296b2f 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -167,7 +167,7 @@ const App = () => { } /> } /> } /> - } /> + {/* } /> */} } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx index 2e54608b0..eecca7a3e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx @@ -100,7 +100,7 @@ const EmployeesPage = () => { const responses = await Promise.all( organizationIds.map((organizationId) => api.get>( - `/employees/${organizationId}/by-organization`, + `/backoffice/organizations/${organizationId}/employees`, { params: { skip: 0, diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx index 177fccbd0..9895ae36a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx @@ -12,6 +12,7 @@ import { SelectTrigger, SelectValue, } from "@edr/ui-common"; +import { CopyPlus, Plus, RefreshCw } from "lucide-react"; import { api } from "@/auth/http"; import { useAuth } from "@/auth/useAuth"; @@ -47,6 +48,7 @@ interface PermissionRecord { id: string; key: string; name?: LocaleText; + applicationId?: string | null; } interface ListResponse { @@ -57,6 +59,42 @@ interface ListResponse { const PAGE_SIZE = 1000; +const inputClassName = + "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:focus:ring-emerald-950"; +const buttonClassName = + "inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-60"; + +const emptyCreateForm = { + copyPermissionFromId: "", + key: "", + nameAm: "", + nameEn: "", +}; + +type PositionTypeEditFormState = { + key: string; + nameAm: string; + nameEn: string; +}; + +const emptyEditForm: PositionTypeEditFormState = { + key: "", + nameAm: "", + nameEn: "", +}; + +const toggleSelection = ( + currentIds: string[], + targetIds: string[], + checked: boolean, +) => { + if (checked) { + return [...new Set([...currentIds, ...targetIds])]; + } + + return currentIds.filter((id) => !targetIds.includes(id)); +}; + const getLocaleLabel = (value?: LocaleText | null, fallback = "Unnamed") => value?.en ?? value?.am ?? fallback; @@ -102,11 +140,22 @@ const PositionTypesPage = () => { const [loadingOrganizations, setLoadingOrganizations] = useState(true); const [loadingUnits, setLoadingUnits] = useState(false); const [loadingPositionTypes, setLoadingPositionTypes] = useState(false); + const [loadingPermissionsCatalog, setLoadingPermissionsCatalog] = useState(false); + const [submitting, setSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [selectedPositionType, setSelectedPositionType] = useState(null); const [positionTypePermissions, setPositionTypePermissions] = useState([]); + const [allPermissions, setAllPermissions] = useState([]); const [permissionsLoading, setPermissionsLoading] = useState(false); const [permissionsError, setPermissionsError] = useState(null); + const [permissionSearch, setPermissionSearch] = useState(""); + const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [createForm, setCreateForm] = useState(emptyCreateForm); + const [createPermissionSearch, setCreatePermissionSearch] = useState(""); + const [createPermissionIds, setCreatePermissionIds] = useState([]); + const [createError, setCreateError] = useState(null); + const [editForm, setEditForm] = useState(emptyEditForm); const isSuperAdmin = Boolean(user?.roles?.some((role) => role.key === "super_admin")); const allowedOrgIds = useMemo( @@ -130,6 +179,65 @@ const PositionTypesPage = () => { const selectedOrganization = visibleOrganizations.find((organization) => organization.id === selectedOrgId) ?? null; const selectedUnit = units.find((unit) => unit.id === selectedUnitId) ?? null; + const availableCopySources = useMemo( + () => positionTypes.filter((positionType) => positionType.id !== selectedPositionType?.id), + [positionTypes, selectedPositionType?.id], + ); + const filteredPermissions = useMemo(() => { + const query = permissionSearch.trim().toLowerCase(); + + return allPermissions.filter((permission) => { + if (!query) { + return true; + } + + const label = getLocaleLabel(permission.name, permission.key).toLowerCase(); + return label.includes(query) || permission.key.toLowerCase().includes(query); + }); + }, [allPermissions, permissionSearch]); + const filteredCreatePermissions = useMemo(() => { + const query = createPermissionSearch.trim().toLowerCase(); + + return allPermissions.filter((permission) => { + if (!query) { + return true; + } + + const label = getLocaleLabel(permission.name, permission.key).toLowerCase(); + return label.includes(query) || permission.key.toLowerCase().includes(query); + }); + }, [allPermissions, createPermissionSearch]); + const allFilteredPermissionIds = filteredPermissions.map((permission) => permission.id); + const allFilteredCreatePermissionIds = filteredCreatePermissions.map((permission) => permission.id); + const areAllFilteredPermissionsSelected = + allFilteredPermissionIds.length > 0 && + allFilteredPermissionIds.every((id) => selectedPermissionIds.includes(id)); + const areAllFilteredCreatePermissionsSelected = + allFilteredCreatePermissionIds.length > 0 && + allFilteredCreatePermissionIds.every((id) => createPermissionIds.includes(id)); + + const loadPositionTypes = async (unitId: string) => { + const response = await api.get>( + `/position-types/list-with-commons/${unitId}`, + { + params: { + skip: 0, + take: PAGE_SIZE, + orderBy: "createdAt:Desc", + }, + }, + ); + + return getItems(response.data); + }; + + const loadPermissionsForPositionType = async (positionTypeId: string) => { + const response = await api.get>( + `/position-type-permissions/given-first/${positionTypeId}`, + ); + + return getItems(response.data); + }; useEffect(() => { let isMounted = true; @@ -170,6 +278,45 @@ const PositionTypesPage = () => { }; }, []); + useEffect(() => { + let isMounted = true; + + const loadPermissionsCatalog = async () => { + setLoadingPermissionsCatalog(true); + + try { + const response = await api.get>("/permissions", { + params: { + skip: 0, + take: 2000, + }, + }); + + if (!isMounted) { + return; + } + + setAllPermissions(getItems(response.data)); + } catch { + if (!isMounted) { + return; + } + + setAllPermissions([]); + } finally { + if (isMounted) { + setLoadingPermissionsCatalog(false); + } + } + }; + + void loadPermissionsCatalog(); + + return () => { + isMounted = false; + }; + }, []); + useEffect(() => { if (!visibleOrganizations.length) { setSelectedOrgId(""); @@ -245,27 +392,18 @@ const PositionTypesPage = () => { let isMounted = true; - const loadPositionTypes = async () => { + const loadItems = async () => { setLoadingPositionTypes(true); setErrorMessage(null); try { - const response = await api.get>( - `/position-types/list-with-commons/${selectedUnitId}`, - { - params: { - skip: 0, - take: PAGE_SIZE, - orderBy: "createdAt:Desc", - }, - }, - ); + const items = await loadPositionTypes(selectedUnitId); if (!isMounted) { return; } - setPositionTypes(getItems(response.data)); + setPositionTypes(items); } catch (error) { if (!isMounted) { return; @@ -284,7 +422,7 @@ const PositionTypesPage = () => { } }; - void loadPositionTypes(); + void loadItems(); return () => { isMounted = false; @@ -294,11 +432,20 @@ const PositionTypesPage = () => { useEffect(() => { if (!selectedPositionType) { setPositionTypePermissions([]); + setSelectedPermissionIds([]); + setEditForm(emptyEditForm); setPermissionsError(null); setPermissionsLoading(false); + setPermissionSearch(""); return; } + setEditForm({ + key: selectedPositionType.key, + nameAm: selectedPositionType.name?.am ?? "", + nameEn: selectedPositionType.name?.en ?? "", + }); + let isMounted = true; const loadPermissions = async () => { @@ -306,15 +453,14 @@ const PositionTypesPage = () => { setPermissionsError(null); try { - const response = await api.get>( - `/position-type-permissions/given-first/${selectedPositionType.id}`, - ); + const items = await loadPermissionsForPositionType(selectedPositionType.id); if (!isMounted) { return; } - setPositionTypePermissions(getItems(response.data)); + setPositionTypePermissions(items); + setSelectedPermissionIds(items.map((permission) => permission.id)); } catch (error) { if (!isMounted) { return; @@ -340,6 +486,181 @@ const PositionTypesPage = () => { }; }, [selectedPositionType]); + const handleRefresh = async () => { + if (!selectedUnitId) { + return; + } + + setLoadingPositionTypes(true); + setErrorMessage(null); + + try { + const items = await loadPositionTypes(selectedUnitId); + setPositionTypes(items); + + if (selectedPositionType) { + const nextSelected = items.find((item) => item.id === selectedPositionType.id) ?? selectedPositionType; + setSelectedPositionType(nextSelected); + } + } catch (error) { + setErrorMessage( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to refresh position types." + : "Unable to refresh position types.", + ); + } finally { + setLoadingPositionTypes(false); + } + }; + + const openCreateDialog = () => { + setCreateForm(emptyCreateForm); + setCreatePermissionIds([]); + setCreatePermissionSearch(""); + setCreateError(null); + setIsCreateOpen(true); + }; + + const handleSelectCopySource = async (positionTypeId: string) => { + setCreateForm((current) => ({ ...current, copyPermissionFromId: positionTypeId })); + + if (!positionTypeId) { + setCreatePermissionIds([]); + return; + } + + try { + const copiedPermissions = await loadPermissionsForPositionType(positionTypeId); + setCreatePermissionIds(copiedPermissions.map((permission) => permission.id)); + } catch (error) { + setCreateError( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to copy permissions." + : "Unable to copy permissions.", + ); + } + }; + + const handleCreatePositionType = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!selectedUnitId) { + setCreateError("Select a unit before creating a position type."); + return; + } + + setSubmitting(true); + setCreateError(null); + + try { + const response = await api.post("/position-types", { + key: createForm.key.trim(), + name: { + am: createForm.nameAm.trim(), + en: createForm.nameEn.trim(), + }, + unitId: selectedUnitId, + }); + + const createdPositionType = response.data; + + await api.post("/position-type-permissions/assign-seconds-for-first", { + firstId: createdPositionType.id, + secondIds: createPermissionIds, + }); + + setIsCreateOpen(false); + setCreateForm(emptyCreateForm); + setCreatePermissionIds([]); + await handleRefresh(); + } catch (error) { + setCreateError( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to create position type." + : "Unable to create position type.", + ); + } finally { + setSubmitting(false); + } + }; + + const handleSavePositionTypeChanges = async () => { + if (!selectedPositionType) { + return; + } + + setSubmitting(true); + setPermissionsError(null); + + try { + if (!selectedPositionType.isSystem) { + await api.put(`/position-types/${selectedPositionType.id}`, { + key: editForm.key.trim(), + name: { + am: editForm.nameAm.trim(), + en: editForm.nameEn.trim(), + }, + unitId: selectedPositionType.unitId, + }); + } + + await api.post("/position-type-permissions/assign-seconds-for-first", { + firstId: selectedPositionType.id, + secondIds: selectedPermissionIds, + }); + + const [refreshedPermissions, refreshedPositionTypes] = await Promise.all([ + loadPermissionsForPositionType(selectedPositionType.id), + selectedUnitId ? loadPositionTypes(selectedUnitId) : Promise.resolve(positionTypes), + ]); + + setPositionTypePermissions(refreshedPermissions); + setSelectedPermissionIds(refreshedPermissions.map((permission) => permission.id)); + setPositionTypes(refreshedPositionTypes); + + const refreshedSelected = refreshedPositionTypes.find((item) => item.id === selectedPositionType.id); + if (refreshedSelected) { + setSelectedPositionType(refreshedSelected); + } + } catch (error) { + setPermissionsError( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to update position type." + : "Unable to update position type.", + ); + } finally { + setSubmitting(false); + } + }; + + const handleSavePermissions = async () => { + if (!selectedPositionType) { + return; + } + + setSubmitting(true); + setPermissionsError(null); + + try { + await api.post("/position-type-permissions/assign-seconds-for-first", { + firstId: selectedPositionType.id, + secondIds: selectedPermissionIds, + }); + + const items = await loadPermissionsForPositionType(selectedPositionType.id); + setPositionTypePermissions(items); + setSelectedPermissionIds(items.map((permission) => permission.id)); + } catch (error) { + setPermissionsError( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to update position type permissions." + : "Unable to update position type permissions.", + ); + } finally { + setSubmitting(false); + } + }; + return (
@@ -351,11 +672,31 @@ const PositionTypesPage = () => {

Position Type

- Browse position types for a selected organization unit, including shared system entries. + Browse position types for a selected organization unit, add new ones, and manage their permissions.

-
- {positionTypes.length} position types +
+
+ {positionTypes.length} position types +
+ +
@@ -497,7 +838,7 @@ const PositionTypesPage = () => { {selectedPositionType - ? `Review the permission set assigned to ${getLocaleLabel(selectedPositionType.name, selectedPositionType.key)}.` + ? `Review and update the permission set assigned to ${getLocaleLabel(selectedPositionType.name, selectedPositionType.key)}.` : undefined} @@ -540,10 +881,51 @@ const PositionTypesPage = () => {
+
+ + + + {selectedPositionType.isSystem ? ( +

+ System position types keep their name and key, but you can still manage permissions here. +

+ ) : null} +
+

Permissions

- {positionTypePermissions.length} permissions + {selectedPermissionIds.length} permissions selected
@@ -555,27 +937,86 @@ const PositionTypesPage = () => {
{permissionsError}
- ) : positionTypePermissions.length ? ( -
- {positionTypePermissions.map((permission) => ( -
-
-

- {getLocaleLabel(permission.name, permission.key)} -

-

- {permission.key} -

-
-
- ))} -
) : ( -
- No permissions are assigned to this position type. +
+ setPermissionSearch(event.target.value)} + placeholder="Search permissions by name or key" + /> + + + + {loadingPermissionsCatalog ? ( +
+ Loading permissions catalog... +
+ ) : filteredPermissions.length ? ( +
+ {filteredPermissions.map((permission) => ( + + ))} +
+ ) : ( +
+ No permissions match the current search. +
+ )} + +
+ + +
)}
@@ -583,6 +1024,175 @@ const PositionTypesPage = () => { ) : null} + + + + + Create position type + + Add a new position type for the selected unit and optionally copy permissions from an existing one. + + + +
void handleCreatePositionType(event)}> +
+ + + + + + + +
+ +
+
+

Permissions

+
+ {createPermissionIds.length} selected +
+
+ + setCreatePermissionSearch(event.target.value)} + placeholder="Search permissions by name or key" + /> + + + + {loadingPermissionsCatalog ? ( +
+ Loading permissions catalog... +
+ ) : filteredCreatePermissions.length ? ( +
+ {filteredCreatePermissions.map((permission) => ( + + ))} +
+ ) : ( +
+ No permissions match the current search. +
+ )} +
+ + {createForm.copyPermissionFromId ? ( +
+
+ + The new position type will inherit permissions from the selected source. +
+
+ ) : null} + + {createError ? ( +
+ {createError} +
+ ) : null} + +
+ + +
+
+
+
); }; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementPage.tsx index 88fa34647..725512c8a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementPage.tsx @@ -188,6 +188,46 @@ const getItems = (payload: ListResponse | T[] | undefined | null) => { return payload.items ?? payload.data ?? []; }; +const mergeEmployeesByUser = (employees: EmployeeRecord[]) => { + const employeesByUserId = new Map(); + + for (const employee of employees) { + const userId = employee.user?.id; + + if (!userId) { + employeesByUserId.set(employee.id, employee); + continue; + } + + const existing = employeesByUserId.get(userId); + + if (!existing) { + employeesByUserId.set(userId, employee); + continue; + } + + const existingPositions = existing.employeePositions ?? []; + const nextPositions = employee.employeePositions ?? []; + const mergedPositions = Array.from( + new Map( + [...existingPositions, ...nextPositions].map((position) => [position.id, position]), + ).values(), + ); + + employeesByUserId.set(userId, { + ...existing, + ...employee, + id: existing.id, + name: existing.name ?? employee.name, + status: existing.status ?? employee.status, + user: existing.user ?? employee.user, + employeePositions: mergedPositions, + }); + } + + return [...employeesByUserId.values()]; +}; + const toInternalKey = (value: string) => value .trim() @@ -611,9 +651,9 @@ const UserManagementPage = () => { try { const response = await api.get>( - `/employees/${organizationId}/by-organization`, + `/backoffice/organizations/${organizationId}/employees`, ); - setOrgEmployees(getItems(response.data)); + setOrgEmployees(mergeEmployeesByUser(getItems(response.data))); } catch { setOrgEmployees([]); } finally { @@ -772,7 +812,7 @@ const UserManagementPage = () => { return; } - setSelectedOrgId(null); + setSelectedOrgId(visibleOrganizations[0]?.id ?? null); setSelectedOrgConfiguration(null); setUnits([]); setPositions([]); @@ -780,6 +820,14 @@ const UserManagementPage = () => { setOrgEmployees([]); }, [selectedOrgId, visibleOrganizations]); + useEffect(() => { + if (!selectedOrgId) { + return; + } + + void refreshSelectedOrg(); + }, [refreshSelectedOrg, selectedOrgId]); + useEffect(() => { if (!selectedOrgId) { setUnits([]); diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UsersPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UsersPage.tsx index 18d7a1511..70d58d313 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UsersPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UsersPage.tsx @@ -104,6 +104,46 @@ const getItems = (payload: ListResponse | T[] | undefined | null) => { return payload.items ?? payload.data ?? []; }; +const mergeEmployeesByUser = (employees: EmployeeRecord[]) => { + const employeesByUserId = new Map(); + + for (const employee of employees) { + const userId = employee.user?.id; + + if (!userId) { + employeesByUserId.set(employee.id, employee); + continue; + } + + const existing = employeesByUserId.get(userId); + + if (!existing) { + employeesByUserId.set(userId, employee); + continue; + } + + const existingPositions = existing.employeePositions ?? []; + const nextPositions = employee.employeePositions ?? []; + const mergedPositions = Array.from( + new Map( + [...existingPositions, ...nextPositions].map((position) => [position.id, position]), + ).values(), + ); + + employeesByUserId.set(userId, { + ...existing, + ...employee, + id: existing.id, + name: existing.name ?? employee.name, + status: existing.status ?? employee.status, + user: existing.user ?? employee.user, + employeePositions: mergedPositions, + }); + } + + return [...employeesByUserId.values()]; +}; + const getErrorMessage = (error: unknown, fallback: string) => { if (isAxiosError(error)) { const message = error.response?.data?.message; @@ -231,9 +271,9 @@ const UsersPage = () => { try { const response = await api.get>( - `/employees/${organizationId}/by-organization`, + `/backoffice/organizations/${organizationId}/employees`, ); - setOrgEmployees(getItems(response.data)); + setOrgEmployees(mergeEmployeesByUser(getItems(response.data))); } catch { setOrgEmployees([]); } finally {