Milers rebrand, /doormile/home, and a set of console fixes

Two bodies of work that arrived in one working tree and are intertwined in
four files, so they commit together rather than pretending to a split the
diffs do not have.

TEAM WORK (pre-existing in the tree, uncommitted)

- "Riders" -> "Milers" across en.json and ~20 pages. Done carefully:
  Deliveries.jsx checks BOTH prefixes ("Miler #" and "Rider #") so rows
  written before the rename still render, and routes and query keys stay
  rider*/riderssummary — renaming those would have broken bookmarks and
  cache keys.
- MileTruth assistant rework: rename from "Doormile AI", HStack composer,
  maximise/minimise/reset controls, open state persisted to localStorage.
- New /doormile/home landing page; / and /doormile now redirect there
  instead of /doormile/dispatch. AdminLayout nav restructured with icons
  and descriptions.

FIXES

Tab strip was unreachable (Deliveries: Delivered and Cancelled)
  The pill variant is one non-wrapping inline-flex row. Given less width
  than it needs, flex COMPRESSED it — 872px squeezed into 780px, truncating
  labels inside the buttons — and with nothing scrollable no gesture could
  recover the last two tabs. Four of six tab pages had each hand-rolled the
  same wrapper; Deliveries and CompetitiveIntel had not. Fixed in the
  component, so no page can forget it. Verified in a browser at the real
  content width: 102px of scroll range, "Cancelled" reachable. 11 tests.

Home.jsx crash: Cannot read properties of null (reading 'flow')
  Introduced by the change below, which made deriveVisualData return null.
  All 12 .flow/.table dereferences are now behind visual?. gates.

Home.jsx presented fabricated figures as live operations
  deriveVisualData keyword-matched the prompt and returned hardcoded values
  for whole topics — revenue as a flat Rs 48,650, a workforce of 46 with 38
  active, six named hubs, four booking numbers — ignoring the real result it
  had been handed. Those branches sat ABOVE the one reading res.stats, so
  the correct code was unreachable: asking about revenue could not return
  the real number because a literal answered first.

  Two were worse than wrong figures. The default branch returned a staffing
  table for ANY unmatched question. And the no-answer path built a
  sourceCalls entry claiming /admin/milers had been called, status
  "complete", "46 milers scanned" — forging the provenance trail that exists
  so an operator can check where a number came from. A fabricated figure is
  a bug; a fabricated citation defeats the mechanism for catching one.

  190 lines removed. Every branch now reads the result and returns null when
  there is nothing to draw. No-answer says so; errors report as errors.

  Found while verifying: 'Inactive'.includes('active') is true, so every hub
  counted as active — carried from the original, whose own sample data
  tripped it (six hubs, one Inactive, shown as 6/6). Now an explicit
  vocabulary match, with unknown treated as down: a hub wrongly shown
  offline gets investigated, one wrongly shown online hides an outage.

Home launcher tiles named pages that do not exist
  Every tile now names its destination. Task Board -> Tripsheets,
  Staff -> Milers, Availability -> Milers Summary (it pointed at the same
  page as the tile beside it), Compliance -> Exceptions,
  Invoices -> Bookings, Payroll -> Profitability,
  AI Reports -> Orders Summary. Group headings: WORKFORCE SUITE -> FLEET,
  FINANCE & CRM -> CLIENTS & REVENUE, AI TOOLS -> REPORTS & AI. "Invoices",
  "Payroll" and "CRM" named systems Doormile does not run.

  Four hardcoded badges removed (3, 38, 7, "New"). None was computed. A red
  badge means "this many things need you", and one that never changes
  teaches operators to ignore the real ones.

Navbar MileTruth control misaligned, hover dead
  One cause: a 36px image (h-9 w-9 max-w-none) inside a 32px button. It
  stood proud of the bell and avatar AND covered its own button, leaving
  hover:bg-surface-sunken nowhere to render. Now 20px.

Assistant dock 30% -> 20%, and responsive
  clamp(320px, 20vw, 460px) rather than max(): the cap is what makes a large
  monitor work, since 20vw is 768px at 4K. Verified across seven screen
  classes — 384px/20% at 1920, capped at 460px beyond 2300px, full-width
  overlay at or below 900px. At 1024px the 320px floor wins (31%), because
  20vw would be 205px and too narrow for the composer.

Suggestion chips took a third of the panel
  .dai-suggestions was flex-direction:column, overriding the component's own
  wrap="wrap", so four chips became four full rows. Row + wrap, trimmed
  padding, ellipsis on the text span where text-overflow can act. Measured
  at the 320px floor: 4 rows/177px -> 2 rows/84px, nothing clipped.

Pricing and Customers were unreachable on mobile
  Moving them off the nav bar into the account dropdown removed them from
  the phone entirely — the mobile sheet builds from NAV plus NAV_GROUPS and
  they were in neither. Both stayed routed, so only a typed URL reached
  them. Now one SETTINGS_NAV array that the dropdown and the sheet share.

calculateDrivingDistance was called and never imported
  CreateOrder.jsx:371 — a ReferenceError on every customer-pickup order with
  a pinned collection address, thrown synchronously inside a useEffect so
  the .catch() on that chain could never see it. Neither the build nor the
  lint config catches an unbound identifier in a .jsx file: eslint.config.js
  spreads pluginJs.configs.recommended and then declares its own rules
  object, which replaces the spread rules wholesale, so no-undef has never
  run. A repo-wide sweep with it enabled found this was the only instance.
  Pre-existing; found while auditing the coordinate changes.

Dead code and stale docs
  ALL_DESTINATIONS (declared, never read; its comment claimed it was what
  the mobile sheet renders). --dai-panel-width (declared twice, read
  nowhere) and the max-width:1279px block that only set it. AdminLayout's
  header comment said "three grouped menus (Fleet Ops, Reports, Settings)"
  when there are two, and Settings is a section in the account menu — that
  difference in routing is what caused the mobile gap above.

VERIFIED

878 tests across 26 suites, build clean, no new lint problems. The tab
strip, dock width, chip wrapping and navbar sizing were measured in a real
browser against the shipped stylesheet. deriveVisualData is module-private,
so it was extracted and driven through 16 logic checks.

NOT VERIFIED

/doormile/home has never been rendered in a browser, and the responsive
pass covers the assistant panel only — no page has been viewed at any
breakpoint. Both need a signed-in session. DataTable carries its own
overflow-x-auto and only two fixed widths above 390px exist in src/pages,
but that is grep, not eyes.

STILL OPEN

customerAppBookings.js lost its customerstatus/customerstage grouping in
49ee0c5 and has not been restored, so the Bookings tabs still read only the
operational status. The Status column (Bookings.jsx:98, :415) was never
stage-aware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 19:21:21 +05:30
parent 05f08efc64
commit 1671a5f4b6
35 changed files with 2941 additions and 475 deletions

View File

@@ -2,10 +2,12 @@ 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,
Activity, Bell, Bike, Building2, Car, ChevronDown, Coins, FileSpreadsheet,
FileText, IndianRupee, ListTodo, LogOut, Menu, Search, Settings, Shield,
ShieldAlert, User, UserCheck, Users, Warehouse,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { DOORMILE_WORDMARK_URL } from '@/assets/brand';
import { DOORMILE_MARK_URL, DOORMILE_WORDMARK_URL } from '@/assets/brand';
import { Avatar } from '@/components/ds/Avatar';
import { Badge } from '@/components/ui/badge';
import {
@@ -23,23 +25,22 @@ 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.
* indicator, mobile sheet.
*
* 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.
* Core destinations sit on the bar; the rest live in two grouped menus on it
* (Fleet Ops, Reports) plus a Settings section inside the account menu, which
* holds the two destinations that are configuration rather than operations
* (Pricing Matrix, Customers Directory).
*
* Said precisely because the previous wording — "three grouped menus (Fleet
* Ops, Reports, Settings)" — sent a reader looking for a third NAV_GROUPS
* entry that does not exist, and hid the fact that the Settings pair is
* reached by a different route from everything else. That difference is
* exactly what made them unreachable on mobile.
*/
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%)',
};
@@ -52,8 +53,6 @@ const NAV = [
{ label: 'Deliveries', path: '/doormile/deliveries' },
{ label: 'Milers', path: '/doormile/milers' },
{ 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. */
@@ -61,27 +60,45 @@ 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: 'Hubs', path: '/doormile/hubs', icon: Warehouse, desc: 'Sortation centers & facilities' },
{ label: 'Vehicles', path: '/doormile/vehicles', icon: Car, desc: 'Fleet register & EV telematics' },
{ label: 'Tripsheets', path: '/doormile/tripsheets', icon: ListTodo, desc: 'Linehaul & middle-mile runs' },
{ label: 'Exceptions', path: '/doormile/exceptions', icon: ShieldAlert, desc: 'Live alerts & delivery blockers' },
{ label: 'Competitive Intel', path: '/doormile/competitive-intel', icon: Activity, desc: 'Benchmarking & market rates' },
{ label: 'App Users', path: '/doormile/app-users', icon: UserCheck, desc: 'System operators & roles' },
],
},
{
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' },
{ label: 'Orders Summary', path: '/doormile/reports/orderssummary', icon: FileSpreadsheet, desc: 'Daily volume & status aggregations' },
{ label: 'Orders Details', path: '/doormile/reports/ordersdetails', icon: FileText, desc: 'Granular leg-by-leg breakdown' },
{ label: 'Milers Summary', path: '/doormile/reports/riderssummary', icon: Bike, desc: 'Fleet KPIs & on-duty shifts' },
{ label: 'Profitability', path: '/doormile/reports/profitability', icon: Coins, desc: 'Gross margin & variable costs' },
],
},
];
/** Every destination, flattened — what the mobile sheet renders. */
const ALL_DESTINATIONS = [...NAV, ...NAV_GROUPS.flatMap((group) => group.items)];
/* Configuration destinations, kept off the operations bar.
*
* Declared here rather than written inline in the account dropdown because
* two surfaces need them: that dropdown on desktop, and the mobile sheet.
* When they lived only in the dropdown's JSX, moving them off the bar quietly
* removed them from the phone entirely — the sheet builds its list from NAV
* plus NAV_GROUPS, and these were in neither. Both pages stayed routed and
* reachable by typed URL, which is the kind of gap nobody reports and nobody
* finds.
*
* (This replaces an ALL_DESTINATIONS constant that flattened NAV and
* NAV_GROUPS. Its comment claimed it was "what the mobile sheet renders"; the
* sheet had moved to a role-filtered local of the same name in lower case, so
* the constant had no readers at all and would have reintroduced the same
* omission if anyone had trusted it.)
*/
const SETTINGS_NAV = [
{ label: 'Pricing Matrix', path: '/doormile/pricing', icon: IndianRupee },
{ label: 'Customers Directory', path: '/doormile/customers', icon: Users },
];
function GlobalSearch() {
const navigate = useNavigate();
@@ -225,7 +242,27 @@ export default function AdminLayout() {
const navigate = useNavigate();
const { user, isClient, logout } = useAuth();
const [menuOpen, setMenuOpen] = useState(false);
const [assistantOpen, setAssistantOpen] = useState(false);
const [assistantOpen, setAssistantOpen] = useState(() => {
try {
const saved = localStorage.getItem('doormileAssistantOpen');
if (saved !== null) return saved === 'true';
} catch {
/* ignore */
}
return typeof window !== 'undefined' ? window.innerWidth >= 1024 : true;
});
const toggleAssistant = (nextState) => {
setAssistantOpen((prev) => {
const next = typeof nextState === 'function' ? nextState(prev) : nextState;
try {
localStorage.setItem('doormileAssistantOpen', String(next));
} catch {
/* ignore */
}
return next;
});
};
const navItems = useMemo(() => {
if (isClient) {
@@ -253,6 +290,7 @@ export default function AdminLayout() {
const groupIsActive = (group) => group.items.some((item) => isActive(item.path));
const displayName = user?.displayname || user?.authname || user?.name || 'Operator';
const isHome = location.pathname === '/doormile/home' || location.pathname === '/doormile' || location.pathname === '/';
return (
/* Transparent so the ambient canvas painted behind the app reads through.
@@ -265,22 +303,14 @@ export default function AdminLayout() {
<header data-app-header className="sticky top-0 z-50 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"
to="/doormile/home"
aria-label="Doormile Express — Home"
className="flex shrink-0 items-center rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50 transition-transform active:scale-95"
>
{/* 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. */}
{/* Capped below `sm`: at its natural 209px the wordmark plus the
170px action cluster overflowed a 375px phone and scrolled the
whole document sideways. Scaling the mark is the one thing here
that can give — the controls beside it all have to stay. */}
<img
src={DOORMILE_WORDMARK_URL}
alt="Doormile Express"
style={LOGO_FILTER}
className="h-7 w-auto max-w-[120px] object-contain sm:max-w-none"
src={DOORMILE_MARK_URL}
alt="Doormile"
className="h-8 w-auto object-contain"
/>
</Link>
@@ -316,13 +346,14 @@ export default function AdminLayout() {
<button
type="button"
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',
'relative flex items-center gap-1.5 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 cursor-pointer',
groupIsActive(group) ? 'text-brand' : 'text-ink-3 hover:text-ink-1'
)}
>
{group.label}
<ChevronDown className="h-3 w-3" aria-hidden="true" />
{group.icon && <group.icon className="h-3.5 w-3.5 opacity-70" />}
<span>{group.label}</span>
<ChevronDown className="h-3 w-3 opacity-60" aria-hidden="true" />
{groupIsActive(group) && (
<motion.span
layoutId="adminNavIndicator"
@@ -332,16 +363,40 @@ export default function AdminLayout() {
)}
</button>
</DropdownMenuTrigger>
<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 align="start" className="w-64 p-1.5 rounded-2xl shadow-xl border-slate-200">
{group.items.map((item) => {
const ItemIcon = item.icon;
const active = isActive(item.path);
return (
<DropdownMenuItem
key={item.path}
onClick={() => navigate(item.path)}
className={cn(
'flex items-start gap-2.5 p-2 rounded-xl cursor-pointer transition-colors my-0.5',
active ? 'bg-brand-tint/80 text-brand' : 'hover:bg-slate-50 text-ink-1'
)}
>
{ItemIcon && (
<div
className={cn(
'w-7 h-7 rounded-lg flex items-center justify-center shrink-0 mt-0.5',
active ? 'bg-brand text-white' : 'bg-slate-100 text-slate-600'
)}
>
<ItemIcon className="w-3.5 h-3.5" />
</div>
)}
<div className="min-w-0 flex-1">
<div className="text-body-sm font-semibold">{item.label}</div>
{item.desc && (
<div className="text-[11px] text-ink-3 leading-tight mt-0.5 line-clamp-1">
{item.desc}
</div>
)}
</div>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
))}
@@ -350,22 +405,32 @@ export default function AdminLayout() {
<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'
)}
>
<img className="h-9 w-9 max-w-none object-contain" src={doormileMark} alt="" aria-hidden="true" />
</button>
{/* MileTruth. A header control rather than a route: shown on all pages except the home page */}
{!isHome && (
<button
type="button"
onClick={() => toggleAssistant((open) => !open)}
aria-label={assistantOpen ? 'Close MileTruth' : 'Open MileTruth'}
aria-expanded={assistantOpen}
className={cn(
'grid h-8 w-8 place-items-center rounded-lg transition-colors cursor-pointer',
'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'
)}
>
{/* 20px inside a 32px button.
Was `h-9 w-9 max-w-none` — a 36px image in a 32px button,
so the mark overflowed its own control by 2px on every
side. Two visible consequences: it stood proud of the
notification bell and avatar beside it, breaking the row's
alignment, and it covered the button's own area completely,
so the `hover:bg-surface-sunken` tint had nowhere to show —
the control looked dead on hover while its neighbours lit
up. Sized to sit inside the button, like the Bell's 16px
icon does. */}
<img className="h-5 w-5 object-contain" src={doormileMark} alt="" aria-hidden="true" />
</button>
)}
{!isClient && <NotificationsMenu />}
@@ -382,27 +447,40 @@ export default function AdminLayout() {
</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'}
<DropdownMenuContent align="end" className="w-64 p-1.5 rounded-2xl shadow-xl border-slate-200">
<DropdownMenuLabel className="font-normal px-2.5 py-2">
<p className="text-body-sm font-bold text-ink-1">{displayName}</p>
<p className="text-caption text-ink-3 truncate">{user?.email || user?.primaryemail || 'admin@doormile.com'}</p>
<Badge variant="soft" size="sm" className="mt-1.5 bg-rose-50 text-rose-700 font-semibold border-rose-200">
{user?.role || 'admin'}
</Badge>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate('/doormile/profile')} className="cursor-pointer">
<User className="mr-2 h-4 w-4" /> Profile
<DropdownMenuSeparator className="my-1" />
<DropdownMenuItem onClick={() => navigate('/doormile/profile')} className="cursor-pointer rounded-xl px-2.5 py-2 font-medium">
<User className="mr-2.5 h-4 w-4 text-slate-500" /> Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/doormile/profile#security')} className="cursor-pointer">
<Shield className="mr-2 h-4 w-4" /> Change password
<DropdownMenuItem onClick={() => navigate('/doormile/profile#security')} className="cursor-pointer rounded-xl px-2.5 py-2 font-medium">
<Shield className="mr-2.5 h-4 w-4 text-slate-500" /> Change password
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSeparator className="my-1" />
<div className="px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 flex items-center gap-1.5">
<Settings className="w-3 h-3 text-slate-400" /> Settings
</div>
{SETTINGS_NAV.map((item) => (
<DropdownMenuItem
key={item.path}
onClick={() => navigate(item.path)}
className="cursor-pointer rounded-xl px-2.5 py-2 font-medium"
>
<item.icon className="mr-2.5 h-4 w-4 text-slate-500" /> {item.label}
</DropdownMenuItem>
))}
<DropdownMenuSeparator className="my-1" />
<DropdownMenuItem
onClick={() => logout()}
className="cursor-pointer text-destructive focus:text-destructive"
className="cursor-pointer text-destructive focus:text-destructive rounded-xl px-2.5 py-2 font-medium"
>
<LogOut className="mr-2 h-4 w-4" /> Sign out
<LogOut className="mr-2.5 h-4 w-4" /> Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -441,6 +519,26 @@ export default function AdminLayout() {
{item.label}
</Link>
))}
{/* Settings — the pair that lives in the account menu on desktop.
Without this block they existed on the bar's dropdown only, so
a phone could not reach Pricing or Customers at all. */}
<div className="my-2 h-px bg-border" />
<div className="px-3 pb-1 text-[10px] font-bold uppercase tracking-wider text-ink-3">
Settings
</div>
{SETTINGS_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-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"
@@ -456,13 +554,16 @@ export default function AdminLayout() {
{/* `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")}>
<div className={cn("dai-page flex flex-col flex-1", !location.pathname.startsWith('/doormile/dispatch') && "px-4 md:flex-row lg:px-6")}>
<main className={cn(
"min-w-0 flex-1 flex flex-col",
!location.pathname.startsWith('/doormile/dispatch') && location.pathname !== '/doormile/home' && location.pathname !== '/doormile' && "py-6"
)}>
<Outlet />
</main>
</div>
{assistantOpen && <AIPanel isOpen={assistantOpen} onClose={() => setAssistantOpen(false)} />}
{!isHome && <AIPanel isOpen={assistantOpen} onClose={() => toggleAssistant(false)} />}
</div>
);
}