mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
661 lines
20 KiB
TypeScript
661 lines
20 KiB
TypeScript
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 type { AuthUser } from "@/auth/types";
|
|
|
|
import type { SidebarItem, SidebarSection } from "./types";
|
|
import {
|
|
FREIGHT_PERMS,
|
|
hasPermission as hasFreightPermission,
|
|
isDjiboutiGl,
|
|
isEthiopianGl,
|
|
isSuperAdmin,
|
|
} from "@/lib/permissions";
|
|
import { getCategorySidebarChildren } from "@/pages/ruleEngine/config/resources";
|
|
|
|
/**
|
|
* Backoffice sidebar model: the nav tree, its permission filter, and the
|
|
* flatten/lookup helpers. Lives outside App.tsx so `lib/landing.ts` can resolve
|
|
* a user's first reachable route without importing the route tree (App.tsx
|
|
* imports RequirePermission, which imports landing — that would cycle).
|
|
*/
|
|
export 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). */
|
|
export const ET_CLEARANCE_HREF = "/dashboard/contracts/clearance";
|
|
export 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).
|
|
export 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).
|
|
*/
|
|
export const filterSidebarByPermission = (
|
|
sections: SidebarSection[],
|
|
user: AuthUser | null | undefined,
|
|
): 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);
|
|
};
|
|
|
|
export const APP_TITLE = "EDR Freight Backoffice";
|
|
|
|
/** Flatten sidebar sections (incl. nested children) into {href, label} pairs. */
|
|
export 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. */
|
|
export 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;
|
|
};
|