import { useCallback, useState } from "react"; import { Download } from "lucide-react"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { ApiError } from "@/lib/api"; import { DrillDrawerHeader } from "@/components/drill/DrillDrawerHeader"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { useAckDetail, type AckDetail } from "@/hooks/useAckDetail"; import { SegmentStatusList } from "./SegmentStatusList"; interface Props { /** * Currently-open ack id (string), or `null` when the drawer is * closed. The URL stores ids as strings so deep links round-trip * cleanly; the hook does the `Number()` coercion before calling * `api.getAck`. */ ackId: string | null; /** Fired when the user dismisses the drawer (X button, Escape, etc.). */ onClose: () => void; } /** * Roll a hex code into a "kind" so the drawer's error branch can * pick the right copy. Mirrors `ProviderDrawer`'s `errorKind` * computation. */ type ErrorKind = "not_found" | "network"; function AckDrawerError({ kind, onRetry, onClose, }: { kind: ErrorKind; onRetry?: () => void; onClose: () => void; }) { const COPY = { not_found: { eyebrow: "NOT FOUND", message: "This 999 ACK doesn't exist or has been removed.", }, network: { eyebrow: "CONNECTION", message: "Couldn't reach the server. Check your connection and try again.", }, } as const; const { eyebrow, message } = COPY[kind]; return (
{eyebrow}

{message}

{kind === "network" && onRetry ? ( ) : null}
); } /** * Inline ack code pill — same color palette as `AcksPage` * (`Acks.tsx`) so the badge reads the same in both surfaces. */ function AckCodePill({ code }: { code: AckDetail["ackCode"] }) { const cfg = code === "A" ? { fg: "hsl(152 64% 30%)", bg: "hsl(152 50% 88%)", border: "hsl(152 64% 38% / 0.30)", label: "Accepted", } : code === "R" ? { fg: "hsl(358 70% 36%)", bg: "hsl(358 70% 92%)", border: "hsl(358 70% 50% / 0.30)", label: "Rejected", } : code === "P" ? { fg: "hsl(36 92% 30%)", bg: "hsl(36 82% 88%)", border: "hsl(36 92% 50% / 0.30)", label: "Partially accepted", } : { fg: "hsl(36 92% 30%)", bg: "hsl(36 82% 88%)", border: "hsl(36 92% 50% / 0.30)", label: "Accepted w/ errors", }; return ( {cfg.label} ); } function StatTile({ label, value, tone, }: { label: string; value: number | string; tone: "success" | "destructive" | "ink"; }) { const color = tone === "success" ? "hsl(152 64% 30%)" : tone === "destructive" ? "hsl(358 70% 36%)" : "hsl(var(--foreground))"; return (
{label}
{value}
); } /** * 999 ACK drill-down drawer (SP21 Phase 5 Task 5.2). * * Mirror of `ProviderDrawer` — same right-anchored side-panel shell, * same `errorKind` + `useAckDetail` shape (404 vs network), same * skeleton-first loading state. The body is slim: rolled-up counts * at the top, the ack code pill, the source batch id, and a * per-segment status list. The header carries a "Download 999" * action so the user can grab the original X12 file from the drawer * without a second round-trip to `/api/acks/{id}/raw`. * * Layout mirrors `ProviderDrawer`/`ClaimDrawer`: Radix Dialog * repositioned to the right edge as a fixed-height side panel, with * the shared `DrillDrawerHeader` on top and a scrollable body below. * * Error branching: * - `ApiError(404)` → "not_found" (no retry, the ack is gone) * - anything else → "network" (retry available) */ export function AckDrawer({ ackId, onClose }: Props) { const { data, isLoading, isError, error, refetch } = useAckDetail(ackId); const errorKind: ErrorKind | null = isError ? error instanceof ApiError && error.status === 404 ? "not_found" : "network" : null; // Download affordance for the regenerated 999 X12 text. Lives in // the drawer (not the page) so a deep-linked user can grab the file // without bouncing back to /acks. `raw_999_text` may be empty for // older rows without the field — we silently no-op rather than // surfacing an error toast (the spec calls this a "low stakes" // affordance). const [downloading, setDownloading] = useState(false); const onDownload = useCallback(async () => { if (!data) return; const raw = (data as AckDetail & { raw_999_text?: string }).raw_999_text ?? ""; if (!raw || downloading) return; setDownloading(true); try { const blob = new Blob([raw], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `ack-${data.sourceBatchId}.999`; a.click(); URL.revokeObjectURL(url); } finally { setDownloading(false); } }, [data, downloading]); const downloadAction = data && (data as AckDetail & { raw_999_text?: string }).raw_999_text ? ( ) : null; return ( { if (!o) onClose(); }}> {errorKind ? ( { void refetch(); }} onClose={onClose} /> ) : isLoading || !data ? (
) : (
ID {data.id} · {data.parsedAt ? data.parsedAt.slice(0, 10) : "—"}
)}
); }