import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { Bell, ChevronDown, LogOut, Menu, Search, Shield, User, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { DOORMILE_WORDMARK_URL } from '@/assets/brand'; import { Avatar } from '@/components/ds/Avatar'; import { Badge } from '@/components/ui/badge'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { useAuth } from '@/lib/AuthContext'; import { useExceptions } from '@/lib/doormileHooks'; import { formatDoormileTimestamp } from '@/lib/doormileTimestamp'; import doormileMark from 'assets/images/doormile-mark.png'; import AIPanel from '@/components/assistant/DoormileAI/AIPanel'; /** * The console shell. * * Single 56px header bar, compact search with a keyboard shortcut, hairline tab * indicator, mobile sheet — the design is unchanged. What changed is what it * points at: the Doormile Express destinations rather than the hiring ones. * * Seven destinations sit on the bar and the remaining eleven live in two * grouped menus (Fleet Ops, Reports), built from the same DropdownMenu the * account menu already uses. Eighteen flat tabs would not fit a 56px bar at any * width, and a console header should recede rather than become the loudest * thing on the page. */ const ROOT = '/doormile'; /* `doormile-logo.png` is a white wordmark — invisible on this bar's light chrome as-is. Recolored to brand red via filter rather than swapping the asset, same trick the source console's own TopNav uses on the identical file, so the two headers render the mark in the same color from the same source PNG. */ const LOGO_FILTER = { filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)', }; /** Destinations that sit directly on the header bar. */ const NAV = [ { label: 'Dispatch', path: '/doormile/dispatch' }, { label: 'Orders', path: '/doormile/orders' }, { label: 'Deliveries', path: '/doormile/deliveries' }, { label: 'Riders', path: '/doormile/riders' }, { label: 'Clients', path: '/doormile/tenants' }, { label: 'Pricing', path: '/doormile/pricing' }, { label: 'Customers', path: '/doormile/customers' }, ]; /** Destinations grouped behind a menu, in the order the console lists them. */ const NAV_GROUPS = [ { label: 'Fleet Ops', items: [ { label: 'Hubs', path: '/doormile/hubs' }, { label: 'Vehicles', path: '/doormile/vehicles' }, { label: 'Tripsheets', path: '/doormile/tripsheets' }, { label: 'Exceptions', path: '/doormile/exceptions' }, { label: 'Competitive Intel', path: '/doormile/competitive-intel' }, { label: 'App Users', path: '/doormile/app-users' }, ], }, { label: 'Reports', items: [ { label: 'Orders Summary', path: '/doormile/reports/orderssummary' }, { label: 'Orders Details', path: '/doormile/reports/ordersdetails' }, { label: 'Riders Summary', path: '/doormile/reports/riderssummary' }, { label: 'Profitability', path: '/doormile/reports/profitability' }, ], }, ]; /** Every destination, flattened — what the mobile sheet renders. */ const ALL_DESTINATIONS = [...NAV, ...NAV_GROUPS.flatMap((group) => group.items)]; function GlobalSearch() { const navigate = useNavigate(); const [query, setQuery] = useState(''); const [focused, setFocused] = useState(false); const inputRef = useRef(null); /* Cmd/Ctrl+K focuses search — jump-to control shortcut */ useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); inputRef.current?.focus(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); return (
); } /** * Open exceptions, as notifications. * * The console's real "needs attention" queue is the exceptions register — a * failed delivery, a damaged parcel, an address that could not be found — so * that is what the bell shows, rather than a second notion of alerting that * nothing writes to. */ function NotificationsMenu() { const navigate = useNavigate(); const { data } = useExceptions({ refetchInterval: 60_000 }); const items = useMemo(() => { const open = (data || []).filter( (row) => !['resolved', 'closed'].includes(String(row?.status || '').toLowerCase()) ); return open.slice(0, 8).map((row) => ({ id: row.exceptionid ?? row.id, severity: String(row.severity || '').toLowerCase() === 'high' ? 'critical' : 'warning', title: row.exceptiontype || row.type || 'Exception raised', detail: row.description || row.reason || 'No description recorded.', time: formatDoormileTimestamp(row.createdat, 'DD MMM, hh:mm A', ''), })); }, [data]); const unread = items.length; return (Open exceptions
{unread > 0 &&Nothing needs attention.
) : ( items.map((n) => ( )) )}{displayName}
{user?.email || user?.primaryemail}