import { useEffect, useMemo, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import type { LucideIcon } from 'lucide-react'; import { Bell, FileText, FileSignature, LogOut, Menu, Search, ShieldCheck, Sparkles, UserCheck, Users, X } from 'lucide-react'; import { cn } from '../lib/cn'; import { Avatar } from '../components/core'; import { AI_EMPLOYEES } from '../api/config'; import { useZino } from '../api/provider'; import { useLeads } from '../api/leads'; import type { Lead } from '../types'; interface NavItem { to: string; label: string; Icon: LucideIcon; title: string; subtitle: string; } const NAV: NavItem[] = [ { to: '/pipeline', label: 'Lead Pipeline', Icon: Users, title: 'Lead pipeline', subtitle: '' }, { to: '/assign', label: 'Assign', Icon: UserCheck, title: 'Assign leads', subtitle: 'Route qualified leads to a sales agent' }, { to: '/recommend', label: 'Recommend', Icon: Sparkles, title: 'Recommend product', subtitle: 'Build and send a product recommendation' }, { to: '/documents', label: 'Documents', Icon: FileText, title: 'Document collection', subtitle: 'KYC capture with OCR auto-extraction' }, { to: '/underwriting', label: 'Underwriting', Icon: ShieldCheck, title: 'Underwriting', subtitle: 'Risk assessment & verdict' }, { to: '/issue', label: 'Issue Policy', Icon: FileSignature, title: 'Issue policy', subtitle: 'Generate policy number & dates' }, ]; interface RosterEntry { name: string; role: string; active: number; } // Live AI-employee roster: each config employee + count of in-flight leads it // currently owns (`ownerAi` is false on terminal states, so those drop off). function buildRoster(leads: Lead[]): RosterEntry[] { return Object.values(AI_EMPLOYEES).map((e) => ({ name: e.name, role: e.role, active: leads.filter((l) => l.ownerAi && l.owner === e.name).length, })); } // Pipeline nav subtitle, computed live: "N leads · X% hot · Y% AI-handled". function pipelineSubtitle(leads: Lead[]): string { if (!leads.length) return 'No leads yet'; const total = leads.length; const hot = Math.round((leads.filter((l) => l.segment === 'Hot').length / total) * 100); const ai = Math.round((leads.filter((l) => l.ownerAi).length / total) * 100); return `${total.toLocaleString('en-IN')} leads · ${hot}% hot · ${ai}% AI-handled`; } function AriaLogo() { return (
A aria
); } /** Sidebar contents — shared between the static desktop rail and the mobile drawer. */ function SidebarInner({ activeTo, onNav, roster }: { activeTo: string; onNav: (to: string) => void; roster: RosterEntry[] }) { return ( <>
AI Employees
{roster.map((e) => (
{e.name}
{e.role}
{e.active}
))}
); } function Topbar({ title, subtitle, onMenu }: { title: string; subtitle: string; onMenu: () => void }) { const { user, logout } = useZino(); const navigate = useNavigate(); return (

{title}

{subtitle &&
{subtitle}
}
{user?.name || 'User'}
{user?.email || ''}
); } /** App shell — navy sidebar (nav + AI-employee roster) + topbar, with routed content. */ export function Shell() { const location = useLocation(); const navigate = useNavigate(); const { leads } = useLeads(); const path = location.pathname; const navMatch = NAV.find((n) => path.startsWith(n.to)); // Routes reached contextually (no sidebar entry) still get a topbar title. const active = navMatch ?? (path.startsWith('/leads') ? { to: '/leads', label: 'Lead 360', Icon: Users, title: 'Lead 360', subtitle: 'Full lead context, AI decision stream & next action' } : path.startsWith('/new') ? { to: '/new', label: 'New lead', Icon: Users, title: 'Capture lead', subtitle: 'Add a new lead to the pipeline' } : NAV[0]); const roster = useMemo(() => buildRoster(leads), [leads]); const subtitle = active.to === '/pipeline' ? pipelineSubtitle(leads) : active.subtitle; const [navOpen, setNavOpen] = useState(false); // Close the mobile drawer whenever the route changes. useEffect(() => setNavOpen(false), [path]); return (
{/* Static rail — lg and up. */} {/* Drawer — below lg. */} {navOpen && (
setNavOpen(false)} />
)}
setNavOpen(true)} />
); }