Files
cyclone/src/components/RemitDrawer/ClaimPaymentsTable.tsx
T

177 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}