)}
- {/* KYC identity cross-check — OCR'd name + DOB vs the captured lead */}
{kyc.hasMismatch ? (
@@ -298,7 +234,7 @@ export function DocumentsScreen() {
))}
- Document status set to “Mismatch”. Override only once you’ve confirmed the documents belong to this lead.
+ Document status set to "Mismatch". Override only once you've confirmed the documents belong to this lead.
- {record ? 'Not enough data (DOB / income / sum assured) to assess.' : 'Select a lead at Eligibility Assessed.'}
+ {record ? 'Not enough data (DOB / income / sum assured) to assess.' : 'Not enough data to assess.'}
)}
diff --git a/src/components/activities/Worklist.tsx b/src/components/activities/Worklist.tsx
new file mode 100644
index 0000000..c640bf0
--- /dev/null
+++ b/src/components/activities/Worklist.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Card, KpiCard, LeadRow, LEAD_GRID, Pagination } from '../';
+import { LeadFilters } from '../LeadFilters';
+import { cn } from '../../lib/cn';
+import { useLeads } from '../../api/leads';
+import { recordToLead } from '../../api/adapters';
+import { applyFilters, EMPTY_FILTER, type FilterState } from '../../api/filters';
+import type { LeadRecord } from '../../api/types';
+import { LeadActions, AddLeadButton } from './ActivityActions';
+
+const PAGE_SIZE = 10;
+
+/** A compact stat tile computed over this screen's (already state-filtered) rows. */
+export interface TileSpec {
+ label: string;
+ value: (rows: LeadRecord[]) => string;
+}
+
+export interface WorklistProps {
+ /** Lifecycle states this screen lists (in display order). */
+ states: readonly string[];
+ emptyHint?: string;
+ showAddLead?: boolean;
+ /** A small set of stat tiles shown above the table (keep it to 2-4). */
+ tiles?: TileSpec[];
+}
+
+/** A state-scoped lead worklist styled like the Lead Pipeline screen: a KPI row
+ * + the shared leads table (LeadRow), with each row's one primary action
+ * (+ Disqualify) in its action cell, launched in a modal. */
+export function Worklist({ states, emptyHint, showAddLead, tiles }: WorklistProps) {
+ const navigate = useNavigate();
+ const { records, fields, loading, refetch } = useLeads();
+ const [filters, setFilters] = useState(EMPTY_FILTER);
+
+ const order = useMemo(() => new Map(states.map((s, i) => [s, i])), [states]);
+ // Scope to this screen's states, then apply the search/filter bar.
+ const scoped = useMemo(
+ () =>
+ records
+ .filter((r) => order.has(r.current_state_name ?? ''))
+ .sort((a, b) => order.get(a.current_state_name ?? '')! - order.get(b.current_state_name ?? '')!),
+ [records, order],
+ );
+ const recs = useMemo(() => applyFilters(scoped, filters, fields), [scoped, filters, fields]);
+ const rows = useMemo(() => recs.map((record) => ({ record, lead: recordToLead(record) })), [recs]);
+
+ // Client-side pagination over the filtered rows (all leads are already loaded
+ // via useLeads). Page is clamped so it stays valid after rows shrink.
+ const [page, setPage] = useState(1);
+ // Snap back to the first page whenever the filter set changes.
+ useEffect(() => setPage(1), [filters]);
+ const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
+ const safePage = Math.min(page, totalPages);
+ const start = (safePage - 1) * PAGE_SIZE;
+ const pageRows = rows.slice(start, start + PAGE_SIZE);
+
+ return (
+
+ {/* KPI row — Pipeline-screen tiles, width-capped so a couple don't stretch huge. */}
+ {tiles?.length ? (
+
}
+ {/* Placeholder only on first load; a manual refresh keeps the list visible
+ (the header button spins) so the stream never blanks out. */}
+ {loading && decisions.length === 0 && (
+
Loading decisions…
+ )}
{!loading && decisions.length === 0 && (
No AI decisions for this lead yet.
)}
diff --git a/src/components/insurance/LeadRow.tsx b/src/components/insurance/LeadRow.tsx
index 9695799..ec5e652 100644
--- a/src/components/insurance/LeadRow.tsx
+++ b/src/components/insurance/LeadRow.tsx
@@ -1,3 +1,4 @@
+import type { ReactNode } from 'react';
import { cn } from '../../lib/cn';
import type { Lead, Tone } from '../../types';
import { Avatar } from '../core/Avatar';
@@ -8,6 +9,9 @@ export interface LeadRowProps {
lead: Lead;
onClick?: () => void;
className?: string;
+ /** When set, replaces the "last action" cell (e.g. a worklist action button).
+ * Clicks inside it don't trigger the row's onClick. */
+ action?: ReactNode;
}
/** Shared grid template for the leads table — keep header + rows in sync. */
@@ -27,8 +31,9 @@ function scoreColor(score: number): string {
return 'var(--slate-500)';
}
-/** Leads-list table row — name, score, segment, channel, owner, last AI action. */
-export function LeadRow({ lead, onClick, className }: LeadRowProps) {
+/** Leads-list table row — name, score, segment, channel, owner, last AI action
+ * (or a custom `action` cell). */
+export function LeadRow({ lead, onClick, className, action }: LeadRowProps) {
return (
{lead.owner}
- {/* last AI action */}
-
-
- {lead.lastAction}
-
+ {/* last AI action — or a custom action cell */}
+ {action !== undefined ? (
+
e.stopPropagation()}>
+ {action}
+
+ ) : (
+
+
+ {lead.lastAction}
+
+ )}
);
}
diff --git a/src/components/insurance/WorkflowStepper.tsx b/src/components/insurance/WorkflowStepper.tsx
index c6799dc..89555ae 100644
--- a/src/components/insurance/WorkflowStepper.tsx
+++ b/src/components/insurance/WorkflowStepper.tsx
@@ -19,11 +19,14 @@ export function WorkflowStepper({
className,
}: WorkflowStepperProps) {
const vertical = orientation === 'vertical';
+ // Every dot sits in a fixed 28px (h-7) band so all centers land on the same
+ // line regardless of dot size; connectors align to that center (14px), not to
+ // the dot+label column. Keeps the rail straight even with the larger active dot.
return (
@@ -33,16 +36,18 @@ export function WorkflowStepper({
const isLast = i === stages.length - 1;
return (
-
-
- {done ? : i + 1}
+
+
+
+ {done ? : i + 1}
+
)}
diff --git a/src/layout/Shell.tsx b/src/layout/Shell.tsx
index 76f1c29..802bf4d 100644
--- a/src/layout/Shell.tsx
+++ b/src/layout/Shell.tsx
@@ -1,10 +1,10 @@
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 { Bell, CalendarClock, FileText, 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 { canSeeScreen } from '../api/config';
import { useZino } from '../api/provider';
import { useLeads } from '../api/leads';
import type { Lead } from '../types';
@@ -19,29 +19,13 @@ interface NavItem {
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' },
+ { to: '/schedule', label: 'Agent Schedule', Icon: CalendarClock, title: 'Agent schedule', subtitle: 'Sales-agent meetings & booking slots' },
+ { to: '/qualify', label: 'Qualify & Assign', Icon: UserCheck, title: 'Qualification & Assignment', subtitle: 'Qualify new leads and assign them to an agent' },
+ { to: '/engage', label: 'Customer Interaction', Icon: Sparkles, title: 'Customer Interaction', subtitle: 'Log contact, schedule meetings, recommend products' },
+ { to: '/documents', label: 'Documents', Icon: FileText, title: 'Document & Assessment', subtitle: 'KYC collection, OCR, eligibility assessment' },
+ { to: '/underwriting', label: 'Underwriting & Policy', Icon: ShieldCheck, title: 'Underwriting & Policy Issuance', subtitle: 'Underwrite, issue policy, onboard customer' },
];
-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';
@@ -63,7 +47,7 @@ function AriaLogo() {
}
/** 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[] }) {
+function SidebarInner({ items, activeTo, onNav }: { items: NavItem[]; activeTo: string; onNav: (to: string) => void }) {
return (
<>