Put dates where the work is decided
The timeline carried timestamps; the two screens people actually act on carried none. A queue with no dates cannot tell you which lead is going stale, and on a renewal book the countdown to expiry is the number that decides whether anyone should act today — it was buried in a field group. Queue gains "Renewal due" and "Added", both absolute plus relative: the absolute answers "when exactly", the relative answers "is this stale". The countdown is toned — lapsed, urgent, soon, later — on the same thresholds the workflow itself uses to decide whether to call, so the screen and the agent are reading the same bands. Lead header gains the countdown, the current insurer, the registration, and when the lead arrived and was last touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
49c51c24d7
commit
1fb8d0dc98
@ -6,6 +6,28 @@ import Timeline from '../components/Timeline.jsx'
|
|||||||
import { ACTIONS, DV_LEAD, STAGES } from '../api/config.js'
|
import { ACTIONS, DV_LEAD, STAGES } from '../api/config.js'
|
||||||
import './screens.css'
|
import './screens.css'
|
||||||
|
|
||||||
|
function fmtWhen(ts) {
|
||||||
|
if (!ts) return null
|
||||||
|
const d = new Date(ts)
|
||||||
|
if (isNaN(d)) return String(ts)
|
||||||
|
return d.toLocaleString('en-IN', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The countdown, not just the date. On a renewal book this is the number
|
||||||
|
* that decides whether anyone should act today. */
|
||||||
|
function expiryOf(dateStr) {
|
||||||
|
if (!dateStr) return null
|
||||||
|
const d = new Date(String(dateStr).substring(0, 10) + 'T00:00:00Z')
|
||||||
|
if (isNaN(d)) return null
|
||||||
|
const days = Math.round((d.getTime() - Date.parse(new Date().toISOString().substring(0, 10) + 'T00:00:00Z')) / 86400000)
|
||||||
|
return {
|
||||||
|
days,
|
||||||
|
tone: days < 0 ? 'lapsed' : days <= 7 ? 'urgent' : days <= 30 ? 'soon' : 'later',
|
||||||
|
label: days < 0 ? Math.abs(days) + ' days overdue' : days === 0 ? 'expires today' : 'in ' + days + ' days',
|
||||||
|
on: d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MONEY = new Set(['quoted_premium','quoted_od_premium','quoted_tp_premium','quoted_addon_premium',
|
const MONEY = new Set(['quoted_premium','quoted_od_premium','quoted_tp_premium','quoted_addon_premium',
|
||||||
'quoted_gst','commission_base','commission_amount','sme_building_si','sme_plant_si','sme_furniture_si',
|
'quoted_gst','commission_base','commission_amount','sme_building_si','sme_plant_si','sme_furniture_si',
|
||||||
'sme_rawmaterial_si','sme_wip_si','sme_finished_si','sme_other_si','sme_value_at_risk','motor_idv',
|
'sme_rawmaterial_si','sme_wip_si','sme_finished_si','sme_other_si','sme_value_at_risk','motor_idv',
|
||||||
@ -73,6 +95,19 @@ export default function Lead() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<dl className="lead__meta">
|
||||||
|
{(() => { const e = expiryOf(row.renewal_due_date); return e ? (
|
||||||
|
<div>
|
||||||
|
<dt>Renewal due</dt>
|
||||||
|
<dd><span className={'due due--' + e.tone}>{e.label}</span> <span className="grid__sub">{e.on}</span></dd>
|
||||||
|
</div>
|
||||||
|
) : null })()}
|
||||||
|
{row.current_insurer ? <div><dt>Current insurer</dt><dd>{row.current_insurer}</dd></div> : null}
|
||||||
|
{row.motor_reg_no ? <div><dt>Registration</dt><dd>{row.motor_reg_no}</dd></div> : null}
|
||||||
|
{row.created_at ? <div><dt>Lead added</dt><dd>{fmtWhen(row.created_at)}</dd></div> : null}
|
||||||
|
{row.updated_at ? <div><dt>Last activity</dt><dd>{fmtWhen(row.updated_at)}</dd></div> : null}
|
||||||
|
</dl>
|
||||||
|
|
||||||
{actions.length ? (
|
{actions.length ? (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel__head">
|
<div className="panel__head">
|
||||||
|
|||||||
@ -2,6 +2,35 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { useZino } from '../api/provider.jsx'
|
import { useZino } from '../api/provider.jsx'
|
||||||
import { CHANNELS, RV_LEADS, STAGES } from '../api/config.js'
|
import { CHANNELS, RV_LEADS, STAGES } from '../api/config.js'
|
||||||
|
|
||||||
|
/** Short absolute date plus how long ago — a queue needs both: the absolute
|
||||||
|
* for "when exactly", the relative for "is this going stale". */
|
||||||
|
function added(ts) {
|
||||||
|
if (!ts) return { abs: '—', rel: '' }
|
||||||
|
const d = new Date(ts)
|
||||||
|
if (isNaN(d)) return { abs: String(ts), rel: '' }
|
||||||
|
const mins = Math.round((Date.now() - d.getTime()) / 60000)
|
||||||
|
const rel = mins < 1 ? 'just now'
|
||||||
|
: mins < 60 ? mins + 'm ago'
|
||||||
|
: mins < 1440 ? Math.round(mins / 60) + 'h ago'
|
||||||
|
: Math.round(mins / 1440) + 'd ago'
|
||||||
|
return {
|
||||||
|
abs: d.toLocaleString('en-IN', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }),
|
||||||
|
rel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Days to expiry, and how alarmed to be about it. This is a renewal book —
|
||||||
|
* the countdown is the most operationally useful number in the row. */
|
||||||
|
function expiry(dateStr) {
|
||||||
|
if (!dateStr) return null
|
||||||
|
const d = new Date(String(dateStr).substring(0, 10) + 'T00:00:00Z')
|
||||||
|
if (isNaN(d)) return null
|
||||||
|
const days = Math.round((d.getTime() - Date.parse(new Date().toISOString().substring(0, 10) + 'T00:00:00Z')) / 86400000)
|
||||||
|
const tone = days < 0 ? 'lapsed' : days <= 7 ? 'urgent' : days <= 30 ? 'soon' : 'later'
|
||||||
|
const label = days < 0 ? Math.abs(days) + 'd overdue' : days === 0 ? 'today' : 'in ' + days + 'd'
|
||||||
|
return { days, tone, label, on: d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) }
|
||||||
|
}
|
||||||
import './screens.css'
|
import './screens.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,25 +107,35 @@ export default function Pipeline() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Lead</th><th>Customer</th><th>Channel</th>
|
<th>Lead</th><th>Customer</th><th>Channel</th>
|
||||||
<th>Product</th><th className="num">Premium</th><th>Attribution</th>
|
<th>Product</th><th>Renewal due</th><th className="num">Premium</th>
|
||||||
|
<th>Attribution</th><th>Added</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{state.rows.map((r) => {
|
{state.rows.map((r) => {
|
||||||
const id = r.instance_id ?? r.id
|
const id = r.instance_id ?? r.id
|
||||||
const ch = CHANNELS[r.source_channel] || { label: r.source_channel || '—' }
|
const ch = CHANNELS[r.source_channel] || { label: r.source_channel || '—' }
|
||||||
|
const exp = expiry(r.renewal_due_date)
|
||||||
|
const add = added(r.created_at)
|
||||||
return (
|
return (
|
||||||
<tr key={id}>
|
<tr key={id}>
|
||||||
<td><Link className="grid__link" to={`/lead/${id}`}>{r.lead_ref || id}</Link></td>
|
<td><Link className="grid__link" to={`/lead/${id}`}>{r.lead_ref || id}</Link></td>
|
||||||
<td>{r.customer_name || '—'}<div className="grid__sub">{r.entity_name || ''}</div></td>
|
<td>{r.customer_name || '—'}<div className="grid__sub">{r.entity_name || ''}</div></td>
|
||||||
<td><span className="chip">{ch.label}</span></td>
|
<td><span className="chip">{ch.label}</span></td>
|
||||||
<td>{r.product_line === 'motor' ? 'Motor' : 'SME Package'}</td>
|
<td>{r.product_line === 'motor' ? 'Motor' : 'SME Package'}</td>
|
||||||
|
<td>
|
||||||
|
{exp
|
||||||
|
? <><span className={'due due--' + exp.tone}>{exp.label}</span>
|
||||||
|
<div className="grid__sub">{exp.on}</div></>
|
||||||
|
: <span className="grid__sub">—</span>}
|
||||||
|
</td>
|
||||||
<td className="num">{r.quoted_premium ? Number(r.quoted_premium).toLocaleString('en-IN') : '—'}</td>
|
<td className="num">{r.quoted_premium ? Number(r.quoted_premium).toLocaleString('en-IN') : '—'}</td>
|
||||||
<td>
|
<td>
|
||||||
{r.attribution_status === 'contested'
|
{r.attribution_status === 'contested'
|
||||||
? <span className="chip chip--warn">Contested</span>
|
? <span className="chip chip--warn">Contested</span>
|
||||||
: <span className="grid__sub">{r.attribution_status || '—'}</span>}
|
: <span className="grid__sub">{r.attribution_status || '—'}</span>}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{add.abs}<div className="grid__sub">{add.rel}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -91,3 +91,19 @@
|
|||||||
.grp__row dt { font-size: .7rem; color: var(--zk-grey); text-transform: capitalize; }
|
.grp__row dt { font-size: .7rem; color: var(--zk-grey); text-transform: capitalize; }
|
||||||
.grp__row dd { margin: 0; font-size: .87rem; color: var(--zk-ink); }
|
.grp__row dd { margin: 0; font-size: .87rem; color: var(--zk-ink); }
|
||||||
.lead__back { margin-top: 18px; }
|
.lead__back { margin-top: 18px; }
|
||||||
|
|
||||||
|
/* ---- renewal countdown ---- */
|
||||||
|
.due {
|
||||||
|
display: inline-block; font-size: .76rem; font-weight: 500;
|
||||||
|
padding: 2px 8px; border-radius: 3px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.due--lapsed { background: #fdeceb; color: #a3261f; border: 1px solid #f0c2bd; }
|
||||||
|
.due--urgent { background: #fdf1e7; color: #93500f; border: 1px solid #f0c69a; }
|
||||||
|
.due--soon { background: var(--zk-tint-blue); color: var(--zk-blue-dark); border: 1px solid var(--zk-blue-light); }
|
||||||
|
.due--later { background: var(--zk-tint); color: var(--zk-muted); border: 1px solid var(--zk-line); }
|
||||||
|
|
||||||
|
/* ---- lead header meta ---- */
|
||||||
|
.lead__meta { display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: 8px; }
|
||||||
|
.lead__meta div { display: flex; flex-direction: column; gap: 1px; }
|
||||||
|
.lead__meta dt { font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; color: var(--zk-grey); }
|
||||||
|
.lead__meta dd { margin: 0; font-size: .84rem; color: var(--zk-ink); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user