diff --git a/src/api/config.js b/src/api/config.js index 58f1e2e..e075119 100644 --- a/src/api/config.js +++ b/src/api/config.js @@ -119,7 +119,7 @@ const STATE_ACTIVITIES = { ], 'zk-state-qualified': [ { uid: 'zk-act-contact', label: 'Log Contact', by: 'Engage AI', role: 'force' }, - { uid: 'zk-act-retry-call', label: 'Retry Call', by: 'Scheduled', role: 'again' }, + { uid: 'zk-act-retry-call', label: 'Retry Call', by: 'Scheduled', role: 'force' }, ], 'zk-state-contacted': [ { uid: 'zk-act-request-docs', label: 'Request Documents', by: 'Engage AI', role: 'force' }, @@ -128,7 +128,7 @@ const STATE_ACTIVITIES = { // be found and uploaded. Everything else here is the AI's own chain. 'zk-state-docs': [ { uid: 'zk-act-collect-docs', label: 'Upload documents', by: 'you', role: 'do' }, - { uid: 'zk-act-doc-reminder', label: 'Send a reminder', by: 'Scheduled', role: 'again' }, + { uid: 'zk-act-doc-reminder', label: 'Send a reminder', by: 'Scheduled', role: 'force' }, { uid: 'zk-act-capture-motor', label: 'Capture Motor Risk', by: 'Engage AI', role: 'force' }, { uid: 'zk-act-capture-sme', label: 'Capture SME Risk', by: 'Engage AI', role: 'force' }, { uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI', role: 'force' }, diff --git a/src/components/ActivityForm.jsx b/src/components/ActivityForm.jsx index 5872860..e980b1e 100644 --- a/src/components/ActivityForm.jsx +++ b/src/components/ActivityForm.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useZino } from '../api/provider.jsx' -import { fieldApplies } from '../api/config.js' +import { baseFieldId, fieldApplies } from '../api/config.js' import { describeError, describeValidation } from '../api/errors.js' import FileField from './FileField.jsx' import './ActivityForm.css' @@ -68,15 +68,36 @@ export default function ActivityForm({ activityUid, instanceId, lead, onDone, on // whichever convention a pipeline is authored in, rather than breaking // again the next time one is written the other way. const pre = s.prefill_data || s.prefillData || s.field_defaults || {} + const seed = {} if (pre && typeof pre === 'object') { - const seed = {} for (const f of s.fields) { - const base = String(f.id ?? '').replace(/_\d+$/, '') + const base = baseFieldId(f.id ?? '') const v = pre[f.id] ?? pre[f.uid] ?? (base ? pre[base] : undefined) if (v !== undefined && v !== null && v !== '') seed[f.id] = v } - if (Object.keys(seed).length) setValues(seed) } + + // THE LEAD ALREADY KNOWS MOST OF THIS. The document form mirrors + // fourteen fields the record carries — registration, make and model, + // previous insurer, expiry, policy number, PAN — so the OCR can write + // into them. Rendered blank, they read as fourteen more things to type. + // Seed each from the lead by its base key, so an agent uploading for + // KA01MF6618 sees KA01MF6618 already there. + // + // Server prefill wins, then anything already typed. Files and generated + // ids are never seeded — a file reference is not a value to copy, and an + // id_gen is the platform's to issue. Only on an existing lead: an INIT + // form has no record behind it. + if (lead && instanceId) { + for (const f of s.fields) { + if (seed[f.id] !== undefined) continue + if (['file', 'ocr', 'id_gen'].includes(f.data_type)) continue + const v = lead[baseFieldId(f.id ?? '')] + if (v !== undefined && v !== null && v !== '' && typeof v !== 'object') seed[f.id] = v + } + } + + if (Object.keys(seed).length) setValues(seed) }) .catch((e) => { if (!dead) setError(e) }) return () => { dead = true } diff --git a/src/layout/Shell.jsx b/src/layout/Shell.jsx index 446d1d3..78e634f 100644 --- a/src/layout/Shell.jsx +++ b/src/layout/Shell.jsx @@ -48,6 +48,12 @@ export default function Shell() { > Overview + 'shell__today' + (isActive ? ' is-active' : '')} + > + All leads + {canFile ? ( diff --git a/src/screens/Lead.jsx b/src/screens/Lead.jsx index 1f1f13b..75c62a7 100644 --- a/src/screens/Lead.jsx +++ b/src/screens/Lead.jsx @@ -348,8 +348,16 @@ export default function Lead() { Mark Lost. Rendering them as one flat row of equals is what made six buttons appear where one was the answer. */} {(() => { - const step = actions.filter((a) => a.role === 'do') - const again = actions.filter((a) => a.role === 'again') + // Once the three documents are in, the upload is no longer the step: + // the lead stays in Document Pending while Engage captures the risk + // and the quote is built, and offering "Upload documents" through + // that reads as though nothing was received. A further upload stays + // possible as a loop — a fourth document, a corrected one — it is + // just not what the lead is waiting on. + const docsDone = stage?.uid === 'zk-state-docs' && row.documents_status === 'complete' + const roleOf = (a) => (docsDone && a.uid === 'zk-act-collect-docs' ? 'again' : a.role) + const step = actions.filter((a) => roleOf(a) === 'do') + const again = actions.filter((a) => roleOf(a) === 'again') const force = actions.filter((a) => a.role === 'force') const exit = actions.filter((a) => a.role === 'exit') const extras = [...force, ...exit] @@ -383,7 +391,9 @@ export default function Lead() { ? 'This is what this lead is waiting on.' : isClosed ? 'This lead is closed.' - : `Nothing is waiting on you — ${stage?.by ?? 'someone else'} has this one.`} + : docsDone + ? 'Documents received. Engage AI is capturing the risk and preparing the quote.' + : `Nothing is waiting on you — ${stage?.by ?? 'someone else'} has this one.`}

diff --git a/src/screens/Pipeline.jsx b/src/screens/Pipeline.jsx index 612a9b6..126c71b 100644 --- a/src/screens/Pipeline.jsx +++ b/src/screens/Pipeline.jsx @@ -43,7 +43,12 @@ import './screens.css' export default function Pipeline() { const { stageUid } = useParams() const { client } = useZino() - const stage = STAGES.find((s) => s.uid === stageUid) + // "all" is not a stage — it is every open lead in one list. An operator + // wanting to find a lead should not have to guess which queue it is in. + const isAll = stageUid === 'all' + const stage = isAll + ? { uid: 'all', name: 'All open leads', kind: 'all', by: 'everyone' } + : STAGES.find((s) => s.uid === stageUid) const [state, setState] = useState({ status: 'loading', rows: [], total: 0, error: null }) @@ -57,15 +62,22 @@ export default function Pipeline() { if (!quiet) setState({ status: 'loading', rows: [], total: 0, error: null }) return client .recordView(RV_LEADS, { - limit: 100, - filters: [{ field_key: 'current_state_name', value: stage?.name ?? '' }], + limit: isAll ? 200 : 100, + // No stage filter on the all view. Closed leads are dropped below + // rather than in the query: one request beats three negations, and + // 200 covers a demo book comfortably. + ...(isAll ? {} : { filters: [{ field_key: 'current_state_name', value: stage?.name ?? '' }] }), }) .then((res) => { if (cancelled) return - const rows = res?.data ?? res?.rows ?? res?.records ?? [] + let rows = res?.data ?? res?.rows ?? res?.records ?? [] + if (isAll) { + const closed = new Set(STAGES.filter((x) => x.kind === 'end').map((x) => x.name)) + rows = rows.filter((r) => !closed.has(r.current_state_name)) + } // total_count is the size of the QUEUE; rows is one page of at most // 100 of it. Counting the page would quietly under-report a busy stage. - const total = res?.pagination?.total_count ?? rows.length + const total = isAll ? rows.length : (res?.pagination?.total_count ?? rows.length) setState({ status: 'ready', rows, total, error: null }) }) .catch((err) => { @@ -82,7 +94,7 @@ export default function Pipeline() { }, 30000) return () => { cancelled = true; clearInterval(id) } - }, [client, stageUid, stage?.name]) + }, [client, stageUid, stage?.name, isAll]) if (!stage) return

Unknown stage.

@@ -92,7 +104,9 @@ export default function Pipeline() {

{stage.need ?? stage.name}

- {stage.kind === 'auto' + {isAll + ? 'Every lead not yet closed, across all stages.' + : stage.kind === 'auto' ? `Automated · ${stage.doing.toLowerCase()}` : stage.kind === 'end' ? 'Closed — retained for reporting.'