From 8389b2735d6f1eaf8ca6687adb8f9f644f53934b Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 22 Jul 2026 11:54:54 +0000 Subject: [PATCH 1/2] fix: sidebar in user management --- apps/edr-freight-web/backoffice/src/App.tsx | 16 +- .../src/layout/components/TenantConfig.ts | 8 +- .../components/common/Top.tsx | 94 ++--- .../src/user-management/AppMenuTabs.tsx | 379 +++++++----------- .../src/user-management/Applayout.tsx | 83 +++- 5 files changed, 271 insertions(+), 309 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index f8e17fe76..70bcc4f9b 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -626,7 +626,9 @@ const filterSidebarByPermission = ( const filterItems = (items: SidebarItem[]): SidebarItem[] => items .map((item) => - item.children ? { ...item, children: filterItems(item.children) } : item, + item.children + ? { ...item, children: filterItems(item.children) } + : item, ) .filter((item) => { if (etGl || djGl) { @@ -642,18 +644,6 @@ const filterSidebarByPermission = ( return true; }); - // Recursive: a group's own permission gates the whole subtree, leaves are - // checked individually, and a group with no surviving children disappears. - const filterItems = (items: SidebarItem[]): SidebarItem[] => - items.flatMap((item) => { - if (item.children?.length) { - if (item.permission && !permissionAllowed(item)) return []; - const children = filterItems(item.children); - return children.length ? [{ ...item, children }] : []; - } - return itemAllowed(item) ? [item] : []; - }); - return sections .map((section) => ({ ...section, diff --git a/apps/edr-freight-web/backoffice/src/layout/components/TenantConfig.ts b/apps/edr-freight-web/backoffice/src/layout/components/TenantConfig.ts index 758e2a75a..f8fd1f47d 100644 --- a/apps/edr-freight-web/backoffice/src/layout/components/TenantConfig.ts +++ b/apps/edr-freight-web/backoffice/src/layout/components/TenantConfig.ts @@ -88,8 +88,8 @@ export const resolveModuleConfig = (config: TenantConfig): ModuleConfig => ({ }); const defaultConfig: TenantConfig = { - appName: "Smart Office", - organizationName: "Smart Office", + appName: "EDR Freight", + organizationName: "Ethio-Djibouti Railways", canUseAttachmentFromDMS: false, logo: "/assets/TriaTradinglogo.png", primaryColor: "#1b354d", @@ -116,8 +116,8 @@ const defaultConfig: TenantConfig = { const tenantConfigs: Record = { localhost: { - appName: "Smart Office", - organizationName: "Addis Ababa City Administration", + appName: "EDR Freight", + organizationName: "Ethio-Djibouti Railways", logo: "", primaryColor: "#0EA371", moduleConfig: { diff --git a/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx b/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx index 507e7da74..71afbee2d 100644 --- a/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx +++ b/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx @@ -104,9 +104,9 @@ const Top: React.FC = ({ const activePositionName = currentLanguage === "am" ? userDetails?.employee?.[0]?.positions?.[0]?.name?.am || - userDetails?.employee?.[0]?.positions?.[0]?.name?.en + userDetails?.employee?.[0]?.positions?.[0]?.name?.en : userDetails?.employee?.[0]?.positions?.[0]?.name?.en || - userDetails?.employee?.[0]?.positions?.[0]?.name?.am; + userDetails?.employee?.[0]?.positions?.[0]?.name?.am; const normalizedUserType = userDetails?.userType?.trim().toLowerCase() || ""; const userTypeLabel = @@ -114,8 +114,8 @@ const Top: React.FC = ({ ? t("header.user") : normalizedUserType ? normalizedUserType - .replace(/[_-]/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()) + .replace(/[_-]/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()) : ""; const roleLabel = @@ -123,8 +123,8 @@ const Top: React.FC = ({ userTypeLabel || (normalizedUserType ? userDetails?.roles?.[0]?.key - ?.replace(/[:_]/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()) + ?.replace(/[:_]/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()) : ""); const { permissionKeys } = useUserDetail(userDetails as MeDto); @@ -159,57 +159,57 @@ const Top: React.FC = ({ }, ...(moduleConfig.recordManagement ? [ - { - id: "recordManagement", - label: t("nav.Record Management", "Record Management"), - path: "/record-management/dashboard", - }, - ] + { + id: "recordManagement", + label: t("nav.Record Management", "Record Management"), + path: "/record-management/dashboard", + }, + ] : []), ...(moduleConfig.performance ? [ - { - id: "performanceManagement", - label: t("nav.PerformanceManagement", "Performance Management"), - path: "/performance-management/plan-years", - }, - ] + { + id: "performanceManagement", + label: t("nav.PerformanceManagement", "Performance Management"), + path: "/performance-management/plan-years", + }, + ] : []), ...(moduleConfig.objective ? [ - { - id: "objectiveManagement", - label: t("nav.objectiveManagement", "Objective Management"), - path: "/objective-management/plan-years", - }, - ] + { + id: "objectiveManagement", + label: t("nav.objectiveManagement", "Objective Management"), + path: "/objective-management/plan-years", + }, + ] : []), ...(moduleConfig.dms ? [ - { - id: "documentManagement", - label: t("nav.DocumentManagement", "Document Management"), - path: "/dms/dashboard", - }, - ] + { + id: "documentManagement", + label: t("nav.DocumentManagement", "Document Management"), + path: "/dms/dashboard", + }, + ] : []), ...(isOrgAdmin && moduleConfig.siteManagement ? [ - { - id: "orgAdmin", - label: t("nav.admin", "Admin"), - path: "/user-management/user_management-dashboard", - }, - ] + { + id: "orgAdmin", + label: t("nav.admin", "Admin"), + path: "/user-management/user_management-dashboard", + }, + ] : []), ...(isSuperAdmin ? [ - { - id: "superAdmin", - label: t("OrganizationAdmin", "Super Admin"), - path: "/user-management/dashboard", - }, - ] + { + id: "superAdmin", + label: t("OrganizationAdmin", "Super Admin"), + path: "/user-management/dashboard", + }, + ] : []), ]; @@ -256,8 +256,8 @@ const Top: React.FC = ({ }; return ( -
-
+
+
@@ -394,7 +394,7 @@ const Top: React.FC = ({ className={cn( "relative h-9 w-9 rounded-xl bg-white/90 text-gray-600 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:bg-primary-50 hover:text-primary-700 hover:ring-primary-200 dark:bg-gray-800/90 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-primary-900/20 dark:hover:text-primary-300 dark:hover:ring-primary-700/40 sm:h-10 sm:w-10", openNotifications && - "bg-primary-50 text-primary-700 ring-primary-300 dark:bg-primary-900/30 dark:text-primary-300 dark:ring-primary-700/60", + "bg-primary-50 text-primary-700 ring-primary-300 dark:bg-primary-900/30 dark:text-primary-300 dark:ring-primary-700/60", )} aria-label={t("header.notifications")} onClick={() => @@ -409,7 +409,7 @@ const Top: React.FC = ({ className={cn( "h-4 w-4 transition-colors", openNotifications && - "fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300", + "fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300", )} /> {unseenCount > 0 && ( @@ -479,7 +479,7 @@ const Top: React.FC = ({ className={cn( "flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/30", currentLanguage === lang.value && - "bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white", + "bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white", )} >
diff --git a/apps/edr-freight-web/backoffice/src/user-management/AppMenuTabs.tsx b/apps/edr-freight-web/backoffice/src/user-management/AppMenuTabs.tsx index fb523fc06..127806334 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/AppMenuTabs.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/AppMenuTabs.tsx @@ -1,4 +1,4 @@ -import { NavLink } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { Archive, BarChart, @@ -8,28 +8,48 @@ import { FileText, Globe, Settings, - ShieldAlert, Users2, UsersRound, } from "lucide-react"; -import { useAuth } from "@/shared/context/AuthContext"; -import { usePermissions } from "@/shared/context/PermissionContext"; - import { useTranslation } from "react-i18next"; +import { useAuth } from "@/shared/context/AuthContext"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/shared/common/ui/sidebar"; + export interface MenuItem { label: string; href: string; icon: React.ReactNode; roles?: string[]; - permissions?: string[]; - isPrimary?: boolean; - displayLabel?: string; - children?: MenuItem[]; + /** Sidebar section this item is bucketed under. */ + group: string; } +// Section render order; groups with no role-visible items are skipped. +const GROUP_ORDER = [ + "Overview", + "Organizations", + "Content", + "Records", + "Configuration", + "Archive", + "System", +]; + export const AppMenuTabs = () => { const { user } = useAuth(); + const { pathname } = useLocation(); + const { setOpenMobile } = useSidebar(); + const { t } = useTranslation(); + const userRoles = user?.roles.map((role) => role.key) || []; const menuItems: MenuItem[] = [ @@ -38,282 +58,193 @@ export const AppMenuTabs = () => { href: "/user-management/dashboard", icon: , roles: ["super_admin"], - isPrimary: true, + group: "Overview", }, { label: "organizations", href: "/user-management/organizations", icon: , roles: ["super_admin"], - isPrimary: true, + group: "Organizations", }, - { - label: "organizationAdmins", // Shortened for mobile - displayLabel: "organizationAdmins", // Full label for desktop + label: "organizationAdmins", href: "/user-management/organization_admins", icon: , roles: ["super_admin"], - isPrimary: true, + group: "Organizations", }, { - label: "externalUsers", // Shortened for mobile - displayLabel: "externalUsers", // Full label for desktop + label: "externalUsers", href: "/user-management/external_users", icon: , roles: ["super_admin"], - isPrimary: true, + group: "Organizations", }, { label: "dashboard", href: "/user-management/user_management-dashboard", icon: , roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, + group: "Overview", }, { label: "userManagement", href: "/user-management/user_management", icon: , roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, + group: "Overview", }, - // { - // label: "userPositionApproval", - // href: "/user-management/user-position-approval", - // icon: , - // roles: ["admin", "organization_admin", "unit_admin"], - // permissions: ["can:activateEmployee"], - // isPrimary: true, - // }, - // { - // label: "All Records", - // displayLabel: "All Records", - // href: "/user-management/all-records", - // icon: , - // roles: ["admin", "organization_admin", "unit_admin"], - // permissions: ["can:canViewAllRecords"], - // isPrimary: true, - // }, { - label: "contentManagement", // Shortened for mobile - displayLabel: "contentManagement", // Full label for desktop + label: "contentManagement", href: "/user-management/content-management", icon: , roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, + group: "Content", }, { label: "webManagement", - displayLabel: "webManagement", href: "/user-management/web-management", icon: , roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, - }, - { - label: "Position", - displayLabel: "positionTypes", - href: "/user-management/position-management", - icon: , - roles: ["admin", "organization_admin", "unit_admin", "super_admin"], - isPrimary: true, - }, - { - label: "migratedRecords", - displayLabel: "migratedRecords", - href: "/user-management/migrated-records-management", - icon: , - roles: ["super_admin"], - isPrimary: true, - }, - { - label: "settings", - displayLabel: "settings", - href: "/user-management/organization-settings", - icon: , - roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, + group: "Content", }, { label: "Bulk", - displayLabel: "bulkUpload", href: "/user-management/bulk-upload", icon: , roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, + group: "Content", }, { - label: "Archive Users", - displayLabel: "Archive Users", - href: "/user-management/archive-users", - icon: , - roles: ["super_admin"], - isPrimary: true, - }, - { - label: "Archived Organizations", - displayLabel: "Archived Organizations", - href: "/user-management/archived-organizations", - icon: , - roles: ["super_admin"], - isPrimary: true, - }, - { - label: "Archive Users", - displayLabel: "Archive Users", - href: "/user-management/archives", - icon: , - roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, - }, - { - label: "Archived Units & Positions", - displayLabel: "Archived Units & Positions", - href: "/user-management/archived", - icon: , - roles: ["admin", "organization_admin", "unit_admin"], - isPrimary: true, - }, - { - label: "Sector Reports", - displayLabel: "Sector Reports", - href: "/user-management/sector-reports", - icon: , - roles: ["unit_admin", "admin", "organization_admin"], - isPrimary: true, - }, - { - label: "activityLog", - href: "/user-management/activity_log", - icon: , - roles: ["super_admin"], - isPrimary: false, - }, - { - label: "setting", - href: "/user-management/settings", - icon: , - roles: ["super_admin"], - isPrimary: false, - }, - { - label: "Letter Template", - href: "/user-management/templates", + label: "Position", + href: "/user-management/position-management", icon: , - roles: ["super_admin"], - isPrimary: false, + roles: ["admin", "organization_admin", "unit_admin", "super_admin"], + group: "Configuration", + }, + { + label: "settings", + href: "/user-management/organization-settings", + icon: , + roles: ["admin", "organization_admin", "unit_admin"], + group: "Configuration", }, { label: "Add Site", href: "/user-management/add-site", icon: , roles: ["super_admin"], - isPrimary: true, + group: "Configuration", + }, + { + label: "migratedRecords", + href: "/user-management/migrated-records-management", + icon: , + roles: ["super_admin"], + group: "Records", + }, + { + label: "Sector Reports", + href: "/user-management/sector-reports", + icon: , + roles: ["unit_admin", "admin", "organization_admin"], + group: "Records", + }, + { + label: "Archive Users", + href: "/user-management/archive-users", + icon: , + roles: ["super_admin"], + group: "Archive", + }, + { + label: "Archived Organizations", + href: "/user-management/archived-organizations", + icon: , + roles: ["super_admin"], + group: "Archive", + }, + { + label: "Archive Users", + href: "/user-management/archives", + icon: , + roles: ["admin", "organization_admin", "unit_admin"], + group: "Archive", + }, + { + label: "Archived Units & Positions", + href: "/user-management/archived", + icon: , + roles: ["admin", "organization_admin", "unit_admin"], + group: "Archive", + }, + { + label: "activityLog", + href: "/user-management/activity_log", + icon: , + roles: ["super_admin"], + group: "System", + }, + { + label: "setting", + href: "/user-management/settings", + icon: , + roles: ["super_admin"], + group: "System", + }, + { + label: "Letter Template", + href: "/user-management/templates", + icon: , + roles: ["super_admin"], + group: "System", }, - - // { - // label: "branding.title", - // href: "/user-management/web-management/Branding/Branding", - // icon: , - // roles: ["super_admin"], - // isPrimary: true, - // }, ]; - const { permissions } = usePermissions(); - const { t } = useTranslation(); const filteredMenu = menuItems.filter((item) => - item?.roles?.some((r) => userRoles.includes(r)), - ); - const primaryMenuItems = filteredMenu.filter( - (item) => item.isPrimary !== false, - ); - const secondaryMenuItems = filteredMenu.filter( - (item) => item.isPrimary === false, + item.roles?.some((r) => userRoles.includes(r)), ); + const isActive = (href: string) => + pathname === href || pathname.startsWith(`${href}/`); + return ( - // Sticky (not fixed) so it stays in flow: content below never needs a - // magic offset matching this bar's responsive height. top-16 keeps it - // pinned just below the fixed 64px header while scrolling. - // -mt-8 cancels the excess of 's in-flow h-24 wrapper over its 64px - // fixed header, so the bar sits flush under the header with no jump. - // shrink-0 is load-bearing: as a flex item with overflow-hidden this bar - // would otherwise be flex-squashed to zero height when the page overflows. -
- {/* Mobile View - Two separate rows */} -
- {/* Primary items row */} -
- {primaryMenuItems.map((item) => ( - - `flex items-center gap-1 px-3 py-2.5 mr-2 text-xs font-medium transition-colors whitespace-nowrap ${ - isActive - ? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400" - : "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400" - }` - } - > - {item.icon} - - {t(`organization.${item.label}`, item.label)} - - - ))} -
+ <> + {GROUP_ORDER.map((group) => { + const items = filteredMenu.filter((item) => item.group === group); + if (items.length === 0) return null; - {/* Secondary items row (if any) */} - {secondaryMenuItems.length > 0 && ( -
- {secondaryMenuItems.map((item) => ( - - `flex items-center gap-1 px-3 py-2.5 mr-2 text-xs font-medium transition-colors whitespace-nowrap ${ - isActive - ? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400" - : "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400" - }` - } - > - {item.icon} - - {t(`organization.${item.label}`, item.label)} - - - ))} -
- )} -
- - {/* Desktop View - Single row with all items */} -
-
- {filteredMenu.map((item) => ( - - `flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${ - isActive - ? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400" - : "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400" - }` - } - > - {item.icon} - - {t(`organization.${item.label}`, item.label)} - - - ))} -
-
-
+ return ( + + {group} + + + {items.map((item) => { + const label = t(`organization.${item.label}`, item.label); + return ( + + + setOpenMobile(false)} + > + {item.icon} + {label} + + + + ); + })} + + + + ); + })} + ); }; diff --git a/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx b/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx index 3b33519b6..25f8700e8 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx @@ -1,23 +1,26 @@ -import { useLocation } from "react-router-dom"; -import { Outlet } from "react-router-dom"; +import { Link, Outlet, useLocation } from "react-router-dom"; import { AppMenuTabs } from "./AppMenuTabs"; -import { useSidebar } from "@/shared/common/ui/sidebar"; +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarInset, + SidebarRail, + SidebarTrigger, +} from "@/shared/common/ui/sidebar"; import Top from "@/record-management/components/common/Top"; import { useAuth } from "@/shared/context/AuthContext"; export const AppLayout = () => { const { pathname } = useLocation(); const isAuthPage = pathname === "/"; - const { toggleSidebar } = useSidebar(); const { user } = useAuth(); - + const userRoles = user?.roles?.map((role) => role.key) || []; const isSuperAdmin = userRoles.includes("super_admin"); - - // html/body/#root are `overflow: hidden` (index.css) — the host chrome - // scrolls inside FreightDashboardLayout. This subtree renders its own - // full-page layout instead, so it must be its own scroll container or - // nothing scrolls. Top/AppMenuTabs are `fixed`, unaffected by the scroller. + + // Standalone full-page scroll container for the auth screen — the host + // chrome is `overflow: hidden`, so this subtree must scroll itself. if (isAuthPage) { return (
@@ -27,17 +30,55 @@ export const AppLayout = () => { } return ( - // renders its own in-flow h-24 wrapper around the fixed 64px header, - // so flow already clears the header — no extra top padding here. - // AppMenuTabs is sticky and in flow, so content starts right below it. -
- - -
-
- + <> + {/* is a shared component rendered as a full-width `fixed` 64px bar, + so the sidebar is offset to start beneath it (top-16) rather than + owning the top-left corner. Collapses to an icon rail on desktop; on + mobile it's a Sheet drawer opened by the below (Top's + own burger is a module-nav dropdown, not the sidebar toggle). */} + + + + EDR +
+ + User Management + + + EDR Freight + +
+ +
+ + + + +
+ + + + {/* Top provides its own in-flow h-24 spacer clearing the fixed header. */} + {/* Mobile-only drawer opener — desktop shows the sidebar/rail directly. */} +
+ + + Menu +
-
-
+
+
+ +
+
+ + ); }; From 9baffb6439d4d03f8de0b172a415154a1ba52386 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 22 Jul 2026 12:57:56 +0000 Subject: [PATCH 2/2] refactor: remove duplicate header components and consolidate layout --- .../components/common/Header.tsx | 413 ------- .../common/NavigationHeader/Profile.tsx | 1049 ----------------- .../components/common/Top.tsx | 494 ++++---- .../src/user-management/Applayout.tsx | 176 +-- .../position-management/PositionLists.tsx | 2 +- .../pages/position-management/index.tsx | 8 +- 6 files changed, 345 insertions(+), 1797 deletions(-) delete mode 100644 apps/edr-freight-web/backoffice/src/record-management/components/common/Header.tsx delete mode 100644 apps/edr-freight-web/backoffice/src/record-management/components/common/NavigationHeader/Profile.tsx diff --git a/apps/edr-freight-web/backoffice/src/record-management/components/common/Header.tsx b/apps/edr-freight-web/backoffice/src/record-management/components/common/Header.tsx deleted file mode 100644 index 901001c93..000000000 --- a/apps/edr-freight-web/backoffice/src/record-management/components/common/Header.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { NavLink, useLocation } from "react-router-dom"; -import { cn } from "@/shared/lib/utils"; -import { - LayoutGrid, - Upload, - Download, - CheckSquare, - UserCheck, - FileText, - LucideIcon, - Settings, - Handshake, - BellRing, - BarChart3, -} from "lucide-react"; -import { useUserDetail } from "../hooks/useUserDetail"; -import { useAuthUser } from "@/shared/hooks/useAuthUser"; -import Cookies from "js-cookie"; -import { useDelegations } from "../hooks/useDelegations"; -import Top from "./Top"; - -import { useReport } from "../hooks/useReport"; -import { hasApprovalPermission } from "@/record-management/routes/routes"; -import { useReportByHooks } from "../hooks/useReportByHooks"; -import { useMyCollaborations } from "../hooks/useMyCollaborations"; - -interface HeaderProps { - onToggleSidebar?: () => void; -} - -export interface INavTabs { - icon: LucideIcon; - isVissible?: boolean; - label?: string; - title?: string; - href?: string; - url?: string; - isActive?: boolean; - count?: boolean; - isPrimary?: boolean; - countBadge?: number; - isUrgent?: boolean; -} - -const Header: React.FC = ({ onToggleSidebar }) => { - const baseParams = { - skip: 0, - take: 10, - orderBy: "employeePosition.createdAt:DESC", - }; - const { t } = useTranslation(); - const location = useLocation(); - const { hasDelegated } = useDelegations(baseParams); - const { userDetails, selectedPositionPermissionKeys, selectedPosition } = - useAuthUser(); - const { permissionKeys } = useUserDetail(userDetails); - const useParentCounts = - permissionKeys.includes("can:viewParentPositionRecord") && - new URLSearchParams(location.search).get("view") === "parent"; - const currentPosition = Cookies.get("current-position-id"); - const canViewApprovalTab = hasApprovalPermission( - selectedPositionPermissionKeys, - ); - - // Reuse the auth layer's already-normalized active position instead of - // re-deriving it here. useAuthUser resolves it by matching BOTH `id` and - // `employeePositionId` (and honors the delegated-position cookie), so a - // delegate's position is found and `isDelegate` is reliable. The previous - // local lookup matched `pos.id === selectedPositionId` only — but - // `selectedPositionId` is normalized to `employeePositionId`, so the find - // returned undefined and `!undefined` left every tab visible for delegates. - const canViewDelegationTab = !selectedPosition?.isDelegate; - const userRoles = - userDetails?.roles?.map((r: { key: string }) => r.key) || []; - const showUserManagementShortcut = - userRoles.includes("admin") || - userRoles.includes("unit_admin") || - userRoles.includes("super_admin"); - - const { - dashboard, - ROdashboard, - TotalDraftExternal, - TotalDraftInternal, - TotalDraftCC, - ROTotalDraftIncoming, - ROPending, - TotalUrgentDraftInternal, - TotalUrgentDraftExternal, - ROPendingUrgent, - } = useReport(""); - const { breakdownCounts } = useReportByHooks({ - isSecretary: useParentCounts, - }); - const { data: draftCollaborations } = useMyCollaborations({ - skip: 0, - take: 1, - signStatus: "draft", - }); - const collaborationDraftCount = - draftCollaborations?.count ?? dashboard?.collaborationCount?.draft ?? 0; - const incomingHref = useParentCounts - ? "/record-management/userIncoming?view=parent" - : "/record-management/userIncoming"; - - const navigationTabs = useMemo( - (): INavTabs[] => [ - { - icon: LayoutGrid, - label: t("header.navigation.dashboard"), - href: "/record-management/dashboard", - isActive: location.pathname.includes("/record-management/dashboard"), - isPrimary: true, - // Delegates see only Outgoing, Incoming, Approval — hide Dashboard. - isVissible: canViewDelegationTab, - }, - { - icon: BarChart3, - label: "Reports", - href: "/record-management/sector-reports", - isActive: location.pathname.startsWith( - "/record-management/sector-reports", - ), - isPrimary: false, - isVissible: false, - }, - { - icon: Upload, - label: t("header.navigation.outgoing"), - href: "/record-management/userRecords", - isActive: - location.pathname.startsWith("/record-management/userRecords") && - new URLSearchParams(location.search).get("from") !== "collaborations", - isPrimary: true, - isVissible: true, - }, - { - icon: Download, - label: t("header.navigation.incoming"), - href: incomingHref, - isActive: - location.pathname.startsWith("/record-management/userIncoming") || - location.pathname.startsWith( - "/record-management/viewIncoming/incoming/", - ) || - location.pathname.startsWith( - "/record-management/viewIncoming/internal", - ) || - location.pathname.startsWith("/record-management/viewIncoming/cc"), - isPrimary: true, - isVissible: true, - countBadge: - breakdownCounts.external + - breakdownCounts.externalSmart + - breakdownCounts.internal + - breakdownCounts.ccUnseen + - breakdownCounts.forYourReference, - isUrgent: TotalUrgentDraftInternal > 0 || TotalUrgentDraftExternal > 0, - }, - { - icon: CheckSquare, - label: t("header.navigation.approval"), - href: "/record-management/approval", - isActive: - location.pathname.startsWith("/record-management/approval") || - location.pathname.startsWith("/record-management/viewApproval"), - isPrimary: false, - isVissible: canViewApprovalTab, - countBadge: breakdownCounts?.approval || 0, - isUrgent: - (dashboard?.activeApprovalCount?.myUrgentWorkflowCount || 0) > 0, - }, - { - icon: UserCheck, - label: t("header.navigation.delegation"), - href: "/record-management/delegation", - isActive: location.pathname.startsWith("/record-management/delegation"), - isPrimary: false, - isVissible: canViewDelegationTab, - }, - { - icon: Handshake, - label: t("header.navigation.collaborations"), - href: "/record-management/collaborations", - isActive: - location.pathname.startsWith("/record-management/collaborations") || - new URLSearchParams(location.search).get("from") === "collaborations", - isPrimary: false, - isVissible: canViewDelegationTab, - countBadge: collaborationDraftCount, - }, - { - icon: Settings, - label: t("header.navigation.settings"), - href: "/record-management/uploadTeeterandSignature", - isActive: location.pathname.startsWith( - "/record-management/uploadTeeterandSignature", - ), - isPrimary: false, - isVissible: canViewDelegationTab, - }, - ], - [ - t, - location.pathname, - location.search, - TotalDraftCC, - TotalDraftInternal, - TotalDraftExternal, - TotalUrgentDraftInternal, - TotalUrgentDraftExternal, - canViewApprovalTab, - canViewDelegationTab, - useParentCounts, - incomingHref, - breakdownCounts.approval, - breakdownCounts.ccUnseen, - breakdownCounts.external, - breakdownCounts.externalSmart, - breakdownCounts.forYourReference, - breakdownCounts.internal, - dashboard?.activeApprovalCount?.myNotUrgentWorkflowsCount, - dashboard?.activeApprovalCount?.myUrgentWorkflowCount, - collaborationDraftCount, - ], - ); - - const recordOfficerNavs = useMemo( - (): INavTabs[] => [ - { - icon: LayoutGrid, - label: t("header.navigation.dashboard"), - href: "/record-management/dashboard", - isActive: location.pathname.includes("/dashboard"), - isVissible: true, - }, - { - icon: Upload, - label: t("header.navigation.outgoing"), - href: "/record-management/recordOfficer/outgoing", - isActive: - location.pathname.startsWith( - "/record-management/recordOfficer/outgoing", - ) || - location.pathname.includes("/record-management/view/") || - location.pathname.includes("/record-management/outgoingview/"), - isVissible: true, - }, - { - icon: Download, - label: t("header.navigation.incoming"), - href: "/record-management/recordOfficer/incoming", - isActive: - location.pathname.startsWith( - "/record-management/recordOfficer/incoming", - ) || - location.pathname.includes("/record-management/viewIncoming/") || - location.pathname.includes("/record-management/recordViewIncoming/"), - isVissible: true, - countBadge: ROTotalDraftIncoming, - }, - { - icon: FileText, - label: t("header.navigation.pending"), - href: "/record-management/pending", - isActive: - location.pathname.startsWith("/record-management/pending") || - location.pathname.includes("/record-management/viewPending/"), - isVissible: true, - countBadge: ROPending, - isUrgent: ROPendingUrgent > 0, - }, - ], - [t, location.pathname, ROTotalDraftIncoming, ROPending, ROPendingUrgent], - ); - - const activeNavigationTabs = useMemo(() => { - if (permissionKeys.includes("can:dispatchRecords")) { - return recordOfficerNavs; - } - return navigationTabs.filter((tab) => tab.isVissible); - }, [permissionKeys, navigationTabs, recordOfficerNavs]); - - // const { user } = useAuth(); - // console.log("User Info:", user); - // const organizationId = - // user?.employee && user.employee.length > 0 - // ? user.employee[0].unitId - // : undefined; - - // const unitId = organizationId; - return ( -
- - - {/* Navigation Tabs */} - {currentPosition ? ( -
- {/* Mobile layout: Simple horizontal scrollable (up to sm) */} -
-
- {activeNavigationTabs.map((tab, index) => ( - - cn( - "flex items-center gap-1 px-3 py-2 mr-2 text-xs font-medium transition-colors whitespace-nowrap relative", - isActive - ? "text-primary-800 dark:text-white border-b-2 border-primary-600 dark:border-primary-400" - : "text-gray-700 dark:text-gray-300 hover:text-primary-800 dark:hover:text-white", - ) - } - > - - {tab.label} - - {/* Count Badge - positioned on tab */} - {typeof tab?.countBadge === "number" && - tab.countBadge > 0 && ( - - {tab.countBadge > 99 ? "99+" : tab.countBadge} - - )} - - {/* Urgent Bell - positioned on tab */} - {typeof tab?.isUrgent === "boolean" && tab.isUrgent && ( - - - - - )} - - {/* Live indicator for Delegation */} - {hasDelegated && - (tab.label?.toLowerCase() === "delegation" || - tab.label?.toLowerCase() === "ዉክልና") && ( - - - - - )} - - ))} -
-
- - {/* Desktop layout: All tabs in one row (sm and up) */} -
-
- {activeNavigationTabs.map((tab, index) => ( - - cn( - "flex items-center justify-center px-2.5 py-2.5 rounded-sm text-sm whitespace-nowrap flex-1 font-Urbanist relative", - tab.isActive - ? "bg-primary-50 dark:bg-primary-900/40 text-primary-800 dark:text-white font-medium" - : "text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700", - ) - } - > - - - {tab.label} - - {/* Count Badge (inline) */} - {typeof tab?.countBadge === "number" && - tab.countBadge > 0 && ( - - {tab.countBadge > 99 ? "99+" : tab.countBadge} - - )} - - {/* Urgent Bell (pulsing) */} - {typeof tab?.isUrgent === "boolean" && tab.isUrgent && ( - - {/* Pulse effect */} - - {/* Bell icon */} - - - - - )} - - - {/* Live indicator for Delegation */} - {hasDelegated && - (tab.label?.toLowerCase() === "delegation" || - tab.label?.toLowerCase() === "ዉክልና") && ( - - - - - )} - - ))} -
-
-
- ) : null} -
- ); -}; - -export default Header; diff --git a/apps/edr-freight-web/backoffice/src/record-management/components/common/NavigationHeader/Profile.tsx b/apps/edr-freight-web/backoffice/src/record-management/components/common/NavigationHeader/Profile.tsx deleted file mode 100644 index 6d128954a..000000000 --- a/apps/edr-freight-web/backoffice/src/record-management/components/common/NavigationHeader/Profile.tsx +++ /dev/null @@ -1,1049 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/shared/common/ui/card"; -import { Badge } from "@/shared/common/ui/badge"; -import { - ChevronLeft, - Mail, - User, - Briefcase, - Key, - CheckCircle2, - Shield, - Lock, - CircleUser, - Star, - ShieldCheck, - Clock, - UserPen, - ShieldOff, - IdCard, - Settings, - Building, - Users, - FileText, - ChevronDown, - LogOut, - Monitor, -} from "lucide-react"; -import { Button } from "@/shared/common/ui/button"; -import { NavLink, useNavigate } from "react-router-dom"; -import { useAuthUser } from "../../../hooks/useAuthUser"; -import { useTranslation } from "react-i18next"; -import { motion } from "framer-motion"; -import { useLocalizedName } from "@/shared/common/localizedName"; -import Top from "../Top"; -import { Label } from "@/shared/common/ui/label"; -import { Switch } from "@/shared/common/ui/switch"; -import { useSessions } from "@/shared/hooks/useSession"; -import { useQueryClient } from "@tanstack/react-query"; -import Loader from "@/record-management/components/Loader/loader"; - -// Define TypeScript interfaces for the props -interface PositionDetailItemProps { - icon: React.ReactNode; - label: string; - value: string; -} - -interface InfoBoxProps { - icon: React.ReactNode; - label: string; - value: string; - color?: "blue" | "purple" | "green" | "gray"; - fullWidth?: boolean; - amharic?: boolean; - capitalize?: boolean; -} - -interface ProfileInfoItemProps { - icon: React.ReactNode; - label: string; - value: string; - capitalize?: boolean; -} - -interface SecurityItemProps { - title: string; - description: string; - action: React.ReactNode; - status?: "secure" | "warning" | "inactive"; - - twoFactorStatus?: "on" | "off"; -} - -interface ActivityItemProps { - icon: React.ReactNode; - title: string; - time: string; - type: "success" | "info" | "warning"; -} - -const ProfilePage = () => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const queryClient = useQueryClient(); - - const { - userDetails, - isLoading, - isError, - refetch, - logout, - setTwoFactorAuth, - twoFactorData, - editTwoFA, - isLoadingStatus, - } = useAuthUser(); - const userRoles = userDetails?.roles?.map((r: { key: string }) => r.key) || []; - const showUserManagementShortcut = - userRoles.includes("admin") || - userRoles.includes("unit_admin") || - userRoles.includes("super_admin"); - const localizedName = useLocalizedName(); - const [isSessionsExpanded, setIsSessionsExpanded] = useState(false); - const [twoFactor, setTwoFactor] = useState(false); - const [permissionsExpanded, setPermissionsExpanded] = useState(false); - const PERMISSIONS_INITIAL_SHOW = 4; - useEffect(() => { - if (!userDetails) return; - if (twoFactorData?.[0]) { - setTwoFactor(twoFactorData?.[0].isMFARequired); - } - }, [twoFactorData, userDetails]); - - const { - data: sessionsQuery, - deleteSession, - deleteAllSessions, - } = useSessions({ - skip: 0, - take: 10, - orderBy: "CreatedAt:DESC", - }); - const userSessions = sessionsQuery?.sessions || []; - const handleLogoutSession = (sessionId: string) => { - deleteSession(sessionId); - queryClient.invalidateQueries({ queryKey: ["my-sessions"] }); - }; - const handleLogoutAllSessions = () => { - const otherSessions = userSessions.filter((s) => s.id); - - if (otherSessions.length > 0) { - deleteAllSessions({ sessionIds: otherSessions.map((s) => s.id) }); - queryClient.invalidateQueries({ queryKey: ["my-sessions"] }); - } - }; - const handleFactorAuthentication = () => { - if (isLoading || isLoadingStatus) return; // prevent duplicate clicks - if (!userDetails) return; - - const item = twoFactorData?.[0]; - const hasExistingRecord = !!item?.id; - if (!hasExistingRecord) { - // ✅ POST only if no record exists at all - setTwoFactorAuth({ isMFARequired: true }); - } else { - // Toggle using PUT/edit if a record already exists - const newStatus = !twoFactor; - editTwoFA({ id: item.id, isEnabled: newStatus }); - } - }; - - const handleLogout = () => { - logout(); - }; - - if (isLoading) { - return ; - } - - if (isError || !userDetails) { - return ( -
- -
- - - -

- {t("profile.error")} -

-
- - - -
-
- ); - } - - const employeeData = userDetails.employee?.[0] || {}; - const position = employeeData.positions?.[0] || {}; - const positionIsDeletation = employeeData.positions.filter( - (item: any) => item.isDelegate - ); - const positionHasDeletation = employeeData.positions.filter( - (item: any) => item.hasDelegated - ); - // Animation variants - const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, - }; - - const item = { - hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0 }, - }; - - // New reusable components for the improved layout - const PositionDetailItem: React.FC = ({ - icon, - label, - value, - }) => ( - -
- {icon} -
-
-

- {label} -

-

- {value} -

-
-
- ); - - const InfoBox: React.FC = ({ - icon, - label, - value, - color = "gray", - fullWidth = false, - amharic = false, - capitalize = false, - }) => ( - -
-
- {icon} -
-

- {label} -

-
-

- {value || "Not specified"} -

-
- ); - - // Reusable Component: ProfileInfoItem - const ProfileInfoItem: React.FC = ({ - icon, - label, - value, - capitalize = false, - }) => ( - -
- {icon} -
-
-

{label}

-

- {value} -

-
-
- ); - - // Reusable Component: SecurityItem - const SecurityItem: React.FC = ({ - title, - description, - status, - action, - twoFactorStatus, - }) => ( - -
-
-
-

- {title} -

-

- {description} -

-
-
- {action} -
- ); - - // Reusable Component: ActivityItem - const ActivityItem: React.FC = ({ - icon, - title, - time, - type, - }) => ( - -
- {icon} -
-
-

{title}

-

{time}

-
-
- ); - - return ( -
- -
- {/* Enhanced Header */} - -
- - - - -
- - -
- - {/* User Quick Stats */} - -
-
- - Online - -
-
- - {t(`profile.userTypes.${userDetails.userType}`)} - -
-
- - {/* Main Content Grid - Reorganized */} - - {/* Left Sidebar - Profile & Account Status */} -
- {/* Enhanced Profile Card */} - - - {/* Profile Header with Gradient */} -
-
-
- - {userDetails.status === "accepted" - ? "Verified" - : "Pending"} - -
- - {/* Profile Avatar */} -
- -
- -
-
-
-
-
- - - {/* User Info */} -
- - {localizedName(userDetails.name) || t("profile.noName")} - -
- - @{userDetails.username} -
-
- - {/* Status Indicators */} -
- -
- {positionIsDeletation ? "Yes" : "No"} -
-
Delegate
-
- -
- {position.permissions?.length || 0} -
-
Permissions
-
-
- - {/* Profile Details */} -
- } - label={t("profile.email")} - value={userDetails.email} - /> - } - label={t("profile.userType")} - value={t(`profile.userTypes.${userDetails.userType}`)} - capitalize - /> - } - label={t("profile.passwordSet")} - value={ - userDetails.hasSetPassword - ? t("common.yes") - : t("common.no") - } - /> -
- - {/* Action Buttons */} -
- - - - - - -
-
-
-
- - {/* Account Status Card */} - - - - - - Account Status - - - - {/* Verification Status */} -
-
-
- {userDetails.status === "accepted" ? ( - - ) : ( - - )} -
-
-

- Account Status -

-

- {userDetails.status === "accepted" - ? "Verified and active" - : "Pending approval"} -

-
-
- - {userDetails.status === "accepted" ? "Active" : "Pending"} - -
- - {/* Password Status */} -
-
-
- -
-
-

- Password -

-

- {userDetails.hasSetPassword - ? "Secured with password" - : "No password set"} -

-
-
- - {userDetails.hasSetPassword ? "Set" : "Not Set"} - -
- - {/* Quick Action */} -
- - - -
-
-
-
-
- - {/* Right Column - Main Content */} -
- - - - -
- -
-
- Employee Information -

- Personal & organizational details -

-
-
-
- -
- {/* Basic Info */} -
- } - label="Employee ID" - value={employeeData?.id || "Not specified"} - color="blue" - /> - } - label="Organization ID" - value={employeeData?.organizationId || "Not specified"} - color="purple" - /> - } - label="User Type" - value={userDetails.userType} - color="green" - capitalize - /> -
- - {/* Names */} -
- } - label="English Name" - value={employeeData?.name?.en || "Not specified"} - fullWidth - /> - } - label="Amharic Name" - value={employeeData?.name?.am || "Not specified"} - fullWidth - amharic - /> -
- - {/* Position Summary */} - {userDetails.employee[0]?.positions?.[0] && ( -
-

- - Current Position -

-
-
-

- Position -

-

- {userDetails.employee[0].positions[0].name?.en || - "Not specified"} -

-
-
-

- Delegation -

- - {positionHasDeletation.length ? "Active" : "None"} - -
-
-
- )} -
-
-
-
- {/* Top Row: Position Details & Security */} -
- {/* Position Details */} - - - - -
- -
-
- {t("profile.positionDetails")} -

- Your current role -

-
-
-
- -
- } - label={t("profile.positionName")} - value={localizedName(position.name) || t("common.na")} - /> - } - label={t("profile.positionKey")} - value={position.key || t("common.na")} - /> - } - label={t("profile.employeeId")} - value={employeeData.id || t("common.na")} - /> -
-
-
-
- - {/* Security Card */} - - - - -
- -
-
- {t("profile.accountSecurity")} -

- Account safety -

-
-
-
- -
- - - - } - /> - - - -
- } - /> -
-
-
- - - {t("profile.sessions")} - - {userSessions.length > 0 && ( - - {userSessions.length} active - - )} -
- -
- - {/* Expandable Sessions Content */} - {isSessionsExpanded && ( - -
- {userSessions.length === 0 ? ( -
- -

No active sessions

-
- ) : ( - <> - {userSessions.map((session) => ( -
-
-
- -
-
-

- Device: {session.device} -

-

- Email: {session.email} -

-

- Created:{" "} - {new Date( - session.createdAt - ).toLocaleString()} -

-
-
- -
- ))} - - - - )} -
-
- )} -
-
- - - -
- - {/* Middle Row: Permissions */} - - - - -
- -
-
- {t("profile.permissions")} -

- Access rights and privileges -

-
-
-
- - {position.permissions?.length > 0 ? ( - - {(permissionsExpanded ? position.permissions : position.permissions.slice(0, PERMISSIONS_INITIAL_SHOW)).map((perm: any, index: number) => ( - -
- -
- - {perm.key} - -
- ))} - {position.permissions.length > PERMISSIONS_INITIAL_SHOW && ( - - )} -
- ) : ( -
-
- -
-

- {t("profile.noPermissions")} -

-

- No special access rights assigned -

-
- )} -
-
-
-
- -
-
- ); -}; - -export default ProfilePage; diff --git a/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx b/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx index 71afbee2d..82e716d66 100644 --- a/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx +++ b/apps/edr-freight-web/backoffice/src/record-management/components/common/Top.tsx @@ -17,7 +17,6 @@ import { ClipboardList, Clock, FileText, - Home, Languages, Key, LogOut, @@ -256,10 +255,28 @@ const Top: React.FC = ({ }; return ( -
-
-
-
+
+
+
+ {onToggleSidebar ? ( + + + + + + Toggle sidebar + + + ) : ( @@ -320,191 +337,183 @@ const Top: React.FC = ({ ))} + )} -
- -
- - {showUserManagementShortcut && ( - - )} +
+
-
+ {showUserManagementShortcut && ( + + )} +
+ +
+ + + {canActivateUsers && ( +
+ + + {pendingUsersCount > 0 && ( + + {pendingUsersCount > 99 ? "99+" : pendingUsersCount} + + )} + + {openPendingUsers && ( +
+ +
+ )} +
+ )} + +
- {canActivateUsers && ( -
- - - {pendingUsersCount > 0 && ( - - {pendingUsersCount > 99 ? "99+" : pendingUsersCount} - - )} - - {openPendingUsers && ( -
- -
- )} + {openNotifications && ( +
+
)} +
-
- + {openReminders && ( +
+ +
+ )} +
+ + + + + + + + {UI_LANGUAGE_OPTIONS.map((lang) => ( + changeLanguage(lang.value)} className={cn( - "h-4 w-4 transition-colors", - openNotifications && - "fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300", + "flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/30", + currentLanguage === lang.value && + "bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white", )} - /> - {unseenCount > 0 && ( - - {unseenCount > 99 ? "99+" : unseenCount} - - )} - + > +
+ + {getUiLanguageShortLabel(lang.value, t)} + + {getUiLanguageLabel(lang.value, t)} +
+ {currentLanguage === lang.value && ( + + )} +
+ ))} +
+
- {openNotifications && ( -
- -
- )} -
- - {/* Reminders */} -
+ + - {openReminders && ( -
- -
- )} -
- - - - - - - - {UI_LANGUAGE_OPTIONS.map((lang) => ( - changeLanguage(lang.value)} - className={cn( - "flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/30", - currentLanguage === lang.value && - "bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white", - )} - > -
- - {getUiLanguageShortLabel(lang.value, t)} - - {getUiLanguageLabel(lang.value, t)} -
- {currentLanguage === lang.value && ( - - )} -
- ))} -
-
- - - - - - - -
-

{fullName}

-

- {roleLabel} -

+ > + {initials.toUpperCase()}
+
+ + {fullName} + + + {roleLabel} + +
+ + + + +
+

{fullName}

+

+ {roleLabel} +

+
+ + navigate("/profile")} + > + + {t("header.viewProfile")} + + + navigate("/update-profile")} + > + + {t("header.editProfile")} + + + navigate("/change-password")} + > + + {t("header.changePassword")} + + + {showRecordManagementShortcut && ( navigate("/profile")} + onClick={() => navigate("/record-management/dashboard")} > - - {t("header.viewProfile")} + + {t("nav.Record Management")} + )} + {showUserManagementShortcut && ( navigate("/update-profile")} + onClick={() => navigate("/user-management")} > - - {t("header.editProfile")} + + + {t("dashboard.userManagement", "User Management")} + + )} - navigate("/change-password")} - > - - {t("header.changePassword")} - + - {showRecordManagementShortcut && ( - navigate("/record-management/dashboard")} - > - - {t("nav.Record Management")} - - )} - - {showUserManagementShortcut && ( - navigate("/user-management")} - > - - - {t("dashboard.userManagement", "User Management")} - - - )} - - - - - - {t("header.signOut")} - -
-
-
+ + + {t("header.signOut")} + + +
-
-
+
+
); }; diff --git a/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx b/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx index 25f8700e8..ade96250d 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/Applayout.tsx @@ -1,84 +1,92 @@ -import { Link, Outlet, useLocation } from "react-router-dom"; -import { AppMenuTabs } from "./AppMenuTabs"; -import { - Sidebar, - SidebarContent, - SidebarHeader, - SidebarInset, - SidebarRail, - SidebarTrigger, -} from "@/shared/common/ui/sidebar"; -import Top from "@/record-management/components/common/Top"; -import { useAuth } from "@/shared/context/AuthContext"; - -export const AppLayout = () => { - const { pathname } = useLocation(); - const isAuthPage = pathname === "/"; - const { user } = useAuth(); - - const userRoles = user?.roles?.map((role) => role.key) || []; - const isSuperAdmin = userRoles.includes("super_admin"); - - // Standalone full-page scroll container for the auth screen — the host - // chrome is `overflow: hidden`, so this subtree must scroll itself. - if (isAuthPage) { - return ( -
- -
- ); - } - - return ( - <> - {/* is a shared component rendered as a full-width `fixed` 64px bar, - so the sidebar is offset to start beneath it (top-16) rather than - owning the top-left corner. Collapses to an icon rail on desktop; on - mobile it's a Sheet drawer opened by the below (Top's - own burger is a module-nav dropdown, not the sidebar toggle). */} - - - - EDR -
- - User Management - - - EDR Freight - -
- -
- - - - -
- - - - {/* Top provides its own in-flow h-24 spacer clearing the fixed header. */} - {/* Mobile-only drawer opener — desktop shows the sidebar/rail directly. */} -
- - - Menu - -
-
-
- -
-
-
- - ); -}; +import { Link, Outlet, useLocation } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import { AppMenuTabs } from "./AppMenuTabs"; +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarInset, + SidebarRail, + useSidebar, +} from "@/shared/common/ui/sidebar"; +import Top from "@/record-management/components/common/Top"; +import { useAuth } from "@/shared/context/AuthContext"; + +export const AppLayout = () => { + const { pathname } = useLocation(); + const isAuthPage = pathname === "/"; + const { toggleSidebar } = useSidebar(); + const { user } = useAuth(); + + const userRoles = user?.roles?.map((role) => role.key) || []; + const isSuperAdmin = userRoles.includes("super_admin"); + + // Standalone full-page scroll container for the auth screen — the host + // chrome is `overflow: hidden`, so this subtree must scroll itself. + if (isAuthPage) { + return ( +
+ +
+ ); + } + + return ( + <> + {/* Full-height sidebar owning the left column (back-to-home + brand at the + top). lives inside , to the right of the sidebar, + and is `sticky` — so it never overlaps the sidebar and re-flows when + the sidebar collapses to its icon rail. Top's burger (onToggleSidebar) + drives collapse on desktop and the Sheet drawer on mobile. */} + + + + + + Back to home + + + + EDR +
+ + User Management + + + EDR Freight + +
+ +
+ + + + +
+ + {/* Internal scroll container: the sidebar stays fixed and full-height + while this column scrolls beneath the sticky bar. */} + + +
+
+ +
+
+
+ + ); +}; diff --git a/apps/edr-freight-web/backoffice/src/user-management/components/position-management/PositionLists.tsx b/apps/edr-freight-web/backoffice/src/user-management/components/position-management/PositionLists.tsx index 9af344caf..d15d82799 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/components/position-management/PositionLists.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/components/position-management/PositionLists.tsx @@ -210,7 +210,7 @@ export default function PositionManagement() { return (
- + {t("contentManagement.permissionType")} diff --git a/apps/edr-freight-web/backoffice/src/user-management/pages/position-management/index.tsx b/apps/edr-freight-web/backoffice/src/user-management/pages/position-management/index.tsx index 57d7a1898..a052caa11 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/pages/position-management/index.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/pages/position-management/index.tsx @@ -1,13 +1,7 @@ import PositionManagement from "@/user-management/components/position-management/PositionLists"; -import { t } from "i18next"; const PositionManagementPage = () => { - return ( -
-

{t("contentManagement.permissionManagement")}

- -
- ); + return ; }; export default PositionManagementPage;