import { useMemo } from "react"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { ApiError } from "@/lib/api"; import { useRemitDetail } from "@/hooks/useRemitDetail"; import { useDrawerKeyboard } from "@/hooks/useDrawerKeyboard"; import { RemitDrawerHeader } from "./RemitDrawerHeader"; import { FinancialSummaryCard } from "./FinancialSummaryCard"; import { PartiesGrid } from "./PartiesGrid"; import { ClaimPaymentsTable } from "./ClaimPaymentsTable"; import { CasAdjustmentsPanel } from "./CasAdjustmentsPanel"; import { RemitDrawerSkeleton } from "./RemitDrawerSkeleton"; import { RemitDrawerError } from "./RemitDrawerError"; type RemitDrawerProps = { /** * Currently-open remit id, or `null` when the drawer is closed. * When `null`, the drawer renders nothing (no DOM) and the keyboard * listener is disabled — matches the `ClaimDrawer` contract so the * two drawers behave identically. */ remitId: string | null; /** * Full ordered list of remit ids in the current view. Used to derive * j/k navigation targets without an extra round-trip to the server. * The list must contain the current `remitId` for j/k to do * anything; an unknown id is treated defensively as "navigation * disabled" rather than throwing. */ remits: { id: string }[]; /** Fired when the user dismisses the drawer (X button, header close, or Escape). */ onClose: () => void; /** Fired when the user navigates via j/k — receives the new remit id. */ onNavigate: (newId: string) => void; /** Fired when the user presses `?` to toggle the keyboard-help overlay. */ onToggleHelp: () => void; }; /** * Root remittance-detail drawer (RemitDrawer). * * Twin of `ClaimDrawer` — same dialog shell, same keyboard contract, * same skeleton/error/loaded tri-state. Composes: * * 1. **Data** — `useRemitDetail(remitId)` returns * `{ data, isLoading, isError, error, refetch }`. The hook * short-circuits when `remitId === null` so a closed drawer is * free (no fetch, no cache slot). * * 2. **Keyboard** — `useDrawerKeyboard` wires j/k/ArrowDown/ArrowUp/ * Escape/`?` on `window`. Listener is only attached when the * drawer is open (`enabled: remitId !== null`). * * 3. **Layout** — right-anchored side panel via the project's * `Dialog` primitive, with sections composed from the leaf * components (header + financial summary + parties + claim * payments + CAS adjustments). Each section owns its own visual * language so the layout stays composable. * * j/k navigation wraps around at both ends. `useMemo` keeps the * navigation callbacks stable across renders so the keyboard effect * doesn't re-subscribe on every parent update. * * Error branching mirrors the claim drawer: * - `ApiError(404)` → "not_found" (no retry, the remit is gone) * - anything else → "network" (retry available) */ export function RemitDrawer({ remitId, remits, onClose, onNavigate, onToggleHelp, }: RemitDrawerProps) { const { data, isLoading, isError, error, refetch } = useRemitDetail(remitId); // j/k navigation: derive next/prev IDs based on the current remit's // index in the parent list. Wrap around at both ends. If the current // remit isn't in the list (stale deep link, etc.) the callbacks are // no-ops — defensive against bad input from the parent. const { onNext, onPrev } = useMemo(() => { if (remitId === null) { return { onNext: () => {}, onPrev: () => {} }; } const idx = remits.findIndex((r) => r.id === remitId); if (idx === -1 || remits.length === 0) { return { onNext: () => {}, onPrev: () => {} }; } return { onNext: () => { const nextId = remits[(idx + 1) % remits.length].id; onNavigate(nextId); }, onPrev: () => { // Add `remits.length` before the modulo so the negative index // (first → wrap to last) wraps correctly without a separate // branch. const prevId = remits[(idx - 1 + remits.length) % remits.length].id; onNavigate(prevId); }, }; }, [remitId, remits, onNavigate]); useDrawerKeyboard({ enabled: remitId !== null, onNext, onPrev, onClose, onToggleHelp, }); // Closed drawer: render nothing. The Dialog's `open` prop must also // reflect this (so Radix tears down its portal / focus trap), and the // early-return keeps a closed drawer free of any DOM at all. if (remitId === null) return null; const errorKind: "not_found" | "network" | null = isError ? error instanceof ApiError && error.status === 404 ? "not_found" : "network" : null; return ( { if (!open) onClose(); }} > {isLoading ? ( ) : errorKind ? ( { void refetch(); }} onClose={onClose} /> ) : data ? (
{data.adjustments && data.adjustments.length > 0 ? ( ) : null}
) : null}
); }