feat(sp7): ServiceLinesTable — Paid + Adjustments columns

This commit is contained in:
Tyler
2026-06-20 19:45:35 -06:00
parent 2432ca0ec4
commit 77f61b675d
2 changed files with 114 additions and 32 deletions
@@ -117,9 +117,9 @@ describe("ServiceLinesTable", () => {
// Row carries the line number as a data attribute for test stability. // Row carries the line number as a data attribute for test stability.
expect(rows[0].getAttribute("data-line-number")).toBe("1"); expect(rows[0].getAttribute("data-line-number")).toBe("1");
// Cells: line #, procedure, charge, units, date. // Cells: line #, procedure, charge, units, date, paid, adjustments.
const cells = rows[0].querySelectorAll("td"); const cells = rows[0].querySelectorAll("td");
expect(cells.length).toBe(5); expect(cells.length).toBe(7);
const cellText = Array.from(cells).map((c) => c.textContent ?? ""); const cellText = Array.from(cells).map((c) => c.textContent ?? "");
expect(cellText[0]).toContain("1"); // line # expect(cellText[0]).toContain("1"); // line #
@@ -128,10 +128,52 @@ describe("ServiceLinesTable", () => {
expect(cellText[3]).toContain("1"); // units count expect(cellText[3]).toContain("1"); // units count
expect(cellText[3]).toContain("UN"); // units type expect(cellText[3]).toContain("UN"); // units type
expect(cellText[4]).toContain("2026"); // date (year at minimum) expect(cellText[4]).toContain("2026"); // date (year at minimum)
// Paid + Adjustments default to em-dash when no lineReconciliation is provided.
expect(cellText[5]).toContain("—");
expect(cellText[6]).toContain("—");
unmount(); unmount();
}); });
it("test_paid_and_adjustments_columns_render_from_lineReconciliation", () => {
const line = makeLine({ lineNumber: 1, charge: 100.0 });
const lineReconciliation = [
{ lineNumber: 1, status: "matched" as const, paid: "80.00", adjustmentsSum: "20.00" },
];
const { container, unmount } = renderIntoContainer(
<ServiceLinesTable
serviceLines={[line]}
lineReconciliation={lineReconciliation}
/>
);
const row = container.querySelector('[data-testid="service-line-row"]');
expect(row).not.toBeNull();
const paid = row?.querySelector('[data-testid="paid-cell"]');
const adjustments = row?.querySelector('[data-testid="adjustments-cell"]');
expect(paid?.textContent ?? "").toContain("$80.00");
expect(adjustments?.textContent ?? "").toContain("$20.00");
unmount();
});
it("test_paid_and_adjustments_render_em_dash_when_null", () => {
const line = makeLine({ lineNumber: 1 });
const lineReconciliation = [
{ lineNumber: 1, status: "unmatched_837_only" as const, paid: null, adjustmentsSum: null },
];
const { container, unmount } = renderIntoContainer(
<ServiceLinesTable
serviceLines={[line]}
lineReconciliation={lineReconciliation}
/>
);
const row = container.querySelector('[data-testid="service-line-row"]');
const paid = row?.querySelector('[data-testid="paid-cell"]');
const adjustments = row?.querySelector('[data-testid="adjustments-cell"]');
expect(paid?.textContent ?? "").toBe("—");
expect(adjustments?.textContent ?? "").toBe("—");
unmount();
});
it("test_multiple_lines_render_in_order", () => { it("test_multiple_lines_render_in_order", () => {
const lines = [ const lines = [
makeLine({ lineNumber: 1, procedureCode: "99213" }), makeLine({ lineNumber: 1, procedureCode: "99213" }),
@@ -1,9 +1,15 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { fmt } from "@/lib/format"; import { fmt } from "@/lib/format";
import type { ClaimDetail } from "@/types"; import type { ClaimDetail, ClaimDetailLineReconciliation } from "@/types";
type ServiceLinesTableProps = { type ServiceLinesTableProps = {
serviceLines: ClaimDetail["serviceLines"]; serviceLines: ClaimDetail["serviceLines"];
/**
* SP7: per-line Paid + Adjustments values from the slim projection
* embedded in the claim-detail response. Defaults to [] (renders
* em-dash placeholders in the new columns).
*/
lineReconciliation?: ClaimDetailLineReconciliation[];
}; };
function formatProcedure(line: ClaimDetail["serviceLines"][number]): string { function formatProcedure(line: ClaimDetail["serviceLines"][number]): string {
@@ -16,14 +22,29 @@ function formatUnits(line: ClaimDetail["serviceLines"][number]): string {
return `${line.units ?? 0} ${line.unitType ?? ""}`.trimEnd(); return `${line.units ?? 0} ${line.unitType ?? ""}`.trimEnd();
} }
function formatMoneyOrDash(value: string | null): string {
if (value === null) return "—";
const n = Number(value);
return Number.isFinite(n) ? fmt.usdPrecise(n) : value;
}
/** /**
* Service-lines table for the claim detail drawer (SP4). * Service-lines table for the claim detail drawer (SP4 + SP7).
* *
* Five columns: line #, procedure (qualifier + code + modifiers), * Seven columns: line #, procedure (qualifier + code + modifiers),
* charge (mono, prominent), units, date. Charge is the headline — * charge (mono, prominent), units, date, paid (from 835 SVC), and
* adjustments (sum of per-line CAS rows). Charge is the headline —
* big mono digits with tabular-nums so columns align across rows. * big mono digits with tabular-nums so columns align across rows.
*/ */
export function ServiceLinesTable({ serviceLines }: ServiceLinesTableProps) { export function ServiceLinesTable({
serviceLines,
lineReconciliation = [],
}: ServiceLinesTableProps) {
const lrByLine: Record<number, ClaimDetailLineReconciliation> = {};
for (const lr of lineReconciliation) {
lrByLine[lr.lineNumber] = lr;
}
return ( return (
<section className="flex flex-col gap-3 px-6 py-4"> <section className="flex flex-col gap-3 px-6 py-4">
<h3 <h3
@@ -49,10 +70,14 @@ export function ServiceLinesTable({ serviceLines }: ServiceLinesTableProps) {
<TableHead className="text-right">Charge</TableHead> <TableHead className="text-right">Charge</TableHead>
<TableHead className="w-20">Units</TableHead> <TableHead className="w-20">Units</TableHead>
<TableHead className="w-28">Date</TableHead> <TableHead className="w-28">Date</TableHead>
<TableHead className="text-right" data-testid="paid-header">Paid</TableHead>
<TableHead className="text-right" data-testid="adjustments-header">Adjustments</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{serviceLines.map((line) => ( {serviceLines.map((line) => {
const lr = lrByLine[line.lineNumber];
return (
<TableRow <TableRow
key={line.lineNumber} key={line.lineNumber}
data-testid="service-line-row" data-testid="service-line-row"
@@ -76,8 +101,23 @@ export function ServiceLinesTable({ serviceLines }: ServiceLinesTableProps) {
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]"> <TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
{line.serviceDate ? fmt.date(line.serviceDate) : ""} {line.serviceDate ? fmt.date(line.serviceDate) : ""}
</TableCell> </TableCell>
<TableCell
data-testid="paid-cell"
className="text-right font-mono tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{formatMoneyOrDash(lr?.paid ?? null)}
</TableCell>
<TableCell
data-testid="adjustments-cell"
className="text-right font-mono tabular-nums text-[color:var(--m-ink-secondary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{formatMoneyOrDash(lr?.adjustmentsSum ?? null)}
</TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
)} )}