updates on the ui changes and changed into doormile

This commit is contained in:
2026-08-24 18:10:47 +05:30
parent dcf9770ada
commit aa42d9ee81
409 changed files with 49950 additions and 66532 deletions

View File

@@ -1,12 +1,11 @@
import React, { useEffect, useRef, useState } from 'react';
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, LayoutGrid, LogOut, Menu, Search, Settings, Shield,
Bell, ChevronDown, LogOut, Menu, Search, Shield, Sparkles, User,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { base44 } from '@/api/base44Client';
import { KROW_LOGO_URL } from '@/assets/brand';
import { DOORMILE_WORDMARK_URL } from '@/assets/brand';
import { Avatar } from '@/components/ds/Avatar';
import { Badge } from '@/components/ui/badge';
import {
@@ -14,38 +13,74 @@ import {
DropdownMenuSeparator, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant';
import { endAdminSession } from '@/lib/admin/session';
import { useCurrentUser } from '@/lib/krowHooks';
import { useAuth } from '@/lib/AuthContext';
import { useExceptions } from '@/lib/doormileHooks';
import { formatDoormileTimestamp } from '@/lib/doormileTimestamp';
import AIPanel from '@/components/assistant/DoormileAI/AIPanel';
/**
* AdminLayout — the shell for the Admin product.
* The console shell.
*
* Built with the new operations console design system: single 56px header bar,
* compact search with keyboard shortcut, dedicated design tokens, and fluid layout.
* 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 ADMIN_ROOT = '/admin';
const ROOT = '/doormile';
/**
* The visible Admin navigation — Admin routes only.
*
* Employer and Talent destinations are deliberately absent, KROW Forge
* (/university) included: it belongs to the Talent product, and linking out of an
* operations console into another product's navigation loses your place rather
* than saving you a step. Their code and their own entry points are untouched.
*/
/* `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: 'Control Center', path: ADMIN_ROOT },
{ label: 'Positions', path: '/admin/positions' },
{ label: 'Candidates', path: '/admin/candidates' },
{ label: 'Hired History', path: '/admin/hired' },
{ label: 'Talent Pool', path: '/admin/talent-pool' },
{ label: 'KROW Forge', path: '/admin/university' },
{ label: 'Analytics', path: '/admin/analytics' },
{ label: 'Activity', path: '/admin/activity' },
{ 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('');
@@ -68,7 +103,10 @@ function GlobalSearch() {
<form
onSubmit={(e) => {
e.preventDefault();
if (query.trim()) navigate(`/admin/candidates?search=${encodeURIComponent(query.trim())}`);
/* Orders is the console's search surface: it holds every booking, and
an operator searching from the header is nearly always chasing one
order number or one customer. */
if (query.trim()) navigate(`/doormile/orders?search=${encodeURIComponent(query.trim())}`);
}}
role="search"
className={cn(
@@ -83,11 +121,11 @@ function GlobalSearch() {
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Search"
aria-label="Search the platform"
placeholder="Search orders"
aria-label="Search orders"
className="h-8 w-full rounded-lg border border-border bg-surface-subtle pl-8 pr-10 text-body-sm text-ink-1
placeholder:text-ink-4 outline-none transition-colors
focus:border-krow-blue focus:bg-surface focus:ring-2 focus:ring-krow-blue/15"
focus:border-brand focus:bg-surface focus:ring-2 focus:ring-brand/15"
/>
{!focused && !query && (
<kbd className="pointer-events-none absolute right-2 rounded border border-border bg-surface px-1 text-[10px] font-medium text-ink-4">
@@ -98,55 +136,82 @@ function GlobalSearch() {
);
}
function NotificationsMenu({ items }) {
const unread = items.filter((n) => !n.read).length;
/**
* 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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={unread ? `Notifications, ${unread} unread` : 'Notifications'}
aria-label={unread ? `Notifications, ${unread} open exceptions` : 'Notifications'}
className="relative grid h-8 w-8 place-items-center rounded-lg text-ink-3 transition-colors
hover:bg-surface-sunken hover:text-ink-1
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50"
>
<Bell className="h-4 w-4" aria-hidden="true" />
{unread > 0 && (
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-krow-blue ring-2 ring-white" />
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-brand ring-2 ring-white" />
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 p-0">
<div className="flex items-center justify-between border-b border-border px-3 py-2.5">
<p className="text-body-sm font-semibold text-ink-1">Notifications</p>
{unread > 0 && <Badge variant="soft" size="sm">{unread} new</Badge>}
<p className="text-body-sm font-semibold text-ink-1">Open exceptions</p>
{unread > 0 && <Badge variant="soft" size="sm">{unread} open</Badge>}
</div>
<div className="max-h-80 overflow-y-auto">
{items.map((n) => (
<div
key={n.id}
className={cn(
'flex gap-2.5 border-b border-border px-3 py-2.5 last:border-0',
!n.read && 'bg-krow-blue-tint/40'
)}
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
n.severity === 'critical' ? 'bg-destructive'
: n.severity === 'warning' ? 'bg-warning' : 'bg-krow-blue'
)}
aria-hidden="true"
/>
<div className="min-w-0">
<p className="text-body-sm font-medium text-ink-1">{n.title}</p>
<p className="mt-0.5 text-caption leading-relaxed text-ink-3">{n.detail}</p>
<p className="mt-1 text-[10px] text-ink-4">{n.time}</p>
</div>
</div>
))}
{items.length === 0 ? (
<p className="px-3 py-6 text-center text-body-sm text-ink-3">Nothing needs attention.</p>
) : (
items.map((n) => (
<button
key={n.id}
type="button"
onClick={() => navigate('/doormile/exceptions')}
className="flex w-full gap-2.5 border-b border-border bg-brand-tint/40 px-3 py-2.5 text-left
last:border-0 hover:bg-brand-tint/70"
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
n.severity === 'critical' ? 'bg-destructive' : 'bg-warning'
)}
aria-hidden="true"
/>
<div className="min-w-0">
<p className="text-body-sm font-medium text-ink-1">{n.title}</p>
<p className="mt-0.5 text-caption leading-relaxed text-ink-3">{n.detail}</p>
{n.time && <p className="mt-1 text-[10px] text-ink-4">{n.time}</p>}
</div>
</button>
))
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
@@ -156,220 +221,219 @@ function NotificationsMenu({ items }) {
export default function AdminLayout() {
const location = useLocation();
const navigate = useNavigate();
const { user, logout } = useAuth();
const [menuOpen, setMenuOpen] = useState(false);
/* Shared cache rather than a local fetch, so saving the profile updates the
header avatar and account menu without a reload. */
const { data: user } = useCurrentUser();
const signOut = () => {
endAdminSession();
base44.auth.logout('/admin/login');
};
const [assistantOpen, setAssistantOpen] = useState(false);
const isActive = (path) => {
if (path === ADMIN_ROOT) return location.pathname === ADMIN_ROOT || location.pathname === '/overview';
const alias = path.replace('/admin', '');
return (
location.pathname === path ||
location.pathname.startsWith(`${path}/`) ||
(Boolean(alias) && alias !== '/' && (location.pathname === alias || location.pathname.startsWith(`${alias}/`)))
);
if (path === ROOT) return location.pathname === ROOT;
return location.pathname === path || location.pathname.startsWith(`${path}/`);
};
const groupIsActive = (group) => group.items.some((item) => isActive(item.path));
const displayName = user?.displayname || user?.authname || user?.name || 'Operator';
return (
<AssistantPanelProvider role="admin" pathname={location.pathname}>
{/* Transparent so the ambient canvas painted behind the app reads through.
An opaque shell here would cover it and every Admin page would lose the
tint at once — which is exactly why the canvas is one layer and not a
background repeated on each page. */}
<div className="min-h-screen">
{/* Single bar header in the new Admin design system */}
<header className="sticky top-0 z-40 border-b border-border bg-white/85 backdrop-blur-xl">
<div className="flex h-14 items-center justify-between gap-4 px-4 lg:px-6">
<Link
to={ADMIN_ROOT}
aria-label="KROW Admin — Control Center"
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<img src={KROW_LOGO_URL} alt="KROW" className="h-6 w-auto object-contain" />
</Link>
/* Transparent so the ambient canvas painted behind the app reads through.
An opaque shell here would cover it and every page would lose the tint at
once — which is why the canvas is one layer and not a background repeated
on each page. */
<div className="min-h-screen">
{/* `data-app-header` is read by the Doormile AI dock, which measures this
bar to know where the panel should start. */}
<header data-app-header className="sticky top-0 z-40 border-b border-border bg-white/85 backdrop-blur-xl">
<div className="flex h-14 items-center justify-between gap-4 px-4 lg:px-6">
<Link
to="/doormile/dispatch"
aria-label="Doormile Express — Dispatch"
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50"
>
{/* Full wordmark, not just the circular mark — matches the source
console's own TopNav, which renders this same logo at the
same relative scale inside its bar. */}
<img src={DOORMILE_WORDMARK_URL} alt="Doormile Express" style={LOGO_FILTER} className="h-7 w-auto object-contain" />
</Link>
{/* Text tabs with a hairline indicator, not filled pills. Eight
destinations means eight competing shapes if each one is a pill,
and a console header should recede — the underline states position
using the brand blue without turning the chrome into the loudest
thing on the page. */}
<nav aria-label="Admin" className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
{NAV.map((item) => (
<Link
key={item.path}
to={item.path}
aria-current={isActive(item.path) ? 'page' : undefined}
className={cn(
'relative whitespace-nowrap rounded-lg px-2.5 py-1.5 text-body-sm font-medium transition-colors duration-base',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50',
isActive(item.path) ? 'text-krow-blue' : 'text-ink-3 hover:text-ink-1'
)}
>
{item.label}
{isActive(item.path) && (
<motion.span
layoutId="adminNavIndicator"
className="absolute inset-x-2.5 -bottom-[13px] h-0.5 rounded-full bg-krow-blue"
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
/>
)}
</Link>
))}
</nav>
{/* Text tabs with a hairline indicator, not filled pills. Each grouped
menu is styled as one more tab, so a destination reached through a
menu still reads as being in the same row as the rest. */}
<nav aria-label="Console" className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
{NAV.map((item) => (
<Link
key={item.path}
to={item.path}
aria-current={isActive(item.path) ? 'page' : undefined}
className={cn(
'relative whitespace-nowrap rounded-lg px-2.5 py-1.5 text-body-sm font-medium transition-colors duration-base',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
isActive(item.path) ? 'text-brand' : 'text-ink-3 hover:text-ink-1'
)}
>
{item.label}
{isActive(item.path) && (
<motion.span
layoutId="adminNavIndicator"
className="absolute inset-x-2.5 -bottom-[13px] h-0.5 rounded-full bg-brand"
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
/>
)}
</Link>
))}
<div className="ml-auto flex items-center gap-1.5">
<GlobalSearch />
<NotificationsMenu items={NOTIFICATIONS} />
<DropdownMenu>
{NAV_GROUPS.map((group) => (
<DropdownMenu key={group.label}>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 rounded-lg py-1 pl-1 pr-1.5 transition-colors hover:bg-surface-sunken
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50 cursor-pointer"
className={cn(
'relative flex items-center gap-1 whitespace-nowrap rounded-lg px-2.5 py-1.5 text-body-sm font-medium',
'transition-colors duration-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
groupIsActive(group) ? 'text-brand' : 'text-ink-3 hover:text-ink-1'
)}
>
<Avatar name={user?.full_name || 'Admin'} size="sm" />
<ChevronDown className="h-3 w-3 text-ink-4" aria-hidden="true" />
<span className="sr-only">Account menu</span>
{group.label}
<ChevronDown className="h-3 w-3" aria-hidden="true" />
{groupIsActive(group) && (
<motion.span
layoutId="adminNavIndicator"
className="absolute inset-x-2.5 -bottom-[13px] h-0.5 rounded-full bg-brand"
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
/>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel className="font-normal">
<p className="text-body-sm font-semibold text-ink-1">{user?.full_name || 'Admin'}</p>
<p className="text-caption text-ink-3">{user?.email}</p>
<Badge variant="soft" size="sm" className="mt-1.5">Platform Administrator</Badge>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Account configuration, then the one thing people come to
the account menu for by name, then the workforce workspace.
Profile is not dropped — it is the first entry inside
Settings, where account information belongs. */}
<DropdownMenuItem onClick={() => navigate('/admin/settings')} className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" /> Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/admin/profile#security')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" /> Security
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/admin/workspace')} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" /> Workspace
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={signOut}
className="cursor-pointer text-destructive focus:text-destructive"
>
<LogOut className="mr-2 h-4 w-4" /> Sign out
</DropdownMenuItem>
<DropdownMenuContent align="start" className="w-52">
{group.items.map((item) => (
<DropdownMenuItem
key={item.path}
onClick={() => navigate(item.path)}
className={cn('cursor-pointer', isActive(item.path) && 'text-brand')}
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
))}
</nav>
<button
type="button"
onClick={() => setMenuOpen(true)}
aria-label="Open navigation"
className="grid h-8 w-8 place-items-center rounded-lg text-ink-3 hover:bg-surface-sunken hover:text-ink-1 lg:hidden
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<Menu className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="ml-auto flex items-center gap-1.5">
<GlobalSearch />
{/* Doormile AI. A header control rather than a route: it answers
about live data while the operator stays on the screen that
raised the question. */}
<button
type="button"
onClick={() => setAssistantOpen((open) => !open)}
aria-label={assistantOpen ? 'Close Doormile AI' : 'Open Doormile AI'}
aria-expanded={assistantOpen}
className={cn(
'grid h-8 w-8 place-items-center rounded-lg transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
assistantOpen ? 'bg-brand-tint text-brand' : 'text-ink-3 hover:bg-surface-sunken hover:text-ink-1'
)}
>
<Sparkles className="h-4 w-4" aria-hidden="true" />
</button>
<NotificationsMenu />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 rounded-lg py-1 pl-1 pr-1.5 transition-colors hover:bg-surface-sunken
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50 cursor-pointer"
>
<Avatar name={displayName} size="sm" />
<ChevronDown className="h-3 w-3 text-ink-4" aria-hidden="true" />
<span className="sr-only">Account menu</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel className="font-normal">
<p className="text-body-sm font-semibold text-ink-1">{displayName}</p>
<p className="text-caption text-ink-3">{user?.email || user?.primaryemail}</p>
<Badge variant="soft" size="sm" className="mt-1.5">
{user?.role || 'Administrator'}
</Badge>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate('/doormile/profile')} className="cursor-pointer">
<User className="mr-2 h-4 w-4" /> Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/doormile/profile#security')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" /> Change password
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => logout()}
className="cursor-pointer text-destructive focus:text-destructive"
>
<LogOut className="mr-2 h-4 w-4" /> Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
onClick={() => setMenuOpen(true)}
aria-label="Open navigation"
className="grid h-8 w-8 place-items-center rounded-lg text-ink-3 hover:bg-surface-sunken hover:text-ink-1 lg:hidden
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50"
>
<Menu className="h-4 w-4" aria-hidden="true" />
</button>
</div>
</header>
{/* Mobile menu Sheet */}
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
<SheetContent side="right" className="w-72 p-0">
<SheetHeader className="border-b border-border px-4 py-4">
<SheetTitle className="text-left font-heading text-title">Admin</SheetTitle>
</SheetHeader>
<nav className="flex flex-col p-2">
{NAV.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={() => setMenuOpen(false)}
className={cn(
'rounded-lg px-3 py-2.5 text-body-sm font-medium transition-colors',
isActive(item.path) ? 'bg-krow-blue-tint text-krow-blue' : 'text-ink-2 hover:bg-surface-sunken'
)}
>
{item.label}
</Link>
))}
<div className="my-2 h-px bg-border" />
{[
{ label: 'Settings', path: '/admin/settings' },
{ label: 'Security', path: '/admin/profile#security' },
{ label: 'Workspace', path: '/admin/workspace' },
].map((item) => (
<Link
key={item.label}
to={item.path}
onClick={() => setMenuOpen(false)}
className="rounded-lg px-3 py-2.5 text-body-sm font-medium text-ink-2 hover:bg-surface-sunken"
>
{item.label}
</Link>
))}
</nav>
</SheetContent>
</Sheet>
{/* Main layout body */}
<div className="flex flex-col px-4 md:flex-row lg:px-6">
<main className="min-w-0 flex-1 py-6">
<Outlet />
</main>
<AssistantPanel
stickyClassName="top-14 pt-6"
panelHeightClassName="h-[calc(100dvh_-_6.5rem)]"
/>
</div>
</header>
{/* Mobile menu Sheet — flat, because a menu inside a drawer is one tap
too many on the device where taps are most expensive. */}
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
<SheetContent side="right" className="w-72 p-0">
<SheetHeader className="border-b border-border px-4 py-4">
<SheetTitle className="text-left font-heading text-title">Doormile Express</SheetTitle>
</SheetHeader>
<nav className="flex max-h-[calc(100dvh-8rem)] flex-col overflow-y-auto p-2">
{ALL_DESTINATIONS.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={() => setMenuOpen(false)}
className={cn(
'rounded-lg px-3 py-2.5 text-body-sm font-medium transition-colors',
isActive(item.path) ? 'bg-brand-tint text-brand' : 'text-ink-2 hover:bg-surface-sunken'
)}
>
{item.label}
</Link>
))}
<div className="my-2 h-px bg-border" />
<Link
to="/doormile/profile"
onClick={() => setMenuOpen(false)}
className="rounded-lg px-3 py-2.5 text-body-sm font-medium text-ink-2 hover:bg-surface-sunken"
>
Profile
</Link>
</nav>
</SheetContent>
</Sheet>
{/* `dai-page` is the handshake with the assistant dock: while the panel
is open, DoormileAI.css reserves exactly its width of padding here so
the page slides open beside it instead of being covered. */}
<div className={cn("dai-page flex flex-col", !location.pathname.startsWith('/doormile/dispatch') && "px-4 md:flex-row lg:px-6")}>
<main className={cn("min-w-0 flex-1", !location.pathname.startsWith('/doormile/dispatch') && "py-6")}>
<Outlet />
</main>
</div>
</AssistantPanelProvider>
{assistantOpen && <AIPanel isOpen={assistantOpen} onClose={() => setAssistantOpen(false)} />}
</div>
);
}
const NOTIFICATIONS = [
{
id: 1,
severity: 'warning',
title: 'Banquet Captain has no applicants',
detail: 'Live for 21 days with zero applications. Pay band may be below market.',
time: '2 hours ago',
read: false,
},
{
id: 2,
severity: 'warning',
title: '8 applicants awaiting screening',
detail: 'Oldest has been waiting 5 days across Event Security and Picker.',
time: '4 hours ago',
read: false,
},
{
id: 3,
severity: 'info',
title: 'Sofia Mendez scored 89',
detail: 'Screened for Event Server – Fine Dining and ready for a decision.',
time: 'Yesterday',
read: true,
},
{
id: 4,
severity: 'info',
title: '3 hires completed onboarding checks',
detail: 'Chef Antoine Dubois, Marcus Williams and Marco Rivera.',
time: '2 days ago',
read: true,
},
];