feat(sp7): ServiceLinesTable — Paid + Adjustments columns
This commit is contained in:
@@ -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,35 +70,54 @@ 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) => {
|
||||||
<TableRow
|
const lr = lrByLine[line.lineNumber];
|
||||||
key={line.lineNumber}
|
return (
|
||||||
data-testid="service-line-row"
|
<TableRow
|
||||||
data-line-number={line.lineNumber}
|
key={line.lineNumber}
|
||||||
>
|
data-testid="service-line-row"
|
||||||
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
data-line-number={line.lineNumber}
|
||||||
{line.lineNumber}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
|
|
||||||
{formatProcedure(line)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
|
|
||||||
style={{ fontFamily: "var(--m-font-mono)" }}
|
|
||||||
>
|
>
|
||||||
{fmt.usdPrecise(line.charge)}
|
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
||||||
</TableCell>
|
{line.lineNumber}
|
||||||
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
</TableCell>
|
||||||
{formatUnits(line)}
|
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
|
||||||
</TableCell>
|
{formatProcedure(line)}
|
||||||
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
</TableCell>
|
||||||
{line.serviceDate ? fmt.date(line.serviceDate) : ""}
|
<TableCell
|
||||||
</TableCell>
|
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
|
||||||
</TableRow>
|
style={{ fontFamily: "var(--m-font-mono)" }}
|
||||||
))}
|
>
|
||||||
|
{fmt.usdPrecise(line.charge)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
||||||
|
{formatUnits(line)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
|
||||||
|
{line.serviceDate ? fmt.date(line.serviceDate) : ""}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user