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,138 @@
|
||||
// @vitest-environment happy-dom
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
||||
true;
|
||||
|
||||
import React, { act, useState } from "react";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { ExportBar } from "./ExportBar";
|
||||
|
||||
function renderBar(
|
||||
props: Partial<React.ComponentProps<typeof ExportBar>> = {},
|
||||
): {
|
||||
container: HTMLDivElement;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root: Root = createRoot(container);
|
||||
const onToggleAll = props.onToggleAll ?? (() => {});
|
||||
const onExport = props.onExport ?? (() => {});
|
||||
act(() => {
|
||||
root.render(
|
||||
React.createElement(ExportBar, {
|
||||
total: 10,
|
||||
selectedCount: 10,
|
||||
exporting: false,
|
||||
onToggleAll,
|
||||
onExport,
|
||||
...props,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("ExportBar", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders the total and selected count", () => {
|
||||
const { container, unmount } = renderBar({ total: 24, selectedCount: 18 });
|
||||
expect(container.textContent).toContain("18");
|
||||
expect(container.textContent).toContain("24");
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the select-all checkbox is checked when all items are selected", () => {
|
||||
const { container, unmount } = renderBar({ total: 5, selectedCount: 5 });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
expect(cb.checked).toBe(true);
|
||||
expect(cb.indeterminate).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the select-all checkbox is unchecked when nothing is selected", () => {
|
||||
const { container, unmount } = renderBar({ total: 5, selectedCount: 0 });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
expect(cb.checked).toBe(false);
|
||||
expect(cb.indeterminate).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the select-all checkbox is indeterminate when some items are selected", () => {
|
||||
const { container, unmount } = renderBar({ total: 5, selectedCount: 2 });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
expect(cb.checked).toBe(false);
|
||||
expect(cb.indeterminate).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clicking the select-all checkbox calls onToggleAll", () => {
|
||||
const onToggleAll = vi.fn();
|
||||
const { container, unmount } = renderBar({ onToggleAll });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
act(() => {
|
||||
cb.click();
|
||||
});
|
||||
expect(onToggleAll).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the export button is disabled when selectedCount is 0", () => {
|
||||
const { container, unmount } = renderBar({ selectedCount: 0 });
|
||||
const btn = container.querySelector<HTMLButtonElement>("button")!;
|
||||
expect(btn.disabled).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the export button is enabled when selectedCount > 0", () => {
|
||||
const { container, unmount } = renderBar({ selectedCount: 3 });
|
||||
const btn = container.querySelector<HTMLButtonElement>("button")!;
|
||||
expect(btn.disabled).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("the export button is disabled while exporting", () => {
|
||||
const { container, unmount } = renderBar({ selectedCount: 3, exporting: true });
|
||||
const btn = container.querySelector<HTMLButtonElement>("button")!;
|
||||
expect(btn.disabled).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clicking the export button calls onExport", () => {
|
||||
const onExport = vi.fn();
|
||||
const { container, unmount } = renderBar({ onExport, selectedCount: 3 });
|
||||
const btn = container.querySelector<HTMLButtonElement>("button")!;
|
||||
act(() => {
|
||||
btn.click();
|
||||
});
|
||||
expect(onExport).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("renders the selected count in the export button label", () => {
|
||||
const { container, unmount } = renderBar({ selectedCount: 7 });
|
||||
const btn = container.querySelector<HTMLButtonElement>("button")!;
|
||||
expect(btn.textContent).toContain("7");
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user