console: all-leads view, document form prefill, and two things that should not be offered

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.
This commit is contained in:
Yashas 2026-09-07 19:26:13 +05:30
parent 07d4608888
commit 6a6801d2bd
5 changed files with 67 additions and 16 deletions

View File

@ -119,7 +119,7 @@ const STATE_ACTIVITIES = {
], ],
'zk-state-qualified': [ 'zk-state-qualified': [
{ uid: 'zk-act-contact', label: 'Log Contact', by: 'Engage AI', role: 'force' }, { 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': [ 'zk-state-contacted': [
{ uid: 'zk-act-request-docs', label: 'Request Documents', by: 'Engage AI', role: 'force' }, { 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. // be found and uploaded. Everything else here is the AI's own chain.
'zk-state-docs': [ 'zk-state-docs': [
{ uid: 'zk-act-collect-docs', label: 'Upload documents', by: 'you', role: 'do' }, { 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-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-capture-sme', label: 'Capture SME Risk', by: 'Engage AI', role: 'force' },
{ uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI', role: 'force' }, { uid: 'zk-act-advise', label: 'AI Cover Recommendation', by: 'Advisor AI', role: 'force' },

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useZino } from '../api/provider.jsx' 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 { describeError, describeValidation } from '../api/errors.js'
import FileField from './FileField.jsx' import FileField from './FileField.jsx'
import './ActivityForm.css' 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 // whichever convention a pipeline is authored in, rather than breaking
// again the next time one is written the other way. // again the next time one is written the other way.
const pre = s.prefill_data || s.prefillData || s.field_defaults || {} const pre = s.prefill_data || s.prefillData || s.field_defaults || {}
if (pre && typeof pre === 'object') {
const seed = {} const seed = {}
if (pre && typeof pre === 'object') {
for (const f of s.fields) { 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) const v = pre[f.id] ?? pre[f.uid] ?? (base ? pre[base] : undefined)
if (v !== undefined && v !== null && v !== '') seed[f.id] = v 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) }) .catch((e) => { if (!dead) setError(e) })
return () => { dead = true } return () => { dead = true }

View File

@ -48,6 +48,12 @@ export default function Shell() {
> >
Overview Overview
</NavLink> </NavLink>
<NavLink
to="/stage/all"
className={({ isActive }) => 'shell__today' + (isActive ? ' is-active' : '')}
>
All leads
</NavLink>
{canFile ? ( {canFile ? (
<NavLink to="/add" className="shell__add"> <NavLink to="/add" className="shell__add">

View File

@ -348,8 +348,16 @@ export default function Lead() {
Mark Lost. Rendering them as one flat row of equals is what made Mark Lost. Rendering them as one flat row of equals is what made
six buttons appear where one was the answer. */} six buttons appear where one was the answer. */}
{(() => { {(() => {
const step = actions.filter((a) => a.role === 'do') // Once the three documents are in, the upload is no longer the step:
const again = actions.filter((a) => a.role === 'again') // 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 force = actions.filter((a) => a.role === 'force')
const exit = actions.filter((a) => a.role === 'exit') const exit = actions.filter((a) => a.role === 'exit')
const extras = [...force, ...exit] const extras = [...force, ...exit]
@ -383,6 +391,8 @@ export default function Lead() {
? 'This is what this lead is waiting on.' ? 'This is what this lead is waiting on.'
: isClosed : isClosed
? 'This lead is closed.' ? 'This lead is closed.'
: 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.`} : `Nothing is waiting on you — ${stage?.by ?? 'someone else'} has this one.`}
</p> </p>
</div> </div>

View File

@ -43,7 +43,12 @@ import './screens.css'
export default function Pipeline() { export default function Pipeline() {
const { stageUid } = useParams() const { stageUid } = useParams()
const { client } = useZino() 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 }) 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 }) if (!quiet) setState({ status: 'loading', rows: [], total: 0, error: null })
return client return client
.recordView(RV_LEADS, { .recordView(RV_LEADS, {
limit: 100, limit: isAll ? 200 : 100,
filters: [{ field_key: 'current_state_name', value: stage?.name ?? '' }], // 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) => { .then((res) => {
if (cancelled) return 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 // 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. // 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 }) setState({ status: 'ready', rows, total, error: null })
}) })
.catch((err) => { .catch((err) => {
@ -82,7 +94,7 @@ export default function Pipeline() {
}, 30000) }, 30000)
return () => { cancelled = true; clearInterval(id) } return () => { cancelled = true; clearInterval(id) }
}, [client, stageUid, stage?.name]) }, [client, stageUid, stage?.name, isAll])
if (!stage) return <p className="empty">Unknown stage.</p> if (!stage) return <p className="empty">Unknown stage.</p>
@ -92,7 +104,9 @@ export default function Pipeline() {
<div> <div>
<h1 className="page__title">{stage.need ?? stage.name}</h1> <h1 className="page__title">{stage.need ?? stage.name}</h1>
<p className="page__sub"> <p className="page__sub">
{stage.kind === 'auto' {isAll
? 'Every lead not yet closed, across all stages.'
: stage.kind === 'auto'
? `Automated · ${stage.doing.toLowerCase()}` ? `Automated · ${stage.doing.toLowerCase()}`
: stage.kind === 'end' : stage.kind === 'end'
? 'Closed — retained for reporting.' ? 'Closed — retained for reporting.'