Files
cyclone/src/components/RemitDrawer/ClaimPaymentsTable.tsx
T
Tyler fbe9940a3f feat(ui): cohesive frontend polish — design system + per-screen refinement
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.
2026-06-20 22:27:01 -06:00

178 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="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>
);
}