zurich_kotak/src/components/Timeline.jsx
2026-09-02 15:33:26 +05:30

141 lines
5.3 KiB
JavaScript

import { useEffect, useState } from 'react'
import { useZino } from '../api/provider.jsx'
import ClampText from './ClampText.jsx'
import { STAGES } from '../api/config.js'
import './Timeline.css'
/** Which employee holds which role — used to badge an entry as machine work. */
const AI_ROLES = {
ai_intake: 'Intake & Attribution',
ai_engage: 'Engage',
ai_kyc: 'KYC & Evidence',
ai_advisor: 'Advisor',
ai_uw_referral: 'Underwriting Referral',
}
/**
* The fields worth surfacing per activity — an agent's reasoning, a rule's
* output, a call's notes. Everything else stays in the file below; a timeline
* that shows every field is a table, not a story.
*/
const NARRATIVE = [
['attribution_reason', 'Attribution'],
['eligibility_reason', 'Eligibility'],
['dedupe_match_ref', 'Duplicate of'],
['contact_notes', 'Call'],
['ai_recommendation_rationale', 'Recommendation'],
['quoted_breakup', 'How the premium was reached'],
['kyc_mismatch_notes', 'KYC'],
['referral_analysis', 'Referral analysis'],
['uw_decision_notes', 'Underwriting decision'],
['documents_notes', 'Documents'],
]
const MONEY = new Set(['quoted_premium', 'commission_amount', 'sme_value_at_risk', 'motor_idv'])
function when(ts) {
if (!ts) return ''
const d = new Date(ts)
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 `${d.toLocaleString('en-IN', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} · ${rel}`
}
export default function Timeline({ instanceId }) {
const { client } = useZino()
const [rows, setRows] = useState(null)
const [err, setErr] = useState(null)
// DATA_UPDATE entries are the platform writing fields, not anyone deciding
// anything. They are the bulk of a busy trail and they are hidden until asked
// for — the story is what people and agents did.
const [showSys, setShowSys] = useState(false)
useEffect(() => {
let dead = false
client.audit(instanceId)
.then((r) => { if (!dead) setRows(Array.isArray(r) ? r : (r?.data ?? [])) })
.catch((e) => { if (!dead) setErr(e) })
return () => { dead = true }
}, [client, instanceId])
if (err) return <div className="tl__err">Could not load the timeline {err.status} {err.message}</div>
if (!rows) return <p className="tl__loading">Loading the timeline</p>
if (!rows.length) return <p className="tl__loading">Nothing has happened yet.</p>
// Oldest first: a timeline reads forwards.
const ordered = [...rows].sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))
const items = ordered.map((r, i) => {
const roles = r.user_roles || []
const aiRole = roles.find((x) => AI_ROLES[x])
const isSystem = r.activity_id === 'DATA_UPDATE'
const kind = aiRole ? 'ai' : isSystem ? 'sys' : 'human'
const d = r.data || {}
return {
key: r.id ?? i,
kind,
actor: aiRole ? AI_ROLES[aiRole] : (r.user_name || 'System'),
what: isSystem ? 'Data updated' : (r.activity_name || r.activity_id),
stage: STAGES.find((s) => s.uid === r.execution_state),
when: when(r.created_at),
narrative: NARRATIVE
.map(([k, label]) => [label, d[k]])
.filter(([, v]) => v !== undefined && v !== null && String(v).trim() !== ''),
figures: Object.entries(d)
.filter(([k, v]) => MONEY.has(k) && v)
.map(([k, v]) => [k.replace(/_/g, ' '), '₹' + Number(v).toLocaleString('en-IN')]),
}
})
const sysCount = items.filter((it) => it.kind === 'sys').length
const visible = showSys ? items : items.filter((it) => it.kind !== 'sys')
return (
<>
{sysCount ? (
<button type="button" className="tl__toggle" onClick={() => setShowSys((v) => !v)}>
{showSys ? 'Hide' : 'Show'} {sysCount} system update{sysCount === 1 ? '' : 's'}
</button>
) : null}
<ol className="tl">
{visible.map((it) => (
<li key={it.key} className={'tl__item tl__item--' + it.kind}>
<span className="tl__dot" aria-hidden="true" />
<div className="tl__body">
<div className="tl__head">
<strong className="tl__what">{it.what}</strong>
{it.stage ? <span className="tl__stage"> {it.stage.name}</span> : null}
</div>
<div className="tl__meta">
<span className={'tl__who tl__who--' + it.kind}>
{it.kind === 'ai' ? 'AI' : it.kind === 'sys' ? 'system' : 'person'} · {it.actor}
</span>
<span className="tl__when">{it.when}</span>
</div>
{it.narrative.map(([label, v]) => (
<div className="tl__say" key={label}>
<span className="tl__saylabel">{label}</span>
<ClampText text={String(v)} lines={2} threshold={120} />
</div>
))}
{it.figures.length ? (
<div className="tl__figs">
{it.figures.map(([k, v]) => (
<span key={k}><em>{k}</em> {v}</span>
))}
</div>
) : null}
</div>
</li>
))}
</ol>
</>
)
}