import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { Ban, CheckCircle2, CircleDashed, Download } from 'lucide-react';
import {
AIDecisionStream,
Avatar,
Badge,
Card,
ChannelBadge,
formatINR,
SegmentBadge,
TimelineEntry,
WorkflowStepper,
} from '../components';
import { LeadActions, STEP_BY_STATE } from '../components/activities';
import { useQuery, useZino } from '../api/provider';
import { useLeads } from '../api/leads';
import { recordToLead, decisionToCard, auditToTimeline } from '../api/adapters';
import { STAGES, ONBOARD_ACTIVITY_UID } from '../api/config';
import { DISQUALIFY_STATES } from '../api/forms';
// Compact Indian currency for the small stat tiles, so large cover figures
// (₹2,00,00,000) never overflow + get clipped by the card. Crore/lakh short
// form above ₹1L; full grouping below.
function compactINR(n: number): string {
if (!n) return '₹0';
if (n >= 1e7) return `₹${+(n / 1e7).toFixed(2)} Cr`;
if (n >= 1e5) return `₹${+(n / 1e5).toFixed(2)} L`;
return `₹${formatINR(n)}`;
}
/** A compact label/value tile for the Profile grid — fills the wide card width
* far better than full-width key→value rows. */
function ProfileTile({ label, value, mono }: { label: string; value: ReactNode; mono?: boolean }) {
return (
);
}
export function Lead360Screen() {
const { id } = useParams();
const { client } = useZino();
// Shared single fetch of all leads; pick the one we're viewing.
const { records, loading: leadsLoading, refetch } = useLeads();
const record = useMemo(
() => records.find((r) => String(r.instance_id) === id) ?? records[0],
[records, id],
);
const instanceId = record?.instance_id;
// No polling — async AI decisions are pulled on demand via the stream's
// refresh button (and after any activity advance), so the view never flickers.
const decisionsQ = useQuery(() => client.aiDecisions(instanceId!), [instanceId], instanceId != null);
const auditQ = useQuery(() => client.audit(instanceId!), [instanceId], instanceId != null);
const onAdvanced = () => {
refetch();
decisionsQ.refetch();
auditQ.refetch();
};
// Manual refresh of the live AI feed + audit (no reload of the whole lead set).
const refreshFeed = () => {
decisionsQ.refetch();
auditQ.refetch();
};
if (leadsLoading) {
return Loading lead…
;
}
if (!record) {
return Lead not found.
;
}
const lead = recordToLead(record);
const decisions = (decisionsQ.data ?? []).map(decisionToCard);
const audit = auditQ.data ?? [];
const timeline = auditToTimeline(audit);
// 5d onboarding outcome: welcome_email_status + policy_doc are written by the
// Onboard trigger onto the lead record. onboarding_notes is still a `local`
// activity field, so it lives in the audit feed, not the recordview.
const onboard = audit.find((e) => e.activity_id === ONBOARD_ACTIVITY_UID)?.data;
const welcomeStatus =
typeof record.welcome_email_status === 'string' ? record.welcome_email_status : null;
const policyDocUrl = record.policy_doc?.[0]?.url ?? null;
const onboarded = !!onboard || !!welcomeStatus || !!policyDocUrl;
const welcomeSent = welcomeStatus === 'sent';
const welcomeFailed = welcomeStatus === 'failed';
const onboardingNotes =
typeof onboard?.onboarding_notes === 'string' ? onboard.onboarding_notes : '';
const issueDate = record.policy_issue_date ? new Date(record.policy_issue_date) : null;
const issueLabel = issueDate && !Number.isNaN(issueDate.getTime())
? issueDate.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })
: null;
const cover = record.sum_assured ?? 0;
const premium = record.premium_amount ?? 0;
const meeting = record.meeting_datetime ? new Date(record.meeting_datetime) : null;
const meetingLabel = meeting && !Number.isNaN(meeting.getTime())
? meeting.toLocaleString('en-IN', { weekday: 'short', day: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit', hour12: true })
: null;
const issued = lead.stage >= STAGES.indexOf('Policy Issued');
const state = record.current_state_name ?? '';
const hasAction = !!STEP_BY_STATE[state] || DISQUALIFY_STATES.has(state);
return (
{/* HERO — identity + lifecycle + headline figures unified into one band so
the page reads as a single record, not a scatter of tiles. */}
{lead.name}
#{lead.id} · {lead.city} · owned by {lead.owner}
{meetingLabel ? ` · meeting ${meetingLabel}` : ''}
{record.current_state_name ?? '—'}
{hasAction && }
{/* lifecycle stepper */}
{/* headline figures */}
Sum assured
{compactINR(cover)}
Premium
{compactINR(premium)}
Annual income
{compactINR(lead.income)}
{/* BODY — primary work on the left, live AI feed + reference on the right. */}
{/* MAIN */}
{onboarded && (
{welcomeSent ? 'Welcome sent' : welcomeFailed ? 'Welcome failed' : 'Welcome pending'}
}
>
Policy number
{record.policy_number || '—'}
Issued
{issueLabel || '—'}
{welcomeSent ? (
) : welcomeFailed ? (
) : (
)}
Welcome email {welcomeSent ? 'sent' : welcomeFailed ? 'failed to send' : 'pending'}
{policyDocUrl && (
)}
{onboardingNotes && (
Onboarding notes
{onboardingNotes}
)}
)}
{timeline.length ? (
timeline.map((t, i) => (
))
) : (
No activity recorded yet.
)}
{/* RAIL — live AI decision feed (manual refresh, no polling). */}
);
}