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) => (
))}
>
);
}
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)} />
);
}