diff --git a/src/components/RemitDrawer/CasAdjustmentsPanel.test.tsx b/src/components/RemitDrawer/CasAdjustmentsPanel.test.tsx index 2c20ccd..60441c9 100644 --- a/src/components/RemitDrawer/CasAdjustmentsPanel.test.tsx +++ b/src/components/RemitDrawer/CasAdjustmentsPanel.test.tsx @@ -71,6 +71,68 @@ describe("CasAdjustmentsPanel", () => { unmount(); }); + it("renders_per_line_section_with_procedure_and_line_number", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const section = container.querySelector( + '[data-testid="per-line-cas-section"]' + ); + expect(section).not.toBeNull(); + const row = container.querySelector('[data-testid="per-line-cas-row"]'); + expect(row).not.toBeNull(); + expect(row?.getAttribute("data-line-number")).toBe("1"); + expect(row?.textContent).toContain("99213"); + expect(row?.textContent).toContain("#1"); + expect(row?.textContent).toContain("CO-45"); + + unmount(); + }); + + it("renders_claim_level_section_with_subheader", () => { + const { container, unmount } = renderIntoContainer( + + ); + + const section = container.querySelector( + '[data-testid="claim-level-cas-section"]' + ); + expect(section).not.toBeNull(); + expect(section?.textContent ?? "").toMatch(/Claim-level adjustments/i); + const entry = container.querySelector('[data-testid="claim-level-cas-entry"]'); + expect(entry).not.toBeNull(); + expect(entry?.textContent).toContain("PR-1"); + + unmount(); + }); + it("toggle_collapses_and_expands_the_list", () => { // A long list (> 3 groups) starts collapsed so the drawer doesn't // get buried under a wall of CARC rows. The user can re-expand by diff --git a/src/components/RemitDrawer/CasAdjustmentsPanel.tsx b/src/components/RemitDrawer/CasAdjustmentsPanel.tsx index 169f6b4..e6feb88 100644 --- a/src/components/RemitDrawer/CasAdjustmentsPanel.tsx +++ b/src/components/RemitDrawer/CasAdjustmentsPanel.tsx @@ -3,9 +3,36 @@ import { ChevronDown, ChevronRight } from "lucide-react"; import { fmt } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { RemitDetail } from "@/hooks/useRemitDetail"; +import type { + RemittanceClaimLevelAdjustment, + ServiceLinePayment, +} from "@/types"; + +/** + * One row of CAS adjustments attached to a single SLP. The parent + * builds these by looking up CAS rows in the line-reconciliation + * endpoint's per-line `adjustments` arrays. We model the row as a + * flat tuple of `(slp, adjustments)` so the panel can sort by + * line number without a join. + */ +export type PerLineCasGroup = { + slp: ServiceLinePayment; + adjustments: Array<{ + groupCode: string; + reasonCode: string; + amount: string; + }>; +}; type CasAdjustmentsPanelProps = { adjustments: NonNullable; + /** + * SP7: optional per-line SVC breakdown. When provided, the panel + * renders a "Per-line" section above the aggregate rollup and a + * "Claim-level" section below it. + */ + perLine?: PerLineCasGroup[]; + claimLevelAdjustments?: RemittanceClaimLevelAdjustment[]; }; /** @@ -98,70 +125,198 @@ function CasRow({ group }: { group: CasGroup }) { * list is longer so the drawer doesn't get buried under a wall of * CARC rows. */ -export function CasAdjustmentsPanel({ adjustments }: CasAdjustmentsPanelProps) { +export function CasAdjustmentsPanel({ + adjustments, + perLine, + claimLevelAdjustments, +}: CasAdjustmentsPanelProps) { const groups = groupAdjustments(adjustments); const [open, setOpen] = useState(groups.length <= 3); - if (groups.length === 0) return null; + const hasPerLine = (perLine?.length ?? 0) > 0; + const hasClaimLevel = (claimLevelAdjustments?.length ?? 0) > 0; + const hasAny = + groups.length > 0 || hasPerLine || hasClaimLevel; + + if (!hasAny) return null; return (
- + {hasPerLine ? ( + + ) : null} - {open ? ( -
- {/* Header row — labels for the 4-column grid */} -
- - CARC - - - Reason - - - Hits - - - Amount - -
- {groups.map((g) => ( - - ))} -
+ {groups.length > 0 ? ( + <> + + + {open ? ( +
+ {/* Header row — labels for the 4-column grid */} +
+ + CARC + + + Reason + + + Hits + + + Amount + +
+ {groups.map((g) => ( + + ))} +
+ ) : null} + + ) : null} + + {hasClaimLevel ? ( + ) : null}
); } + +/** + * SP7: per-line section. One card per SVC composite, showing the + * procedure code + line number + its CAS rows. + */ +function PerLineSection({ perLine }: { perLine: PerLineCasGroup[] }) { + return ( +
+

+ Per-line Adjustments ({perLine.length}) +

+ {perLine.map((g) => ( + + ))} +
+ ); +} + +function PerLineRow({ group }: { group: PerLineCasGroup }) { + const total = group.adjustments.reduce( + (acc, a) => acc + Number(a.amount || 0), + 0, + ); + return ( +
+
+ + {group.slp.procedureCode} + + #{group.slp.lineNumber} + + + + {fmt.usdPrecise(total)} + +
+ {group.adjustments.length > 0 ? ( +
    + {group.adjustments.map((a, i) => ( +
  • + {a.groupCode}-{a.reasonCode} · {fmt.usdPrecise(Number(a.amount))} +
  • + ))} +
+ ) : null} +
+ ); +} + +/** + * SP7: claim-level (CLP-level) CAS bucket. Rendered as a flat list + * under a "Claim-level adjustments" sub-header, below the per-line + * section and the aggregate rollup. + */ +function ClaimLevelSection({ + adjustments, +}: { + adjustments: RemittanceClaimLevelAdjustment[]; +}) { + return ( +
+

+ Claim-level adjustments ({adjustments.length}) +

+
    + {adjustments.map((a) => ( +
  • + + {a.groupCode}-{a.reasonCode} + + + {fmt.usdPrecise(Number(a.amount))} + +
  • + ))} +
+
+ ); +}