diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 10a69ae5e..7947060ef 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -5,6 +5,7 @@ import { LayoutDashboard, Network } from "lucide-react"; import { useAuth } from "./auth/useAuth"; import LoginPage from "./pages/auth/LoginPage"; import OverviewPage from "./pages/dashboard/OverviewPage"; +import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage"; import RolesPage from "./pages/dashboard/user-management/RolesPage"; import UserManagementPage from "./pages/dashboard/user-management/UserManagementPage"; @@ -21,6 +22,10 @@ const sidebarItems: SidebarItem[] = [ href: "/dashboard/user-management", icon: , children: [ + { + label: "Employees", + href: "/dashboard/user-management/employees", + }, { label: "Permissions", href: "/dashboard/user-management/permissions", @@ -79,6 +84,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 new file mode 100644 index 000000000..2e54608b0 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx @@ -0,0 +1,321 @@ +import { useEffect, useState } from "react"; +import { isAxiosError } from "axios"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@edr/ui-common"; + +import { api } from "@/auth/http"; +import { useAuth } from "@/auth/useAuth"; + +interface LocaleText { + en?: string; + am?: string; +} + +interface EmployeeUserRecord { + id: string; + name?: LocaleText; + email?: string; + phoneNumber?: string; + username?: string; +} + +interface EmployeePositionSummary { + id: string; + position?: { + id: string; + name?: LocaleText; + key?: string; + }; +} + +interface EmployeeRecord { + id: string; + name?: LocaleText; + status?: string; + user?: EmployeeUserRecord; + employeePositions?: EmployeePositionSummary[]; +} + +interface ListResponse { + count?: number; + items?: T[]; + data?: T[]; +} + +const getLocaleLabel = (value?: LocaleText | null, fallback = "Unnamed") => + value?.en ?? value?.am ?? fallback; + +const getItems = (payload: ListResponse | T[] | undefined | null) => { + if (!payload) { + return [] as T[]; + } + + if (Array.isArray(payload)) { + return payload; + } + + return payload.items ?? payload.data ?? []; +}; + +const getEmployeeDisplayName = (employee: EmployeeRecord) => + getLocaleLabel( + employee.name ?? employee.user?.name, + employee.user?.email ?? employee.user?.username ?? employee.id, + ); + +const EmployeesPage = () => { + const { user } = useAuth(); + const [employees, setEmployees] = useState([]); + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(null); + const [selectedEmployee, setSelectedEmployee] = useState(null); + + useEffect(() => { + let isMounted = true; + const organizationIds = Array.from( + new Set( + (user?.employee ?? []) + .map((employee) => employee.organizationId) + .filter((organizationId): organizationId is string => Boolean(organizationId)), + ), + ); + + const loadEmployees = async () => { + setLoading(true); + setErrorMessage(null); + + if (!organizationIds.length) { + setEmployees([]); + setErrorMessage("No employee organization scope is available for this account."); + setLoading(false); + return; + } + + try { + const responses = await Promise.all( + organizationIds.map((organizationId) => + api.get>( + `/employees/${organizationId}/by-organization`, + { + params: { + skip: 0, + take: 1000, + }, + }, + ), + ), + ); + + if (!isMounted) { + return; + } + + const uniqueEmployees = Array.from( + new Map( + responses + .flatMap((response) => getItems(response.data)) + .map((employee) => [employee.id, employee]), + ).values(), + ); + + setEmployees( + uniqueEmployees.sort((left, right) => + getEmployeeDisplayName(left).localeCompare(getEmployeeDisplayName(right)), + ), + ); + } catch (error) { + if (!isMounted) { + return; + } + + setErrorMessage( + isAxiosError(error) + ? error.response?.data?.message ?? "Unable to load employees." + : "Unable to load employees.", + ); + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + void loadEmployees(); + + return () => { + isMounted = false; + }; + }, [user]); + + return ( +
+
+
+

+ User Management +

+
+
+

Employees

+

+ Browse employees within your accessible scope and open a record to review contact and position details. +

+
+
+ {employees.length} employees +
+
+
+ + {loading ? ( +
+ Loading employees... +
+ ) : errorMessage ? ( +
+ {errorMessage} +
+ ) : employees.length ? ( +
+ + + + + + + + + + + + + {employees.map((employee) => { + const positions = employee.employeePositions?.map((item) => getLocaleLabel(item.position?.name, item.position?.key ?? "Unnamed")) ?? []; + + return ( + setSelectedEmployee(employee)} + className="cursor-pointer border-t border-border bg-background transition hover:bg-accent/20" + > + + + + + + + + ); + })} + +
NameUsernameEmailPhoneStatusPositions
{getEmployeeDisplayName(employee)}{employee.user?.username ?? "-"}{employee.user?.email ?? "-"}{employee.user?.phoneNumber ?? "-"}{employee.status ?? "-"}{positions.join(", ") || "-"}
+
+ ) : ( +
+ No employees are available in your scope. +
+ )} +
+ + !open && setSelectedEmployee(null)}> + + + + {selectedEmployee ? getEmployeeDisplayName(selectedEmployee) : "Employee details"} + + + {selectedEmployee + ? "Review the employee profile, contact information, and assigned positions." + : undefined} + + + + {selectedEmployee ? ( +
+
+
+

+ Employee name +

+

+ {getEmployeeDisplayName(selectedEmployee)} +

+
+
+

+ Status +

+

+ {selectedEmployee.status ?? "-"} +

+
+
+

+ Username +

+

{selectedEmployee.user?.username ?? "-"}

+
+
+

+ Email +

+

{selectedEmployee.user?.email ?? "-"}

+
+
+

+ Phone +

+

{selectedEmployee.user?.phoneNumber ?? "-"}

+
+
+

+ Employee ID +

+

{selectedEmployee.id}

+
+
+ +
+
+

Assigned positions

+
+ {selectedEmployee.employeePositions?.length ?? 0} positions +
+
+ + {selectedEmployee.employeePositions?.length ? ( +
+ {selectedEmployee.employeePositions.map((item) => ( +
+

+ {getLocaleLabel(item.position?.name, item.position?.key ?? "Unnamed")} +

+

+ {item.position?.key ?? "-"} +

+
+ ))} +
+ ) : ( +
+ No positions are assigned to this employee. +
+ )} +
+
+ ) : null} +
+
+
+ ); +}; + +export default EmployeesPage;