141 lines
5.3 KiB
JavaScript
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>
|
|
</>
|
|
)
|
|
}
|