import { useCallback, useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { RemitStatusBadge } from "@/components/StatusBadge"; import { Skeleton } from "@/components/ui/skeleton"; import { EmptyState } from "@/components/ui/empty-state"; import { ErrorState } from "@/components/ui/error-state"; import { FilterChips, type FilterChipOption } from "@/components/ui/filter-chips"; import { Pagination } from "@/components/ui/pagination"; import { KeyboardCheatsheet } from "@/components/KeyboardCheatsheet"; import { PageHeader } from "@/components/PageHeader"; import { TailStatusPill } from "@/components/TailStatusPill"; import { RemitDrawer } from "@/components/RemitDrawer"; import { useRemittances } from "@/hooks/useRemittances"; import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState"; import { useRowKeyboard } from "@/hooks/useRowKeyboard"; import { useTailStream } from "@/hooks/useTailStream"; import { useMergedTail } from "@/hooks/useMergedTail"; import { fmt } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { Remittance, RemittanceStatus } from "@/types"; const PAGE_SIZE = 25; const STATUS_OPTIONS: FilterChipOption[] = [ { value: "received", label: "Received" }, { value: "posted", label: "Posted" }, { value: "reconciled", label: "Reconciled" }, ]; export function Remittances() { const [page, setPage] = useState(1); const [status, setStatus] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null); const [helpOpen, setHelpOpen] = useState(false); // SP21 Phase 4 Task 4.3: row click → RemitDrawer. The drawer is // URL-driven (`?remit=ID`) so deep links restore the open remit // on reload — same pattern as the ClaimDrawer on /claims. // `remits` is the j/k navigation list (the current page of rows). // `setRemitId` (REPLACE history, not push) is what j/k uses so a // single keypress doesn't add a history entry. const { remitId, open, close, setRemitId } = useRemitDrawerUrlState(); const { data, isLoading, isError, error, refetch, dataUpdatedAt } = useRemittances({ sort: "receivedDate", order: "desc", limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, }); const tailFilterFn = useMemo( () => (r: Remittance) => { if (status && r.status !== status) return false; return true; }, [status], ); const { status: tailStatus, lastEventAt: tailLastEventAt, forceReconnect } = useTailStream("remittances"); const items = useMergedTail("remittances", data?.items ?? [], tailFilterFn); const total = items.reduce( (acc, r) => ({ paid: acc.paid + r.paidAmount, adjustments: acc.adjustments + r.adjustmentAmount, }), { paid: 0, adjustments: 0 } ); const moveNext = useCallback(() => { setSelectedIndex((i) => { if (items.length === 0) return null; if (i === null) return 0; return (i + 1) % items.length; }); }, [items.length]); const movePrev = useCallback(() => { setSelectedIndex((i) => { if (items.length === 0) return null; if (i === null) return items.length - 1; return (i - 1 + items.length) % items.length; }); }, [items.length]); useRowKeyboard({ // Page-level j/k only fires when the drawer is closed — once // `?remit=` is set, the drawer's own `useDrawerKeyboard` listener // owns the j/k keys (with its own wrap-around semantics over // `remits`). Letting the page-level listener stay active here // would mean a single `j` keypress both advances the drawer's // remittance AND bumps the page-level selectedIndex — exactly // the "double navigation" surprise we want to avoid. enabled: !helpOpen && items.length > 0 && remitId === null, onNext: moveNext, onPrev: movePrev, onClose: () => setHelpOpen(false), onToggleHelp: () => setHelpOpen((v) => !v), }); // j/k navigation through the remits list. The drawer's own keyboard // handler (useDrawerKeyboard, only attached while the drawer is // open) uses the same keys with its own wrap-around semantics, so // page-level nav only fires when the drawer is closed. const drawerRemits = useMemo( () => items.map((r) => ({ id: r.id })), [items], ); return ( <> setHelpOpen(false)} /> setHelpOpen((v) => !v)} />
835 remittances} subtitle="Electronic remittance advice from payers, matched to submitted claims." actions={ } /> {isError ? ( refetch()} /> ) : null}
Remits
{fmt.num(data?.total ?? 0)}
Total paid
{fmt.usd(total.paid)}
Adjustments
{fmt.usd(total.adjustments)}
{ setStatus((v as RemittanceStatus | null) ?? null); setPage(1); }} eyebrow="Status" />
{isLoading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : items.length === 0 ? ( ) : ( <> Remit Claim Payer Paid Adjustment Status Received {items.map((r, idx) => { const isSelected = selectedIndex === idx; return ( open(r.id)} > {r.id} {r.claimId} {r.payerName} {fmt.usdPrecise(r.paidAmount)} {r.adjustmentAmount > 0 ? fmt.usdPrecise(r.adjustmentAmount) : "—"} {fmt.dateShort(r.receivedDate)} ); })}
)}
); }