feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens

Backend:

- New POST /api/batches/{id}/export-837: regenerate X12 837 files
  for a list of claim_ids into a ZIP using HCPF file naming standards,
  with a unique interchange/group control number per export. Wire
  the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
  (NM1*40) blocks so the serializer no longer falls back to
  CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
  batch_id in both JSON and NDJSON response shapes so the frontend
  can hit batch-scoped endpoints without an extra listBatches
  round-trip.
- Filename helpers and the 837 serializer updated to match the new
  HCPF envelope; tests cover batch export, parse batch_id, and the
  serializer's control-number uniqueness guarantee.

Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
  EditorialNote, ExportBar, TickerTape, and a charts/ set
  (BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
  the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
  colors to Tailwind theme tokens (bg-card, text-foreground,
  border/60, etc.) for consistency with the rest of the instrument
  chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
  reworked to compose the new shared components and consume the new
  batch-scoped API surface (notably ExportBar wired into Batches).

Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
  components and the useBatchExport hook.

Rolls up into the v0.2.0 release tag.
This commit is contained in:
Tyler
2026-06-22 11:01:58 -06:00
parent 35298907bc
commit 9bca4b608a
54 changed files with 6224 additions and 3871 deletions
+119
View File
@@ -0,0 +1,119 @@
// ---------------------------------------------------------------------------
// SegmentedBar — thin horizontal status distribution strip.
//
// A single bar split into N color segments showing the share of each
// status. The legend below the bar shows the segment label + count +
// percentage, color-coded to the segment. Designed for 46 statuses.
// ---------------------------------------------------------------------------
import { fmt } from "@/lib/format";
export interface Segment {
id: string;
label: string;
count: number;
color: string;
}
export interface SegmentedBarProps {
segments: Segment[];
/** Optional caption shown above the bar (e.g. "Status distribution"). */
caption?: string;
}
export function SegmentedBar({ segments, caption }: SegmentedBarProps) {
const total = segments.reduce((s, x) => s + x.count, 0);
if (total <= 0) {
return (
<div
className="mono text-[11px] py-6 text-center"
style={{ color: "hsl(var(--surface-ink-3))" }}
>
No claims yet.
</div>
);
}
return (
<div className="w-full">
{caption ? (
<div
className="mono text-[10.5px] uppercase tracking-[0.16em] mb-2"
style={{ color: "hsl(var(--surface-ink-3))" }}
>
{caption}
</div>
) : null}
{/* The bar */}
<div
className="flex w-full h-[10px] overflow-hidden rounded-[3px]"
style={{ boxShadow: "inset 0 0 0 1px hsl(30 14% 14% / 0.08)" }}
>
{segments.map((s, i) => {
const pct = (s.count / total) * 100;
if (pct <= 0) return null;
return (
<div
key={s.id}
className="h-full"
style={{
width: `${pct}%`,
backgroundColor: s.color,
animation: `bar-grow-h 700ms cubic-bezier(0.2,0.8,0.2,1) ${
i * 50
}ms both`,
transformOrigin: "left center",
}}
title={`${s.label}: ${s.count} (${((s.count / total) * 100).toFixed(0)}%)`}
aria-label={`${s.label}: ${s.count} of ${total}`}
/>
);
})}
</div>
{/* Legend */}
<ul className="mt-3 grid grid-cols-2 gap-x-4 gap-y-1.5">
{segments.map((s) => {
const pct = (s.count / total) * 100;
if (s.count === 0) return null;
return (
<li
key={s.id}
className="flex items-center justify-between gap-2 text-[11.5px] min-w-0"
>
<div className="flex items-center gap-2 min-w-0">
<span
aria-hidden
className="inline-block h-2 w-2 rounded-sm shrink-0"
style={{ backgroundColor: s.color }}
/>
<span
className="truncate"
style={{ color: "hsl(var(--surface-ink-2))" }}
>
{s.label}
</span>
</div>
<div className="flex items-center gap-2 shrink-0 mono tabular-nums">
<span style={{ color: "hsl(var(--surface-ink))" }}>
{fmt.num(s.count)}
</span>
<span
style={{
color: "hsl(var(--surface-ink-3))",
fontSize: 10.5,
minWidth: 30,
textAlign: "right",
}}
>
{pct.toFixed(0)}%
</span>
</div>
</li>
);
})}
</ul>
</div>
);
}