feat: better navigation in backoffice

This commit is contained in:
Nathnael
2026-08-07 12:24:30 +00:00
parent d5d7c91e24
commit 1e9149ce00
12 changed files with 861 additions and 676 deletions

View File

@@ -1,36 +1,3 @@
import {
ArrowLeftRight,
Boxes,
Building2,
BarChart3,
Container,
FileSignature,
FileText,
Hammer,
History,
LayoutDashboard,
LayoutGrid,
MapPin,
Network,
Package,
PackageCheck,
PackageOpen,
Paperclip,
Receipt,
ScrollText,
Send,
Settings,
ShieldCheck,
Ship,
SlidersHorizontal,
Train,
Truck,
Users,
Wallet,
LifeBuoy,
TrainFront,
XCircle,
} from "lucide-react";
import { useEffect } from "react";
import {
Navigate,
@@ -42,11 +9,7 @@ import {
useParams,
} from "react-router-dom";
import {
FreightDashboardLayout,
type SidebarItem,
type SidebarSection,
} from "@/components/layout";
import { FreightDashboardLayout, type SidebarItem } from "@/components/layout";
import { useAuth } from "./auth/useAuth";
import LoadingScreen from "./components/LoadingScreen";
import LoginPage from "./pages/auth/LoginPage";
@@ -84,11 +47,12 @@ import AuditLogsPage from "./pages/audit/AuditLogsPage";
import { RequirePermission } from "./components/auth/RequirePermission";
import {
FREIGHT_PERMS,
hasPermission as hasFreightPermission,
isDjiboutiGl,
isEthiopianGl,
isSuperAdmin,
} from "./lib/permissions";
import { NO_ACCESS_PATH, resolveLandingPath } from "./lib/landing";
import NoAccessPage from "./pages/NoAccessPage";
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage";
import ContractTemplatesPage from "./pages/contract_templates/ContractTemplatesPage";
@@ -108,7 +72,6 @@ import CompliancePage from "./pages/fleet/CompliancePage";
import IncidentsPage from "./pages/fleet/IncidentsPage";
import WorkOrdersPage from "./pages/fleet/WorkOrdersPage";
import ProcurementPage from "./pages/fleet/ProcurementPage";
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
import CargoTypesPage from "./pages/ruleEngine/CargoTypesPage";
@@ -151,615 +114,15 @@ import FaydaCallbackPage from "./pages/FaydaCallbackPage";
import { UserManagementRoutes } from "./user-management/route";
import SetPassword from "./shared/components/SetPassword";
import SupportInboxPage from "./pages/support/SupportInboxPage";
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
{
title: "Main menu",
items: [
{
label: "Overview",
href: "/dashboard/overview",
icon: <LayoutDashboard />,
permission: FREIGHT_PERMS.overview.view,
},
{
label: "Reports",
href: "/dashboard/reports",
icon: <BarChart3 />,
permission: FREIGHT_PERMS.reports.view,
},
{
label: "Customers",
href: "/dashboard/customers",
icon: <Building2 />,
permission: FREIGHT_PERMS.customers.view,
},
{
label: "Contracts",
href: "/dashboard/contract-requests",
icon: <FileSignature />,
permission: FREIGHT_PERMS.contracts.view,
},
{
label: "Bookings",
href: "/dashboard/booking-requests",
icon: <FileText />,
permission: FREIGHT_PERMS.bookings.view,
},
{
label: "Wagon cancellations",
href: "/dashboard/wagon-cancellations",
icon: <XCircle />,
permission: FREIGHT_PERMS.bookings.wagonCancellationView,
},
// Operations hub: per-shipment clearance-document review for services
// WITHOUT customs clearing (self-clearance) — bookings only.
{
label: "Clearance Documents",
href: "/dashboard/contracts/clearance-documents",
icon: <ShieldCheck />,
permission: FREIGHT_PERMS.contracts.opsClearanceReview,
},
{
label: "Payments",
href: "/dashboard/payments",
icon: <Wallet />,
permission: FREIGHT_PERMS.payments.view,
},
{
label: "Invoices",
href: "/dashboard/invoices",
icon: <Receipt />,
permission: FREIGHT_PERMS.invoices.view,
},
{
label: "Support",
href: "/dashboard/support",
icon: <LifeBuoy />,
permission: FREIGHT_PERMS.support.agentView,
},
...demoItems,
],
},
{
// title: "Port & Terminal",
items: [
{
label: "Operations",
icon: <Settings />,
children: [
{
label: "Clearance",
href: "/dashboard/contracts/clearance",
icon: <ShieldCheck />,
permission: [
FREIGHT_PERMS.contracts.clearanceReview,
FREIGHT_PERMS.contracts.clearanceEtActions,
],
},
// {
// label: "Shipment Requests",
// href: "/dashboard/shipment-requests",
// icon: <Send />,
// permission: FREIGHT_PERMS.contracts.createBooking,
// },
// Operations Path A queue: per-booking self-clearance review for
// GENERAL non-customs booking instances (and legacy self-clear bookings).
// {
// label: "Self-Clearance Review",
// href: "/dashboard/contracts/ops-clearance",
// icon: <ShieldCheck />,
// permission: FREIGHT_PERMS.contracts.opsClearanceReview,
// },
{
label: "GL Djibouti Clearance",
href: "/dashboard/gl-djibouti/clearance",
icon: <Ship />,
permission: FREIGHT_PERMS.contracts.clearanceDjActions,
},
{
label: "Train Schedules",
href: "/dashboard/operations/train-scheduling-v2",
icon: <Train />,
permission: FREIGHT_PERMS.trainScheduling.view,
},
{
label: "Batch Board",
href: "/dashboard/operations/batch-board",
icon: <LayoutGrid />,
permission: FREIGHT_PERMS.trainScheduling.view,
},
{
label: "First Mile",
href: "/dashboard/operations/first-mile",
icon: <Truck />,
permission: FREIGHT_PERMS.firstMile.view,
},
{
label: "Last Mile",
href: "/dashboard/operations/last-mile",
icon: <Truck />,
permission: FREIGHT_PERMS.lastMile.view,
},
],
},
{
label: "Fleet Management",
icon: <Truck />,
children: [
{
label: "Fleet Dashboard",
href: "/dashboard/fleet-dashboard",
icon: <LayoutDashboard />,
permission: FREIGHT_PERMS.fleetDashboard.view,
},
{
label: "Routes",
href: "/dashboard/routes",
icon: <Network />,
permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Locomotives",
href: "/dashboard/locomotives",
icon: <Train />,
permission: [
FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.fleet.view,
],
},
{
label: "Train Builder",
href: "/dashboard/train-builder",
icon: <Hammer />,
permission: [FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view],
},
// {
// label: "Wagon types",
// href: "/dashboard/wagon-types",
// icon: <Boxes />,
// },
{
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Wagon Transfers",
href: "/dashboard/wagon-transfers",
icon: <ArrowLeftRight />,
permission: [
FREIGHT_PERMS.wagons.transferView,
FREIGHT_PERMS.wagons.view,
],
},
{
label: "Vehicles",
href: "/dashboard/vehicles",
icon: <Truck />,
permission: FREIGHT_PERMS.vehicles.view,
},
{
label: "Drivers",
href: "/dashboard/drivers",
icon: <Users />,
permission: FREIGHT_PERMS.drivers.view,
},
{
label: "Track Vehicles",
href: "/dashboard/tracking",
icon: <MapPin />,
permission: FREIGHT_PERMS.tracking.view,
},
{
label: "Fuel Purchases",
href: "/dashboard/fuel-purchases",
icon: <Truck />,
permission: FREIGHT_PERMS.fuel.view,
},
{
label: "Fuel Analytics",
href: "/dashboard/fuel-stats",
icon: <Truck />,
permission: FREIGHT_PERMS.fuel.view,
},
{
label: "Maintenance",
href: "/dashboard/maintenance",
icon: <Truck />,
permission: FREIGHT_PERMS.maintenance.view,
},
{
label: "Work Orders",
href: "/dashboard/work-orders",
icon: <SlidersHorizontal />,
permission: FREIGHT_PERMS.maintenance.view,
},
{
label: "Compliance & Alerts",
href: "/dashboard/compliance",
icon: <ShieldCheck />,
permission: FREIGHT_PERMS.fleet.view,
},
{
label: "Incidents",
href: "/dashboard/incidents",
icon: <FileText />,
permission: FREIGHT_PERMS.fleet.view,
},
{
label: "Procurement",
href: "/dashboard/procurement",
icon: <Package />,
permission: FREIGHT_PERMS.fleet.view,
},
{
label: "Financial Reports",
href: "/dashboard/financial-reports",
icon: <Wallet />,
permission: FREIGHT_PERMS.fleetReports.view,
},
// {
// label: "Containers",
// href: "/dashboard/containers",
// icon: <Container />,
// },
// {
// label: "Cargoes",
// href: "/dashboard/cargoes",
// icon: <Package />,
// },
],
},
{
label: "Imports",
href: "/dashboard/import-warehouse",
icon: <PackageOpen />,
permission: FREIGHT_PERMS.warehouseInventory.view,
children: [
{
label: "Import Overview",
href: "/dashboard/import-warehouse",
icon: <PackageOpen />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Arrival Queue",
href: "/dashboard/arrival-queue",
icon: <PackageOpen />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Import Trucks",
href: "/dashboard/import-trucks",
icon: <Truck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "EDR Last Mile Returns",
href: "/dashboard/edr-last-mile-returns",
icon: <Container />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Container Returns",
href: "/dashboard/container-returns",
icon: <Container />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Terminal Inventory",
href: "/dashboard/warehouse-inventory?direction=IMPORT",
icon: <Package />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Inventory Inquiry",
href: "/dashboard/inventory-inquiry",
icon: <Boxes />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
],
},
{
label: "Exports",
href: "/dashboard/export-warehouse",
icon: <Truck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
children: [
{
label: "Export Overview",
href: "/dashboard/export-warehouse",
icon: <Truck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Loading Queue",
href: "/dashboard/loading-queue",
icon: <Truck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Loaded Inventory",
href: "/dashboard/loaded-inventory",
icon: <PackageCheck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Dispatch Queue",
href: "/dashboard/dispatch-queue",
icon: <Send />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Djibouti Unloading",
href: "/dashboard/export-djibouti-unloading",
icon: <PackageOpen />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Interchange Documents",
href: "/dashboard/interchange-documents",
icon: <FileText />,
permission: FREIGHT_PERMS.interchangeDocuments.view,
},
{
label: "Terminal Inventory",
href: "/dashboard/warehouse-inventory?direction=EXPORT",
icon: <Package />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
],
},
{
label: "Intercity",
href: "/dashboard/intercity",
icon: <TrainFront />,
permission: FREIGHT_PERMS.trainScheduling.view,
children: [
{
label: "Intercity Cargo",
href: "/dashboard/intercity",
icon: <TrainFront />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
],
},
{
label: "Warehouse Management",
icon: <Container />,
children: [
{
label: "Warehouse Dashboard",
href: "/dashboard/warehouse-dashboard",
icon: <LayoutDashboard />,
permission: FREIGHT_PERMS.warehouseDashboard.view,
},
{
// Yard-wide, not per-direction: the gate sees import and export
// trucks at the same barrier.
label: "Trucks on Site",
href: "/dashboard/trucks-on-site",
icon: <Truck />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Warehouses",
href: "/dashboard/warehouses",
icon: <Container />,
permission: FREIGHT_PERMS.warehouses.view,
},
{
label: "Allocation & Fees",
href: "/dashboard/warehouse-rules",
icon: <SlidersHorizontal />,
permission: [
FREIGHT_PERMS.warehouseAllocationRules.view,
FREIGHT_PERMS.warehouseFeeRules.view,
],
},
{
label: "Fee Invoices",
href: "/dashboard/warehouse-fee-invoices",
icon: <Wallet />,
permission: FREIGHT_PERMS.warehouseFeeInvoices.view,
},
],
},
],
},
{
title: "Freight configuration",
mutedTitle: true,
items: [
{
label: "File settings",
href: "/dashboard/file-settings",
icon: <Paperclip />,
permission: FREIGHT_PERMS.admin,
},
{
label: "Dropdown settings",
href: "/dashboard/dropdown-settings",
icon: <Settings />,
permission: FREIGHT_PERMS.admin,
},
{
label: "Contract templates",
href: "/dashboard/contract-templates",
icon: <ScrollText />,
permission: FREIGHT_PERMS.admin,
},
{
label: "Audit logs",
href: "/dashboard/audit-logs",
icon: <History />,
permission: FREIGHT_PERMS.audit.view,
},
{
label: "Configuration",
href: "/dashboard/configuration",
icon: <Boxes />,
children: [
...getCategorySidebarChildren("configuration"),
{
label: "Train scheduling rules",
href: "/dashboard/configuration/train-scheduling-rules",
permission: FREIGHT_PERMS.trainScheduling.rulesManage,
},
{
label: "Trade access",
href: "/dashboard/configuration/trade-access",
permission: FREIGHT_PERMS.admin,
},
{
label: "Exchange rate",
href: "/dashboard/configuration/exchange-rate",
permission: FREIGHT_PERMS.admin,
},
],
},
{
label: "Rules",
href: "/dashboard/rules",
icon: <SlidersHorizontal />,
children: getCategorySidebarChildren("rules"),
},
{
label: "Staff",
href: "/user-management",
icon: <Users />,
permission: [
FREIGHT_PERMS.admin,
FREIGHT_PERMS.staff.roles.view,
FREIGHT_PERMS.staff.employeeRegistration.view,
FREIGHT_PERMS.staff.roleAssignment.view,
],
},
],
},
];
/** Hrefs of the two document-clearance menu items (stable identifiers). */
const ET_CLEARANCE_HREF = "/dashboard/contracts/clearance";
const DJ_CLEARANCE_HREF = "/dashboard/gl-djibouti/clearance";
// Routes a GL officer may reach beyond their clearance hub. Path B booking is
// part of their job (create/rebook under a cleared contract, then view that
// booking's clearance), but those routes live outside the clearance prefix —
// without this allowlist the single-prefix lock bounces them out of their own
// workflow. Matched against location.pathname (no query string).
const GL_WORKFLOW_PATH_PATTERNS: RegExp[] = [
/^\/dashboard\/contracts\/[^/]+\/create-booking(\/|$)/,
/^\/dashboard\/bookings\/[^/]+\/clearance(\/|$)/,
// The ET hub's rows open the shipment clearance detail at this URL.
/^\/dashboard\/clearance\/[^/]+(\/|$)/,
];
const isEtClearanceItem = (item: SidebarItem): boolean =>
item.href === ET_CLEARANCE_HREF;
const isDjClearanceItem = (item: SidebarItem): boolean =>
item.href === DJ_CLEARANCE_HREF;
const isClearanceItem = (item: SidebarItem): boolean =>
isEtClearanceItem(item) || isDjClearanceItem(item);
/**
* Keep only items the user is permitted to see; drop now-empty sections.
*
* Position-scoped visibility (super_admin sees everything):
* - Super Admin → sees all items (all permissions pass, all tabs visible)
* - Ethiopian GL → sees ONLY the ET document-clearance page.
* - Djibouti GL → sees ONLY the DJ clearance page.
* - Everyone else → sees everything they have permission for, EXCEPT the two
* clearance pages (those are GL-only).
*/
const filterSidebarByPermission = (
sections: SidebarSection[],
user: ReturnType<typeof useAuth>["user"],
): SidebarSection[] => {
// Superadmin sees every section and item — no permission filtering.
if (isSuperAdmin(user)) return sections;
const etGl = isEthiopianGl(user);
const djGl = isDjiboutiGl(user);
const permissionAllowed = (item: SidebarItem): boolean => {
if (!item.permission) return true;
const keys = Array.isArray(item.permission)
? item.permission
: [item.permission];
return keys.some((key) => hasFreightPermission(user, key));
};
// Recursive: children are filtered first; a group (item with children) stays
// only while it still has visible children — so parents without their own
// permission key never leak a whole subtree the user cannot open.
const filterItems = (items: SidebarItem[]): SidebarItem[] =>
items
.map((item) =>
item.children
? { ...item, children: filterItems(item.children) }
: item,
)
.filter((item) => {
if (etGl || djGl) {
// GL positions are locked to their single clearance page (parents
// survive only as the path to that page).
const isTarget = etGl ? isEtClearanceItem : isDjClearanceItem;
return isTarget(item) || (item.children?.length ?? 0) > 0;
}
// Everyone else: hide the GL-only clearance pages entirely.
if (isClearanceItem(item)) return false;
if (!permissionAllowed(item)) return false;
if (item.children) return item.children.length > 0;
return true;
});
return sections
.map((section) => ({
...section,
items: filterItems(section.items),
}))
.filter((section) => section.items.length > 0);
};
const APP_TITLE = "EDR Freight Backoffice";
/** Flatten sidebar sections (incl. nested children) into {href, label} pairs. */
const flattenSidebarItems = (
sections: SidebarSection[],
): { href: string; label: string }[] =>
sections.flatMap((section) =>
section.items.flatMap((item) => [
...(item.href ? [{ href: item.href, label: item.label }] : []),
...(item.children ?? [])
.filter((child): child is SidebarItem & { href: string } =>
Boolean(child.href),
)
.map((child) => ({ href: child.href, label: child.label })),
]),
);
/** Find the sidebar label whose href matches (exactly or as a prefix of) the current path. */
const findActiveSidebarLabel = (
pathname: string,
sections: SidebarSection[],
): string | undefined => {
const path = pathname.toLowerCase();
const candidates = flattenSidebarItems(sections)
.map(({ href, label }) => ({
label,
href: href.split("?")[0].toLowerCase(),
}))
.sort((a, b) => b.href.length - a.href.length);
return candidates.find(
({ href }) => path === href || path.startsWith(`${href}/`),
)?.label;
};
import {
APP_TITLE,
buildSidebarSections,
DJ_CLEARANCE_HREF,
ET_CLEARANCE_HREF,
filterSidebarByPermission,
findActiveSidebarLabel,
GL_WORKFLOW_PATH_PATTERNS,
} from "@/components/layout/sidebar-sections";
const DashboardShell = () => {
const navigate = useNavigate();
@@ -840,17 +203,19 @@ const App = () => {
);
}
const landingPath = resolveLandingPath(user);
return (
<Routes>
{UserManagementRoutes()}
{/* <Route path="/um/*" element={<UserManagementHostPage />} /> */}
<Route path="/health" element={<HealthCheck />} />
<Route path="/fayda/callback" element={<FaydaCallbackPage />} />
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
<Route
path="/dashboard"
element={<Navigate to="/dashboard/overview" replace />}
/>
<Route path={NO_ACCESS_PATH} element={<NoAccessPage />} />
{/* Landing is per-user: /dashboard/overview is gated on overview:view, so
a fixed target strands anyone without that key on a blank page. */}
<Route path="/" element={<Navigate to={landingPath} replace />} />
<Route path="/dashboard" element={<Navigate to={landingPath} replace />} />
<Route path="/dashboard" element={<DashboardShell />}>
<Route path="overview" element={<RequirePermission permission={FREIGHT_PERMS.overview.view}><OverviewPage /></RequirePermission>} />
<Route path="reports" element={<RequirePermission permission={FREIGHT_PERMS.reports.view}><ReportsHubPage /></RequirePermission>} />
@@ -1554,7 +919,9 @@ const App = () => {
<Route path="org-structure/*" element={<Navigate to="/um" replace />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard/overview" replace />} />
{/* Post-login landing: the browser is still on /auth, which matches
nothing here, so this is what actually decides where users start. */}
<Route path="*" element={<Navigate to={landingPath} replace />} />
</Routes>
);
};
@@ -1575,3 +942,4 @@ function LegacyGlEthiopiaClearanceRedirect() {
}
export default App;