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,162 @@
|
||||
// @vitest-environment happy-dom
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
||||
true;
|
||||
|
||||
import React, { act } from "react";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { useAppStore } from "@/store";
|
||||
import { ClaimCard837 } from "./ClaimCard837";
|
||||
import type { ClaimOutput } from "@/types";
|
||||
|
||||
// Tiny fixture — just enough to exercise the card's UI surface.
|
||||
const CLAIM: ClaimOutput = {
|
||||
claim_id: "CLM-TEST",
|
||||
subscriber: { first_name: "Jane", last_name: "Doe", member_id: "MEM-1" },
|
||||
payer: { name: "Test Payer", id: "P1" },
|
||||
billing_provider: { npi: "1234567890" },
|
||||
claim: {
|
||||
total_charge: 100,
|
||||
place_of_service: "11",
|
||||
frequency_code: "1",
|
||||
prior_auth: null,
|
||||
},
|
||||
service_lines: [
|
||||
{
|
||||
line_number: 1,
|
||||
procedure: { qualifier: "HC", code: "99213", modifiers: [] },
|
||||
charge: "100",
|
||||
units: "1",
|
||||
unit_type: "UN",
|
||||
service_date: "2026-06-01",
|
||||
},
|
||||
],
|
||||
diagnoses: [{ qualifier: "ABK", code: "J20.9" }],
|
||||
validation: { passed: true, errors: [], warnings: [] },
|
||||
};
|
||||
|
||||
function renderCard(
|
||||
props: Partial<React.ComponentProps<typeof ClaimCard837>> = {},
|
||||
): {
|
||||
container: HTMLDivElement;
|
||||
unmount: () => void;
|
||||
} {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root: Root = createRoot(container);
|
||||
const onToggleSelect = props.onToggleSelect ?? (() => {});
|
||||
act(() => {
|
||||
root.render(
|
||||
React.createElement(
|
||||
MemoryRouter,
|
||||
null,
|
||||
React.createElement(ClaimCard837, {
|
||||
claim: CLAIM,
|
||||
selected: false,
|
||||
onToggleSelect,
|
||||
...props,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("ClaimCard837 — checkbox + select interaction", () => {
|
||||
beforeEach(() => {
|
||||
useAppStore.setState({ parsedBatches: [] });
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders a checkbox with checked={selected}", () => {
|
||||
const { container, unmount } = renderCard({ selected: true });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
);
|
||||
expect(cb).not.toBeNull();
|
||||
expect(cb!.checked).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("renders an unchecked checkbox when selected={false}", () => {
|
||||
const { container, unmount } = renderCard({ selected: false });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
);
|
||||
expect(cb!.checked).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clicking the checkbox fires onToggleSelect with the claim_id", () => {
|
||||
const onToggleSelect = vi.fn();
|
||||
const { container, unmount } = renderCard({ onToggleSelect });
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
act(() => {
|
||||
cb.click();
|
||||
});
|
||||
expect(onToggleSelect).toHaveBeenCalledWith("CLM-TEST");
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clicking the checkbox does NOT toggle the card's expand state", () => {
|
||||
// Regression guard: the card body is a <button> that toggles expand
|
||||
// on click. The new checkbox must not bubble its click into that
|
||||
// button (which would also fire onToggleSelect, double-counting).
|
||||
// We assert this by checking the expanded content (the service lines
|
||||
// table) is NOT in the DOM after a checkbox click.
|
||||
const { container, unmount } = renderCard();
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)!;
|
||||
act(() => {
|
||||
cb.click();
|
||||
});
|
||||
// The service lines table only renders when the card is expanded.
|
||||
const table = container.querySelector("table");
|
||||
expect(table).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clicking the card body still toggles expand (existing behavior preserved)", () => {
|
||||
const { container, unmount } = renderCard();
|
||||
// The card body is a <button aria-expanded="false">. Clicking it
|
||||
// should reveal the expanded details (service lines table).
|
||||
const expandButton = container.querySelector<HTMLButtonElement>(
|
||||
'button[aria-expanded]',
|
||||
);
|
||||
expect(expandButton).not.toBeNull();
|
||||
act(() => {
|
||||
expandButton!.click();
|
||||
});
|
||||
// After expand, the service lines table is in the DOM.
|
||||
const table = container.querySelector("table");
|
||||
expect(table).not.toBeNull();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("checkbox has an accessible label that names the claim", () => {
|
||||
const { container, unmount } = renderCard();
|
||||
const cb = container.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
);
|
||||
// aria-label or wrapping <label> — at minimum the input must be
|
||||
// findable by an accessible name. The simplest assertion is that an
|
||||
// aria-label exists on the input itself.
|
||||
expect(
|
||||
cb!.getAttribute("aria-label") ||
|
||||
cb!.closest("label")?.textContent,
|
||||
).toContain("CLM-TEST");
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user