diff --git a/src/components/ClaimDrawer/ClaimDrawer.test.tsx b/src/components/ClaimDrawer/ClaimDrawer.test.tsx
index 05ed931..facf7d7 100644
--- a/src/components/ClaimDrawer/ClaimDrawer.test.tsx
+++ b/src/components/ClaimDrawer/ClaimDrawer.test.tsx
@@ -9,6 +9,7 @@ import { createRoot, type Root } from "react-dom/client";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ClaimDrawer } from "./ClaimDrawer";
+import { DrillStackProvider } from "@/components/drill/DrillStackProvider";
import { api, ApiError } from "@/lib/api";
import type { ClaimDetail } from "@/types";
@@ -162,14 +163,22 @@ function renderDrawer(
React.createElement(
QueryClientProvider,
{ client: qc },
- React.createElement(ClaimDrawer, {
- claimId: props.claimId,
- claims,
- onClose,
- onNavigate,
- onToggleHelp,
- })
- )
+ // SP21 Phase 5 Task 5.8: PartiesGrid (mounted by ClaimDrawer)
+ // calls useDrillStack(). Wrap the drawer in DrillStackProvider
+ // so the hook has a context. (The provider is also mounted at
+ // the App root in production.)
+ React.createElement(
+ DrillStackProvider,
+ null,
+ React.createElement(ClaimDrawer, {
+ claimId: props.claimId,
+ claims,
+ onClose,
+ onNavigate,
+ onToggleHelp,
+ }),
+ ),
+ ),
);
});
diff --git a/src/components/ClaimDrawer/PartiesGrid.test.tsx b/src/components/ClaimDrawer/PartiesGrid.test.tsx
index cc16137..b68ce4d 100644
--- a/src/components/ClaimDrawer/PartiesGrid.test.tsx
+++ b/src/components/ClaimDrawer/PartiesGrid.test.tsx
@@ -6,13 +6,23 @@
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi, beforeEach } from "vitest";
import { PartiesGrid } from "./PartiesGrid";
+import { DrillStackProvider } from "@/components/drill/DrillStackProvider";
import type {
ClaimDetailAddress,
ClaimDetailParties,
} from "@/types";
+// Mock the api module so PayerPeekContent's usePayerSummary call
+// doesn't make a real network request. The spy is set per-test in
+// beforeEach.
+vi.mock("@/hooks/usePayerSummary", () => ({
+ usePayerSummary: vi.fn(),
+}));
+
+import { usePayerSummary } from "@/hooks/usePayerSummary";
+
function renderIntoContainer(element: React.ReactElement): {
container: HTMLDivElement;
unmount: () => void;
@@ -20,8 +30,11 @@ function renderIntoContainer(element: React.ReactElement): {
const container = document.createElement("div");
document.body.appendChild(container);
const root: Root = createRoot(container);
+ // SP21 Phase 5 Task 5.8: PartiesGrid now uses useDrillStack() to
+ // open payer peeks. Wrap every render in a DrillStackProvider so
+ // the hook has a context to read from.
act(() => {
- root.render(element);
+ root.render({element});
});
return {
container,
@@ -279,4 +292,112 @@ describe("PartiesGrid", () => {
unmount();
});
+
+ it("SP21 Task 5.8: payer name is drillable (renders as a button)", () => {
+ // The payer card's name now wraps in a button with the
+ // `drillable` affordance + `data-testid="party-payer-name-drill"`.
+ // The other two cards (billing-provider, subscriber) keep the
+ // name as a plain div.
+ const { container, unmount } = renderIntoContainer(
+
+ );
+
+ const payerNameBtn = container.querySelector(
+ '[data-testid="party-payer-name-drill"]',
+ );
+ expect(payerNameBtn).not.toBeNull();
+ expect(payerNameBtn?.textContent).toContain("Aetna");
+
+ // The billing-provider and subscriber names stay plain divs.
+ expect(
+ container.querySelector(
+ '[data-testid="party-billing-provider-name-drill"]',
+ ),
+ ).toBeNull();
+ expect(
+ container.querySelector(
+ '[data-testid="party-subscriber-name-drill"]',
+ ),
+ ).toBeNull();
+
+ unmount();
+ });
+
+ it("SP21 Task 5.8: clicking the payer name opens the PeekModal", async () => {
+ // Mock the payer summary fetch so PayerPeekContent resolves
+ // without a real network call. The peek modal renders
+ // regardless of fetch state (loading skeleton → content).
+ (usePayerSummary as unknown as ReturnType).mockReturnValue({
+ data: null,
+ isLoading: true,
+ });
+
+ const { container, unmount } = renderIntoContainer(
+
+ );
+
+ const payerNameBtn = container.querySelector(
+ '[data-testid="party-payer-name-drill"]',
+ ) as HTMLButtonElement | null;
+ expect(payerNameBtn).not.toBeNull();
+ await act(async () => {
+ payerNameBtn!.click();
+ await Promise.resolve();
+ });
+
+ // The PeekModal is a Radix Dialog that portals into document.body.
+ // Look for the [role="dialog"] (modal) with our eyebrow "Payer".
+ const dialogs = document.body.querySelectorAll('[role="dialog"]');
+ // One dialog is the modal itself (the peek). PartiesGrid doesn't
+ // open a drawer, so we expect exactly one.
+ expect(dialogs.length).toBeGreaterThanOrEqual(1);
+ const peekDialog = Array.from(dialogs).find((d) =>
+ d.textContent?.includes("Payer"),
+ );
+ expect(peekDialog).not.toBeNull();
+ expect(peekDialog?.textContent).toContain("Aetna");
+
+ unmount();
+ });
+
+ it("SP21 Task 5.8: peek uses the X12 payer id, not the human name", () => {
+ // The PayerPeekContent is given the X12 payer id from
+ // `payer.id`. Verify the click flow passes the right id by
+ // checking usePayerSummary was called with "PAYER01".
+ const mockUsePayerSummary = vi.fn().mockReturnValue({
+ data: null,
+ isLoading: true,
+ });
+ (usePayerSummary as unknown as ReturnType).mockImplementation(
+ mockUsePayerSummary,
+ );
+
+ const { container, unmount } = renderIntoContainer(
+
+ );
+
+ // No peek yet → no usePayerSummary call.
+ expect(mockUsePayerSummary).not.toHaveBeenCalled();
+
+ const payerNameBtn = container.querySelector(
+ '[data-testid="party-payer-name-drill"]',
+ ) as HTMLButtonElement | null;
+ act(() => {
+ payerNameBtn!.click();
+ });
+
+ // After the click, PayerPeekContent mounts and calls
+ // usePayerSummary("PAYER01") — NOT the human name "Aetna".
+ expect(mockUsePayerSummary).toHaveBeenCalledWith("PAYER01");
+
+ unmount();
+ });
+});
+
+beforeEach(() => {
+ // Reset the mock between tests so per-test implementations stick.
+ (usePayerSummary as unknown as ReturnType).mockReturnValue({
+ data: null,
+ isLoading: true,
+ });
});
diff --git a/src/components/ClaimDrawer/PartiesGrid.tsx b/src/components/ClaimDrawer/PartiesGrid.tsx
index 1abc39d..3a9f70c 100644
--- a/src/components/ClaimDrawer/PartiesGrid.tsx
+++ b/src/components/ClaimDrawer/PartiesGrid.tsx
@@ -1,4 +1,7 @@
import type { ClaimDetail, ClaimDetailAddress } from "@/types";
+import { useDrillStack } from "@/components/drill/DrillStackProvider";
+import { PeekModal } from "@/components/drill/PeekModal";
+import { PayerPeekContent } from "@/components/drill/PayerPeekContent";
type PartiesGridProps = {
parties: ClaimDetail["parties"];
@@ -48,12 +51,14 @@ function PartyCard({
name,
identity,
address,
+ onNameClick,
}: {
testId: string;
label: string;
name: string;
identity: React.ReactNode;
address?: AddressLike;
+ onNameClick?: () => void;
}) {
return (
{label}
-
- {name}
-
+ {onNameClick ? (
+
+ ) : (
+
+ {name}
+
+ )}
@@ -88,6 +105,13 @@ function PartyCard({
*/
export function PartiesGrid({ parties }: PartiesGridProps) {
const { billingProvider, subscriber, payer } = parties;
+ // SP21 Phase 5 Task 5.8: payer name opens a PeekModal on top of
+ // the drawer — the peek stack (DrillStackProvider) holds at most
+ // one peek at a time, so opening payer-peek replaces any earlier
+ // peek. The X12 payer id (`payer.id`) is what the peek endpoint
+ // expects, NOT the human name.
+ const { stack, openPeek, closeTop } = useDrillStack();
+ const topPeek = stack[stack.length - 1];
return (
ID {payer.id}
>
}
+ // SP21 Phase 5 Task 5.8: payer name is drillable. Clicking
+ // pushes a payer peek onto the drill stack; the PeekModal
+ // at the bottom of this section renders when the top of
+ // the stack is "payer". The payer id used here is the X12
+ // payer_id (e.g. "SKCO0") — verified in ClaimDetailPayer
+ // type — which is what usePayerSummary expects.
+ onNameClick={() => openPeek({ kind: "payer", payerId: payer.id })}
/>
+
+ {/* SP21 Phase 5 Task 5.8: payer peek. Mounted at the bottom
+ of PartiesGrid so the peek sits on top of the drawer (Radix
+ Dialog portals). Closing the peek pops the stack. Only one
+ peek renders at a time (the stack caps at 1) — when the
+ user opens a payer peek, any earlier peek is replaced. */}
+ {topPeek?.kind === "payer" ? (
+
+
+
+ ) : null}
);
}