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 (
{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) => (
))}
);
}