9bca4b608a
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.
152 lines
4.9 KiB
TypeScript
152 lines
4.9 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// TickerTape — live vital-signs strip for the dashboard hero.
|
|
//
|
|
// A horizontal tape of small numeric readouts separated by hairline
|
|
// dividers, rendered on the dark canvas. Each cell shows a label
|
|
// (uppercase mono) and a value (mono). One cell can be marked
|
|
// `accent` to draw the eye, and the optional `live` cell shows a
|
|
// pulsing dot — the dashboard's only acknowledgement of time.
|
|
//
|
|
// This is a static ticker (not scrolling); the cells are just laid
|
|
// out left-to-right with dividers. We use it to give the dark hero
|
|
// data density without committing to a real chart.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface TickerCell {
|
|
/** Stable id used as React key. */
|
|
id: string;
|
|
/** Uppercase mono label (e.g. "Billed", "Pending", "Denial"). */
|
|
label: string;
|
|
/** The value, rendered verbatim in mono. Format it before passing. */
|
|
value: string;
|
|
/** Optional secondary value shown muted (e.g. delta). */
|
|
delta?: string;
|
|
/** Highlight this cell with the accent colour. */
|
|
accent?: boolean;
|
|
}
|
|
|
|
export interface TickerTapeProps {
|
|
cells: TickerCell[];
|
|
/** Show the "LIVE" pulse on the leading edge. */
|
|
live?: boolean;
|
|
/** Live label (default "Live"). */
|
|
liveLabel?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function TickerTape({
|
|
cells,
|
|
live = false,
|
|
liveLabel = "Live",
|
|
className,
|
|
}: TickerTapeProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative flex items-stretch w-full overflow-hidden",
|
|
"rounded-md border border-border/50 bg-card/40 backdrop-blur",
|
|
className
|
|
)}
|
|
>
|
|
{/* Left rail — amber signal accent for the LIVE indicator */}
|
|
{live ? (
|
|
<div
|
|
className="flex items-center gap-2 px-3 border-r border-border/60 shrink-0"
|
|
style={{
|
|
background:
|
|
"linear-gradient(180deg, hsl(36 92% 56% / 0.06), transparent)",
|
|
}}
|
|
>
|
|
<span className="relative inline-flex h-1.5 w-1.5 shrink-0">
|
|
<span
|
|
className="absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping"
|
|
style={{ backgroundColor: "hsl(var(--signal))" }}
|
|
/>
|
|
<span
|
|
className="relative inline-flex h-1.5 w-1.5 rounded-full"
|
|
style={{ backgroundColor: "hsl(var(--signal))" }}
|
|
/>
|
|
</span>
|
|
<span
|
|
className="mono uppercase tracking-[0.18em] font-semibold"
|
|
style={{
|
|
color: "hsl(var(--signal))",
|
|
fontSize: 10,
|
|
}}
|
|
>
|
|
{liveLabel}
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Cells */}
|
|
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto">
|
|
{cells.map((c, i) => (
|
|
<div
|
|
key={c.id}
|
|
className={cn(
|
|
"flex-1 min-w-[110px] px-4 py-2.5 flex flex-col gap-0.5",
|
|
i > 0 ? "border-l border-border/40" : ""
|
|
)}
|
|
>
|
|
<span
|
|
className="mono uppercase tracking-[0.18em] font-medium"
|
|
style={{
|
|
color: c.accent
|
|
? "hsl(var(--accent))"
|
|
: "hsl(var(--muted-foreground))",
|
|
fontSize: 9.5,
|
|
}}
|
|
>
|
|
{c.label}
|
|
</span>
|
|
<div className="flex items-baseline gap-1.5">
|
|
<span
|
|
className="display mono tabular-nums"
|
|
style={{
|
|
color: c.accent
|
|
? "hsl(var(--foreground))"
|
|
: "hsl(var(--foreground))",
|
|
fontSize: 16,
|
|
letterSpacing: "-0.01em",
|
|
}}
|
|
>
|
|
{c.value}
|
|
</span>
|
|
{c.delta ? (
|
|
<span
|
|
className="mono tabular-nums"
|
|
style={{
|
|
color: "hsl(var(--muted-foreground))",
|
|
fontSize: 10.5,
|
|
}}
|
|
>
|
|
{c.delta}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right rail — perforated tick pattern, decoratively marks the
|
|
end of the tape without using a heavy border. */}
|
|
<div
|
|
aria-hidden
|
|
className="shrink-0 w-3 self-stretch"
|
|
style={{
|
|
backgroundImage:
|
|
"radial-gradient(circle, hsl(36 14% 88% / 0.16) 1px, transparent 1.5px)",
|
|
backgroundSize: "6px 6px",
|
|
backgroundPosition: "0 center",
|
|
maskImage:
|
|
"linear-gradient(to right, transparent, black 30%, black 100%)",
|
|
WebkitMaskImage:
|
|
"linear-gradient(to right, transparent, black 30%, black 100%)",
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
} |