177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
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>
|
||
);
|
||
}
|