diff --git a/src/pages/Reconciliation.test.tsx b/src/pages/Reconciliation.test.tsx index 39b88e4..0c6e12a 100644 --- a/src/pages/Reconciliation.test.tsx +++ b/src/pages/Reconciliation.test.tsx @@ -19,6 +19,7 @@ vi.mock("@/lib/api", () => ({ listUnmatched: vi.fn(), matchRemit: vi.fn(), unmatchClaim: vi.fn(), + getRemittance: vi.fn(), }, ApiError: class ApiError extends Error { constructor(public status: number, message: string) { @@ -86,6 +87,73 @@ async function waitForText( describe("ReconciliationPage", () => { beforeEach(() => { vi.clearAllMocks(); + // Default for the per-remit detail fetch — the drawer fetches + // this whenever `?remit=` is in the URL. Return a never-resolving + // promise so the drawer stays in the loading state; the smoke + // test only asserts the drawer mounts. + ( + api.getRemittance as unknown as ReturnType + ).mockReturnValue(new Promise(() => {})); + }); + + it("SP21 Task 4.6: deep-link ?remit=ID opens the RemitDrawer on mount", async () => { + // Non-empty unmatched payload so the page renders the two-column + // matching surface (the "Pair them." branch). The pre-existing + // empty-state branch has a flaky happy-dom/race that's unrelated + // to Phase 4 — using non-empty data sidesteps that bug and still + // proves the deep-link → drawer mount works. We pre-set + // `window.location` (which `useRemitDrawerUrlState` reads on + // mount) so `?remit=REM-7` resolves to a truthy `remitId`. + ( + api.listUnmatched as unknown as ReturnType + ).mockResolvedValue({ + claims: [ + { + id: "CLM-1", + patientName: "Patient A", + billedAmount: 100, + providerNpi: "1234567890", + serviceDate: "2026-06-01", + payerId: "P1", + state: "submitted", + }, + ], + remittances: [ + { + id: "REM-7", + payerClaimControlNumber: "PCN-A", + status: "received", + paidAmount: 100, + adjustmentAmount: 0, + receivedDate: "2026-06-01", + isReversal: false, + totalCharge: 100, + serviceDate: "2026-06-01", + batchId: "b1", + }, + ], + }); + (window as unknown as { happyDOM: { setURL: (u: string) => void } }) + .happyDOM.setURL("http://localhost/reconciliation?remit=REM-7"); + + const { unmount } = renderIntoContainer( + React.createElement(ReconciliationPage), + ); + + // Wait for the loaded two-column view (the "Pair them." headline + // is the clearest signal that listUnmatched has resolved and the + // page is past the loading + empty branches), then assert the + // drawer is mounted. + await waitForText("Pair them."); + expect( + document.body.querySelector('[data-testid="remit-drawer"]'), + ).not.toBeNull(); + + unmount(); + + // Reset URL so the next test sees a clean /reconciliation URL. + (window as unknown as { happyDOM: { setURL: (u: string) => void } }) + .happyDOM.setURL("http://localhost/reconciliation"); }); it("renders both columns with unmatched rows when api returns data", async () => { diff --git a/src/pages/Reconciliation.tsx b/src/pages/Reconciliation.tsx index 6139d55..a47d062 100644 --- a/src/pages/Reconciliation.tsx +++ b/src/pages/Reconciliation.tsx @@ -11,6 +11,9 @@ import { ApiError } from "@/lib/api"; import { Skeleton } from "@/components/ui/skeleton"; import { ErrorState } from "@/components/ui/error-state"; import { Button } from "@/components/ui/button"; +import { RemitDrawer } from "@/components/RemitDrawer"; +import { DrillableCell } from "@/components/drill/DrillableCell"; +import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState"; import { cn } from "@/lib/utils"; /** @@ -26,6 +29,11 @@ import { cn } from "@/lib/utils"; */ export function ReconciliationPage() { const { unmatched, match } = useReconciliation(); + // SP21 Phase 4 Task 4.6: drill into the RemitDrawer from the + // remits column. The hook reads `?remit=` from the URL so deep + // links land with the drawer open. Selection state stays local + // — the drill is a separate gesture from the match selection. + const { remitId, open, close } = useRemitDrawerUrlState(); const [selectedClaim, setSelectedClaim] = useState(null); const [selectedRemit, setSelectedRemit] = useState(null); @@ -735,11 +743,18 @@ export function ReconciliationPage() { {remittances.map((r) => { const active = selectedRemit === r.id; return ( - + ); })} @@ -881,6 +910,22 @@ export function ReconciliationPage() { + + {/* SP21 Phase 4 Task 4.6: RemitDrawer mount. The remits column + drills into the parent remit via the PCN text (DrillableCell + + open). The drawer portals into document.body, so the + surrounding paper plane stays put while the drawer is open. + `remits` is empty (we don't keep a list of all remits on + this page), so j/k is a no-op while the drawer is open. */} + { + // Reconciliation has no cheatsheet; `?` is a no-op here. + }} + /> ); }