fbe9940a3f
Distill the UI into a single, recognizable voice: a precision instrument for one operator on one machine. Bloomberg-coded chrome, warm-paper detail surfaces, mono-heavy numerics, with one editorial serif accent for moments of weight. Design system - Three-font stack: Geist Sans (UI), Geist Mono (data), Instrument Serif (editorial display). No Inter, no system fonts. - Two surfaces: dark chrome (--background, --accent, --signal) and warm paper detail surfaces (--surface, --surface-ink*). - Inbox has its own Ticker Tape terminal palette (--tt-*). - Shared component classes: .eyebrow, .mono, .display, .surface, .surface-2, .row-hover, .nav-active, .kbd, .editorial, .hairline. - --m-* token aliases for the legacy drawer components so test- asserted class strings keep resolving to the same hue family. Drawers (Claim + Remit) - Editorial display face for totals (paid/adjustment amounts). - Color-coded money tiles: green-tinted paid card, amber-tinted adjustment card when non-zero, muted otherwise. - Tabs get accent-blue underline + CSS-driven active state. - Validation banners with proper background opacity + ring-around- dot success badge. - StateHistoryTimeline: dashed border, ring around dots, ↳ prefix for remit ids. - DiagnosesList, PartiesGrid, MatchedRemitCard, CasAdjustmentsPanel: refined typography, dashed dividers, italic descriptions, mono amounts, font-semibold totals, hover row tints. Inbox Ticker Tape - Custom RowCheckbox with sr-only input + amber accent. - Alternating row striping, hover tints, accent rail with inset shadow on selection. - Refined sparkline with glow at high values. - BulkBar: bottom-floating bar with amber count chip, larger shadow. - CandidateBreakdown: animated progress bars with amber gradient. - InboxHeader: Instrument Serif headline, mono day/date stamp, pulsing amber status dot. Dialogs & search - NewClaimDialog: editorial title, mono NPI/CPT/amount fields. - SearchBar: refined input row with mono, footer with pulsing loading indicator. - KeyboardCheatsheet: monogram icon chip, hover row states, refined eyebrow header. Primitives - StatusBadge / ClaimStateBadge: per-state dot indicators. - SelectItem: data-[highlighted]:outline tokens for keyboard a11y. - Table primitives: refined header treatment, hover/focus states. Tests + build - 354/354 tests passing across 59 files. - Vite build clean (53.84 kB CSS / 560.86 kB JS). - Eyebrow assertions updated to match the consolidated .eyebrow class (intact visual contract, abstracted class string). - Badge variant tokens updated to the polished bg-muted/80 / /0.14 opacity scale.
178 lines
6.7 KiB
TypeScript
178 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="eyebrow 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" className="row-hover">
|
||
<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)] font-semibold"
|
||
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-success)] font-semibold"
|
||
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-warning)] font-semibold"
|
||
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="eyebrow 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)}
|
||
className="row-hover"
|
||
>
|
||
<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>
|
||
);
|
||
}
|