"use client"; import { useState, useEffect, useRef, useCallback, KeyboardEvent, ComponentType } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { LayoutDashboard, ShoppingCart, MapPin, Package, Truck, Ship, Mail, Store, Sparkles, Upload, BrainCircuit, Clock, Droplets, Route, ChartBar, Receipt, Settings, LogOut, Menu, X, ArrowLeft, } from "lucide-react"; import { signOutAction } from "@/actions/auth-actions"; import BrandSelector from "@/components/admin/BrandSelector"; import SideNavGroup from "@/components/admin/SideNavGroup"; // Sidebar tokens used (from src/styles/admin-design-system.css): // --admin-sidebar-bg (#2A2520) // --admin-sidebar-text (#B8B4A8) // --admin-sidebar-hover (#3A352F) // --admin-sidebar-active (#4A443C) // --admin-sidebar-accent (#D4A24C — amber, used for active left-border + dot) // The "amber accent" replaces the old `--admin-accent` (citrus orange) in the // sidebar only — primary buttons elsewhere still use the deep botanical green. type IconComponent = ComponentType<{ className?: string; size?: number | string }>; type NavItemDef = { href: string; label: string; icon: IconComponent; /** If set, this item is hidden when `enabledAddons[key] === false`. */ addonKey?: string; /** When true, only `userRole === "platform_admin"` sees this item. */ requiresPlatformAdmin?: boolean; }; type NavGroup = { label: string; items: NavItemDef[]; }; /** * Grouped nav IA per `docs/superpowers/specs/2026-06-17-admin-redesign.md` §4. * Order is intentional (top-to-bottom = frequency-of-use + mental flow). * * - Workspace : Dashboard is always shown; Command Center is platform_admin-only. * - Operations : Day-to-day order/stop/product flow. * - Communications: Harvest Reach only — the other sub-items live inside that page. * - Growth : Wholesale portal, data imports, AI assistant. * - Tracking : Time tracking is always on; Water Log / Route Trace are * gated on their respective add-ons via `enabledAddons`. * - Insights : Reporting + tax. * - Settings : Single entry; sub-pages are tabs inside /admin/settings. */ const NAV_GROUPS: NavGroup[] = [ { label: "Workspace", items: [ { href: "/admin", label: "Dashboard", icon: LayoutDashboard }, { href: "/admin/command-center", label: "Command Center", icon: Sparkles, requiresPlatformAdmin: true, }, ], }, { label: "Operations", items: [ { href: "/admin/orders", label: "Orders", icon: ShoppingCart }, { href: "/admin/stops", label: "Stops & Routes", icon: MapPin }, { href: "/admin/products", label: "Products", icon: Package }, { href: "/admin/pickup", label: "Driver Pickup", icon: Truck }, { href: "/admin/shipping", label: "Shipping", icon: Ship }, ], }, { label: "Communications", items: [ { href: "/admin/communications", label: "Harvest Reach", icon: Mail }, ], }, { label: "Growth", items: [ { href: "/admin/wholesale", label: "Wholesale", icon: Store }, { href: "/admin/import", label: "Import Center", icon: Upload }, { href: "/admin/settings/ai", label: "AI Intelligence", icon: BrainCircuit }, ], }, { label: "Tracking", items: [ { href: "/admin/time-tracking", label: "Time Tracking", icon: Clock }, { href: "/admin/water-log", label: "Water Log", icon: Droplets, addonKey: "water_log" }, { href: "/admin/route-trace", label: "Route Trace", icon: Route, addonKey: "route_trace" }, ], }, { label: "Insights", items: [ { href: "/admin/reports", label: "Reports", icon: ChartBar }, { href: "/admin/taxes", label: "Tax Dashboard", icon: Receipt }, ], }, { label: "Settings", items: [ { href: "/admin/settings", label: "Settings", icon: Settings }, ], }, ]; type SidebarProps = { userRole?: string | null; brandIds?: string[]; activeBrandId?: string | null; brands?: { id: string; name: string; slug: string; logo_url: string | null }[]; /** * Map of add-on key → enabled. When a key is `false` (or omitted, with the * add-on not in the map), any nav item with that `addonKey` is hidden. * When the prop itself is omitted, all add-on-gated items are shown * (back-compat with the current admin layout, which doesn't pass it). */ enabledAddons?: Record; }; export default function AdminSidebar({ userRole, brandIds, activeBrandId, brands, enabledAddons, }: SidebarProps) { const pathname = usePathname(); const router = useRouter(); const [mobileOpen, setMobileOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); const sidebarRef = useRef(null); const mobileMenuRef = useRef(null); const closeButtonRef = useRef(null); const roleLabel = userRole === "platform_admin" ? "Platform Admin" : userRole === "brand_admin" ? "Brand Admin" : userRole === "store_employee" ? "Store Employee" : null; /** * Build the filtered list of visible nav items for the current viewer. * Honors role-gating (Command Center) and add-on gating (Water Log, Route Trace). * If `enabledAddons` is undefined, add-on-gated items are shown (no gating). */ const visibleItems: NavItemDef[] = NAV_GROUPS.flatMap((group) => { return group.items.filter((item) => { if (item.requiresPlatformAdmin && userRole !== "platform_admin") { return false; } if (item.addonKey && enabledAddons && enabledAddons[item.addonKey] === false) { return false; } return true; }); }); /** * Groups rendered, with empty ones removed. The Communications group is * always rendered today (Harvest Reach is always visible) — but if a * future iteration hides Harvest Reach for some role, the entire group * header goes away rather than rendering an empty section. */ const visibleGroups: NavGroup[] = NAV_GROUPS .map((group) => ({ ...group, items: group.items.filter((item) => visibleItems.includes(item)), })) .filter((group) => group.items.length > 0); const isActive = useCallback( (href: string) => { if (href === "/admin") return pathname === "/admin"; return pathname.startsWith(href); }, [pathname], ); // Close mobile menu with animation const closeMobileMenu = useCallback(() => { setIsClosing(true); setTimeout(() => { setMobileOpen(false); setIsClosing(false); }, 200); }, []); // Handle escape key useEffect(() => { const handleEscape = (e: globalThis.KeyboardEvent) => { if (e.key === "Escape" && mobileOpen) { closeMobileMenu(); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [mobileOpen, closeMobileMenu]); // Focus trap and body scroll lock useEffect(() => { if (mobileOpen) { document.body.style.overflow = "hidden"; // Focus close button after animation setTimeout(() => { closeButtonRef.current?.focus(); }, 100); } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [mobileOpen]); // Keyboard navigation for nav items (arrow up/down loops, enter/space activates) const handleNavKeyDown = useCallback( (e: KeyboardEvent, href: string, index: number) => { const total = visibleItems.length; if (total === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); const nextIndex = (index + 1) % total; const nextLink = document.querySelector( `[data-nav-index="${nextIndex}"]`, ) as HTMLAnchorElement | null; nextLink?.focus(); } else if (e.key === "ArrowUp") { e.preventDefault(); const prevIndex = index === 0 ? total - 1 : index - 1; const prevLink = document.querySelector( `[data-nav-index="${prevIndex}"]`, ) as HTMLAnchorElement | null; prevLink?.focus(); } else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); router.push(href); if (mobileOpen) closeMobileMenu(); } }, [router, mobileOpen, closeMobileMenu, visibleItems.length], ); async function handleLogout() { await signOutAction(); } // Shared nav-link renderer. Used by both the desktop sidebar and the mobile // slide-in panel — they're the same list, just in a different container. const renderNavLink = (item: NavItemDef, index: number) => { const active = isActive(item.href); const Icon = item.icon; return (
  • closeMobileMenu()} onKeyDown={(e) => handleNavKeyDown(e, item.href, index)} className={[ "group flex items-center gap-2.5 pl-3 pr-2.5 py-2 rounded-r-md text-xs font-medium", "border-l-[3px] transition-all duration-200", "focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--admin-sidebar-bg)]", ].join(" ")} style={ active ? { backgroundColor: "var(--admin-sidebar-active)", color: "#F4F1E8", borderLeftColor: "var(--admin-sidebar-accent)", } : { color: "var(--admin-sidebar-text)", borderLeftColor: "transparent", } } onMouseEnter={(e) => { if (!active) { e.currentTarget.style.backgroundColor = "var(--admin-sidebar-hover)"; } }} onMouseLeave={(e) => { if (!active) { e.currentTarget.style.backgroundColor = "transparent"; } }} aria-current={active ? "page" : undefined} tabIndex={0} > {item.label} {active && (
  • ); }; return ( <> {/* Mobile hamburger button - accessible */} {/* Mobile overlay backdrop with blur */} {mobileOpen && (