import { AppShell, Avatar, Box, Button, Divider, FileInput, Group, Menu, Modal, NavLink, ScrollArea, Stack, Text, UnstyledButton, useComputedColorScheme, useMantineColorScheme, useMantineTheme, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { ChevronDown, FileSignature, LogOut, Menu as MenuIcon, Moon, Plus, Search, Settings, Sun, Upload, User, X, } from "lucide-react"; import { type CSSProperties, Fragment, type ReactNode, useState } from "react"; import { PROFILE_TYPE_LABELS } from "@/constants/profileMode"; import NotificationBellContainer from "@/features/notifications/NotificationBellContainer"; export interface SidebarItem { label: string; href: string; icon?: ReactNode; children?: SidebarItem[]; section?: string; } export interface AppLayoutProps { title?: string; sidebarItems: SidebarItem[]; activeHref?: string; /** * Navigate to a route. Accepts an optional options object (e.g. `{ state }`) * forwarded to the router — used to pass navigation state like `fresh: true` * to the new-booking wizard. Compatible with react-router's `navigate`. */ onNavigate?: (href: string, options?: { state?: unknown }) => void; enableThemeToggle?: boolean; userName?: string; userEmail?: string; /** Operational profiles for the company — surfaced as reference chips in the account menu. */ companyProfiles?: { type: string; reference: string; status?: string }[]; /** Company type (e.g. "customer", "forwarder") — gates the "Add service" control. */ companyType?: string | null; /** Create a new service profile of the given type (with business license). */ onCreateProfile?: ( type: ServiceType, licenseFiles: File[], ) => Promise | void; children: ReactNode; } /** Service profiles a customer company can operate under and switch between. */ type ServiceType = "importer" | "exporter" | "freight_forwarder"; /** Services a customer company can select in the header. */ const CUSTOMER_SERVICES: ServiceType[] = [ "importer", "exporter", "freight_forwarder", ]; type SwitchResult = | { success: true; data?: unknown } | { success: false; error?: { message?: string } }; function getInitials(name: string): string { return name .split(" ") .filter(Boolean) .slice(0, 2) .map((n) => n[0].toUpperCase()) .join(""); } function getActivePage( items: SidebarItem[], activePath: string, ): { label: string } | null { const path = activePath.toLowerCase(); for (const item of items) { if ( path === item.href.toLowerCase() || path.startsWith(item.href.toLowerCase() + "/") ) { return { label: item.label }; } if (item.children) { const childMatch = item.children.find( (c) => path === c.href.toLowerCase() || path.startsWith(c.href.toLowerCase() + "/"), ); if (childMatch) return { label: childMatch.label }; } } return null; } const navClassNames = (active: boolean) => { if (active) { // Active tab gets the strong brand color: a green gradient pill, white // label + icon, and a soft lifted shadow so it clearly stands out from the // light rail around it. return { root: `rounded-[12px] font-medium transition-all duration-150 bg-gradient-to-r from-[#0EA371] to-[#0A8A60]! shadow-[0_6px_16px_rgba(14,163,113,0.30)]!`, label: `text-white! font-bold!`, section: `text-white!`, }; } return { root: `rounded-[12px] font-medium transition-all duration-150 hover:bg-[#EBF4EF]!`, label: `text-edr-text! font-semibold! hover:text-[#0A6F4D]!`, section: `text-[#64748B]! hover:text-[#0A6F4D]!`, }; }; export function AppLayout({ title = "EDR Freight", sidebarItems, activeHref = "", onNavigate, enableThemeToggle = false, userName = "User", userEmail, companyProfiles = [], companyType, onCreateProfile, children, }: AppLayoutProps) { const [mobileOpen, { toggle: toggleMobile }] = useDisclosure(); const theme = useMantineTheme(); const { setColorScheme } = useMantineColorScheme(); const computedColorScheme = useComputedColorScheme("light"); const borderColor = theme.colors["edr-border"][6]; const mutedColor = theme.colors["edr-muted"][6]; const textColor = theme.colors["edr-text"][6]; const primaryColor = theme.colors["edr-green"][5]; const primaryDarkColor = theme.colors["edr-green"][7]; const activePath = activeHref.toLowerCase(); const navigate = (href: string, options?: { state?: unknown }) => onNavigate?.(href, options); const toggleTheme = () => { setColorScheme(computedColorScheme === "dark" ? "light" : "dark"); }; const initials = getInitials(userName); const activePage = getActivePage(sidebarItems, activePath); // ── Add a service (customer companies only) ── // A customer can operate as importer, exporter and/or freight forwarder. The // header lets them ADD a service they don't have yet (creating a profile with // its business license). Data is no longer scoped by an "active" service — // every page shows all the company's data, with an optional per-page filter. const isCustomer = companyType === "customer"; const profileExists = (type: ServiceType) => companyProfiles.some((p) => p.type === type); const addableServices = CUSTOMER_SERVICES.filter((t) => !profileExists(t)); const canAddService = isCustomer && addableServices.length > 0; const [switching, setSwitching] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [createTarget, setCreateTarget] = useState("importer"); const [licenseFiles, setLicenseFiles] = useState([]); const [createError, setCreateError] = useState(null); const handleAddService = (type: ServiceType) => { // Collect a business license, then create the profile. setCreateTarget(type); setLicenseFiles([]); setCreateError(null); setCreateOpen(true); }; const handleCreateConfirm = async () => { if (licenseFiles.length === 0) { setCreateError("Please upload at least one business license file."); return; } setSwitching(true); setCreateError(null); try { const res = await onCreateProfile?.(createTarget, licenseFiles); if (res && !res.success) { setCreateError(res.error?.message ?? "Failed to create profile"); return; } setCreateOpen(false); } finally { setSwitching(false); } }; const serviceLabel = (m: ServiceType) => PROFILE_TYPE_LABELS[m] ?? m; const isItemActive = (item: SidebarItem) => activePath === item.href.toLowerCase() || activePath.startsWith(item.href.toLowerCase() + "/"); // Shared header "island" styles — every control is a consistent 36px chip. const islandStyle: CSSProperties = { width: 36, height: 36, borderRadius: 999, border: `1px solid ${borderColor}`, backgroundColor: "#fff", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, cursor: "pointer", }; return ( {/* ── Header (frosted glass; compact floating islands, mirrors the pen) ── */} {/* Left: sidebar toggle + page title */} {activePage ? activePage.label : title} {/* Right: switch + search + bell + avatar */} {/* Add a service (customer companies that don't yet have all three) */} {canAddService && ( Add a service {addableServices.map((type) => ( handleAddService(type)} leftSection={} > {serviceLabel(type)} ))} )} {/* Search pill */} Search shipments, bookings… {/* Notifications */} {enableThemeToggle && ( {computedColorScheme === "dark" ? ( ) : ( )} )} {/* Avatar pill */} {initials} {userName} {userEmail && ( {userEmail} )} {companyProfiles.length > 0 && ( <> {companyProfiles.map((p) => ( {PROFILE_TYPE_LABELS[p.type] ?? p.type} {p.reference} ))} )} } onClick={() => navigate("/profile")} > Profile } onClick={() => navigate("/signature")} > My signature } onClick={() => navigate("/settings")} > Settings } color="edr-green" onClick={() => navigate("/bookings/new", { state: { fresh: true } }) } > New Booking } color="red" onClick={() => navigate("/logout")} > Logout {/* ── Sidebar ── */} {/* Brand */} EDR EDR FREIGHT Ethio–Djibouti Railway {/* Nav */} {sidebarItems.map((item, i) => { const active = isItemActive(item); const hasChildren = !!item.children?.length; const childActive = item.children?.some((c) => activePath.startsWith(c.href.toLowerCase()), ) ?? false; const prevSection = sidebarItems[i - 1]?.section; const sectionLabel = item.section && item.section !== prevSection ? ( {item.section} ) : null; if (hasChildren) { return ( {sectionLabel} {item.children!.map((child) => { const cActive = activePath === child.href.toLowerCase(); return ( navigate(child.href)} classNames={navClassNames(cActive)} /> ); })} ); } return ( {sectionLabel} navigate(item.href)} classNames={navClassNames(active)} /> ); })} {/* Promo card */} {/* Train image fills the full card */} {/* Diagonal green overlay: lower-left → upper-right cut */} {/* Text sits on top of the green overlay */} Moving Africa Forward Reliable. Efficient. Connected. {/* Learn More — visible on the image area */} Learn More {/* Profile */} {initials} {userName} {userEmail && ( {userEmail} )} {/* ── Main ── */} {children} {/* Create-profile modal — opens when switching to a mode the company doesn't have a profile for yet. */} (switching ? undefined : setCreateOpen(false))} title={`Set up your ${serviceLabel(createTarget)} profile`} centered radius="lg" > You don't have a {serviceLabel(createTarget).toLowerCase()} profile yet. Add your business license to create one and switch to{" "} {serviceLabel(createTarget).toLowerCase()}. } placeholder="Select license file(s)" value={licenseFiles} onChange={(files) => setLicenseFiles(files ?? [])} error={createError ?? undefined} /> ); } export default AppLayout;