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(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 (
{/* KPI row — Pipeline-screen tiles, width-capped so a couple don't stretch huge. */} {tiles?.length ? (
{tiles.map((t) => ( ))}
) : null} {/* Leads — shared table, with a per-row action cell. */} : undefined} > {loading && !rows.length ? (
Loading leads…
) : (
Lead Score Segment Channel Owner Action
{rows.length ? ( pageRows.map(({ record, lead }) => ( navigate(`/leads/${record.instance_id}`)} action={} /> )) ) : (
{scoped.length ? 'No leads match these filters.' : emptyHint ?? 'No leads at this stage.'}
)}
)}
); }