Files
cyclone/src/components/ClaimDrawer/ClaimDrawerHeader.tsx
T

135 lines
4.4 KiB
TypeScript

import { useState } from "react";
import { Download } from "lucide-react";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DrillDrawerHeader } from "@/components/drill/DrillDrawerHeader";
import { api } from "@/lib/api";
import { downloadTextFile } from "@/lib/download";
import { fmt } from "@/lib/format";
import type { ClaimDetail } from "@/types";
type ClaimDrawerHeaderProps = {
claim: ClaimDetail;
onClose: () => void;
/**
* Optional toast callback for surfacing download failures (network error,
* 404 if the claim was deleted out from under us, 422 if the stored
* payload can't be regenerated). Defaults to a no-op so consumers that
* don't wire a toast still get the happy-path download.
*/
onError?: (message: string) => void;
};
/**
* State → Badge variant. The state machine value (see
* `cyclone.db.ClaimState`) drives a color-coded chip so the user can
* scan the drawer's status at a glance:
*
* submitted → secondary (neutral, in-flight)
* accepted → default (brand-colored, accepted by payer)
* matched → success (paired with an ERA)
* paid → success (funds received)
* denied → destructive (needs attention)
* pending → warning (waiting on something)
* anything else → muted
*/
const STATE_VARIANT: Record<string, BadgeProps["variant"]> = {
submitted: "secondary",
accepted: "default",
matched: "success",
paid: "success",
denied: "destructive",
pending: "warning",
};
function badgeVariantFor(state: string): BadgeProps["variant"] {
return STATE_VARIANT[state] ?? "muted";
}
/**
* Header band for the claim detail drawer (SP4 → refactored SP21
* Phase 5 Task 5.10).
*
* The shell is now the shared `DrillDrawerHeader` (same as
* `ProviderDrawer` / `AckDrawer`) — eyebrow + title on the left,
* close button on the right. The right-side `action` slot carries
* the state badge, total billed amount, and the "Download 837"
* button, all of which used to live in a custom <header> block.
*
* Top-left: instrument-style "Claim" eyebrow + the claim ID.
* Top-right: state badge + total billed amount + download button.
* The badge color encodes state so the user can read the drawer's
* status at a glance without scrolling.
*/
export function ClaimDrawerHeader({
claim,
onClose,
onError,
}: ClaimDrawerHeaderProps) {
// Local "is downloading" flag so the button can show feedback while the
// fetch is in flight. The header is rendered above the fold, so a stuck
// spinner is the difference between "the click did something" and "did
// my click even register?"
const [downloading, setDownloading] = useState(false);
async function handleDownload() {
if (downloading) return;
setDownloading(true);
try {
const { text, filename } = await api.serializeClaim837(claim.id);
downloadTextFile(filename, "text/x12", text);
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to download 837 file.";
onError?.(message);
} finally {
setDownloading(false);
}
}
// The action slot is rendered by DrillDrawerHeader to the left of
// the close button. Group the three action pieces (badge, amount,
// download) in a single flex row so they read as a unit.
const action = (
<div className="flex items-center gap-2" data-testid="claim-header-actions">
<div className="flex items-center gap-2">
<Badge
variant={badgeVariantFor(claim.state)}
data-testid="header-state"
className="uppercase tracking-[0.14em]"
>
{claim.stateLabel}
</Badge>
<span
data-testid="header-amount"
className="mono text-sm tabular-nums text-muted-foreground"
>
{fmt.usdPrecise(claim.billedAmount)}
</span>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleDownload}
disabled={downloading}
aria-label={downloading ? "Downloading 837 file" : "Download 837 file"}
title="Download 837 file"
data-testid="header-download-837"
>
<Download className="h-4 w-4" strokeWidth={1.75} />
</Button>
</div>
);
return (
<header data-testid="claim-drawer-header">
<DrillDrawerHeader
eyebrow="Claim"
title={claim.id}
onClose={onClose}
action={action}
/>
</header>
);
}