sud-life-insurance/src/components/activities/Worklist.tsx
2026-07-03 14:52:45 +05:30

133 lines
5.2 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, KpiCard, LeadRow, LEAD_GRID, Pagination } from '../';
import { LeadFilters } from '../LeadFilters';
import { cn } from '../../lib/cn';
import { useLeads } from '../../api/leads';
import { recordToLead } from '../../api/adapters';
import { applyFilters, EMPTY_FILTER, type FilterState } from '../../api/filters';
import type { LeadRecord } from '../../api/types';
import { LeadActions, AddLeadButton } from './ActivityActions';
const PAGE_SIZE = 10;
/** Epoch ms for a recordview timestamp; 0 (oldest) when missing/unparseable. */
function ms(ts?: string | null): number {
if (!ts) return 0;
const t = new Date(ts).getTime();
return Number.isNaN(t) ? 0 : t;
}
/** A compact stat tile computed over this screen's (already state-filtered) rows. */
export interface TileSpec {
label: string;
value: (rows: LeadRecord[]) => string;
}
export interface WorklistProps {
/** Lifecycle states this screen lists (in display order). */
states: readonly string[];
emptyHint?: string;
showAddLead?: boolean;
/** A small set of stat tiles shown above the table (keep it to 2-4). */
tiles?: TileSpec[];
}
/** A state-scoped lead worklist styled like the Lead Pipeline screen: a KPI row
* + the shared leads table (LeadRow), with each row's one primary action
* (+ Disqualify) in its action cell, launched in a modal. */
export function Worklist({ states, emptyHint, showAddLead, tiles }: WorklistProps) {
const navigate = useNavigate();
const { records, fields, loading, refetch } = useLeads();
const [filters, setFilters] = useState<FilterState>(EMPTY_FILTER);
const allowed = useMemo(() => new Set(states), [states]);
// Scope to this screen's states, latest-first: most recently actioned
// (updated_at) at the top, instance_id as the tiebreaker. Sorting by state
// would bury a freshly-updated lead under older ones in an earlier state.
const scoped = useMemo(
() =>
records
.filter((r) => allowed.has(r.current_state_name ?? ''))
.sort((a, b) => ms(b.updated_at) - ms(a.updated_at) || Number(b.instance_id) - Number(a.instance_id)),
[records, allowed],
);
const recs = useMemo(() => applyFilters(scoped, filters, fields), [scoped, filters, fields]);
const rows = useMemo(() => recs.map((record) => ({ record, lead: recordToLead(record) })), [recs]);
// Client-side pagination over the filtered rows (all leads are already loaded
// via useLeads). Page is clamped so it stays valid after rows shrink.
const [page, setPage] = useState(1);
// Snap back to the first page whenever the filter set changes.
useEffect(() => setPage(1), [filters]);
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
const safePage = Math.min(page, totalPages);
const start = (safePage - 1) * PAGE_SIZE;
const pageRows = rows.slice(start, start + PAGE_SIZE);
return (
<div className="mx-auto flex w-full max-w-[1240px] flex-col gap-5">
{/* KPI row — Pipeline-screen tiles, width-capped so a couple don't stretch huge. */}
{tiles?.length ? (
<div className="grid grid-cols-2 gap-4 sm:flex sm:flex-wrap">
{tiles.map((t) => (
<KpiCard key={t.label} label={t.label} value={t.value(recs)} className="sm:w-[240px]" />
))}
</div>
) : null}
{/* Leads — shared table, with a per-row action cell. */}
<Card
title="Leads in queue"
pad={false}
action={showAddLead ? <AddLeadButton onDone={refetch} /> : undefined}
>
<LeadFilters
records={scoped}
fields={fields}
value={filters}
onChange={setFilters}
excludeKeys={['current_state_name']}
/>
{loading && !rows.length ? (
<div className="p-10 text-center text-sm text-faint">Loading leads</div>
) : (
<div className="overflow-x-auto">
<div className="min-w-[820px]">
<div
className={cn(
'grid gap-3 px-[18px] py-[11px] bg-slate-50 border-b border-border-subtle text-[10px] font-bold uppercase tracking-[0.06em] text-faint',
LEAD_GRID,
)}
>
<span>Lead</span>
<span>Score</span>
<span>Segment</span>
<span>Channel</span>
<span>Owner</span>
<span className="text-right">Action</span>
</div>
{rows.length ? (
pageRows.map(({ record, lead }) => (
<LeadRow
key={record.instance_id}
lead={lead}
onClick={() => navigate(`/leads/${record.instance_id}`)}
action={<LeadActions record={record} onDone={refetch} />}
/>
))
) : (
<div className="p-10 text-center text-sm text-faint">
{scoped.length ? 'No leads match these filters.' : emptyHint ?? 'No leads at this stage.'}
</div>
)}
</div>
</div>
)}
<Pagination page={safePage} pageSize={PAGE_SIZE} total={rows.length} onPage={setPage} />
</Card>
</div>
);
}