feat(frontend): RemitDrawer component with CAS adjustments + parties + claim payments
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user