import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { GitCompareArrows, CircleDashed, AlertCircle } from "lucide-react"; import { ApiError } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { EmptyState } from "@/components/ui/empty-state"; import { ErrorState } from "@/components/ui/error-state"; import { BatchDiffView, BatchDiffViewSkeleton } from "@/components/BatchDiffView"; import { useBatches } from "@/hooks/useBatches"; import { useBatchDiff } from "@/hooks/useBatchDiff"; import type { BatchSummary as ApiBatchSummary } from "@/lib/api"; import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Inline URL state — uses react-router's useSearchParams so the page // participates in the standard routing lifecycle (MemoryRouter in // tests, BrowserRouter in production). `replace: true` keeps the // picker tweaks from polluting the back-button stack. // --------------------------------------------------------------------------- function readIdsFromParams(params: URLSearchParams): { a: string | null; b: string | null; } { const a = params.get("a"); const b = params.get("b"); return { a: a && a.length > 0 ? a : null, b: b && b.length > 0 ? b : null, }; } // --------------------------------------------------------------------------- // Batch picker — one Select dropdown for each side. Keeps the same // kind-colored badge inline so the operator can spot which batch is // which at a glance. // --------------------------------------------------------------------------- function KindBadge({ kind }: { kind: ApiBatchSummary["kind"] }) { const cls = kind === "837p" ? "text-sky-300 border-sky-400/30 bg-sky-400/10" : "text-amber-300 border-amber-400/30 bg-amber-400/10"; return ( {kind} ); } function BatchPicker({ label, value, onChange, items, excludeId, testid, }: { label: string; value: string | null; onChange: (id: string) => void; items: ApiBatchSummary[]; excludeId: string | null; testid: string; }) { // Filter out the value already chosen on the *other* side so the // operator can't accidentally pick the same batch twice (which // produces a meaningless diff). Disabled rather than hidden so the // selection stays transparent. const visible = useMemo( () => items.filter((it) => it.id !== excludeId), [items, excludeId], ); const selected = items.find((it) => it.id === value); return (
{label}
); } // --------------------------------------------------------------------------- // Not-found branch — surfaces when the backend reports 404 for one of // the picks. Most likely cause: a bookmark referencing a now-deleted // batch, or another agent deleting between the picker load and the // diff fetch. // --------------------------------------------------------------------------- function NotFoundState({ a, b, onReset, }: { a: string; b: string; onReset: () => void; }) { return ( ); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function BatchDiff() { // URL → state via react-router. Keeps MemoryRouter (tests) and // BrowserRouter (production) symmetric: both update the // useSearchParams hook on every navigation. const [searchParams, setSearchParams] = useSearchParams(); const { a, b } = useMemo( () => readIdsFromParams(searchParams), [searchParams], ); const updateIds = useCallback( (next: { a: string | null; b: string | null }) => { const params = new URLSearchParams(searchParams); if (next.a === null) params.delete("a"); else params.set("a", next.a); if (next.b === null) params.delete("b"); else params.set("b", next.b); setSearchParams(params, { replace: true }); }, [searchParams, setSearchParams], ); const setA = useCallback( (id: string) => updateIds({ a: id, b }), [b, updateIds], ); const setB = useCallback( (id: string) => updateIds({ a, b: id }), [a, updateIds], ); const reset = useCallback(() => updateIds({ a: null, b: null }), [updateIds]); const { data: batches, isLoading: loadingBatches, isError: batchesError, error: batchesErrorMsg, refetch: refetchBatches } = useBatches(100); // Only fire the diff once both pickers have a value. The hook also // gates on this internally (defense-in-depth), so a half-picked URL // never hits the backend. const diff = useBatchDiff(a, b); const ready = a !== null && b !== null; // --- error / status derivation ------------------------------------ const errKind: "network" | "not_found" | null = diff.error ? diff.error instanceof ApiError && diff.error.status === 404 ? "not_found" : "network" : null; // --- render ------------------------------------------------------- return (
Diff

Batch diff

Pick two parsed batches — typically a submitted 837P and its corrected follow-up — and see what was added, removed, or changed between them.

{batchesError ? ( refetchBatches()} /> ) : null} {!ready ? (
} eyebrow="Batch diff · awaiting picks" message="Choose one batch for A and one for B to compute the diff." />
) : loadingBatches ? ( ) : errKind === "not_found" ? ( ) : diff.isError ? ( diff.refetch()} /> ) : diff.isLoading || !diff.data ? ( ) : ( )} {/* Action footer — always visible so the operator can clear or force-refresh without scrolling back to the picker. */} {ready ? (
{diff.isFetching ? ( Refreshing… ) : null}
) : null}
); }