diff --git a/src/screens/Lead.jsx b/src/screens/Lead.jsx
index d4a208e..bc7c342 100644
--- a/src/screens/Lead.jsx
+++ b/src/screens/Lead.jsx
@@ -6,6 +6,28 @@ import Timeline from '../components/Timeline.jsx'
import { ACTIONS, DV_LEAD, STAGES } from '../api/config.js'
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',
'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',
@@ -73,6 +95,19 @@ export default function Lead() {
+
+ {(() => { const e = expiryOf(row.renewal_due_date); return e ? (
+
+
- Renewal due
+ - {e.label} {e.on}
+
+ ) : null })()}
+ {row.current_insurer ? - Current insurer
- {row.current_insurer}
: null}
+ {row.motor_reg_no ? - Registration
- {row.motor_reg_no}
: null}
+ {row.created_at ? - Lead added
- {fmtWhen(row.created_at)}
: null}
+ {row.updated_at ? - Last activity
- {fmtWhen(row.updated_at)}
: null}
+
+
{actions.length ? (
diff --git a/src/screens/Pipeline.jsx b/src/screens/Pipeline.jsx
index c0e6c24..c05e260 100644
--- a/src/screens/Pipeline.jsx
+++ b/src/screens/Pipeline.jsx
@@ -2,6 +2,35 @@ import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useZino } from '../api/provider.jsx'
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'
/**
@@ -78,25 +107,35 @@ export default function Pipeline() {
| Lead | Customer | Channel |
- Product | Premium | Attribution |
+ Product | Renewal due | Premium |
+ Attribution | Added |
{state.rows.map((r) => {
const id = r.instance_id ?? r.id
const ch = CHANNELS[r.source_channel] || { label: r.source_channel || '—' }
+ const exp = expiry(r.renewal_due_date)
+ const add = added(r.created_at)
return (
| {r.lead_ref || id} |
{r.customer_name || '—'} {r.entity_name || ''} |
{ch.label} |
{r.product_line === 'motor' ? 'Motor' : 'SME Package'} |
+
+ {exp
+ ? <>{exp.label}
+ {exp.on} >
+ : —}
+ |
{r.quoted_premium ? Number(r.quoted_premium).toLocaleString('en-IN') : '—'} |
{r.attribution_status === 'contested'
? Contested
: {r.attribution_status || '—'}}
|
+ {add.abs} {add.rel} |
)
})}
diff --git a/src/screens/screens.css b/src/screens/screens.css
index d6921e6..8a514ff 100644
--- a/src/screens/screens.css
+++ b/src/screens/screens.css
@@ -91,3 +91,19 @@
.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); }
.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); }