diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 51ee212d7..2591d395c 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -63,11 +63,8 @@ import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; import CustomersPage from "./pages/customers/CustomersPage"; import InvoiceDetailPage from "./pages/invoices/InvoiceDetailPage"; import InvoicesPage from "./pages/invoices/InvoicesPage"; -import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; -import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import MyProfilePage from "./pages/dashboard/MyProfilePage"; import OverviewPage from "./pages/dashboard/OverviewPage"; -import UserManagementHostPage from "./pages/dashboard/user-management/UserManagementHostPage"; import PaymentsPage from "./pages/payments/PaymentsPage"; //import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; import { RequirePermission } from "./components/auth/RequirePermission"; @@ -78,11 +75,6 @@ import { isEthiopianGl, isSuperAdmin, } from "./lib/permissions"; -import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage"; -import PositionTypesPage from "./pages/dashboard/user-management/PositionTypesPage"; -import RolesPage from "./pages/dashboard/user-management/RolesPage"; -import UserManagementPage from "./pages/dashboard/user-management/UserManagementPage"; -import UsersPage from "./pages/dashboard/user-management/UsersPage"; import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage"; import ContractTemplatesPage from "./pages/contract_templates/ContractTemplatesPage"; @@ -661,7 +653,7 @@ const App = () => { return ( } /> - } /> + {/* } /> */} } /> } /> @@ -671,7 +663,7 @@ const App = () => { return ( {UserManagementRoutes()} - } /> + {/* } /> */} } /> } /> } /> @@ -1342,10 +1334,6 @@ const App = () => { path="rule-engine/:resource" element={} /> - - } /> - } /> - } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser1Page.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser1Page.tsx deleted file mode 100644 index e47cb95bd..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser1Page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useState } from "react"; - -import { api } from "@/auth/http"; - -const DemoUser1Page = () => { - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let cancelled = false; - - const run = async () => { - setLoading(true); - setError(null); - - try { - const response = await api.get("/test_user1"); - if (cancelled) return; - setData(response.data); - } catch (e: any) { - if (cancelled) return; - const message = - e?.response?.data?.message || - e?.response?.data?.error || - e?.message || - "Request failed"; - setError(String(message)); - } finally { - if (!cancelled) setLoading(false); - } - }; - - void run(); - return () => { - cancelled = true; - }; - }, []); - - return ( -
-
-

User1 Demo

-

- Calls GET /api/test_user1 (requires{' '} - can:demo:user1). -

- -
- {loading ?

Loading...

: null} - {error ? ( -
- {error} -
- ) : null} - {!loading && !error ? ( -
-              {JSON.stringify(data, null, 2)}
-            
- ) : null} -
-
-
- ); -}; - -export default DemoUser1Page; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser2Page.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser2Page.tsx deleted file mode 100644 index 5ef7ad172..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/demo/DemoUser2Page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useState } from "react"; - -import { api } from "@/auth/http"; - -const DemoUser2Page = () => { - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let cancelled = false; - - const run = async () => { - setLoading(true); - setError(null); - - try { - const response = await api.get("/test_user2"); - if (cancelled) return; - setData(response.data); - } catch (e: any) { - if (cancelled) return; - const message = - e?.response?.data?.message || - e?.response?.data?.error || - e?.message || - "Request failed"; - setError(String(message)); - } finally { - if (!cancelled) setLoading(false); - } - }; - - void run(); - return () => { - cancelled = true; - }; - }, []); - - return ( -
-
-

User2 Demo

-

- Calls GET /api/test_user2 (requires{' '} - can:demo:user2). -

- -
- {loading ?

Loading...

: null} - {error ? ( -
- {error} -
- ) : null} - {!loading && !error ? ( -
-              {JSON.stringify(data, null, 2)}
-            
- ) : null} -
-
-
- ); -}; - -export default DemoUser2Page; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/org-structure/OrgStructurePage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/org-structure/OrgStructurePage.tsx deleted file mode 100644 index fdfc522af..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/org-structure/OrgStructurePage.tsx +++ /dev/null @@ -1,1604 +0,0 @@ -import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@edr/ui-common"; -import { - Building2, - ChevronRight, - FolderTree, - Network, - Plus, - RefreshCw, - Save, - Search, - Trash2, - UserPlus, - Users, -} from "lucide-react"; - -import { api } from "@/auth/http"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface OrganizationRecord { - id: string; - key: string; - name: LocaleText; -} - -interface UnitRecord { - id: string; - key: string; - name: LocaleText; - parentUnitId?: string | null; -} - -interface PositionRecord { - id: string; - key: string; - name: LocaleText; - parentPositionId?: string | null; - rank?: number; -} - -interface EmployeeUserRecord { - id: string; - name?: LocaleText; - email?: string; - username?: string; -} - -interface EmployeePositionSummary { - id: string; - position?: { - id: string; - name?: LocaleText; - }; -} - -interface EmployeeRecord { - id: string; - name?: LocaleText; - user?: EmployeeUserRecord; - status?: string; - employeePositions?: EmployeePositionSummary[]; -} - -interface PositionTreeNode extends PositionRecord { - children: PositionTreeNode[]; -} - -interface PositionFormState { - id?: string; - nameEn: string; - nameAm: string; - key: string; - parentPositionId: string; -} - -interface EmployeeInviteFormState { - nameEn: string; - nameAm: string; - email: string; - username: string; - phoneNumber: string; -} - -interface ListResponse { - count?: number; - items?: T[]; - data?: T[]; -} - -const ORG_KEY = "edr_freight"; -const emptyInviteForm: EmployeeInviteFormState = { - nameEn: "", - nameAm: "", - email: "", - username: "", - phoneNumber: "", -}; -const emptyPositionForm: PositionFormState = { - nameEn: "", - nameAm: "", - key: "", - parentPositionId: "", -}; - -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 getLocaleLabel = (value?: LocaleText | null, fallback = "Unnamed") => { - if (!value) { - return fallback; - } - - return 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 toInternalKey = (value: string) => - value - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .replace(/_+/g, "_"); - -const buildTree = (positions: PositionRecord[]) => { - const nodes = new Map(); - - positions.forEach((position) => { - nodes.set(position.id, { ...position, children: [] }); - }); - - const roots: PositionTreeNode[] = []; - - nodes.forEach((node) => { - if (node.parentPositionId) { - const parent = nodes.get(node.parentPositionId); - - if (parent) { - parent.children.push(node); - return; - } - } - - roots.push(node); - }); - - const sortNodes = (items: PositionTreeNode[]) => { - items.sort((left, right) => { - const rankDiff = (left.rank ?? 0) - (right.rank ?? 0); - if (rankDiff !== 0) { - return rankDiff; - } - - return getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ); - }); - - items.forEach((item) => sortNodes(item.children)); - }; - - sortNodes(roots); - - return roots; -}; - -const Field = ({ label, children }: { label: string; children: ReactNode }) => ( - -); - -const ReadOnlyField = ({ label, value }: { label: string; value: string }) => ( -
- {label} -
- {value || "-"} -
-
-); - -const TreeNode = ({ - node, - depth = 0, - selectedPositionId, - onSelect, -}: { - node: PositionTreeNode; - depth?: number; - selectedPositionId: string | null; - onSelect: (node: PositionRecord) => void; -}) => { - const hasChildren = node.children.length > 0; - const isSelected = selectedPositionId === node.id; - - return ( -
  • - - - {hasChildren ? ( -
      - {node.children.map((child) => ( - - ))} -
    - ) : null} -
  • - ); -}; - -const OrgStructurePage = () => { - const navigate = useNavigate(); - const { unitId, section } = useParams(); - const activeSection = section === "employees" ? "employees" : "positions"; - - const [organization, setOrganization] = useState(null); - const [units, setUnits] = useState([]); - const [positions, setPositions] = useState([]); - const [orgEmployees, setOrgEmployees] = useState([]); - const [positionMembers, setPositionMembers] = useState([]); - const [selectedPositionId, setSelectedPositionId] = useState(null); - const [loading, setLoading] = useState(true); - const [membersLoading, setMembersLoading] = useState(false); - const [error, setError] = useState(null); - const [actionError, setActionError] = useState(null); - const [actionSuccess, setActionSuccess] = useState(null); - const [unitSearch, setUnitSearch] = useState(""); - const [isCreateUnitOpen, setIsCreateUnitOpen] = useState(false); - const [isCreatePositionOpen, setIsCreatePositionOpen] = useState(false); - const [isInviteEmployeeOpen, setIsInviteEmployeeOpen] = useState(false); - const [isUnitEditing, setIsUnitEditing] = useState(false); - const [isPositionEditing, setIsPositionEditing] = useState(false); - const [unitNameEn, setUnitNameEn] = useState(""); - const [unitNameAm, setUnitNameAm] = useState(""); - const [unitFormKey, setUnitFormKey] = useState(""); - const [createPositionMode, setCreatePositionMode] = useState<"create-root" | "create-child">( - "create-root", - ); - const [positionForm, setPositionForm] = useState(emptyPositionForm); - const [inviteForm, setInviteForm] = useState(emptyInviteForm); - const [submitting, setSubmitting] = useState(false); - - const selectedUnit = useMemo( - () => units.find((item) => item.id === unitId) ?? null, - [unitId, units], - ); - const tree = useMemo(() => buildTree(positions), [positions]); - const selectedPosition = useMemo( - () => positions.find((item) => item.id === selectedPositionId) ?? null, - [positions, selectedPositionId], - ); - const filteredUnits = useMemo(() => { - const query = unitSearch.trim().toLowerCase(); - if (!query) { - return units; - } - - return units.filter((item) => { - const label = getLocaleLabel(item.name, item.key).toLowerCase(); - return label.includes(query) || item.key.toLowerCase().includes(query); - }); - }, [unitSearch, units]); - const parentOptions = useMemo( - () => positions.filter((item) => item.id !== selectedPositionId), - [positions, selectedPositionId], - ); - const selectedPositionParent = useMemo( - () => positions.find((item) => item.id === selectedPosition?.parentPositionId) ?? null, - [positions, selectedPosition], - ); - - const resetMessages = () => { - setActionError(null); - setActionSuccess(null); - }; - - const resetUnitForm = useCallback(() => { - setUnitNameEn(""); - setUnitNameAm(""); - setUnitFormKey(""); - }, []); - - const syncUnitFormFromSelectedUnit = useCallback((unit: UnitRecord | null) => { - setUnitNameEn(unit?.name.en ?? ""); - setUnitNameAm(unit?.name.am ?? ""); - setUnitFormKey(unit?.key ?? ""); - }, []); - - const syncPositionFormForCreate = useCallback( - (mode: "create-root" | "create-child") => { - setCreatePositionMode(mode); - setPositionForm({ - ...emptyPositionForm, - parentPositionId: mode === "create-child" && selectedPosition ? selectedPosition.id : "", - }); - }, - [selectedPosition], - ); - - const syncPositionFormFromSelectedPosition = useCallback((position: PositionRecord | null) => { - if (!position) { - setPositionForm(emptyPositionForm); - return; - } - - setPositionForm({ - id: position.id, - nameEn: position.name.en ?? "", - nameAm: position.name.am ?? "", - key: position.key, - parentPositionId: position.parentPositionId ?? "", - }); - }, []); - - const loadData = useCallback(async () => { - setLoading(true); - setError(null); - - try { - const organizationsResponse = await api.get>("/organizations"); - const organizations = getItems(organizationsResponse.data); - const currentOrganization = organizations.find((item) => item.key === ORG_KEY); - - if (!currentOrganization) { - throw new Error("EDR Freight organization has not been seeded yet."); - } - - const [unitsResponse, employeesResponse] = await Promise.all([ - api.get>(`/units/list/${currentOrganization.id}`), - api.get>(`/employees/${currentOrganization.id}/by-organization`), - ]); - - const currentUnits = getItems(unitsResponse.data); - let unitPositions: PositionRecord[] = []; - - if (unitId) { - const targetUnit = currentUnits.find((item) => item.id === unitId); - if (targetUnit) { - const positionsResponse = await api.get>( - `/positions/list/${targetUnit.id}`, - ); - unitPositions = getItems(positionsResponse.data); - } - } - - setOrganization(currentOrganization); - setUnits(currentUnits); - setPositions(unitPositions); - setOrgEmployees(getItems(employeesResponse.data)); - } catch (loadError) { - setError( - loadError instanceof Error - ? loadError.message - : "Failed to load organization structure.", - ); - setOrganization(null); - setUnits([]); - setPositions([]); - setOrgEmployees([]); - } finally { - setLoading(false); - } - }, [unitId]); - - const loadPositionMembers = useCallback(async (positionId: string | null) => { - if (!positionId) { - setPositionMembers([]); - return; - } - - setMembersLoading(true); - - try { - const response = await api.get>( - `/positions/current/${positionId}/employees`, - ); - setPositionMembers(getItems(response.data)); - } catch { - setPositionMembers([]); - } finally { - setMembersLoading(false); - } - }, []); - - useEffect(() => { - void loadData(); - }, [loadData]); - - useEffect(() => { - if (!selectedUnit) { - resetUnitForm(); - setSelectedPositionId(null); - setIsUnitEditing(false); - setIsPositionEditing(false); - setIsCreateUnitOpen(false); - setIsCreatePositionOpen(false); - setIsInviteEmployeeOpen(false); - return; - } - - syncUnitFormFromSelectedUnit(selectedUnit); - setIsUnitEditing(false); - setIsPositionEditing(false); - setIsCreatePositionOpen(false); - setIsInviteEmployeeOpen(false); - }, [resetUnitForm, selectedUnit, syncUnitFormFromSelectedUnit]); - - useEffect(() => { - if (!positions.length) { - setSelectedPositionId(null); - setPositionMembers([]); - setIsPositionEditing(false); - return; - } - - if (selectedPositionId && positions.some((item) => item.id === selectedPositionId)) { - return; - } - - setSelectedPositionId(positions[0].id); - }, [positions, selectedPositionId]); - - useEffect(() => { - if (!selectedPosition) { - setIsPositionEditing(false); - setIsInviteEmployeeOpen(false); - setPositionForm(emptyPositionForm); - setPositionMembers([]); - return; - } - - setIsPositionEditing(false); - syncPositionFormFromSelectedPosition(selectedPosition); - void loadPositionMembers(selectedPosition.id); - }, [loadPositionMembers, selectedPosition, syncPositionFormFromSelectedPosition]); - - const goToUnit = (nextUnitId: string, nextSection = activeSection) => { - navigate(`/dashboard/org-structure/units/${nextUnitId}/${nextSection}`); - }; - - const handleRefresh = async () => { - resetMessages(); - await loadData(); - }; - - const handleCreateUnit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!organization) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.post("/units", { - name: { am: unitNameAm.trim(), en: unitNameEn.trim() }, - key: unitFormKey.trim(), - organizationId: organization.id, - }); - - resetUnitForm(); - setActionSuccess("Unit created."); - setIsCreateUnitOpen(false); - await loadData(); - } catch (saveError) { - setActionError(saveError instanceof Error ? saveError.message : "Failed to create unit."); - } finally { - setSubmitting(false); - } - }; - - const handleUpdateUnit = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!organization || !selectedUnit) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.put(`/units/${selectedUnit.id}`, { - name: { am: unitNameAm.trim(), en: unitNameEn.trim() }, - key: unitFormKey.trim(), - organizationId: organization.id, - }); - - setActionSuccess("Unit updated."); - setIsUnitEditing(false); - await loadData(); - } catch (saveError) { - setActionError(saveError instanceof Error ? saveError.message : "Failed to update unit."); - } finally { - setSubmitting(false); - } - }; - - const handleDeleteUnit = async () => { - if (!selectedUnit || !window.confirm(`Delete unit '${getLocaleLabel(selectedUnit.name, selectedUnit.key)}'?`)) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.delete(`/units/${selectedUnit.id}`); - setActionSuccess("Unit deleted."); - setSelectedPositionId(null); - navigate("/dashboard/org-structure"); - await loadData(); - } catch (deleteError) { - setActionError(deleteError instanceof Error ? deleteError.message : "Failed to delete unit."); - } finally { - setSubmitting(false); - } - }; - - const handleSavePosition = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!organization || !selectedUnit) { - setActionError("Select a unit before managing positions."); - return; - } - - setSubmitting(true); - resetMessages(); - - try { - const payload = { - id: positionForm.id, - name: { am: positionForm.nameAm.trim(), en: positionForm.nameEn.trim() }, - key: positionForm.key.trim(), - unitId: selectedUnit.id, - organizationId: organization.id, - parentPositionId: positionForm.parentPositionId || null, - rank: 0, - }; - - if (isPositionEditing && positionForm.id) { - const existing = positions.find((item) => item.id === positionForm.id); - await api.put(`/positions/${positionForm.id}`, payload); - - if ((existing?.parentPositionId ?? "") !== (positionForm.parentPositionId || "")) { - await api.patch("/positions/change-parent", { - positionId: positionForm.id, - newParentId: positionForm.parentPositionId || null, - }); - } - - setActionSuccess("Position updated."); - setIsPositionEditing(false); - } else { - await api.post("/positions", payload); - setActionSuccess("Position created."); - syncPositionFormForCreate(createPositionMode); - setIsCreatePositionOpen(false); - } - - await loadData(); - } catch (saveError) { - setActionError( - saveError instanceof Error ? saveError.message : "Failed to save position.", - ); - } finally { - setSubmitting(false); - } - }; - - const handleDeletePosition = async () => { - if (!selectedPosition) { - return; - } - - if (!window.confirm(`Delete position '${getLocaleLabel(selectedPosition.name, selectedPosition.key)}'?`)) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.delete(`/positions/${selectedPosition.id}`); - setActionSuccess("Position deleted."); - await loadData(); - } catch (deleteError) { - setActionError( - deleteError instanceof Error ? deleteError.message : "Failed to delete position.", - ); - } finally { - setSubmitting(false); - } - }; - - const handleInviteEmployee = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!selectedPosition) { - setActionError("Select a position before inviting an employee."); - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.post("/employee-positions/invite", { - positionId: selectedPosition.id, - username: inviteForm.username.trim(), - phoneNumber: inviteForm.phoneNumber.trim(), - email: inviteForm.email.trim(), - name: { am: inviteForm.nameAm.trim(), en: inviteForm.nameEn.trim() }, - }); - - setInviteForm(emptyInviteForm); - setActionSuccess("Employee invited and assigned to the selected position."); - setIsInviteEmployeeOpen(false); - await Promise.all([loadData(), loadPositionMembers(selectedPosition.id)]); - } catch (inviteError) { - setActionError( - inviteError instanceof Error ? inviteError.message : "Failed to invite employee.", - ); - } finally { - setSubmitting(false); - } - }; - - const positionMemberNames = positionMembers.map((item) => ({ - id: item.id, - name: getLocaleLabel(item.name ?? item.user?.name, item.user?.email ?? item.id), - email: item.user?.email, - })); - - return ( -
    -
    -
    -
    -
    - -
    -
    -

    - Org structure -

    -

    Organization management

    -

    - Manage units, department positions, and employee onboarding for EDR Freight from a - single workspace. -

    -
    -
    - - -
    -
    - - {actionSuccess ? ( -
    - {actionSuccess} -
    - ) : null} - - {actionError ? ( -
    - {actionError} -
    - ) : null} - - {loading ? ( -
    - Loading organization structure... -
    - ) : null} - - {!loading && error ? ( -
    - {error} -
    - ) : null} - - {!loading && !error ? ( -
    - - -
    - {selectedUnit ? ( - <> -
    -
    -
    -

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

    -

    {selectedUnit.key}

    -
    -
    - - -
    -
    - - {isUnitEditing ? ( -
    - - setUnitNameEn(event.target.value)} - required - /> - - - setUnitNameAm(event.target.value)} - required - /> - - -
    - setUnitFormKey(event.target.value)} - required - /> - -
    -
    -
    - - -
    -
    - ) : ( -
    - - - -
    - )} -
    - -
    -
    -
    - - -
    -
    - - {activeSection === "positions" ? ( -
    -
    -
    - - -
    - - {tree.length ? ( -
      - {tree.map((node) => ( - { - resetMessages(); - setSelectedPositionId(position.id); - }} - /> - ))} -
    - ) : ( -
    - No positions yet. Create the first root position for this unit. -
    - )} -
    - -
    -
    -
    -
    -
    Position details
    -
    - {selectedPosition - ? "View a selected position and edit it only when needed." - : "Select a position from the tree to inspect its details."} -
    -
    - {selectedPosition ? ( -
    - - -
    - ) : null} -
    - - {selectedPosition ? ( - isPositionEditing ? ( -
    - - - setPositionForm((current) => ({ ...current, nameEn: event.target.value })) - } - placeholder="Dispatch" - required - /> - - - - setPositionForm((current) => ({ ...current, nameAm: event.target.value })) - } - placeholder="ዲስፓች" - required - /> - - -
    - - setPositionForm((current) => ({ ...current, key: event.target.value })) - } - placeholder="dispatch" - required - /> - -
    -
    - - - -
    - - -
    -
    - ) : ( -
    - - - - -
    - ) - ) : ( -
    - Select a position from the tree to view its details. -
    - )} -
    -
    -
    - ) : ( -
    -
    -
    -
    -
    - -

    - Invite employee -

    -
    - -
    - {selectedPosition ? ( -
    -

    - New users are created during onboarding and assigned directly to the selected position. -

    - -
    - ) : ( -

    - Select a position from the tree first, then onboard employees into it. -

    - )} -
    - -
    -

    - Organization employees -

    - {orgEmployees.length ? ( -
      - {orgEmployees.map((employee) => ( -
    • -
      - {getLocaleLabel(employee.name ?? employee.user?.name, employee.id)} -
      -
      - {employee.user?.email ?? employee.user?.username ?? "No login details"} -
      - {employee.employeePositions?.length ? ( -
      - {employee.employeePositions - .map((item) => getLocaleLabel(item.position?.name, item.id)) - .join(", ")} -
      - ) : null} -
    • - ))} -
    - ) : ( -
    - No employees have been onboarded yet. -
    - )} -
    -
    - - -
    - )} -
    - - ) : ( -
    -
    - -
    -

    - Choose a unit to manage -

    -

    - Use the unit list on the left to manage positions and employees strictly inside one selected unit. -

    -
    - )} -
    -
    - ) : null} - - { - setIsCreateUnitOpen(open); - if (!open) { - resetUnitForm(); - } - }} - > - - - Create unit - - Add a new unit under the EDR Freight organization. - - -
    - - setUnitNameEn(event.target.value)} - placeholder="Operations" - required - /> - - - setUnitNameAm(event.target.value)} - placeholder="ኦፕሬሽንስ" - required - /> - - -
    - setUnitFormKey(event.target.value)} - placeholder="operations" - required - /> - -
    -
    -
    - - -
    -
    -
    -
    - - { - setIsCreatePositionOpen(open); - if (!open) { - syncPositionFormForCreate(createPositionMode); - } - }} - > - - - - {createPositionMode === "create-child" ? "Create child position" : "Create root position"} - - - {createPositionMode === "create-child" - ? "Add a child position under the currently selected position." - : "Add a top-level position for the selected unit."} - - -
    - - - setPositionForm((current) => ({ ...current, nameEn: event.target.value })) - } - placeholder="Dispatch" - required - /> - - - - setPositionForm((current) => ({ ...current, nameAm: event.target.value })) - } - placeholder="ዲስፓች" - required - /> - - -
    - - setPositionForm((current) => ({ ...current, key: event.target.value })) - } - placeholder="dispatch" - required - /> - -
    -
    - - - -
    - - -
    -
    -
    -
    - - { - setIsInviteEmployeeOpen(open); - if (!open) { - setInviteForm(emptyInviteForm); - } - }} - > - - - Invite employee - - Create a new employee account and assign it directly to the selected position. - - -
    - -
    - {selectedPosition ? getLocaleLabel(selectedPosition.name, selectedPosition.key) : "No position selected"} -
    -
    - - - setInviteForm((current) => ({ ...current, nameEn: event.target.value })) - } - required - /> - - - - setInviteForm((current) => ({ ...current, nameAm: event.target.value })) - } - required - /> - - - - setInviteForm((current) => ({ ...current, email: event.target.value })) - } - required - /> - - - - setInviteForm((current) => ({ ...current, username: event.target.value })) - } - required - /> - - - - setInviteForm((current) => ({ ...current, phoneNumber: event.target.value })) - } - required - /> - -
    - - -
    -
    -
    -
    -
    - ); -}; - -export default OrgStructurePage; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/DepartmentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/DepartmentsPage.tsx deleted file mode 100644 index 4d2e50920..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/DepartmentsPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import FeaturePlaceholder from "@/components/FeaturePlaceholder"; - -const DepartmentsPage = () => { - return ( - - ); -}; - -export default DepartmentsPage; 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 deleted file mode 100644 index eecca7a3e..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/EmployeesPage.tsx +++ /dev/null @@ -1,321 +0,0 @@ -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>( - `/backoffice/organizations/${organizationId}/employees`, - { - 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; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PermissionsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PermissionsPage.tsx deleted file mode 100644 index 10c5aba25..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PermissionsPage.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { useEffect, useState } from "react"; -import { isAxiosError } from "axios"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@edr/ui-common"; - -import { api } from "@/auth/http"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface PermissionRecord { - id: string; - key: string; - name?: LocaleText; - applicationId?: string | null; -} - -interface ApplicationRecord { - id: string; - key: string; - name?: LocaleText; -} - -interface ListResponse { - count?: number; - items?: T[]; - data?: T[]; -} - -const PAGE_SIZE = 2000; -const ALL_APPLICATIONS_VALUE = "all"; -const SYSTEM_APPLICATION_VALUE = "system"; - -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 sortPermissions = (items: PermissionRecord[]) => - [...items].sort((left, right) => - getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ), - ); - -const PermissionsPage = () => { - const [permissions, setPermissions] = useState([]); - const [applications, setApplications] = useState([]); - const [selectedApplication, setSelectedApplication] = useState(ALL_APPLICATIONS_VALUE); - const [count, setCount] = useState(0); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); - - useEffect(() => { - let isMounted = true; - - const loadPageData = async () => { - setLoading(true); - setErrorMessage(null); - - try { - const [permissionsResponse, applicationsResponse] = await Promise.all([ - api.get>("/permissions", { - params: { - skip: 0, - take: PAGE_SIZE, - }, - }), - api.get>("/applications"), - ]); - - if (!isMounted) { - return; - } - - const items = sortPermissions(getItems(permissionsResponse.data)); - const applicationItems = [...getItems(applicationsResponse.data)].sort((left, right) => - getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ), - ); - - setPermissions(items); - setApplications(applicationItems); - setCount(permissionsResponse.data.count ?? items.length); - } catch (error) { - if (!isMounted) { - return; - } - - setErrorMessage( - isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load permissions." - : "Unable to load permissions.", - ); - } finally { - if (isMounted) { - setLoading(false); - } - } - }; - - void loadPageData(); - - return () => { - isMounted = false; - }; - }, []); - - const hasMore = count > permissions.length; - const filteredPermissions = permissions.filter((permission) => { - if (selectedApplication === ALL_APPLICATIONS_VALUE) { - return true; - } - - if (selectedApplication === SYSTEM_APPLICATION_VALUE) { - return !permission.applicationId; - } - - return permission.applicationId === selectedApplication; - }); - - const handleLoadMore = async () => { - setLoadingMore(true); - setErrorMessage(null); - - try { - const response = await api.get>("/permissions", { - params: { - skip: permissions.length, - take: PAGE_SIZE, - }, - }); - - const nextItems = sortPermissions(getItems(response.data)); - - setPermissions((current) => [...current, ...nextItems]); - setCount(response.data.count ?? permissions.length + nextItems.length); - } catch (error) { - setErrorMessage( - isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load more permissions." - : "Unable to load more permissions.", - ); - } finally { - setLoadingMore(false); - } - }; - - return ( -
    -
    -
    -

    - User Management -

    -
    -
    -

    Permissions

    -

    - Browse the full IAM permission catalog for the freight backoffice environment. -

    -
    -
    - {filteredPermissions.length} permissions -
    -
    -
    - -
    -
    -

    - Application -

    - -
    -

    - Filter the IAM permission catalog by application, or view the shared system permissions that do not belong to any application. -

    -
    - - {loading ? ( -
    - Loading permissions... -
    - ) : errorMessage ? ( -
    - {errorMessage} -
    - ) : filteredPermissions.length ? ( -
    - {filteredPermissions.map((permission) => ( -
    -
    -

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

    -

    - {permission.key} -

    -
    -
    - ))} -
    - ) : ( -
    - No permissions match the selected application. -
    - )} - - {hasMore ? ( -
    -

    - Showing {permissions.length} of {count} permissions. -

    - -
    - ) : null} -
    -
    - ); -}; - -export default PermissionsPage; 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 deleted file mode 100644 index 55a4df00e..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx +++ /dev/null @@ -1,1309 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { isAxiosError } from "axios"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@edr/ui-common"; -import { CopyPlus, Plus, RefreshCw } from "lucide-react"; - -import { api } from "@/auth/http"; -import { useAuth } from "@/auth/useAuth"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface OrganizationRecord { - id: string; - key: string; - name?: LocaleText; -} - -interface UnitRecord { - id: string; - key: string; - name?: LocaleText; -} - -interface PositionTypeRecord { - id: string; - key: string; - name?: LocaleText; - isSystem?: boolean; - unitId?: string | null; - createdAt?: string; - updatedAt?: string | null; -} - -interface PermissionRecord { - id: string; - key: string; - name?: LocaleText; - applicationId?: string | null; -} - -interface ListResponse { - count?: number; - items?: T[]; - data?: T[]; -} - -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; - -const getItems = (payload: ListResponse | T[] | undefined | null) => { - if (!payload) { - return [] as T[]; - } - - if (Array.isArray(payload)) { - return payload; - } - - return payload.items ?? payload.data ?? []; -}; - -const formatDate = (value?: string | null) => { - if (!value) { - return "-"; - } - - const date = new Date(value); - - if (Number.isNaN(date.getTime())) { - return "-"; - } - - return new Intl.DateTimeFormat("en", { - year: "numeric", - month: "short", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }).format(date); -}; - -const PositionTypesPage = () => { - const { user } = useAuth(); - const [organizations, setOrganizations] = useState([]); - const [units, setUnits] = useState([]); - const [positionTypes, setPositionTypes] = useState([]); - const [selectedOrgId, setSelectedOrgId] = useState(""); - const [selectedUnitId, setSelectedUnitId] = useState(""); - 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 [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( - () => - new Set( - (user?.employee ?? []) - .map((employee) => employee.organizationId) - .filter((organizationId): organizationId is string => - Boolean(organizationId), - ), - ), - [user?.employee], - ); - - const visibleOrganizations = useMemo(() => { - if (isSuperAdmin) { - return organizations; - } - - return organizations.filter((organization) => - allowedOrgIds.has(organization.id), - ); - }, [allowedOrgIds, isSuperAdmin, organizations]); - - 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; - - const loadOrganizations = async () => { - setLoadingOrganizations(true); - setErrorMessage(null); - - try { - const response = - await api.get>("/organizations"); - - if (!isMounted) { - return; - } - - setOrganizations(getItems(response.data)); - } catch (error) { - if (!isMounted) { - return; - } - - setErrorMessage( - isAxiosError(error) - ? (error.response?.data?.message ?? "Unable to load organizations.") - : "Unable to load organizations.", - ); - } finally { - if (isMounted) { - setLoadingOrganizations(false); - } - } - }; - - void loadOrganizations(); - - return () => { - isMounted = false; - }; - }, []); - - 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(""); - setSelectedUnitId(""); - setUnits([]); - setPositionTypes([]); - return; - } - - if ( - selectedOrgId && - visibleOrganizations.some( - (organization) => organization.id === selectedOrgId, - ) - ) { - return; - } - - setSelectedOrgId(visibleOrganizations[0]?.id ?? ""); - }, [selectedOrgId, visibleOrganizations]); - - useEffect(() => { - if (!selectedOrgId) { - setUnits([]); - setSelectedUnitId(""); - setPositionTypes([]); - return; - } - - let isMounted = true; - - const loadUnits = async () => { - setLoadingUnits(true); - setErrorMessage(null); - setSelectedUnitId(""); - setPositionTypes([]); - - try { - const response = await api.get>( - `/units/list/${selectedOrgId}`, - ); - const items = getItems(response.data); - - if (!isMounted) { - return; - } - - setUnits(items); - setSelectedUnitId(items[0]?.id ?? ""); - } catch (error) { - if (!isMounted) { - return; - } - - setUnits([]); - setErrorMessage( - isAxiosError(error) - ? (error.response?.data?.message ?? "Unable to load units.") - : "Unable to load units.", - ); - } finally { - if (isMounted) { - setLoadingUnits(false); - } - } - }; - - void loadUnits(); - - return () => { - isMounted = false; - }; - }, [selectedOrgId]); - - useEffect(() => { - if (!selectedUnitId) { - setPositionTypes([]); - return; - } - - let isMounted = true; - - const loadItems = async () => { - setLoadingPositionTypes(true); - setErrorMessage(null); - - try { - const items = await loadPositionTypes(selectedUnitId); - - if (!isMounted) { - return; - } - - setPositionTypes(items); - } catch (error) { - if (!isMounted) { - return; - } - - setPositionTypes([]); - setErrorMessage( - isAxiosError(error) - ? (error.response?.data?.message ?? - "Unable to load position types.") - : "Unable to load position types.", - ); - } finally { - if (isMounted) { - setLoadingPositionTypes(false); - } - } - }; - - void loadItems(); - - return () => { - isMounted = false; - }; - }, [selectedUnitId]); - - useEffect(() => { - if (!selectedPositionType) { - 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 () => { - setPermissionsLoading(true); - setPermissionsError(null); - - try { - const items = await loadPermissionsForPositionType( - selectedPositionType.id, - ); - - if (!isMounted) { - return; - } - - setSelectedPermissionIds(items.map((permission) => permission.id)); - } catch (error) { - if (!isMounted) { - return; - } - - setPermissionsError( - isAxiosError(error) - ? (error.response?.data?.message ?? - "Unable to load position type permissions.") - : "Unable to load position type permissions.", - ); - } finally { - if (isMounted) { - setPermissionsLoading(false); - } - } - }; - - void loadPermissions(); - - return () => { - isMounted = false; - }; - }, [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), - ]); - - 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); - } - }; - - return ( -
    -
    -
    -

    - User Management -

    -
    -
    -

    - Position Type -

    -

    - Browse position types for a selected organization unit, add new - ones, and manage their permissions. -

    -
    -
    -
    - {positionTypes.length} position types -
    - - -
    -
    -
    - -
    -
    -

    - Organization -

    - -
    - -
    -

    - Unit -

    - -
    - -

    - {selectedOrganization && selectedUnit - ? `Showing position types for ${getLocaleLabel(selectedUnit.name, selectedUnit.key)} in ${getLocaleLabel(selectedOrganization.name, selectedOrganization.key)}.` - : "Select an organization and unit to load position types."} -

    -
    - - {errorMessage ? ( -
    - {errorMessage} -
    - ) : loadingOrganizations || loadingUnits || loadingPositionTypes ? ( -
    - Loading position types... -
    - ) : !visibleOrganizations.length ? ( -
    - No organization scope is available for this account. -
    - ) : !selectedOrgId ? ( -
    - Select an organization to continue. -
    - ) : !units.length ? ( -
    - No units are available for the selected organization. -
    - ) : !selectedUnitId ? ( -
    - Select a unit to load position types. -
    - ) : positionTypes.length ? ( -
    - - - - - - - - - - - - - {positionTypes.map((positionType) => ( - setSelectedPositionType(positionType)} - className="cursor-pointer border-t border-border bg-background transition hover:bg-accent/20" - > - - - - - - - - ))} - -
    NameKeyScopeUnit IDCreated AtUpdated At
    - {getLocaleLabel(positionType.name, positionType.key)} - - {positionType.key} - - {positionType.isSystem ? "System" : "Unit"} - - {positionType.unitId ?? "-"} - - {formatDate(positionType.createdAt)} - - {formatDate(positionType.updatedAt)} -
    -
    - ) : ( -
    - No position types were found for the selected unit. -
    - )} -
    - - !open && setSelectedPositionType(null)} - > - - - - {selectedPositionType - ? getLocaleLabel( - selectedPositionType.name, - selectedPositionType.key, - ) - : "Position type details"} - - - {selectedPositionType - ? `Review and update the permission set assigned to ${getLocaleLabel(selectedPositionType.name, selectedPositionType.key)}.` - : undefined} - - - - {selectedPositionType ? ( -
    -
    -
    -

    - Position type -

    -

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

    -
    -
    -

    - Key -

    -

    - {selectedPositionType.key} -

    -
    -
    -

    - Scope -

    -

    - {selectedPositionType.isSystem ? "System" : "Unit"} -

    -
    -
    -

    - Unit ID -

    -

    - {selectedPositionType.unitId ?? "-"} -

    -
    -
    - -
    -
    - - - - {selectedPositionType.isSystem ? ( -

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

    - ) : null} -
    - -
    -

    - Permissions -

    -
    - {selectedPermissionIds.length} permissions selected -
    -
    - - {permissionsLoading ? ( -
    - Loading position type permissions... -
    - ) : permissionsError ? ( -
    - {permissionsError} -
    - ) : ( -
    - - 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. -
    - )} - -
    - - -
    -
    - )} -
    -
    - ) : 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} - -
    - - -
    -
    -
    -
    -
    - ); -}; - -export default PositionTypesPage; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/RolesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/RolesPage.tsx deleted file mode 100644 index 862df0119..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/RolesPage.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { useEffect, useState } from "react"; -import { isAxiosError } from "axios"; -import { Shield } from "lucide-react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@edr/ui-common"; - -import { api } from "@/auth/http"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface RoleRecord { - id: string; - key: string; - name?: LocaleText; -} - -interface PermissionRecord { - id: string; - key: string; - name?: LocaleText; -} - -interface ListResponse { - 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 RolesPage = () => { - const [roles, setRoles] = useState([]); - const [loading, setLoading] = useState(true); - const [errorMessage, setErrorMessage] = useState(null); - const [selectedRole, setSelectedRole] = useState(null); - const [rolePermissions, setRolePermissions] = useState([]); - const [rolePermissionsLoading, setRolePermissionsLoading] = useState(false); - const [rolePermissionsError, setRolePermissionsError] = useState(null); - - useEffect(() => { - let isMounted = true; - - const loadRoles = async () => { - setLoading(true); - setErrorMessage(null); - - try { - const response = await api.get>("/roles"); - - if (!isMounted) { - return; - } - - setRoles( - getItems(response.data).sort((left, right) => - getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ), - ), - ); - } catch (error) { - if (!isMounted) { - return; - } - - setErrorMessage( - isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load roles." - : "Unable to load roles.", - ); - } finally { - if (isMounted) { - setLoading(false); - } - } - }; - - void loadRoles(); - - return () => { - isMounted = false; - }; - }, []); - - useEffect(() => { - if (!selectedRole) { - setRolePermissions([]); - setRolePermissionsError(null); - setRolePermissionsLoading(false); - return; - } - - let isMounted = true; - - const loadRolePermissions = async () => { - setRolePermissionsLoading(true); - setRolePermissionsError(null); - - try { - const response = await api.get>( - `/role-permissions/given-first/${selectedRole.id}`, - ); - - if (!isMounted) { - return; - } - - setRolePermissions( - getItems(response.data).sort((left, right) => - getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ), - ), - ); - } catch (error) { - if (!isMounted) { - return; - } - - setRolePermissionsError( - isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load role details." - : "Unable to load role details.", - ); - } finally { - if (isMounted) { - setRolePermissionsLoading(false); - } - } - }; - - void loadRolePermissions(); - - return () => { - isMounted = false; - }; - }, [selectedRole]); - - return ( -
    -
    -
    -

    - User Management -

    -
    -
    -

    Roles

    -

    - Browse freight backoffice roles and their internal keys in a simple grid view. -

    -
    -
    - {roles.length} roles -
    -
    -
    - - {loading ? ( -
    - Loading roles... -
    - ) : errorMessage ? ( -
    - {errorMessage} -
    - ) : roles.length ? ( -
    - {roles.map((role) => ( - - ))} -
    - ) : ( -
    - No roles available. -
    - )} -
    - - !open && setSelectedRole(null)}> - - - {selectedRole ? getLocaleLabel(selectedRole.name, selectedRole.key) : "Role details"} - - {selectedRole - ? `Review the permission set assigned to ${getLocaleLabel(selectedRole.name, selectedRole.key)}.` - : undefined} - - - - {selectedRole ? ( -
    -
    -
    -

    - Role name -

    -

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

    -
    -
    -

    - Role key -

    -

    {selectedRole.key}

    -
    -
    - -
    -
    -

    Permissions

    -
    - {rolePermissions.length} permissions -
    -
    - - {rolePermissionsLoading ? ( -
    - Loading role details... -
    - ) : rolePermissionsError ? ( -
    - {rolePermissionsError} -
    - ) : rolePermissions.length ? ( -
    - {rolePermissions.map((permission) => ( -
    -

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

    -

    - {permission.key} -

    -
    - ))} -
    - ) : ( -
    - No permissions are assigned to this role. -
    - )} -
    -
    - ) : null} -
    -
    -
    - ); -}; - -export default RolesPage; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx deleted file mode 100644 index d473bdc90..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementHostPage.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { createRoot, type Root } from 'react-dom/client'; -import { - UserManagementApp, - type UserManagementRuntimeOptions, - type UserManagementSessionSeed, -} from '@tria-plc/iamui'; -import { iamConfig } from './iamConfig'; - -function readCookieValue(name: string): string | null { - const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1'); - const match = document.cookie.match( - new RegExp(`(?:^|; )${escaped}=([^;]*)`), - ); - - return match ? decodeURIComponent(match[1]) : null; -} - -function readInitialSession(): UserManagementSessionSeed | null { - const token = - localStorage.getItem('fhc-backoffice-auth-token') ?? - readCookieValue('auth-token'); - - if (!token) { - return null; - } - - const refreshToken = - localStorage.getItem('fhc-backoffice-auth-refresh-token') ?? - readCookieValue('refresh-token') ?? - undefined; - - return { - token, - refreshToken, - rememberMe: true, - }; -} - -export default function UserManagementHostPage() { - const mountRef = useRef(null); - const rootRef = useRef(null); - const unmountTimerRef = useRef(null); - - useEffect(() => { - const mountNode = mountRef.current; - - if (!mountNode) { - return; - } - - if (unmountTimerRef.current !== null) { - window.clearTimeout(unmountTimerRef.current); - unmountTimerRef.current = null; - } - - if (!rootRef.current) { - rootRef.current = createRoot(mountNode); - } - - const apiBaseUrl = import.meta.env.VITE_BASE_API_URL.replace(/\/+$/, ''); - const runtime: UserManagementRuntimeOptions = { - basename: '/um', - apiBaseUrl, - apiUrl: `${apiBaseUrl}/api`, - recordApiUrl: `${apiBaseUrl}/api`, - chronicleUrl: `${apiBaseUrl}/api`, - auditApiUrl: `${apiBaseUrl}/api`, - }; - - rootRef.current.render( - , - ); - - return () => { - unmountTimerRef.current = window.setTimeout(() => { - rootRef.current?.unmount(); - rootRef.current = null; - unmountTimerRef.current = null; - }, 0); - }; - }, []); - - return
    ; -} 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 deleted file mode 100644 index 317d50842..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UserManagementPage.tsx +++ /dev/null @@ -1,2311 +0,0 @@ -import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; -import { isAxiosError } from "axios"; -import { - Badge, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@edr/ui-common"; -import { - Building2, - ChevronRight, - FolderTree, - MoreHorizontal, - Network, - RefreshCw, - Search, - ShieldCheck, - UserCheck, - UserMinus, - Users, -} from "lucide-react"; - -import { api } from "@/auth/http"; -import { useAuth } from "@/auth/useAuth"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface OrganizationRecord { - id: string; - key: string; - name: LocaleText; -} - -interface UnitRecord { - id: string; - key: string; - name: LocaleText; - parentUnitId?: string | null; -} - -interface PositionRecord { - id: string; - key: string; - name: LocaleText; - parentPositionId?: string | null; - positionTypeId?: string | null; - rank?: number; -} - -interface PositionTypeRecord { - id: string; - key: string; - name: LocaleText; -} - -interface EmployeeUserRecord { - id: string; - name?: LocaleText; - email?: string; - phoneNumber?: string; - username?: string; -} - -interface EmployeePositionSummary { - id: string; - position?: { - id: string; - name?: LocaleText; - }; -} - -interface EmployeeRecord { - id: string; - name?: LocaleText; - user?: EmployeeUserRecord; - status?: string; - employeePositions?: EmployeePositionSummary[]; -} - -interface RoleRecord { - id: string; - key: string; - name: LocaleText; -} - -interface PositionTreeNode extends PositionRecord { - children: PositionTreeNode[]; -} - -interface OrganizationConfigurationRecord { - id: string; - organizationId: string; - maximumNumberOfUnits?: number | null; - canCreateBranchByItself?: boolean; - canStartReceivingRecord?: boolean; -} - -interface PositionFormState { - id?: string; - nameEn: string; - nameAm: string; - key: string; - parentPositionId: string; - positionTypeId: string; -} - -interface UnitFormState { - nameEn: string; - nameAm: string; - key: string; -} - -interface EmployeeInviteFormState { - nameEn: string; - nameAm: string; - email: string; - username: string; - phoneNumber: string; -} - -interface ListResponse { - count?: number; - items?: T[]; - data?: T[]; -} - -const RESERVED_ROLE_KEYS = new Set(["super_admin", "organization_admin", "unit_admin"]); - -const emptyInviteForm: EmployeeInviteFormState = { - nameEn: "", - nameAm: "", - email: "", - username: "", - phoneNumber: "", -}; - -const emptyUnitForm: UnitFormState = { - nameEn: "", - nameAm: "", - key: "", -}; - -const emptyPositionForm: PositionFormState = { - nameEn: "", - nameAm: "", - key: "", - parentPositionId: "", - positionTypeId: "", -}; - -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 getLocaleLabel = (value?: LocaleText | null, fallback = "Unnamed") => { - if (!value) { - return fallback; - } - - return 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 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() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .replace(/_+/g, "_"); - -const buildTree = (positions: PositionRecord[]) => { - const nodes = new Map(); - - positions.forEach((position) => { - nodes.set(position.id, { ...position, children: [] }); - }); - - const roots: PositionTreeNode[] = []; - - nodes.forEach((node) => { - if (node.parentPositionId) { - const parent = nodes.get(node.parentPositionId); - - if (parent) { - parent.children.push(node); - return; - } - } - - roots.push(node); - }); - - const sortNodes = (items: PositionTreeNode[]) => { - items.sort((left, right) => { - const rankDiff = (left.rank ?? 0) - (right.rank ?? 0); - if (rankDiff !== 0) { - return rankDiff; - } - - return getLocaleLabel(left.name, left.key).localeCompare( - getLocaleLabel(right.name, right.key), - ); - }); - - items.forEach((item) => sortNodes(item.children)); - }; - - sortNodes(roots); - - return roots; -}; - -const getErrorMessage = (error: unknown, fallback: string) => { - if (isAxiosError(error)) { - const message = error.response?.data?.message; - if (typeof message === "string") { - return message; - } - if (Array.isArray(message) && typeof message[0] === "string") { - return message[0]; - } - } - - return error instanceof Error ? error.message : fallback; -}; - -const Field = ({ label, children }: { label: string; children: ReactNode }) => ( - -); - -const ManagementDialog = ({ - open, - title, - description, - onOpenChange, - children, -}: { - open: boolean; - title: string; - description?: string; - onOpenChange: (open: boolean) => void; - children: ReactNode; -}) => ( - - - - {title} - {description ? {description} : null} - - {children} - - -); - -const TreeNode = ({ - node, - depth = 0, - selectedPositionId, - onSelect, - expandedDepartmentIds, - onToggleExpand, - onInviteUser, - onAssignUser, - onAddSubDepartment, - onEdit, - onDelete, - disabled, -}: { - node: PositionTreeNode; - depth?: number; - selectedPositionId: string | null; - onSelect: (node: PositionRecord) => void; - expandedDepartmentIds: Set; - onToggleExpand: (nodeId: string) => void; - onInviteUser: (node: PositionRecord) => void; - onAssignUser: (node: PositionRecord) => void; - onAddSubDepartment: (node: PositionRecord) => void; - onEdit: (node: PositionRecord) => void; - onDelete: (node: PositionRecord) => void; - disabled?: boolean; -}) => { - const hasChildren = node.children.length > 0; - const isSelected = selectedPositionId === node.id; - const isExpanded = expandedDepartmentIds.has(node.id); - - return ( -
  • -
    - {hasChildren ? ( - - ) : ( -
    - - {hasChildren && isExpanded ? ( -
      - {node.children.map((child) => ( - - ))} -
    - ) : null} -
  • - ); -}; - -const UserManagementPage = () => { - const { user } = useAuth(); - const [organizations, setOrganizations] = useState([]); - const [units, setUnits] = useState([]); - const [positions, setPositions] = useState([]); - const [positionTypes, setPositionTypes] = useState([]); - const [orgEmployees, setOrgEmployees] = useState([]); - const [positionMembers, setPositionMembers] = useState([]); - const [selectedOrgConfiguration, setSelectedOrgConfiguration] = - useState(null); - const [expandedDepartmentIds, setExpandedDepartmentIds] = useState>(new Set()); - const [orgAdminUserIds, setOrgAdminUserIds] = useState>(new Set()); - const [unitAdminUserIds, setUnitAdminUserIds] = useState>(new Set()); - const [selectedOrgId, setSelectedOrgId] = useState(null); - const [selectedUnitId, setSelectedUnitId] = useState(null); - const [selectedPositionId, setSelectedPositionId] = useState(null); - const [loading, setLoading] = useState(true); - const [membersLoading, setMembersLoading] = useState(false); - const [orgEmployeesLoading, setOrgEmployeesLoading] = useState(false); - const [positionTypesLoading, setPositionTypesLoading] = useState(false); - const [actionError, setActionError] = useState(null); - const [actionSuccess, setActionSuccess] = useState(null); - const [loadError, setLoadError] = useState(null); - const [positionTypesError, setPositionTypesError] = useState(null); - const [submitting, setSubmitting] = useState(false); - const [unitSearch, setUnitSearch] = useState(""); - const [positionSearch, setPositionSearch] = useState(""); - const [employeeSearch, setEmployeeSearch] = useState(""); - const [assignSearch, setAssignSearch] = useState(""); - const [unitForm, setUnitForm] = useState(emptyUnitForm); - const [positionForm, setPositionForm] = useState(emptyPositionForm); - const [inviteForm, setInviteForm] = useState(emptyInviteForm); - const [roleIds, setRoleIds] = useState([]); - const [availableRoles, setAvailableRoles] = useState([]); - const [rolesLoading, setRolesLoading] = useState(false); - const [isCreateUnitOpen, setIsCreateUnitOpen] = useState(false); - const [isEditUnitOpen, setIsEditUnitOpen] = useState(false); - const [isCreatePositionOpen, setIsCreatePositionOpen] = useState(false); - const [isEditPositionOpen, setIsEditPositionOpen] = useState(false); - const [isInviteEmployeeOpen, setIsInviteEmployeeOpen] = useState(false); - const [isAssignEmployeeOpen, setIsAssignEmployeeOpen] = useState(false); - const [isManageRolesOpen, setIsManageRolesOpen] = useState(false); - const [selectedRoleUser, setSelectedRoleUser] = useState(null); - const [createPositionMode, setCreatePositionMode] = useState<"create-root" | "create-child">( - "create-root", - ); - - const isSuperAdmin = Boolean(user?.roles?.some((role) => role.key === "super_admin")); - const allowedOrgIds = useMemo( - () => new Set((user?.employee ?? []).map((employee) => employee.organizationId).filter(Boolean)), - [user?.employee], - ); - - // Both paths are already scoped server-side: super admins get every org from - // /organizations, org/unit admins get only theirs from my-admin-organizations. - // So no client-side re-filtering (allowedOrgIds, from employee.organizationId, - // wouldn't include orgs administered without an employee record there). - const visibleOrganizations = organizations; - void allowedOrgIds; - - const selectedOrganization = useMemo( - () => visibleOrganizations.find((item) => item.id === selectedOrgId) ?? null, - [selectedOrgId, visibleOrganizations], - ); - const selectedUnit = useMemo( - () => units.find((item) => item.id === selectedUnitId) ?? null, - [selectedUnitId, units], - ); - const selectedPosition = useMemo( - () => positions.find((item) => item.id === selectedPositionId) ?? null, - [positions, selectedPositionId], - ); - const positionTree = useMemo(() => buildTree(positions), [positions]); - const parentOptions = useMemo( - () => positions.filter((item) => item.id !== selectedPositionId), - [positions, selectedPositionId], - ); - const unitsAllowedLabel = - selectedOrgConfiguration?.maximumNumberOfUnits == null - ? "∞" - : `${selectedOrgConfiguration.maximumNumberOfUnits}`; - - const filteredUnits = useMemo(() => { - const query = unitSearch.trim().toLowerCase(); - if (!query) { - return units; - } - - return units.filter((item) => { - const label = getLocaleLabel(item.name, item.key).toLowerCase(); - return label.includes(query) || item.key.toLowerCase().includes(query); - }); - }, [unitSearch, units]); - - const filteredPositionTree = useMemo(() => { - const query = positionSearch.trim().toLowerCase(); - if (!query) { - return positionTree; - } - - const filterNode = (node: PositionTreeNode): PositionTreeNode | null => { - const label = `${getLocaleLabel(node.name, node.key)} ${node.key}`.toLowerCase(); - const matchingChildren = node.children - .map((child) => filterNode(child)) - .filter(Boolean) as PositionTreeNode[]; - - if (label.includes(query) || matchingChildren.length) { - return { ...node, children: matchingChildren }; - } - - return null; - }; - - return positionTree.map((node) => filterNode(node)).filter(Boolean) as PositionTreeNode[]; - }, [positionSearch, positionTree]); - - const filteredPositionMembers = useMemo(() => { - const query = employeeSearch.trim().toLowerCase(); - if (!query) { - return positionMembers; - } - - return positionMembers.filter((employee) => { - const name = getLocaleLabel(employee.name ?? employee.user?.name, employee.user?.email ?? employee.id).toLowerCase(); - const email = employee.user?.email?.toLowerCase() ?? ""; - const username = employee.user?.username?.toLowerCase() ?? ""; - return name.includes(query) || email.includes(query) || username.includes(query); - }); - }, [employeeSearch, positionMembers]); - - const assignableEmployees = useMemo(() => { - const query = assignSearch.trim().toLowerCase(); - return orgEmployees.filter((employee) => { - const label = getLocaleLabel(employee.name ?? employee.user?.name, employee.user?.email ?? employee.id).toLowerCase(); - const email = employee.user?.email?.toLowerCase() ?? ""; - if (!query) { - return true; - } - return label.includes(query) || email.includes(query); - }); - }, [assignSearch, orgEmployees]); - - const resetMessages = () => { - setActionError(null); - setActionSuccess(null); - }; - - const loadOrganizations = useCallback(async () => { - setLoading(true); - setLoadError(null); - - try { - // Super admins may list every organization (needs can:find_all:organization). - // Org/unit admins are scoped to the orgs they administer via my-admin-organizations. - const endpoint = isSuperAdmin - ? "/organizations" - : "/organizations/with-admin-flag"; - const response = await api.get>(endpoint); - const nextOrganizations = getItems(response.data); - setOrganizations(nextOrganizations); - } catch (error) { - setLoadError(getErrorMessage(error, "Failed to load organizations.")); - } finally { - setLoading(false); - } - }, [isSuperAdmin]); - - const loadOrgEmployees = useCallback(async (organizationId: string) => { - setOrgEmployeesLoading(true); - - try { - const response = await api.get>( - `/backoffice/organizations/${organizationId}/employees`, - ); - setOrgEmployees(mergeEmployeesByUser(getItems(response.data))); - } catch { - setOrgEmployees([]); - } finally { - setOrgEmployeesLoading(false); - } - }, []); - - const loadOrgAdmins = useCallback(async (organizationId: string) => { - try { - const response = await api.get>( - `/organizations/org-admins/${organizationId}`, - ); - setOrgAdminUserIds( - new Set(getItems(response.data).map((employee) => employee.user?.id).filter(Boolean) as string[]), - ); - } catch { - setOrgAdminUserIds(new Set()); - } - }, []); - - const loadUnitAdmins = useCallback(async (unitId: string | null) => { - if (!unitId) { - setUnitAdminUserIds(new Set()); - return; - } - - try { - const response = await api.get>(`/units/unit-admins/${unitId}`); - setUnitAdminUserIds( - new Set(getItems(response.data).map((employee) => employee.user?.id).filter(Boolean) as string[]), - ); - } catch { - setUnitAdminUserIds(new Set()); - } - }, []); - - const loadUnits = useCallback(async (organizationId: string) => { - try { - const response = await api.get>(`/units/list/${organizationId}`); - setUnits(getItems(response.data)); - } catch (error) { - setLoadError(getErrorMessage(error, "Failed to load units.")); - setUnits([]); - } - }, []); - - const loadOrganizationConfiguration = useCallback(async (organizationId: string) => { - try { - const response = await api.get>( - `/organization-configurations/list/${organizationId}`, - ); - setSelectedOrgConfiguration(getItems(response.data)[0] ?? null); - } catch { - setSelectedOrgConfiguration(null); - } - }, []); - - const loadPositions = useCallback(async (unitId: string) => { - try { - const response = await api.get>(`/positions/list/${unitId}`); - setPositions(getItems(response.data)); - } catch (error) { - setLoadError(getErrorMessage(error, "Failed to load positions.")); - setPositions([]); - } - }, []); - - const loadPositionTypes = useCallback(async (unitId: string) => { - setPositionTypesLoading(true); - setPositionTypesError(null); - - try { - const response = await api.get>( - `/position-types/list-with-commons/${unitId}?take=1000&skip=0&orderBy=createdAt:Desc`, - ); - setPositionTypes(getItems(response.data)); - } catch (error) { - const message = getErrorMessage(error, "Failed to load position types."); - setPositionTypes([]); - setPositionTypesError(message); - throw error; - } finally { - setPositionTypesLoading(false); - } - }, []); - - const loadPositionMembers = useCallback(async (positionId: string | null) => { - if (!positionId) { - setPositionMembers([]); - return; - } - - setMembersLoading(true); - - try { - const response = await api.get>( - `/positions/current/${positionId}/employees`, - ); - setPositionMembers(getItems(response.data)); - } catch { - setPositionMembers([]); - } finally { - setMembersLoading(false); - } - }, []); - - const refreshOrganizations = useCallback(async () => { - await loadOrganizations(); - }, [loadOrganizations]); - - const refreshSelectedOrg = useCallback(async () => { - if (!selectedOrgId) { - setUnits([]); - setPositions([]); - setPositionMembers([]); - setOrgEmployees([]); - setSelectedOrgConfiguration(null); - return; - } - - await Promise.all([ - loadUnits(selectedOrgId), - loadOrgEmployees(selectedOrgId), - loadOrgAdmins(selectedOrgId), - loadOrganizationConfiguration(selectedOrgId), - ]); - }, [loadOrgAdmins, loadOrgEmployees, loadOrganizationConfiguration, loadUnits, selectedOrgId]); - - const refreshSelectedUnit = useCallback(async () => { - if (!selectedUnitId) { - setPositions([]); - setPositionTypes([]); - setPositionTypesError(null); - setPositionMembers([]); - setUnitAdminUserIds(new Set()); - return; - } - - await Promise.all([ - loadPositions(selectedUnitId), - loadPositionTypes(selectedUnitId), - loadUnitAdmins(selectedUnitId), - ]); - }, [loadPositionTypes, loadPositions, loadUnitAdmins, selectedUnitId]); - - useEffect(() => { - void loadOrganizations(); - }, [loadOrganizations]); - - useEffect(() => { - if (!visibleOrganizations.length) { - setSelectedOrgId(null); - setSelectedOrgConfiguration(null); - setUnits([]); - setPositions([]); - setPositionMembers([]); - setOrgEmployees([]); - return; - } - - if (selectedOrgId && visibleOrganizations.some((organization) => organization.id === selectedOrgId)) { - return; - } - - setSelectedOrgId(visibleOrganizations[0]?.id ?? null); - setSelectedOrgConfiguration(null); - setUnits([]); - setPositions([]); - setPositionMembers([]); - setOrgEmployees([]); - }, [selectedOrgId, visibleOrganizations]); - - useEffect(() => { - if (!selectedOrgId) { - return; - } - - void refreshSelectedOrg(); - }, [refreshSelectedOrg, selectedOrgId]); - - useEffect(() => { - if (!selectedOrgId) { - setUnits([]); - setPositions([]); - setPositionTypes([]); - setPositionTypesError(null); - setSelectedUnitId(null); - setSelectedPositionId(null); - setExpandedDepartmentIds(new Set()); - setPositionMembers([]); - setOrgEmployees([]); - setOrgAdminUserIds(new Set()); - setSelectedOrgConfiguration(null); - return; - } - }, [selectedOrgId]); - - useEffect(() => { - if (!selectedOrgId || !selectedUnitId) { - return; - } - - if (units.some((unit) => unit.id === selectedUnitId)) { - return; - } - - setSelectedUnitId(units[0]?.id ?? null); - }, [selectedOrgId, selectedUnitId, units]); - - useEffect(() => { - if (!selectedOrgId || !units.length) { - return; - } - - if (selectedUnitId && units.some((unit) => unit.id === selectedUnitId)) { - return; - } - - setSelectedUnitId(units[0]?.id ?? null); - }, [selectedOrgId, selectedUnitId, units]); - - useEffect(() => { - if (!selectedUnitId) { - setPositions([]); - setPositionTypes([]); - setPositionTypesError(null); - setSelectedPositionId(null); - setExpandedDepartmentIds(new Set()); - setPositionMembers([]); - setUnitAdminUserIds(new Set()); - return; - } - }, [selectedUnitId]); - - useEffect(() => { - if (!selectedUnitId) { - return; - } - - void refreshSelectedUnit(); - }, [refreshSelectedUnit, selectedUnitId]); - - useEffect(() => { - if (!selectedUnitId || !selectedPositionId) { - return; - } - - if (!positions.some((position) => position.id === selectedPositionId)) { - setSelectedPositionId(null); - } - }, [positions, selectedPositionId, selectedUnitId]); - - useEffect(() => { - if (!selectedPositionId) { - setPositionMembers([]); - return; - } - }, [selectedPositionId]); - - useEffect(() => { - void loadPositionMembers(selectedPositionId); - }, [loadPositionMembers, selectedPositionId]); - - const handleRefresh = async () => { - resetMessages(); - await Promise.all([ - refreshOrganizations(), - selectedOrgId ? refreshSelectedOrg() : Promise.resolve(), - selectedUnitId ? refreshSelectedUnit() : Promise.resolve(), - loadPositionMembers(selectedPositionId), - ]); - }; - - const handleSelectOrganization = async (organizationId: string) => { - setSelectedOrgId(organizationId); - setSelectedUnitId(null); - setSelectedPositionId(null); - setExpandedDepartmentIds(new Set()); - setUnits([]); - setPositions([]); - setPositionMembers([]); - setOrgEmployees([]); - setOrgAdminUserIds(new Set()); - setUnitAdminUserIds(new Set()); - setSelectedOrgConfiguration(null); - resetMessages(); - - try { - await Promise.all([ - loadUnits(organizationId), - loadOrgEmployees(organizationId), - loadOrgAdmins(organizationId), - loadOrganizationConfiguration(organizationId), - ]); - } catch { - // Individual loaders already handle their own error state. - } - }; - - const handleSelectUnit = async (unitId: string) => { - setSelectedUnitId(unitId); - setSelectedPositionId(null); - setExpandedDepartmentIds(new Set()); - setPositions([]); - setPositionTypes([]); - setPositionTypesError(null); - setPositionMembers([]); - setUnitAdminUserIds(new Set()); - resetMessages(); - }; - - const handleToggleDepartment = (departmentId: string) => { - setExpandedDepartmentIds((current) => { - const next = new Set(current); - if (next.has(departmentId)) { - next.delete(departmentId); - } else { - next.add(departmentId); - } - return next; - }); - }; - - const openCreateSubDepartmentDialog = async (parent: PositionRecord) => { - if (!selectedUnit) { - setActionError("Select a unit before creating a department."); - return; - } - - resetMessages(); - - try { - await loadPositionTypes(selectedUnit.id); - setSelectedPositionId(parent.id); - setExpandedDepartmentIds((current) => new Set(current).add(parent.id)); - setCreatePositionMode("create-child"); - setPositionForm({ - ...emptyPositionForm, - parentPositionId: parent.id, - }); - setIsCreatePositionOpen(true); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to load position types.")); - } - }; - - const openInviteForDepartment = (position: PositionRecord) => { - setSelectedPositionId(position.id); - resetMessages(); - setIsInviteEmployeeOpen(true); - }; - - const openAssignForDepartment = (position: PositionRecord) => { - setSelectedPositionId(position.id); - resetMessages(); - setIsAssignEmployeeOpen(true); - }; - - const openCreateUnitDialog = () => { - setUnitForm(emptyUnitForm); - resetMessages(); - setIsCreateUnitOpen(true); - }; - - const openEditUnitDialog = (unit: UnitRecord) => { - setUnitForm({ - nameEn: unit.name.en ?? "", - nameAm: unit.name.am ?? "", - key: unit.key, - }); - resetMessages(); - setIsEditUnitOpen(true); - }; - - const openCreatePositionDialog = async ( - mode: "create-root" | "create-child", - unitId = selectedUnit?.id, - ) => { - if (!unitId) { - setActionError("Select a unit before creating a department."); - return; - } - - resetMessages(); - - try { - await loadPositionTypes(unitId); - setCreatePositionMode(mode); - setPositionForm({ - ...emptyPositionForm, - parentPositionId: mode === "create-child" && selectedPosition ? selectedPosition.id : "", - }); - setIsCreatePositionOpen(true); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to load position types.")); - } - }; - - const openEditPositionDialog = async (position: PositionRecord) => { - if (!selectedUnit) { - setActionError("Select a unit before editing a department."); - return; - } - - resetMessages(); - - try { - await loadPositionTypes(selectedUnit.id); - setPositionForm({ - id: position.id, - nameEn: position.name.en ?? "", - nameAm: position.name.am ?? "", - key: position.key, - parentPositionId: position.parentPositionId ?? "", - positionTypeId: position.positionTypeId ?? "", - }); - setIsEditPositionOpen(true); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to load position types.")); - } - }; - - const handleCreateUnit = async (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedOrganization) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.post("/units", { - name: { am: unitForm.nameAm.trim(), en: unitForm.nameEn.trim() }, - key: unitForm.key.trim(), - organizationId: selectedOrganization.id, - }); - setActionSuccess("Unit created."); - setIsCreateUnitOpen(false); - await refreshSelectedOrg(); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to create unit.")); - } finally { - setSubmitting(false); - } - }; - - const handleUpdateUnit = async (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedOrganization || !selectedUnit) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.put(`/units/${selectedUnit.id}`, { - name: { am: unitForm.nameAm.trim(), en: unitForm.nameEn.trim() }, - key: unitForm.key.trim(), - organizationId: selectedOrganization.id, - }); - setActionSuccess("Unit updated."); - setIsEditUnitOpen(false); - await refreshSelectedOrg(); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update unit.")); - } finally { - setSubmitting(false); - } - }; - - const handleDeleteUnit = async (unit: UnitRecord) => { - if (!unit) { - return; - } - - if (!window.confirm(`Delete unit '${getLocaleLabel(unit.name, unit.key)}'?`)) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.delete(`/units/${unit.id}`); - setActionSuccess("Unit deleted."); - await refreshSelectedOrg(); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to delete unit.")); - } finally { - setSubmitting(false); - } - }; - - const handleSavePosition = async (event: React.FormEvent, isEditing: boolean) => { - event.preventDefault(); - if (!selectedOrganization || !selectedUnit) { - setActionError("Select a unit before managing positions."); - return; - } - - if (!positionForm.positionTypeId) { - setActionError("Select a position type before saving the department."); - return; - } - - setSubmitting(true); - resetMessages(); - - try { - const payload = { - id: positionForm.id, - name: { am: positionForm.nameAm.trim(), en: positionForm.nameEn.trim() }, - key: positionForm.key.trim(), - unitId: selectedUnit.id, - organizationId: selectedOrganization.id, - parentPositionId: positionForm.parentPositionId || null, - positionTypeId: positionForm.positionTypeId, - rank: 0, - }; - - if (isEditing && positionForm.id) { - const existing = positions.find((item) => item.id === positionForm.id); - await api.put(`/positions/${positionForm.id}`, payload); - - if ((existing?.parentPositionId ?? "") !== (positionForm.parentPositionId || "")) { - await api.patch("/positions/change-parent", { - positionId: positionForm.id, - newParentId: positionForm.parentPositionId || null, - }); - } - - setActionSuccess("Position updated."); - setIsEditPositionOpen(false); - } else { - await api.post("/positions", payload); - setActionSuccess("Position created."); - setIsCreatePositionOpen(false); - } - - await refreshSelectedUnit(); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to save position.")); - } finally { - setSubmitting(false); - } - }; - - const handleDeletePosition = async (position: PositionRecord) => { - if (!position) { - return; - } - - if (!window.confirm(`Delete position '${getLocaleLabel(position.name, position.key)}'?`)) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.delete(`/positions/${position.id}`); - setActionSuccess("Position deleted."); - await refreshSelectedUnit(); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to delete position.")); - } finally { - setSubmitting(false); - } - }; - - const handleInviteEmployee = async (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedPosition) { - setActionError("Select a position before inviting an employee."); - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.post("/employee-positions/invite", { - positionId: selectedPosition.id, - username: inviteForm.username.trim(), - phoneNumber: inviteForm.phoneNumber.trim(), - email: inviteForm.email.trim(), - name: { am: inviteForm.nameAm.trim(), en: inviteForm.nameEn.trim() }, - }); - setInviteForm(emptyInviteForm); - setActionSuccess("Employee invited and assigned to the selected position."); - setIsInviteEmployeeOpen(false); - await Promise.all([loadOrgEmployees(selectedOrgId!), loadPositionMembers(selectedPosition.id)]); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to invite employee.")); - } finally { - setSubmitting(false); - } - }; - - const handleAssignEmployee = async (employeeId: string) => { - if (!selectedPosition) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.post("/employee-positions/assign", { - positionId: selectedPosition.id, - employeeId, - }); - setActionSuccess("Employee assigned to the selected position."); - setIsAssignEmployeeOpen(false); - setAssignSearch(""); - await Promise.all([loadOrgEmployees(selectedOrgId!), loadPositionMembers(selectedPosition.id)]); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to assign employee.")); - } finally { - setSubmitting(false); - } - }; - - const handleToggleUserActivation = async (employee: EmployeeRecord) => { - if (!employee.user?.id) { - return; - } - - const isInactive = employee.status?.toLowerCase() === "inactive"; - setSubmitting(true); - resetMessages(); - - try { - await api.patch( - `/users/${isInactive ? "activate-user" : "deactivate-user"}/${employee.user.id}`, - ); - setActionSuccess(isInactive ? "User activated." : "User deactivated."); - await Promise.all([loadOrgEmployees(selectedOrgId!), loadPositionMembers(selectedPositionId)]); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update user status.")); - } finally { - setSubmitting(false); - } - }; - - const handleAdminRoleMutation = async ( - endpoint: string, - payload: { organizationId?: string; unitId?: string; userId: string }, - successMessage: string, - ) => { - setSubmitting(true); - resetMessages(); - - try { - await api.post(endpoint, payload); - setActionSuccess(successMessage); - await Promise.all([ - selectedOrgId ? loadOrgAdmins(selectedOrgId) : Promise.resolve(), - loadUnitAdmins(selectedUnitId), - ]); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update admin role.")); - } finally { - setSubmitting(false); - } - }; - - const openManageRolesDialog = async (employee: EmployeeRecord) => { - if (!selectedOrgId || !employee.user?.id) { - return; - } - - setRolesLoading(true); - resetMessages(); - setSelectedRoleUser(employee); - setIsManageRolesOpen(true); - - try { - const [rolesResponse, assignedResponse] = await Promise.all([ - api.get>("/roles"), - api.get(`/backoffice/organizations/${selectedOrgId}/employee-users/${employee.user.id}/roles`), - ]); - const roles = getItems(rolesResponse.data).filter((role) => !RESERVED_ROLE_KEYS.has(role.key)); - setAvailableRoles(roles); - setRoleIds(getItems(assignedResponse.data).map((role) => role.id)); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to load user roles.")); - setAvailableRoles([]); - setRoleIds([]); - } finally { - setRolesLoading(false); - } - }; - - const handleSaveRoles = async () => { - if (!selectedOrgId || !selectedRoleUser?.user?.id) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.put( - `/backoffice/organizations/${selectedOrgId}/employee-users/${selectedRoleUser.user.id}/roles`, - { roleIds }, - ); - setActionSuccess("User roles updated."); - setIsManageRolesOpen(false); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update user roles.")); - } finally { - setSubmitting(false); - } - }; - - return ( -
    -
    -
    -
    -
    - -
    -
    -

    - User management -

    -

    Organization workspace

    -

    - Manage organizations, branches, departments, team members, and admin access from a - single SmartOffice-style workspace. -

    -
    -
    - - -
    -
    - - {actionSuccess ? ( -
    - {actionSuccess} -
    - ) : null} - - {actionError ? ( -
    - {actionError} -
    - ) : null} - - {loadError ? ( -
    - {loadError} -
    - ) : null} - - {loading ? ( -
    - Loading user management workspace... -
    - ) : ( -
    - - - - - - -
    -
    -
    -

    Team members

    -

    - {selectedPosition - ? `${getLocaleLabel(selectedPosition.name, selectedPosition.key)} in ${getLocaleLabel(selectedUnit?.name, selectedUnit?.key ?? "")}` - : "Select a department"} -

    -
    -
    - -
    - - setEmployeeSearch(event.target.value)} - placeholder="Search team members" - /> -
    - - {membersLoading ? ( -
    - Loading team members... -
    - ) : filteredPositionMembers.length ? ( -
    - {filteredPositionMembers.map((employee) => { - const userId = employee.user?.id; - const isOrgAdmin = Boolean(userId && orgAdminUserIds.has(userId)); - const isUnitAdmin = Boolean(userId && unitAdminUserIds.has(userId)); - const displayName = getLocaleLabel( - employee.name ?? employee.user?.name, - employee.user?.email ?? employee.id, - ); - const assignedPositions = employee.employeePositions - ?.map((position) => getLocaleLabel(position.position?.name, position.position?.id ?? "")) - .filter(Boolean) - .join(", "); - - return ( -
    -
    -
    -
    -

    {displayName}

    - {isOrgAdmin ? ( - Org admin - ) : null} - {isUnitAdmin ? ( - Unit admin - ) : null} - {employee.status ? ( - {employee.status} - ) : null} -
    -
    - {employee.user?.email || employee.user?.username || "No contact info"} -
    - {assignedPositions ? ( -
    - Current positions: {assignedPositions} -
    - ) : null} -
    - -
    - - - - - - - -
    -
    -
    - ); - })} -
    - ) : ( -
    - No team members found for this department. -
    - )} -
    -
    - )} - - -
    - - setUnitForm((current) => ({ ...current, nameEn: event.target.value }))} - /> - - - setUnitForm((current) => ({ ...current, nameAm: event.target.value }))} - /> - - -
    - setUnitForm((current) => ({ ...current, key: event.target.value }))} - /> - -
    -
    -
    - - -
    -
    -
    - - -
    - - setUnitForm((current) => ({ ...current, nameEn: event.target.value }))} - /> - - - setUnitForm((current) => ({ ...current, nameAm: event.target.value }))} - /> - - - setUnitForm((current) => ({ ...current, key: event.target.value }))} - /> - -
    - - -
    -
    -
    - - -
    void handleSavePosition(event, false)}> - - - setPositionForm((current) => ({ ...current, nameEn: event.target.value })) - } - /> - - - - setPositionForm((current) => ({ ...current, nameAm: event.target.value })) - } - /> - - -
    - setPositionForm((current) => ({ ...current, key: event.target.value }))} - /> - -
    -
    - - - {positionTypesError ? ( -

    {positionTypesError}

    - ) : null} -
    - - - -
    - - -
    -
    -
    - - -
    void handleSavePosition(event, true)}> - - - setPositionForm((current) => ({ ...current, nameEn: event.target.value })) - } - /> - - - - setPositionForm((current) => ({ ...current, nameAm: event.target.value })) - } - /> - - - setPositionForm((current) => ({ ...current, key: event.target.value }))} - /> - - - - {positionTypesError ? ( -

    {positionTypesError}

    - ) : null} -
    - - - -
    - - -
    -
    -
    - - -
    - - setInviteForm((current) => ({ ...current, nameEn: event.target.value }))} - /> - - - setInviteForm((current) => ({ ...current, nameAm: event.target.value }))} - /> - - - setInviteForm((current) => ({ ...current, email: event.target.value }))} - /> - - - setInviteForm((current) => ({ ...current, username: event.target.value }))} - /> - - - - setInviteForm((current) => ({ ...current, phoneNumber: event.target.value })) - } - /> - -
    - - -
    -
    -
    - - -
    -
    - - setAssignSearch(event.target.value)} - placeholder="Search organization employees" - /> -
    - -
    - {orgEmployeesLoading ? ( -
    - Loading organization employees... -
    - ) : assignableEmployees.length ? ( - assignableEmployees.map((employee) => { - const displayName = getLocaleLabel( - employee.name ?? employee.user?.name, - employee.user?.email ?? employee.id, - ); - const assignedPositions = employee.employeePositions - ?.map((position) => getLocaleLabel(position.position?.name, position.position?.id ?? "")) - .filter(Boolean) - .join(", "); - - return ( -
    -
    -
    {displayName}
    -
    - {employee.user?.email || employee.user?.phoneNumber || "No contact info"} -
    - {assignedPositions ? ( -
    - Existing positions: {assignedPositions} -
    - ) : null} -
    - -
    - ); - }) - ) : ( -
    - No employees found for this organization. -
    - )} -
    -
    -
    - - -
    - {rolesLoading ? ( -
    - Loading roles... -
    - ) : ( -
    - {availableRoles.map((role) => ( - - ))} - {!availableRoles.length ? ( -
    - No assignable roles available. -
    - ) : null} -
    - )} -
    - - -
    -
    -
    -
    - ); -}; - -export default UserManagementPage; 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 deleted file mode 100644 index 70d58d313..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/UsersPage.tsx +++ /dev/null @@ -1,825 +0,0 @@ -import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; -import { isAxiosError } from "axios"; -import { - Badge, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@edr/ui-common"; -import { Network, RefreshCw, Search, UserCheck, UserMinus, Users } from "lucide-react"; - -import { api } from "@/auth/http"; -import { useAuth } from "@/auth/useAuth"; - -interface LocaleText { - en?: string; - am?: string; -} - -interface OrganizationRecord { - id: string; - key: string; - name: LocaleText; -} - -interface EmployeeUserRecord { - id: string; - name?: LocaleText; - email?: string; - phoneNumber?: string; - username?: string; -} - -interface EmployeePositionSummary { - id: string; - position?: { - id: string; - name?: LocaleText; - }; -} - -interface EmployeeRecord { - id: string; - name?: LocaleText; - user?: EmployeeUserRecord; - status?: string; - employeePositions?: EmployeePositionSummary[]; -} - -interface RoleRecord { - id: string; - key: string; - name: LocaleText; -} - -interface UserFormState { - nameEn: string; - nameAm: string; - email: string; - username: string; - phoneNumber: string; - assignOrganizationAdmin: boolean; -} - -interface ListResponse { - items?: T[]; - data?: T[]; -} - -const RESERVED_ROLE_KEYS = new Set(["super_admin", "organization_admin", "unit_admin"]); - -const emptyUserForm: UserFormState = { - nameEn: "", - nameAm: "", - email: "", - username: "", - phoneNumber: "", - assignOrganizationAdmin: false, -}; - -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 getLocaleLabel = (value?: LocaleText | null, fallback = "Unnamed") => { - if (!value) { - return fallback; - } - - return 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 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; - if (typeof message === "string") { - return message; - } - if (Array.isArray(message) && typeof message[0] === "string") { - return message[0]; - } - } - - return error instanceof Error ? error.message : fallback; -}; - -const Field = ({ label, children }: { label: string; children: ReactNode }) => ( - -); - -const ManagementDialog = ({ - open, - title, - description, - onOpenChange, - children, -}: { - open: boolean; - title: string; - description?: string; - onOpenChange: (open: boolean) => void; - children: ReactNode; -}) => ( - - - - {title} - {description ? {description} : null} - - {children} - - -); - -const UsersPage = () => { - const { user } = useAuth(); - const [organizations, setOrganizations] = useState([]); - const [orgEmployees, setOrgEmployees] = useState([]); - const [selectedOrgId, setSelectedOrgId] = useState(null); - const [orgUserSearch, setOrgUserSearch] = useState(""); - const [createUserForm, setCreateUserForm] = useState(emptyUserForm); - const [availableRoles, setAvailableRoles] = useState([]); - const [roleIds, setRoleIds] = useState([]); - const [selectedRoleUser, setSelectedRoleUser] = useState(null); - const [loading, setLoading] = useState(true); - const [orgEmployeesLoading, setOrgEmployeesLoading] = useState(false); - const [rolesLoading, setRolesLoading] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [loadError, setLoadError] = useState(null); - const [actionError, setActionError] = useState(null); - const [actionSuccess, setActionSuccess] = useState(null); - const [isCreateUserOpen, setIsCreateUserOpen] = useState(false); - const [isManageRolesOpen, setIsManageRolesOpen] = useState(false); - - const isSuperAdmin = Boolean(user?.roles?.some((role) => role.key === "super_admin")); - const allowedOrgIds = useMemo( - () => new Set((user?.employee ?? []).map((employee) => employee.organizationId).filter(Boolean)), - [user?.employee], - ); - - const visibleOrganizations = useMemo(() => { - if (isSuperAdmin) { - return organizations; - } - - return organizations.filter((organization) => allowedOrgIds.has(organization.id)); - }, [allowedOrgIds, isSuperAdmin, organizations]); - - const selectedOrganization = useMemo( - () => visibleOrganizations.find((item) => item.id === selectedOrgId) ?? null, - [selectedOrgId, visibleOrganizations], - ); - - const filteredOrgEmployees = useMemo(() => { - const query = orgUserSearch.trim().toLowerCase(); - - return orgEmployees.filter((employee) => { - const label = getLocaleLabel( - employee.name ?? employee.user?.name, - employee.user?.email ?? employee.id, - ).toLowerCase(); - const email = employee.user?.email?.toLowerCase() ?? ""; - const username = employee.user?.username?.toLowerCase() ?? ""; - - if (!query) { - return true; - } - - return label.includes(query) || email.includes(query) || username.includes(query); - }); - }, [orgEmployees, orgUserSearch]); - - const resetMessages = () => { - setActionError(null); - setActionSuccess(null); - }; - - const loadOrganizations = useCallback(async () => { - setLoading(true); - setLoadError(null); - - try { - const response = await api.get>("/organizations"); - setOrganizations(getItems(response.data)); - } catch (error) { - setLoadError(getErrorMessage(error, "Failed to load organizations.")); - } finally { - setLoading(false); - } - }, []); - - const loadOrgEmployees = useCallback(async (organizationId: string) => { - setOrgEmployeesLoading(true); - - try { - const response = await api.get>( - `/backoffice/organizations/${organizationId}/employees`, - ); - setOrgEmployees(mergeEmployeesByUser(getItems(response.data))); - } catch { - setOrgEmployees([]); - } finally { - setOrgEmployeesLoading(false); - } - }, []); - - useEffect(() => { - void loadOrganizations(); - }, [loadOrganizations]); - - useEffect(() => { - if (!visibleOrganizations.length) { - setSelectedOrgId(null); - setOrgEmployees([]); - return; - } - - if (selectedOrgId && visibleOrganizations.some((organization) => organization.id === selectedOrgId)) { - return; - } - - setSelectedOrgId(visibleOrganizations[0]?.id ?? null); - }, [selectedOrgId, visibleOrganizations]); - - useEffect(() => { - if (!selectedOrgId) { - setOrgEmployees([]); - return; - } - - void loadOrgEmployees(selectedOrgId); - }, [loadOrgEmployees, selectedOrgId]); - - const handleRefresh = async () => { - resetMessages(); - await Promise.all([ - loadOrganizations(), - selectedOrgId ? loadOrgEmployees(selectedOrgId) : Promise.resolve(), - ]); - }; - - const handleSelectOrganization = async (organizationId: string) => { - setSelectedOrgId(organizationId); - setOrgEmployees([]); - resetMessages(); - - try { - await loadOrgEmployees(organizationId); - } catch { - // Loader already handles fallback state. - } - }; - - const openCreateUserDialog = () => { - if (!selectedOrgId) { - setActionError("Select an organization before adding a user."); - return; - } - - setCreateUserForm(emptyUserForm); - resetMessages(); - setIsCreateUserOpen(true); - }; - - const openManageRolesDialog = async (employee: EmployeeRecord) => { - if (!selectedOrgId || !employee.user?.id) { - return; - } - - setRolesLoading(true); - resetMessages(); - setSelectedRoleUser(employee); - setIsManageRolesOpen(true); - - try { - const [rolesResponse, assignedResponse] = await Promise.all([ - api.get>("/roles"), - api.get(`/backoffice/organizations/${selectedOrgId}/employee-users/${employee.user.id}/roles`), - ]); - const roles = getItems(rolesResponse.data).filter((role) => !RESERVED_ROLE_KEYS.has(role.key)); - setAvailableRoles(roles); - setRoleIds(getItems(assignedResponse.data).map((role) => role.id)); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to load user roles.")); - setAvailableRoles([]); - setRoleIds([]); - } finally { - setRolesLoading(false); - } - }; - - const handleCreateUser = async (event: React.FormEvent) => { - event.preventDefault(); - - if (!selectedOrgId) { - setActionError("Select an organization before adding a user."); - return; - } - - setSubmitting(true); - resetMessages(); - - try { - const response = await api.post( - `/backoffice/organizations/${selectedOrgId}/users`, - { - username: createUserForm.username.trim(), - phoneNumber: createUserForm.phoneNumber.trim(), - email: createUserForm.email.trim(), - name: { - am: createUserForm.nameAm.trim(), - en: createUserForm.nameEn.trim(), - }, - assignOrganizationAdmin: createUserForm.assignOrganizationAdmin, - }, - ); - - const shouldAssignOrganizationAdmin = createUserForm.assignOrganizationAdmin; - setCreateUserForm(emptyUserForm); - setIsCreateUserOpen(false); - setActionSuccess( - shouldAssignOrganizationAdmin - ? "User created as organization admin. Default password: 12345678." - : "User created. Default password: 12345678.", - ); - await loadOrgEmployees(selectedOrgId); - - if (!shouldAssignOrganizationAdmin) { - await openManageRolesDialog(response.data); - } - } catch (error) { - setActionError(getErrorMessage(error, "Failed to create user.")); - } finally { - setSubmitting(false); - } - }; - - const handleSaveRoles = async () => { - if (!selectedOrgId || !selectedRoleUser?.user?.id) { - return; - } - - setSubmitting(true); - resetMessages(); - - try { - await api.put( - `/backoffice/organizations/${selectedOrgId}/employee-users/${selectedRoleUser.user.id}/roles`, - { roleIds }, - ); - setActionSuccess("User roles updated."); - setIsManageRolesOpen(false); - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update user roles.")); - } finally { - setSubmitting(false); - } - }; - - const handleToggleUserActivation = async (employee: EmployeeRecord) => { - if (!employee.user?.id) { - return; - } - - const isInactive = employee.status?.toLowerCase() === "inactive"; - setSubmitting(true); - resetMessages(); - - try { - await api.patch(`/users/${isInactive ? "activate-user" : "deactivate-user"}/${employee.user.id}`); - setActionSuccess(isInactive ? "User activated." : "User deactivated."); - if (selectedOrgId) { - await loadOrgEmployees(selectedOrgId); - } - } catch (error) { - setActionError(getErrorMessage(error, "Failed to update user status.")); - } finally { - setSubmitting(false); - } - }; - - return ( -
    -
    -
    -
    -
    - -
    -
    -

    - User management -

    -

    Users

    -

    - Create organization users, activate or deactivate access, and assign organization-scoped roles. -

    -
    -
    - - -
    -
    - - {actionSuccess ? ( -
    - {actionSuccess} -
    - ) : null} - - {actionError ? ( -
    - {actionError} -
    - ) : null} - - {loadError ? ( -
    - {loadError} -
    - ) : null} - - {loading ? ( -
    - Loading users workspace... -
    - ) : ( -
    - - -
    -
    -
    -

    Users

    -

    - {selectedOrganization - ? `Manage users in ${getLocaleLabel(selectedOrganization.name, selectedOrganization.key)}` - : "Select an organization"} -

    -
    -
    - {orgEmployees.length} users -
    -
    - -
    - - setOrgUserSearch(event.target.value)} - placeholder="Search users" - /> -
    - - {orgEmployeesLoading ? ( -
    - Loading users... -
    - ) : filteredOrgEmployees.length ? ( -
    - {filteredOrgEmployees.map((employee) => { - const userId = employee.user?.id; - const displayName = getLocaleLabel( - employee.name ?? employee.user?.name, - employee.user?.email ?? employee.id, - ); - const assignedPositions = employee.employeePositions - ?.map((position) => getLocaleLabel(position.position?.name, position.position?.id ?? "")) - .filter(Boolean) - .join(", "); - - return ( -
    -
    -
    -
    -

    {displayName}

    - {employee.status ? ( - {employee.status} - ) : null} -
    -
    - {employee.user?.email || employee.user?.username || "No contact info"} -
    -
    - {assignedPositions ? `Current positions: ${assignedPositions}` : "No positions assigned."} -
    -
    - -
    - - - -
    -
    -
    - ); - })} -
    - ) : ( -
    - {selectedOrganization - ? "No users found for this organization." - : "Select an organization to load users."} -
    - )} -
    -
    - )} - - -
    - - setCreateUserForm((current) => ({ ...current, nameEn: event.target.value }))} - /> - - - setCreateUserForm((current) => ({ ...current, nameAm: event.target.value }))} - /> - - - setCreateUserForm((current) => ({ ...current, email: event.target.value }))} - /> - - - setCreateUserForm((current) => ({ ...current, username: event.target.value }))} - /> - - - setCreateUserForm((current) => ({ ...current, phoneNumber: event.target.value }))} - /> - - -
    - - -
    -
    -
    - - -
    - {rolesLoading ? ( -
    - Loading roles... -
    - ) : ( -
    - {availableRoles.map((role) => ( - - ))} - {!availableRoles.length ? ( -
    - No assignable roles available. -
    - ) : null} -
    - )} -
    - - -
    -
    -
    -
    - ); -}; - -export default UsersPage; diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts deleted file mode 100644 index ec27b5c76..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts +++ /dev/null @@ -1,207 +0,0 @@ -import type { DesignConfig } from "@tria-plc/iamui"; - -import { - FREIGHT_BRAND, - FREIGHT_BRAND_LIGHT, - freightBrand, -} from "@/theme/freight-brand"; - -export const iamConfig: DesignConfig = { - brand: { - appName: "EDR Freight Backoffice", - logoUrl: "/assets/logo.svg", - }, - colors: { - primary: FREIGHT_BRAND, - primaryForeground: "#ffffff", - secondary: "#f4f7fb", - background: "#f7f9fb", - foreground: "#0f172a", - border: "#eef1f4", - muted: "#f1f5f9", - mutedForeground: "#64748b", - card: "#ffffff", - sidebar: "#ffffff", - danger: "#ef4444", - }, - typography: { - fontFamily: "'Outfit', var(--font-sans), system-ui, sans-serif", - headingFontFamily: "'Outfit', var(--font-sans), system-ui, sans-serif", - baseFontSize: "15px", - fontWeight: "500", - }, - shape: { - radius: "1rem", - }, - shadows: { - card: "0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -16px rgba(15, 23, 42, 0.12)", - dropdown: "0 12px 30px rgba(15, 23, 42, 0.12)", - modal: "0 20px 45px rgba(15, 23, 42, 0.2)", - }, - components: { - buttonDefaultVariant: "filled", - inputDefaultSize: "sm", - inputRadius: "md", - modalRadius: "lg", - tableHighlightOnHover: true, - }, - layout: { - userManagementView: "classic", - showTopBar: true as any, - sidebarWidth: "280px", - sidebarCollapsedWidth: "80px", - headerHeight: "80px", - contentMaxWidth: "none", - sidebarBackground: "#ffffff", - sidebarColor: "#475569", - sidebarMutedColor: "#94a3b8", - sidebarActiveBackground: - "linear-gradient(135deg, rgba(45, 191, 149, 0.14) 0%, rgba(27, 158, 122, 0.06) 100%)", - sidebarActiveColor: FREIGHT_BRAND, - sidebarHoverBackground: "#f5f7fa", - sidebarBorder: "#eef1f4", - sidebarRail: `linear-gradient(180deg, ${FREIGHT_BRAND_LIGHT} 0%, ${FREIGHT_BRAND} 100%)`, - sidebarBrandLabel: "EDR Freight", - sidebarBrandSublabel: "Backoffice Console", - menuBackground: "#ffffff", - menuActiveColor: FREIGHT_BRAND, - menuActiveBorderColor: FREIGHT_BRAND, - menuColor: "#64748b", - menuHoverColor: "#0f172a", - modalAccentColor: `linear-gradient(135deg, ${FREIGHT_BRAND_LIGHT} 0%, ${FREIGHT_BRAND} 100%)`, - modalHeaderBackground: "#ffffff", - modalHeaderEditBackground: "#ffffff", - modalIconBackground: freightBrand.mutedBg, - modalIconColor: FREIGHT_BRAND, - modalTitleColor: "#0f172a", - modalFocusColor: FREIGHT_BRAND, - modalSurface: "#ffffff", - }, - appearance: { - colorScheme: "light", - slots: { - root: { - styles: { - background: "#f7f9fb", - color: "#0f172a", - fontFamily: "'Outfit', var(--font-sans), system-ui, sans-serif", - }, - }, - shell: { - styles: { - background: "#f7f9fb", - }, - }, - content: { - styles: { - background: "#f7f9fb", - }, - }, - page: { - styles: { - background: "#ffffff", - border: "1px solid #eef1f4", - borderRadius: "24px", - boxShadow: - "0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -16px rgba(15, 23, 42, 0.12)", - }, - }, - card: { - styles: { - background: "#ffffff", - border: "1px solid #eef1f4", - borderRadius: "20px", - boxShadow: - "0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -16px rgba(15, 23, 42, 0.12)", - }, - }, - sidebar: { - styles: { - background: "#ffffff", - border: "1px solid #eef1f4", - borderRadius: "16px", - boxShadow: - "0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -16px rgba(15, 23, 42, 0.12)", - }, - }, - "sidebar-brand": { - styles: { - minHeight: "80px", - borderBottom: "1px solid #f1f5f9", - }, - }, - topbar: { - styles: { - background: "#ffffff", - border: "1px solid #eef1f4", - borderRadius: "16px", - boxShadow: - "0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -16px rgba(15, 23, 42, 0.12)", - }, - }, - "topbar-panel": { - styles: { - background: "#f7f9fb", - border: "1px solid #eef1f4", - borderRadius: "12px", - }, - }, - "topbar-user-summary": { - styles: { - borderRadius: "14px", - }, - }, - table: { - styles: { - background: "#ffffff", - border: "1px solid #eef1f4", - borderRadius: "20px", - overflow: "hidden", - }, - }, - "table-header": { - styles: { - background: "#f8fafc", - }, - }, - modal: { - styles: { - borderRadius: "24px", - overflow: "hidden", - }, - }, - "modal-header": { - styles: { - background: "#ffffff", - borderBottom: "1px solid #eef1f4", - }, - }, - }, - customCss: ` - [data-um-app="user-management"] { - --um-page-gap: 20px; - } - - [data-um-app="user-management"] h1, - [data-um-app="user-management"] h2, - [data-um-app="user-management"] h3, - [data-um-app="user-management"] h4, - [data-um-app="user-management"] h5, - [data-um-app="user-management"] h6 { - letter-spacing: -0.02em; - color: #0f172a; - } - - [data-um-app="user-management"] [data-um-slot="sidebar-item"][aria-current="page"] { - box-shadow: inset 3px 0 0 ${FREIGHT_BRAND}; - } - - [data-um-app="user-management"] button, - [data-um-app="user-management"] input, - [data-um-app="user-management"] select, - [data-um-app="user-management"] textarea { - font-family: 'Outfit', var(--font-sans), system-ui, sans-serif; - } - `, - }, -}; diff --git a/apps/edr-freight-web/backoffice/src/user-management/route.tsx b/apps/edr-freight-web/backoffice/src/user-management/route.tsx index 910712d5d..76d3182ad 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/route.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/route.tsx @@ -6,20 +6,15 @@ import { isSuperAdmin } from "@/lib/permissions"; import { WithPermission } from "@/shared/hooks/useHas"; import PendingExternalUsers from "@/super-admin/components/externalUsers/PendingExternalUsers"; import TemplatePage from "@/super-admin/components/templates/components/templates"; -import UserManagementPage from "@/pages/dashboard/user-management/UserManagementPage"; - import CreatePositionPage from "./pages/position-management/create"; import EditPositionPage from "./pages/position-management/edit"; import PositionManagementPage from "./pages/position-management"; import MigratedDataManagementPage from "./pages/position-management/MigratedDataManagementPage"; import UserPositionApprovalPage from "./pages/UserPositionApprovalPage"; import ViewMigratedDataPage from "./components/MigratedRecords/ViewMigratedDataPage"; -import ContentManagement from "./components/content/ContentManagement"; -import { BulkUserUpload } from "./bulkUpload/bulkUpload"; import AllRecordsPage from "./all-records/pages/AllRecordsPage"; import AllRecordDetailsPage from "./all-records/pages/AllRecordDetailsPage"; import Branding from "./web-Management/Branding/Branding"; -import { WebManagementHomePage } from "./web-Management/webManagement"; import { AppLayout } from "./Applayout"; import ActivityLogPage from "@/pages/ActivityLogPage"; import AdminRegistrationPage from "@/pages/Organizations/AdminRegistrationPage"; @@ -46,6 +41,7 @@ import ConfigurationPage from "@/pages/ConfigurationPage"; import { SidebarProvider } from "@/shared/common/ui/sidebar"; import { AuthProvider as UmAuthProvider } from "@/shared/context/AuthContext"; import { PermissionProvider } from "@/shared/context/PermissionContext"; +import UserManagementPage from "@/pages/UserManagementPage"; /** * Provider shell for the vendored IAM UI. Feeds its Auth + Permission contexts