import { Plus, Minus, Equal, ArrowRight, type LucideIcon } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { KpiCard } from "@/components/KpiCard"; import { fmt } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { BatchDiff, BatchDiffChangedRow, BatchDiffSideMeta, BatchDiffSummary, BatchClaimDiffSummary, } from "@/types"; // --------------------------------------------------------------------------- // Side meta header — small panel identifying which batch is on the left / right. // --------------------------------------------------------------------------- function SideMeta({ side, label, }: { side: BatchDiffSideMeta; label: string; }) { // Color mirrors the BatchesList kind badge so the visual identity is // consistent across the app: 837p in cool blue, 835 in warm amber. const kindClass = side.kind === "837p" ? "text-sky-300 border-sky-400/30 bg-sky-400/10" : "text-amber-300 border-amber-400/30 bg-amber-400/10"; return (
{label}
{side.kind} {side.id}
{side.inputFilename}
Parsed {side.parsedAt ? fmt.dateShort(side.parsedAt) : "—"} {fmt.num(side.claimCount)} claim{side.claimCount === 1 ? "" : "s"}
); } // --------------------------------------------------------------------------- // Summary cards — the four-count tile row. // --------------------------------------------------------------------------- function SummaryCards({ summary }: { summary: BatchDiffSummary }) { return (
); } // --------------------------------------------------------------------------- // Row indicator — `+` / `-` / `~` gutter on the left of every row. // --------------------------------------------------------------------------- function RowIndicator({ kind, }: { kind: "added" | "removed" | "changed"; }) { const cfg = { added: { Icon: Plus, cls: "text-[hsl(var(--success))] bg-[hsl(var(--success)/0.10)] border-[hsl(var(--success)/0.30)]", label: "added", }, removed: { Icon: Minus, cls: "text-destructive bg-destructive/10 border-destructive/30", label: "removed", }, changed: { Icon: ArrowRight, cls: "text-[hsl(var(--warning))] bg-[hsl(var(--warning)/0.10)] border-[hsl(var(--warning)/0.30)]", label: "changed", }, }[kind]; const Icon: LucideIcon = cfg.Icon; return ( ); } // --------------------------------------------------------------------------- // Cell renderers for the summary columns. Kept tiny — these are // hairline cells in a tabular diff, not full claim cards. // --------------------------------------------------------------------------- function ClaimIdCell({ id }: { id: string }) { return ( {id} ); } function PatientCell({ name }: { name: string }) { if (!name) { return ; } return {name}; } function ChargeCell({ value }: { value: number }) { return ( {fmt.usdPrecise(value)} ); } function DateCell({ value }: { value: string | null }) { if (!value) { return ; } return {value}; } function StatusCell({ value }: { value: string }) { if (!value) { return ; } return ( {value} ); } function CptCell({ codes }: { codes: string[] }) { if (!codes || codes.length === 0) { return ; } return ( {codes.join(", ")} ); } // --------------------------------------------------------------------------- // Section tables — one per bucket (added / removed / changed). // --------------------------------------------------------------------------- type Side = "a" | "b"; function summaryCells(summary: BatchClaimDiffSummary, side: Side) { // Single-row builder that keeps the column order consistent across // all three buckets. ``side`` is just a key for the testid so // tests can target a specific cell. return { claimId: , patient: , charge: , serviceDate: , status: , cpt: , // Suppress an unused-param warning while leaving the key in the // object for readability / future use. _side: side, }; } function SummaryRow({ summary, indicator, testid, }: { summary: BatchClaimDiffSummary; indicator: "added" | "removed"; testid: string; }) { const cells = summaryCells(summary, indicator === "added" ? "b" : "a"); return ( {cells.claimId} {cells.patient} {cells.charge} {cells.serviceDate} {cells.status} {cells.cpt} ); } function ChangedRowView({ row, testid, }: { row: BatchDiffChangedRow; testid: string; }) { const a = row.a; const b = row.b; const changedFields = new Set(row.diff.map((d) => d.field)); return (
{a.patient_name || "—"} {a.patient_name !== b.patient_name ? ( {b.patient_name || "—"} ) : null}
{fmt.usdPrecise(a.total_charge)} {changedFields.has("total_charge") ? ( {fmt.usdPrecise(b.total_charge)} ) : null}
{a.service_date || "—"} {changedFields.has("service_date") ? ( {b.service_date || "—"} ) : null}
{changedFields.has("status") ? ( ) : null}
{a.cpt_codes.length === 0 ? "—" : a.cpt_codes.join(", ")} {changedFields.has("cpt_codes") ? ( {b.cpt_codes.length === 0 ? "—" : b.cpt_codes.join(", ")} ) : null}
); } function SectionTable({ title, eyebrow, count, children, testid, emptyMessage, }: { title: string; eyebrow: string; count: number; children: React.ReactNode; testid: string; emptyMessage: string; }) { return (
{eyebrow}

{title}

{fmt.num(count)}
{count === 0 ? (
{emptyMessage}
) : (
Claim ID Patient Charge Service date Status CPT {children}
)}
); } // --------------------------------------------------------------------------- // Skeleton — used by the page while the diff is loading. // --------------------------------------------------------------------------- export function BatchDiffViewSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
{Array.from({ length: 3 }).map((_, i) => ( ))}
); } // --------------------------------------------------------------------------- // Empty state — both batches selected but no deltas. // --------------------------------------------------------------------------- export function BatchDiffEmpty({ data }: { data: BatchDiff }) { return (
Diff · no deltas
These two batches are identical — no claims added, removed, or changed between A and B.
); } // --------------------------------------------------------------------------- // Main view — rendered once both batches are picked and the diff returns. // --------------------------------------------------------------------------- export function BatchDiffView({ data }: { data: BatchDiff }) { const { a, b, added, removed, changed, summary } = data; const allEmpty = summary.addedCount === 0 && summary.removedCount === 0 && summary.changedCount === 0; if (allEmpty) { return ; } return (
{added.map((row, idx) => ( ))} {removed.map((row, idx) => ( ))} {changed.map((row, idx) => ( ))}
); }