Files
edr-platform/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx
Nathnael Wondisha f83894ca70 Merge pull request #1493 from Tria-plc/nati-main
feat: add publication
2026-09-04 14:56:27 +03:00

721 lines
23 KiB
TypeScript

import {
ArrowLeftRight,
BookOpen,
Boxes,
Building2,
BarChart3,
Container,
FileSignature,
FileText,
Hammer,
History,
Image as ImageIcon,
LayoutDashboard,
LayoutGrid,
Link2,
MapPin,
Network,
Package,
PackageCheck,
PackageOpen,
PackagePlus,
Paperclip,
Receipt,
Stamp,
ScrollText,
Send,
Settings,
ShieldCheck,
HandCoins,
Ship,
SlidersHorizontal,
Train,
Truck,
Undo2,
Users,
Wallet,
LifeBuoy,
MessageSquare,
TrainFront,
XCircle,
} from "lucide-react";
import type { AuthUser } from "@/auth/types";
import type { SidebarItem, SidebarSection } from "./types";
import {
FREIGHT_PERMS,
hasPermission as hasFreightPermission,
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[],
reportItems: SidebarItem[] = [],
): SidebarSection[] => [
{
title: "Main menu",
items: [
{
label: "Overview",
href: "/dashboard/overview",
icon: <LayoutDashboard />,
permission: FREIGHT_PERMS.overview.view,
},
{
label: "Wagon Performance",
href: "/dashboard/wagon-performance",
icon: <TrainFront />,
permission: FREIGHT_PERMS.wagons.view,
},
{
label: "Customers",
href: "/dashboard/customers",
icon: <Building2 />,
permission: FREIGHT_PERMS.customers.view,
},
{
label: "Shipping Lines",
href: "/dashboard/shipping-lines",
icon: <Ship />,
permission: FREIGHT_PERMS.shippingLines.view,
},
{
label: "Shipping Line Credits",
href: "/dashboard/shipping-line-credits",
icon: <HandCoins />,
permission: FREIGHT_PERMS.shippingLineCredits.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,
},
// Shared-wagon gate: a consolidated pair waits for a human decision
// before either half reaches Operations.
{
label: "Shared wagon approvals",
href: "/dashboard/consolidation-approvals",
icon: <Link2 />,
permission: FREIGHT_PERMS.bookings.approveConsolidation,
},
{
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,
},
{
// Invoices, Payments, and USD Payments live on one page as tabs
// (FinanceHubPage) — single nav entry, OR'd across both keys so
// either permission alone still gets a user in.
label: "Transactions",
href: "/dashboard/invoices",
icon: <Receipt />,
permission: [FREIGHT_PERMS.invoices.view, FREIGHT_PERMS.payments.view],
},
{
label: "Support",
href: "/dashboard/support",
icon: <LifeBuoy />,
permission: FREIGHT_PERMS.support.agentView,
},
{
label: "Chat",
href: "/dashboard/chat",
icon: <MessageSquare />,
permission: FREIGHT_PERMS.chat.view,
},
...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,
},
{
label: "Locomotives",
href: "/dashboard/locomotives",
icon: <Train />,
permission: FREIGHT_PERMS.locomotives.view,
},
{
label: "Train Builder",
href: "/dashboard/train-builder",
icon: <Hammer />,
permission: FREIGHT_PERMS.trains.view,
},
// {
// label: "Wagon types",
// href: "/dashboard/wagon-types",
// icon: <Boxes />,
// },
{
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
permission: FREIGHT_PERMS.wagons.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.compliance.view,
},
{
label: "Incidents",
href: "/dashboard/incidents",
icon: <FileText />,
// No dedicated backend key exists for incidents yet. Not part of
// the fleet.view/admin fallback cleanup — removing fleet.view
// here with nothing to replace it would lock the page to
// super-admin only, so it stays as the sole (if coarse) gate.
permission: FREIGHT_PERMS.fleet.view,
},
{
label: "Procurement",
href: "/dashboard/procurement",
icon: <Package />,
permission: FREIGHT_PERMS.procurement.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: "Container Returns",
href: "/dashboard/container-returns",
icon: <Container />,
permission: FREIGHT_PERMS.warehouseInventory.view,
},
{
label: "Empty Return Requests",
href: "/dashboard/empty-return-requests",
icon: <Undo2 />,
// Visible to whoever already runs container returns, OR'd with
// the queue's own key — so the dedicated permission can be handed
// out per position later without the menu disappearing now.
permission: [
FREIGHT_PERMS.emptyReturnRequests.view,
FREIGHT_PERMS.warehouseInventory.view,
],
},
{
label: "Register Full Containers",
href: "/dashboard/register-full-containers",
icon: <PackagePlus />,
permission: FREIGHT_PERMS.warehouseInventory.receive,
},
{
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,
},
],
},
{
label: "Reports",
href: "/dashboard/reports",
icon: <BarChart3 />,
permission: FREIGHT_PERMS.reports.view,
// Populated from the live GET /reports catalog (already permission-
// filtered server-side) — no report key is ever hand-listed here.
...(reportItems.length ? { children: reportItems } : {}),
},
],
},
{
title: "Freight configuration",
mutedTitle: true,
items: [
{
label: "File settings",
href: "/dashboard/file-settings",
icon: <Paperclip />,
permission: FREIGHT_PERMS.settings.fileUpload.view,
},
{
label: "Dropdown settings",
href: "/dashboard/dropdown-settings",
icon: <Settings />,
permission: FREIGHT_PERMS.settings.dropdown.view,
},
{
// One entry, one stamp. The former "Stamp settings" entry here pointed
// at the per-officer teeter (ማህተም), not a company seal — it moved to
// /user-management/teeter-and-signature.
label: "Company stamp",
href: "/dashboard/stamp-settings",
icon: <Stamp />,
permission: FREIGHT_PERMS.settings.stamp.view,
},
{
label: "Company logo",
href: "/dashboard/logo-settings",
icon: <ImageIcon />,
permission: FREIGHT_PERMS.settings.logo.view,
},
{
label: "Contract templates",
href: "/dashboard/contract-templates",
icon: <ScrollText />,
// `view` opens the page; `read` alone is API-only and shows no menu.
permission: FREIGHT_PERMS.settings.contractTemplates.view,
},
{
label: "Portal content",
href: "/dashboard/portal-content",
icon: <BookOpen />,
permission: [
FREIGHT_PERMS.settings.supportContent.view,
FREIGHT_PERMS.settings.supportContent.manage,
],
},
{
label: "Publications",
href: "/dashboard/publications",
icon: <FileText />,
permission: [
FREIGHT_PERMS.settings.publications.view,
FREIGHT_PERMS.settings.publications.manage,
],
},
{
label: "Audit logs",
href: "/dashboard/audit-logs",
icon: <History />,
permission: FREIGHT_PERMS.auditLog.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.tradeAccess.view,
},
{
label: "Exchange rate",
href: "/dashboard/configuration/exchange-rate",
permission: FREIGHT_PERMS.settings.exchangeRate.view,
},
{
label: "Operating standards",
href: "/dashboard/configuration/operations-standards",
permission: FREIGHT_PERMS.settings.operationsStandards.view,
},
{
label: "Manual payments",
href: "/dashboard/configuration/manual-payments",
permission: FREIGHT_PERMS.settings.manualPayment.view,
},
],
},
{
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,
],
},
],
},
];
/**
* Keep only items the user is permitted to see; drop now-empty sections.
* Super Admin sees everything; everyone else is filtered purely by each
* item's `permission` field (OR across the array when one is given).
*/
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 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 (!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;
};