feat(frontend): RemitDrawer component with CAS adjustments + parties + claim payments

This commit is contained in:
Tyler
2026-06-20 17:16:23 -06:00
parent 4cd52c3084
commit 25d47a5621
21 changed files with 2899 additions and 0 deletions
@@ -0,0 +1,176 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { fmt } from "@/lib/format";
import type { RemitDetail } from "@/hooks/useRemitDetail";
type ClaimPaymentsTableProps = {
remit: RemitDetail;
};
/**
* Format a CARC reason code as `GROUP-REASON` (e.g. `CO-45`). Both
* halves come straight off the backend `CasAdjustment` row; the group
* is the 2-letter ANSI code (CO/PR/OA/PI/CR) and the reason is the
* numeric code within that group.
*/
function carcKey(group: string, reason: string): string {
return `${group}-${reason}`;
}
/**
* Aggregate per-CARC adjustment totals so the table footer can show
* the dollar impact per reason code. The remit's flat `adjustments`
* array is summed in-place — no server round-trip.
*/
function summarizeAdjustments(remit: RemitDetail): Array<{
group: string;
reason: string;
label: string;
total: number;
count: number;
}> {
const byKey = new Map<
string,
{ group: string; reason: string; label: string; total: number; count: number }
>();
for (const adj of remit.adjustments ?? []) {
const key = carcKey(adj.group, adj.reason);
const existing = byKey.get(key);
if (existing) {
existing.total += adj.amount;
existing.count += 1;
} else {
byKey.set(key, {
group: adj.group,
reason: adj.reason,
label: adj.label,
total: adj.amount,
count: 1,
});
}
}
// Largest dollar impact first — easy to scan for the biggest hit.
return Array.from(byKey.values()).sort((a, b) => Math.abs(b.total) - Math.abs(a.total));
}
/**
* Claim payments table for the remittance detail drawer.
*
* The current backend `/api/remittances/{id}` payload exposes one
* `ClaimPayment` per remit (the persistence layer keys adjustments by
* remittance, not by claim), so the table renders one row carrying the
* remit's claim identity (PCN + claim id) plus the headline figures
* (paid + adjustments). Below the headline row we surface a per-CARC
* breakdown — one row per (group, reason) pair, with the cumulative
* amount and the reason-code label — so the user can see WHY the
* adjustment totals moved without leaving the drawer.
*
* When the backend grows to expose per-`ClaimPayment.service_payment`
* rows in the detail payload, the table grows naturally: a parent row
* per claim, expandable to per-service-payment children. The current
* single-row layout is the v1 shape that works against the v1 API.
*/
export function ClaimPaymentsTable({ remit }: ClaimPaymentsTableProps) {
const summary = summarizeAdjustments(remit);
const claimLabel = remit.payerClaimControlNumber ?? remit.claimId ?? remit.id;
const hasCharge = typeof remit.totalCharge === "number" && Number.isFinite(remit.totalCharge);
return (
<section className="flex flex-col gap-3 px-6 py-4" data-testid="claim-payments-table-section">
<h3
data-testid="section-label"
className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]"
>
Claim Payments (1)
</h3>
<Table data-testid="claim-payments-table">
<TableHeader>
<TableRow>
<TableHead className="w-44">Claim</TableHead>
<TableHead className="w-28">Status</TableHead>
<TableHead className="text-right">Charge</TableHead>
<TableHead className="text-right">Paid</TableHead>
<TableHead className="text-right">Adjustments</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow data-testid="claim-payments-row">
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
{claimLabel}
</TableCell>
<TableCell className="font-mono text-[color:var(--m-ink-secondary)]">
{remit.status}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{hasCharge ? fmt.usdPrecise(remit.totalCharge as number) : "—"}
</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(remit.paidAmount)}
</TableCell>
<TableCell
className="text-right font-mono text-base tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{Math.abs(remit.adjustmentAmount) > 0
? fmt.usdPrecise(remit.adjustmentAmount)
: "—"}
</TableCell>
</TableRow>
</TableBody>
</Table>
{summary.length > 0 ? (
<div className="flex flex-col gap-2 pt-2" data-testid="claim-payments-carc-summary">
<h4 className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-[color:var(--m-ink-tertiary)]">
CARC breakdown
</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-20">CARC</TableHead>
<TableHead>Reason</TableHead>
<TableHead className="w-16 text-right">Hits</TableHead>
<TableHead className="w-32 text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{summary.map((row) => (
<TableRow
key={carcKey(row.group, row.reason)}
data-testid="carc-row"
data-carc={carcKey(row.group, row.reason)}
>
<TableCell className="font-mono text-[color:var(--m-ink-primary)]">
{carcKey(row.group, row.reason)}
</TableCell>
<TableCell className="text-sm text-[color:var(--m-ink-secondary)]">
{row.label}
</TableCell>
<TableCell
className="text-right font-mono tabular-nums text-[color:var(--m-ink-secondary)]"
data-testid="carc-count"
>
×{row.count}
</TableCell>
<TableCell
className="text-right font-mono tabular-nums text-[color:var(--m-ink-primary)]"
style={{ fontFamily: "var(--m-font-mono)" }}
>
{fmt.usdPrecise(row.total)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : null}
</section>
);
}