From 6a6801d2bdd155473dd3700abf506b773c153bbb Mon Sep 17 00:00:00 2001
From: Yashas
Date: Mon, 7 Sep 2026 19:26:13 +0530
Subject: [PATCH] console: all-leads view, document form prefill, and two
things that should not be offered
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Four operator-reported issues, one commit because two of them are about
the same form.
ALL LEADS WAS GONE. The team's rewrite of the queue screen dropped the
/stage/all view, so finding a lead meant guessing which of nine queues
it sat in. Restored: no stage filter, closed leads dropped client-side,
one request for the whole open book. Linked under Overview in the
sidebar so it is one click from anywhere.
THE DOCUMENT FORM ASKED FOR WHAT THE LEAD ALREADY KNEW. Collect
Documents mirrors fourteen record fields — registration, make and model,
previous insurer, expiry, policy number, PAN — so the OCR has somewhere
to write. Rendered blank, they read as fourteen more things to type, on
a form whose whole point is uploading three files. They are now seeded
from the lead by base key: an agent uploading for KA01MF6618 sees
KA01MF6618 already there. Server prefill wins, then anything typed;
files and generated ids are never seeded, and an INIT form has no
record to seed from. SME-only fields were already hidden on a motor
lead by fieldLine — that half was working.
"SEND A REMINDER" IS THE SCHEDULER'S JOB, NOT A BUTTON. It fires at
24h, 72h and 120h on its own. Offering it as a loop action invited a
person to send a reminder the platform was already about to send. It
and Retry Call move to `force` — reachable behind "Lead not moving?"
for the day the schedule itself fails, and otherwise out of sight.
THE UPLOAD STAYED THE "NEXT STEP" AFTER THE UPLOAD. Once
documents_status is complete the lead remains in Document Pending while
Engage captures the risk and the quote is built — and the panel kept
saying "Upload documents", which reads as though nothing was received.
Now, when complete, the upload becomes a loop (a fourth document, a
correction) and the panel says what is actually happening: documents
received, Engage AI is capturing the risk and preparing the quote.
Permissions unchanged; ACTIONS still matches
tbl_wf_state_allowed_activities exactly.
---
src/api/config.js | 4 ++--
src/components/ActivityForm.jsx | 29 +++++++++++++++++++++++++----
src/layout/Shell.jsx | 6 ++++++
src/screens/Lead.jsx | 16 +++++++++++++---
src/screens/Pipeline.jsx | 28 +++++++++++++++++++++-------
5 files changed, 67 insertions(+), 16 deletions(-)
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.'