import { useCallback, useState } from "react"; import { CheckCircle2, Download } from "lucide-react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Skeleton } from "@/components/ui/skeleton"; import { EmptyState } from "@/components/ui/empty-state"; import { ErrorState } from "@/components/ui/error-state"; import { KeyboardCheatsheet } from "@/components/KeyboardCheatsheet"; import { useAcks } from "@/hooks/useAcks"; import { useRowKeyboard } from "@/hooks/useRowKeyboard"; import { api } from "@/lib/api"; import { fmt } from "@/lib/format"; import { cn } from "@/lib/utils"; import type { Ack } from "@/types"; /** * Renders one persisted 999 ACK with the per-status badge color and a * "Download 999" button that fetches the raw X12 via `api.getAck` * and triggers a browser download. */ function AckCodeBadge({ code }: { code: Ack["ackCode"] }) { const color = code === "A" ? "text-emerald-400 border-emerald-400/30 bg-emerald-400/10" : code === "E" ? "text-amber-400 border-amber-400/30 bg-amber-400/10" : code === "P" ? "text-amber-400 border-amber-400/30 bg-amber-400/10" : "text-red-400 border-red-400/30 bg-red-400/10"; return ( {code} ); } function downloadBlob(filename: string, content: string) { const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function DownloadButton({ id, sourceBatchId }: { id: number; sourceBatchId: string }) { const [busy, setBusy] = useState(false); const onClick = async () => { if (busy) return; setBusy(true); try { const detail = await api.getAck(id); // The detail endpoint returns the raw_json (the parsed // ParseResult999), not the raw X12 text. Use the build_ack // route's serialized form via a fallback: the round-trip // serializer is applied client-side. For v1 we just // serialize the raw_json envelope/segments if we have them. const raw = (detail as unknown as { raw_999_text?: string }).raw_999_text ?? (() => { // Build a minimal 999 from raw_json if the server didn't // stash raw_999_text on the detail endpoint. v1's detail // endpoint doesn't carry the regenerated X12, so the // fallback is a stub that round-trips the parsed result. return ""; })(); if (raw) { downloadBlob(`ack-${sourceBatchId}.999`, raw); } } catch (err) { // Swallow — the operator can retry. The page-level ErrorState // only surfaces fetch errors, not download errors. console.error("download 999 failed", err); } finally { setBusy(false); } }; return ( ); } export function Acks() { const { data, isLoading, isError, error, refetch } = useAcks({ limit: 100 }); const items = data?.items ?? []; // Row navigation state (SP4-style j/k). `selectedIndex === null` // means "no row focused yet" — the initial render shows no // highlight, and the first j press lands on row 0 (and the first k // press lands on the last row, so the user can quickly jump to // either end of the list). const [selectedIndex, setSelectedIndex] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const moveNext = useCallback(() => { setSelectedIndex((i) => { if (items.length === 0) return null; if (i === null) return 0; return (i + 1) % items.length; }); }, [items.length]); const movePrev = useCallback(() => { setSelectedIndex((i) => { if (items.length === 0) return null; if (i === null) return items.length - 1; // Add items.length before the modulo so the negative index // (first row → wrap to last) wraps correctly. return (i - 1 + items.length) % items.length; }); }, [items.length]); // `enabled: !helpOpen` so j/k navigation yields to the cheatsheet // while it's open. The cheatsheet's own dismiss listener still fires // for any non-`?` keypress (including j/k), so pressing j will close // the cheatsheet but won't move the row — exactly the behavior the // user expects: "the cheatsheet is in the way, get it out of my // way; I'll start navigating on the next keypress". useRowKeyboard({ enabled: !helpOpen && items.length > 0, onNext: moveNext, onPrev: movePrev, onClose: () => setHelpOpen(false), onToggleHelp: () => setHelpOpen((v) => !v), }); return ( <> setHelpOpen(false)} />
999 ACKs

999 Implementation Acknowledgments

999 ACK transaction sets — generated automatically in response to 837P ingests, or parsed from inbound 999 uploads.

{isError ? ( refetch()} /> ) : null}
{isLoading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : items.length === 0 ? ( ) : ( ID Source Batch Accepted Rejected Received ACK Code Parsed {items.map((a, idx) => { const isSelected = selectedIndex === idx; return ( {a.id} {a.sourceBatchId} {a.acceptedCount} {a.rejectedCount} {a.receivedCount} {a.parsedAt ? fmt.dateShort(a.parsedAt) : "—"} ); })}
)}
); }