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.
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
import { ArrowRight } from "lucide-react";
|
|
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { fmt } from "@/lib/format";
|
|
import type { ClaimDetail } from "@/types";
|
|
|
|
type MatchedRemitCardProps = {
|
|
matchedRemittance: ClaimDetail["matchedRemittance"];
|
|
};
|
|
|
|
/**
|
|
* Remittance status → Badge variant. Mirrors the list-endpoint mapping
|
|
* in ``RemittanceStatus`` (received / posted / reconciled). Unknown
|
|
* statuses (the type is an unconstrained string) fall back to ``muted``
|
|
* so a future backend status doesn't blow up the drawer.
|
|
*/
|
|
const STATUS_VARIANT: Record<string, BadgeProps["variant"]> = {
|
|
received: "secondary",
|
|
posted: "default",
|
|
reconciled: "success",
|
|
};
|
|
|
|
function statusVariantFor(status: string): BadgeProps["variant"] {
|
|
return STATUS_VARIANT[status] ?? "muted";
|
|
}
|
|
|
|
/**
|
|
* Matched-remittance card for the claim detail drawer (SP4).
|
|
*
|
|
* Renders only when the claim has been paired with an ERA (the
|
|
* ``matchedRemittance`` field is non-null). Layout: a single card with
|
|
* remit id + status badge + received date on the left, big mono
|
|
* ``totalPaid`` on the right, plus a "View remittance →" link that
|
|
* deep-links to ``/remittances?id={id}`` (v1: full page navigation via
|
|
* ``window.location.href``; a router-based nav handler is a v2 concern).
|
|
*/
|
|
export function MatchedRemitCard({ matchedRemittance }: MatchedRemitCardProps) {
|
|
if (matchedRemittance === null) return null;
|
|
|
|
const { id, totalPaid, status, receivedAt } = matchedRemittance;
|
|
// SP7: per-line counts may be missing on pre-SP7 claim-detail responses.
|
|
const matchedLines = matchedRemittance.matchedLines;
|
|
const totalLines = matchedRemittance.totalLines;
|
|
const hasLineCounts =
|
|
typeof matchedLines === "number" && typeof totalLines === "number";
|
|
const allMatched = hasLineCounts && matchedLines === totalLines;
|
|
|
|
const handleViewRemittance = () => {
|
|
window.location.href = `/remittances?id=${id}`;
|
|
};
|
|
|
|
return (
|
|
<section
|
|
className="flex flex-col gap-3 px-6 py-4"
|
|
data-testid="matched-remit-card"
|
|
>
|
|
<h3
|
|
data-testid="section-label"
|
|
className="eyebrow text-muted-foreground"
|
|
>
|
|
Matched Remittance
|
|
</h3>
|
|
|
|
<div
|
|
data-testid="matched-remit-card-inner"
|
|
className="flex flex-col gap-4 rounded-lg border border-border/60 bg-muted/30 p-4 sm:flex-row sm:items-start sm:justify-between"
|
|
>
|
|
{/* Left: remit id + status badge + received date */}
|
|
<div className="flex min-w-0 flex-col gap-2">
|
|
<span
|
|
data-testid="matched-remit-id"
|
|
className="mono text-sm text-foreground"
|
|
>
|
|
{id}
|
|
</span>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge
|
|
variant={statusVariantFor(status)}
|
|
data-testid="matched-remit-status"
|
|
className="uppercase tracking-[0.14em]"
|
|
>
|
|
{status}
|
|
</Badge>
|
|
<span
|
|
data-testid="matched-remit-received"
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
Received {fmt.date(receivedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: total paid + view-remittance link */}
|
|
<div className="flex flex-col items-start gap-2 sm:items-end">
|
|
<span
|
|
data-testid="matched-remit-total-paid"
|
|
className="display text-3xl text-foreground tabular-nums"
|
|
>
|
|
{fmt.usdPrecise(totalPaid)}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleViewRemittance}
|
|
data-testid="view-remittance-link"
|
|
className="gap-1 text-muted-foreground hover:text-foreground"
|
|
>
|
|
View remittance
|
|
<ArrowRight
|
|
className="h-3.5 w-3.5"
|
|
strokeWidth={1.75}
|
|
aria-hidden
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{hasLineCounts ? (
|
|
<span
|
|
data-testid="matched-remit-line-counts"
|
|
data-all-matched={allMatched ? "true" : "false"}
|
|
className="mono text-xs uppercase tracking-[0.14em]"
|
|
style={{ color: allMatched ? "var(--tt-amber)" : "var(--tt-oxblood)" }}
|
|
>
|
|
lines: {matchedLines}/{totalLines} matched
|
|
{!allMatched ? ` (${totalLines! - matchedLines!} unmatched)` : null}
|
|
</span>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|