lead-to-policy/src/screens/AssignScreen.tsx
2026-06-23 11:20:31 +05:30

210 lines
8.3 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { CheckCircle2, UserCheck } from 'lucide-react';
import { AIDecisionCard, Avatar, Badge, Button, Card, formatINR, Select } from '../components';
import { LookupField } from '../components/form';
import type { LookupValue } from '../components/form';
import { useQuery, useZino } from '../api/provider';
import { useLeads } from '../api/leads';
import { decisionToCard } from '../api/adapters';
import { fieldFromSchema } from '../api/schema';
import { ACTIVITY_META } from '../api/forms';
const DEF = ACTIVITY_META.assign;
export function AssignScreen() {
const { client } = useZino();
const navigate = useNavigate();
const [params] = useSearchParams();
const instanceParam = params.get('instance');
const { records, refetch } = useLeads();
// Lookup field config (templates / display+storage cols) comes from the live
// form schema, not hardcoded.
const schemaQ = useQuery(() => client.formSchema(DEF.uid), []);
const REGION = useMemo(() => fieldFromSchema(schemaQ.data, 'assigned_region_input'), [schemaQ.data]);
const OWNER = useMemo(() => fieldFromSchema(schemaQ.data, 'owner_agent_input'), [schemaQ.data]);
// Assign Lead is valid at the Qualified state.
const eligible = useMemo(() => records.filter((r) => r.current_state_name === 'Qualified'), [records]);
const [selectedId, setSelectedId] = useState('');
useEffect(() => {
if (instanceParam) setSelectedId(instanceParam);
else if (!selectedId && eligible[0]) setSelectedId(String(eligible[0].instance_id));
}, [instanceParam, eligible, selectedId]);
const record = useMemo(() => records.find((r) => String(r.instance_id) === selectedId), [records, selectedId]);
const [region, setRegion] = useState<LookupValue | null>(null);
const [agent, setAgent] = useState<LookupValue | null>(null);
useEffect(() => {
if (!record) return;
setRegion((record.assigned_region as LookupValue) ?? null);
setAgent((record.owner_agent as LookupValue) ?? null);
}, [record]);
const [submitting, setSubmitting] = useState(false);
const [result, setResult] = useState<{ ok: boolean; msg: string } | null>(null);
async function submit() {
if (!record) return;
if (!region) {
setResult({ ok: false, msg: 'Assigned region is required.' });
return;
}
setSubmitting(true);
setResult(null);
try {
await client.performActivity(record.instance_id, DEF.uid, {
assigned_region_input: region,
...(agent ? { owner_agent_input: agent } : {}),
});
setResult({ ok: true, msg: 'Lead assigned — advanced to Assigned.' });
refetch();
} catch (e) {
setResult({ ok: false, msg: e instanceof Error ? e.message : ((e as { message?: string })?.message ?? 'Submit failed') });
} finally {
setSubmitting(false);
}
}
return (
<div className="grid gap-[18px] max-w-[1140px] mx-auto items-start grid-cols-1 lg:grid-cols-[1fr_340px]">
<div className="flex flex-col gap-[18px] min-w-0">
{/* context strip + lead picker */}
<div className="flex items-center gap-3 bg-card border border-border-subtle rounded-lg px-[18px] py-3.5 shadow-sm">
{record ? (
<>
<Avatar name={record.lead_name ?? `Lead ${record.instance_id}`} size={40} />
<div className="flex-1">
<div className="text-base font-bold text-strong">
{record.lead_name ?? `Lead ${record.instance_id}`} · #{record.instance_id}
</div>
<div className="text-xs text-faint">
{record.city ?? record.state_region ?? '—'} · {record.product_interest ?? 'No stated interest'} · income
{formatINR(record.annual_income ?? 0)}
</div>
</div>
{record.lead_segment && <Badge tone="accent">{record.lead_segment}</Badge>}
</>
) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Qualified stage.</div>
)}
<Select
options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No qualified leads' },
...records.map((r) => ({
value: String(r.instance_id),
label: `#${r.instance_id} ${r.lead_name ?? ''} · ${r.current_state_name ?? ''}`,
})),
]}
value={selectedId}
onChange={(e) => {
setSelectedId(e.target.value);
setResult(null);
}}
className="w-[260px]"
/>
</div>
{result && (
<div
className={
result.ok
? 'flex items-center gap-2 text-sm text-emerald-700 bg-emerald-100 border border-emerald-300 rounded-md px-4 py-3'
: 'text-sm text-amber-700 bg-escalated-soft border border-amber-300 rounded-md px-4 py-3'
}
>
{result.ok && <CheckCircle2 size={16} />}
{result.msg}
{result.ok && record && (
<button
className="ml-auto text-sm font-semibold text-link underline"
onClick={() => navigate(`/leads/${record.instance_id}`)}
>
View lead
</button>
)}
</div>
)}
<Card title="Assign to agent">
<div className="grid grid-cols-2 gap-4">
{REGION && (
<LookupField
label={REGION.label}
required={REGION.required}
templateUid={REGION.lookupTemplate!}
displayCols={REGION.lookupDisplay ?? []}
storageCols={REGION.lookupStorage ?? []}
activityId={DEF.uid}
fieldId={REGION.id}
instanceId={record?.instance_id}
value={region}
onChange={setRegion}
/>
)}
{OWNER && (
<LookupField
label={OWNER.label}
templateUid={OWNER.lookupTemplate!}
displayCols={OWNER.lookupDisplay ?? []}
storageCols={OWNER.lookupStorage ?? []}
activityId={DEF.uid}
fieldId={OWNER.id}
instanceId={record?.instance_id}
value={agent}
onChange={setAgent}
/>
)}
{schemaQ.loading && <div className="col-span-full text-sm text-faint">Loading form</div>}
</div>
<div className="flex gap-2.5 mt-5">
<Button variant="primary" disabled={!record || submitting} onClick={submit}>
{submitting ? 'Assigning…' : 'Assign lead'}
</Button>
</div>
</Card>
</div>
{/* RIGHT — assignment summary + AI suggestion */}
<div className="flex flex-col gap-4">
<div className="bg-navy-grad rounded-lg px-5 py-[22px] shadow-md">
<div className="text-2xs font-bold uppercase tracking-[0.06em] text-on-navy-muted mb-3.5">Assignment</div>
<div className="flex items-center gap-3">
<span className="w-10 h-10 rounded-md bg-[rgba(251,169,76,0.18)] text-sunrise-300 flex items-center justify-center">
<UserCheck size={20} />
</span>
<div className="min-w-0">
<div className="text-base font-bold text-on-navy truncate">
{agent ? String(agent.name ?? '—') : 'No agent yet'}
</div>
<div className="text-sm text-on-navy-muted truncate">
{agent?.region ? String(agent.region) : region?.region ? String(region.region) : 'Pick a region & agent'}
</div>
</div>
</div>
</div>
<AssignAiSuggestion instanceId={record?.instance_id} />
</div>
</div>
);
}
function AssignAiSuggestion({ instanceId }: { instanceId?: number }) {
const { client } = useZino();
const q = useQuery(() => client.aiDecisions(instanceId!), [instanceId], instanceId != null);
const latest = (q.data ?? [])[0];
if (!latest) return null;
const card = decisionToCard(latest);
return (
<>
<div className="text-2xs font-bold uppercase tracking-[0.06em] text-faint">AI suggestion</div>
<AIDecisionCard {...card} defaultExpanded />
</>
);
}