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:
@@ -0,0 +1,150 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgingBars — AR aging stacked horizontal bar.
|
||||
//
|
||||
// One bar split into up to 4 segments by aging bucket (0–30 / 31–60 /
|
||||
// 61–90 / 90+ days). Each segment is coloured along a cool-to-warm ramp
|
||||
// so the eye reads left-to-right as "freshest to most stale". Below
|
||||
// the bar, four legend rows show the bucket label, dollar amount, and
|
||||
// share of total. The component is data-driven — pass any four-bucket
|
||||
// distribution.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { fmt } from "@/lib/format";
|
||||
|
||||
export interface AgingBucket {
|
||||
/** Bucket id for keying. */
|
||||
id: string;
|
||||
/** Display label (e.g. "0–30", "31–60", "61–90", "90+"). */
|
||||
label: string;
|
||||
/** Dollar amount outstanding in this bucket. */
|
||||
amount: number;
|
||||
/** Tailwind/HSL colour for the segment. */
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface AgingBarsProps {
|
||||
buckets: AgingBucket[];
|
||||
/** Total AR amount for share calc. Defaults to sum of buckets. */
|
||||
total?: number;
|
||||
/** Optional caption shown right-aligned above the bar. */
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export function AgingBars({ buckets, total, caption }: AgingBarsProps) {
|
||||
const sum = total ?? buckets.reduce((s, b) => s + b.amount, 0);
|
||||
if (sum <= 0) {
|
||||
return (
|
||||
<div
|
||||
className="mono text-[11px] py-6 text-center"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Nothing outstanding.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{caption ? (
|
||||
<div
|
||||
className="mono text-[10.5px] uppercase tracking-[0.16em] mb-2 flex items-center justify-between"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
<span>Aging buckets</span>
|
||||
<span>{caption}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* The stacked bar */}
|
||||
<div
|
||||
className="flex w-full h-3 overflow-hidden rounded-[3px]"
|
||||
style={{ boxShadow: "inset 0 0 0 1px hsl(30 14% 14% / 0.08)" }}
|
||||
>
|
||||
{buckets.map((b, i) => {
|
||||
const pct = (b.amount / sum) * 100;
|
||||
if (pct <= 0) return null;
|
||||
return (
|
||||
<div
|
||||
key={b.id}
|
||||
className="h-full"
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
backgroundColor: b.color,
|
||||
animation: `bar-grow-h 800ms cubic-bezier(0.2,0.8,0.2,1) ${
|
||||
i * 70
|
||||
}ms both`,
|
||||
transformOrigin: "left center",
|
||||
}}
|
||||
title={`${b.label}: ${fmt.usd(b.amount)}`}
|
||||
aria-label={`${b.label} days: ${fmt.usd(b.amount)}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legend rows */}
|
||||
<ul className="mt-4 space-y-2">
|
||||
{buckets.map((b) => {
|
||||
const share = sum > 0 ? (b.amount / sum) * 100 : 0;
|
||||
return (
|
||||
<li
|
||||
key={b.id}
|
||||
className="flex items-center justify-between gap-3 text-[12px]"
|
||||
>
|
||||
<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: b.color }}
|
||||
/>
|
||||
<span
|
||||
className="mono tabular-nums"
|
||||
style={{ color: "hsl(var(--surface-ink-2))" }}
|
||||
>
|
||||
{b.label}d
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div
|
||||
className="relative h-1 w-16 rounded-full overflow-hidden"
|
||||
style={{ backgroundColor: "hsl(30 14% 14% / 0.06)" }}
|
||||
aria-hidden
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full"
|
||||
style={{
|
||||
width: `${share}%`,
|
||||
backgroundColor: b.color,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="mono tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
minWidth: 70,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{fmt.usd(b.amount)}
|
||||
</span>
|
||||
<span
|
||||
className="mono tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink-3))",
|
||||
minWidth: 40,
|
||||
textAlign: "right",
|
||||
fontSize: 10.5,
|
||||
}}
|
||||
>
|
||||
{share.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user