lead-to-policy/src/screens/worklists.tsx
Bhanu Prakash Sai Potteri 52a691fd75 feat: form RBAC guard, file fields, AI eligibility card, worklist recency sort + UI polish
- SchemaGuard: gate all activity bodies on form-screens (403 → permission card, not live form); dedup ActivityForm/RecommendBody
- ActivityForm: generic file-field upload (Onboard policy doc etc)
- UnderwritingBody: AI eligibility-notes parser (+raw fallback) in navy card, drop placeholder assess() panel
- DocumentUploadCard: equal-height cards + Replace/Remove
- Worklist + leads: sort latest-first by updated_at (instance_id tiebreak)
- Modal: cap 85vh, scroll body; slim themed scrollbars (schedule/rail/modal/kanban)
- Schedule agenda + Kanban columns: cap height, scroll internally; minimal kanban card
- Topbar: back button each screen; Avatar lucide type fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:10:11 +05:30

84 lines
3.0 KiB
TypeScript

// The four operational worklist screens. Each lists the leads at the lifecycle
// states it owns; every row carries its one primary action (+ Disqualify),
// launched in a modal. Each screen shows a few compact stat tiles over its rows.
// (The Lead Pipeline screen is the read-only all-leads dashboard.)
import { Worklist, type TileSpec } from '../components/activities';
import { formatINR } from '../components';
import type { LeadRecord } from '../api/types';
const count = (rows: LeadRecord[]) => String(rows.length);
const countWhere = (pred: (r: LeadRecord) => boolean) => (rows: LeadRecord[]) => String(rows.filter(pred).length);
const sumINR = (col: keyof LeadRecord) => (rows: LeadRecord[]) =>
`${formatINR(rows.reduce((s, r) => s + (Number(r[col]) || 0), 0))}`;
const avg = (col: keyof LeadRecord) => (rows: LeadRecord[]) => {
const vals = rows.map((r) => Number(r[col])).filter((n) => !Number.isNaN(n));
return vals.length ? String(Math.round(vals.reduce((a, b) => a + b, 0) / vals.length)) : '—';
};
/** Qualify (New Lead) + Assign (Qualified). */
const QUALIFY_TILES: TileSpec[] = [
{ label: 'In queue', value: count },
{ label: 'Avg lead score', value: avg('lead_score') },
];
export function QualificationScreen() {
return (
<Worklist
states={['New Lead', 'Qualified']}
tiles={QUALIFY_TILES}
emptyHint="No new or qualified leads in the queue."
/>
);
}
/** Log Contact (Assigned) · Schedule (Contacted) · Recommend (Meeting Scheduled). */
const ENGAGE_TILES: TileSpec[] = [
{ label: 'In queue', value: count },
{ label: 'Meetings booked', value: countWhere((r) => !!r.meeting_mode) },
{ label: 'Total premium', value: sumINR('premium_amount') },
];
export function InteractionScreen() {
return (
<Worklist
states={['Assigned', 'Contacted', 'Meeting Scheduled']}
tiles={ENGAGE_TILES}
emptyHint="No leads awaiting contact, scheduling, or a recommendation."
/>
);
}
/** Collect Documents (Product Recommended) · Assess Eligibility (Documents Collected). */
const DOCS_TILES: TileSpec[] = [
{ label: 'In queue', value: count },
{ label: 'Docs collected', value: countWhere((r) => r.current_state_name === 'Documents Collected') },
];
export function DocAssessmentScreen() {
return (
<Worklist
states={['Product Recommended', 'Documents Collected']}
tiles={DOCS_TILES}
emptyHint="No leads awaiting document collection or assessment."
/>
);
}
/** Underwrite (Eligibility Assessed) · Issue (Underwriting) · Onboard (Policy Issued). */
const UW_TILES: TileSpec[] = [
{ label: 'In queue', value: count },
{ label: 'Policies issued', value: countWhere((r) => r.current_state_name === 'Policy Issued') },
{ label: 'Total premium', value: sumINR('premium_amount') },
];
export function UnderwritingPolicyScreen() {
return (
<Worklist
states={['Eligibility Assessed', 'Underwriting', 'Policy Issued']}
tiles={UW_TILES}
emptyHint="No leads in underwriting, issuance, or onboarding."
/>
);
}