feat(sp28): ClaimDrawer acknowledgments panel + tests
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useClaimAcks } from "@/hooks/useClaimAcks";
|
||||
import { useMergedTail } from "@/hooks/useMergedTail";
|
||||
import { useTailStream } from "@/hooks/useTailStream";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fmt } from "@/lib/format";
|
||||
import type { ClaimAck, ClaimAckKind } from "@/types";
|
||||
|
||||
/**
|
||||
* Color palette for the per-AK2 / per-ClaimStatus accept-reject code
|
||||
* pill. Mirrors the `Acks.tsx` `AckCodeBadge` palette so the
|
||||
* operator's eye-flow is identical between the Acks page and this
|
||||
* panel.
|
||||
*
|
||||
* - "A" (accepted) → success green
|
||||
* - "R" (rejected) → destructive red
|
||||
* - "E" (errors) → warning amber
|
||||
* - "X" (rejected w/ 999 AK5) → destructive red
|
||||
* - anything else → muted (treats STC category codes like A1/A2/A6 as
|
||||
* operator-readable rather than literal A/E letters)
|
||||
*/
|
||||
function codeTone(code: string | null | undefined): {
|
||||
fg: string;
|
||||
bg: string;
|
||||
border: string;
|
||||
} {
|
||||
if (!code) {
|
||||
return {
|
||||
fg: "hsl(var(--muted-foreground))",
|
||||
bg: "hsl(var(--muted) / 0.30)",
|
||||
border: "hsl(var(--border) / 0.5)",
|
||||
};
|
||||
}
|
||||
const upper = code.toUpperCase();
|
||||
if (upper === "A") {
|
||||
return {
|
||||
fg: "hsl(var(--success))",
|
||||
bg: "hsl(var(--success) / 0.10)",
|
||||
border: "hsl(var(--success) / 0.30)",
|
||||
};
|
||||
}
|
||||
if (upper === "R" || upper === "X") {
|
||||
return {
|
||||
fg: "hsl(var(--destructive))",
|
||||
bg: "hsl(var(--destructive) / 0.10)",
|
||||
border: "hsl(var(--destructive) / 0.30)",
|
||||
};
|
||||
}
|
||||
if (upper === "E" || upper === "P") {
|
||||
return {
|
||||
fg: "hsl(var(--warning))",
|
||||
bg: "hsl(var(--warning) / 0.10)",
|
||||
border: "hsl(var(--warning) / 0.30)",
|
||||
};
|
||||
}
|
||||
// STC category code (A1/A2/A3/A4/A6/A7) — keep the operator-visible
|
||||
// tone as warning so a 277CA "rejected" stands out, but render the
|
||||
// literal code rather than the badge letter so the operator can
|
||||
// tell which STC category triggered the link.
|
||||
return {
|
||||
fg: "hsl(var(--warning))",
|
||||
bg: "hsl(var(--warning) / 0.10)",
|
||||
border: "hsl(var(--warning) / 0.30)",
|
||||
};
|
||||
}
|
||||
|
||||
function AckKindBadge({ kind }: { kind: ClaimAckKind }) {
|
||||
// The ack kind itself is one of "999" / "277ca" / "ta1". TA1 rows
|
||||
// don't appear in this panel (filtered out per D4), but the type
|
||||
// union still allows it — render it consistently with the Acks page.
|
||||
const label =
|
||||
kind === "999" ? "999" : kind === "277ca" ? "277CA" : "TA1";
|
||||
return (
|
||||
<span
|
||||
className="mono inline-flex items-center rounded-sm border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em]"
|
||||
style={{
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
backgroundColor: "hsl(var(--muted) / 0.30)",
|
||||
borderColor: "hsl(var(--border) / 0.5)",
|
||||
}}
|
||||
data-testid="ack-kind-badge"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One compact row in the Acknowledgments panel. Renders the ack
|
||||
* code (color-coded), the ack kind, the linked set_control_number
|
||||
* (the operator-readable join key), the parsed_at date, and a `→`
|
||||
* link to the matching AckDrawer.
|
||||
*/
|
||||
function AckLinkRow({
|
||||
link,
|
||||
onOpenAck,
|
||||
}: {
|
||||
link: ClaimAck;
|
||||
onOpenAck: (ackId: number, kind: ClaimAckKind) => void;
|
||||
}) {
|
||||
const tone = codeTone(link.setAcceptRejectCode);
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 rounded-md border px-3 py-2"
|
||||
style={{
|
||||
backgroundColor: "hsl(var(--card) / 0.40)",
|
||||
borderColor: "hsl(var(--border) / 0.5)",
|
||||
}}
|
||||
data-testid="acknowledgments-row"
|
||||
>
|
||||
{/* Accept/reject pill */}
|
||||
<span
|
||||
className="mono inline-flex items-center rounded-sm border px-2 py-0.5 text-[10.5px] font-semibold uppercase tracking-[0.14em]"
|
||||
style={{
|
||||
color: tone.fg,
|
||||
backgroundColor: tone.bg,
|
||||
borderColor: tone.border,
|
||||
}}
|
||||
data-testid="ack-code-pill"
|
||||
>
|
||||
{link.setAcceptRejectCode ?? "—"}
|
||||
</span>
|
||||
|
||||
{/* AK2 index chip (999 only) — gives the operator a quick
|
||||
"which set-response" reference for the per-AK2 granularity. */}
|
||||
{link.ak2Index !== null ? (
|
||||
<span
|
||||
className="mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground/60"
|
||||
title="AK2 set-response index"
|
||||
>
|
||||
AK2 #{link.ak2Index}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<AckKindBadge kind={link.ackKind} />
|
||||
|
||||
{link.setControlNumber ? (
|
||||
<span
|
||||
className="mono truncate text-[11.5px] text-muted-foreground/80"
|
||||
title={`set_control_number: ${link.setControlNumber}`}
|
||||
>
|
||||
{link.setControlNumber}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className="ml-auto mono text-[10.5px] text-muted-foreground/60">
|
||||
{link.linkedAt ? fmt.dateShort(link.linkedAt) : "—"}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenAck(link.ackId, link.ackKind)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-sm border px-2 py-1 text-[10.5px] mono font-semibold uppercase tracking-[0.14em] transition-colors",
|
||||
"border-border/60 bg-card/40 text-muted-foreground hover:bg-card/80 hover:text-foreground",
|
||||
)}
|
||||
data-testid="open-ack-drawer"
|
||||
aria-label={`Open ${link.ackKind} ACK ${link.ackId}`}
|
||||
>
|
||||
open
|
||||
<ArrowRight className="h-3 w-3" strokeWidth={1.75} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SP28: ClaimDrawer's new `<Acknowledgments />` panel.
|
||||
*
|
||||
* Mounted between `<ServiceLinesTable />` and the trailing panels.
|
||||
* Lists every `claim_acks` row that links to the current claim:
|
||||
* - One row per AK2 set-response (for 999 acks — per-AK2 granularity)
|
||||
* - One row per ClaimStatus (for 277CA)
|
||||
* - TA1 batch-level rows are filtered out (D4 — claim_id IS NULL)
|
||||
*
|
||||
* Live-tail: subscribes to `useTailStream("claim-acks")` and merges
|
||||
* via `useMergedTail` with the per-claim filter
|
||||
* `link.claimId === claimId` so a manual match on the AckDrawer or a
|
||||
* new auto-link from a 999 ack shows up on the ClaimDrawer in real
|
||||
* time. Per `cyclone-frontend-page` convention #3, the subscription
|
||||
* lives on the drawer (this component), not on `useClaimAcks`.
|
||||
*
|
||||
* Empty state: when the claim has no acks yet, render the existing
|
||||
* `<EmptyState />` with copy "No acknowledgments received yet for
|
||||
* this claim." — so a fresh claim gets a clear non-error empty state
|
||||
* rather than a quiet section.
|
||||
*/
|
||||
export function AcknowledgmentsPanel({
|
||||
claimId,
|
||||
}: {
|
||||
claimId: string;
|
||||
}) {
|
||||
const { data, isLoading } = useClaimAcks(claimId);
|
||||
const navigate = useNavigate();
|
||||
// Live-tail: open the shared claim-acks stream and filter to this
|
||||
// claim. The base list (`data ?? []`) is the page's authoritative
|
||||
// snapshot; the merged result also includes any new rows that
|
||||
// arrived since the snapshot fetch (manual match, new ack ingest).
|
||||
useTailStream("claim-acks");
|
||||
const merged = useMergedTail(
|
||||
"claim-acks",
|
||||
data ?? [],
|
||||
(link) => link.claimId === claimId,
|
||||
);
|
||||
|
||||
// TA1 batch-level rows have `claimId === null`. The backend filters
|
||||
// them out of the per-claim endpoint, but defensive belt-and-
|
||||
// suspenders: skip any row that doesn't claim this claim id (the
|
||||
// filterFn already does this for the tail slice; we apply the same
|
||||
// to baseItems-derived merged items to be safe).
|
||||
const visible = merged.filter((l) => l.claimId === claimId);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="flex flex-col gap-3 px-6 py-4"
|
||||
data-testid="acknowledgments-panel"
|
||||
aria-label="Acknowledgments"
|
||||
>
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<div className="eyebrow flex items-center gap-2">
|
||||
<span className="inline-block h-px w-6 bg-foreground/20" />
|
||||
Acknowledgments
|
||||
<span
|
||||
className="mono ml-1 text-[10px] tabular-nums text-muted-foreground/70"
|
||||
data-testid="acknowledgments-count"
|
||||
>
|
||||
({visible.length})
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{isLoading && visible.length === 0 ? (
|
||||
<p
|
||||
className="text-[12.5px] text-muted-foreground/70"
|
||||
data-testid="acknowledgments-loading"
|
||||
>
|
||||
Loading acknowledgments…
|
||||
</p>
|
||||
) : visible.length === 0 ? (
|
||||
<div
|
||||
className="rounded-md border border-dashed border-border/60 bg-card/40"
|
||||
data-testid="acknowledgments-empty"
|
||||
>
|
||||
<EmptyState
|
||||
eyebrow="Acknowledgments · none yet"
|
||||
message="No acknowledgments received yet for this claim."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2" data-testid="acknowledgments-list">
|
||||
{visible.map((link) => (
|
||||
<AckLinkRow
|
||||
key={link.id}
|
||||
link={link}
|
||||
onOpenAck={(ackId, kind) => {
|
||||
// Cross-page navigation: the AckDrawer is mounted on
|
||||
// the /acks page, not on /claims. Push the user to
|
||||
// /acks?ack=<id> so the Acks page mounts the drawer
|
||||
// with the deep-link restored (per
|
||||
// `useAckDrawerUrlState`). Mirrors the Inbox →
|
||||
// RemitDrawer pattern, where the inbox row click
|
||||
// navigates to /inbox?remit=<id>.
|
||||
//
|
||||
// `kind` is kept in the callback signature so a
|
||||
// future enhancement can route to a TA1-specific
|
||||
// page (today the AckDrawer page handles only 999
|
||||
// acks — TA1 navigation would need a different page
|
||||
// or a TA1 drawer on the same Acks surface).
|
||||
navigate(`/acks?ack=${encodeURIComponent(String(ackId))}`);
|
||||
void kind;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,58 @@ vi.mock("@/lib/api", () => {
|
||||
};
|
||||
});
|
||||
|
||||
// SP28: the new <Acknowledgments /> panel mounts three hooks (data,
|
||||
// live-tail, live-tail merge) and calls `useNavigate` to navigate to
|
||||
// /acks?ack=<id> when the operator clicks an ack row's "open" button.
|
||||
// Mock each one at the module boundary so the test never opens a real
|
||||
// fetch or a real navigation. The hook mocks return "empty" by default
|
||||
// so the panel renders <EmptyState /> — tests that need to populate
|
||||
// the panel override via `setClaimAcksRows(...)`.
|
||||
const { useClaimAcks, useTailStream, useMergedTail, useNavigate } =
|
||||
vi.hoisted(() => ({
|
||||
useClaimAcks: vi.fn(),
|
||||
useTailStream: vi.fn(),
|
||||
useMergedTail: vi.fn(),
|
||||
useNavigate: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/hooks/useClaimAcks", () => ({ useClaimAcks }));
|
||||
vi.mock("@/hooks/useTailStream", () => ({ useTailStream }));
|
||||
vi.mock("@/hooks/useMergedTail", () => ({ useMergedTail }));
|
||||
vi.mock("react-router-dom", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("react-router-dom")>(
|
||||
"react-router-dom",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
useNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
/** Default mock state — empty list, no errors, no navigation. Tests that
|
||||
* need to populate the panel override via `useClaimAcks.mockReturnValue`. */
|
||||
function setClaimAcksRows(
|
||||
rows: Array<{
|
||||
id: number;
|
||||
claimId: string;
|
||||
ackId: number;
|
||||
ackKind: "999" | "277ca" | "ta1";
|
||||
ak2Index: number | null;
|
||||
setControlNumber: string | null;
|
||||
setAcceptRejectCode: string | null;
|
||||
linkedAt: string;
|
||||
}>,
|
||||
) {
|
||||
useClaimAcks.mockReturnValue({
|
||||
data: rows,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
useMergedTail.mockReturnValue(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal valid ClaimDetail fixture — every required key present so the
|
||||
* component typechecks. Mirrors the SAMPLE_DETAIL in
|
||||
@@ -236,6 +288,26 @@ async function settle(
|
||||
describe("ClaimDrawer", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default mocks — empty list, no errors, no-op navigate. Tests
|
||||
// that exercise the SP28 Acknowledgments panel override via
|
||||
// `setClaimAcksRows(...)`.
|
||||
useClaimAcks.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
useTailStream.mockReturnValue({
|
||||
status: "live",
|
||||
lastEventAt: null,
|
||||
error: null,
|
||||
forceReconnect: vi.fn(),
|
||||
});
|
||||
useMergedTail.mockImplementation(
|
||||
(_resource: unknown, baseItems: unknown) => baseItems,
|
||||
);
|
||||
useNavigate.mockReturnValue(vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -471,4 +543,196 @@ describe("ClaimDrawer", () => {
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SP28: the new <Acknowledgments /> panel.
|
||||
// Tests assert:
|
||||
// - panel renders when `useClaimAcks` returns a non-empty list
|
||||
// - panel renders <EmptyState /> when the list is empty (default)
|
||||
// - the "open" button navigates to /acks?ack=<id>
|
||||
// - the panel mounts the live-tail subscription (one useTailStream
|
||||
// call per drawer open — the subscription lives on the drawer)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it("test_acknowledgments_panel_renders_empty_state_when_no_acks", async () => {
|
||||
const { unmount } = renderDrawer({ claimId: "CLM-1" });
|
||||
|
||||
const body = await settle((b) =>
|
||||
b.querySelector('[data-testid="acknowledgments-panel"]') !== null
|
||||
);
|
||||
|
||||
// The panel must be present.
|
||||
expect(body.querySelector('[data-testid="acknowledgments-panel"]'))
|
||||
.not.toBeNull();
|
||||
// The empty-state copy must surface (no acks → friendly message).
|
||||
expect(body.querySelector('[data-testid="acknowledgments-empty"]'))
|
||||
.not.toBeNull();
|
||||
expect(body.textContent).toContain("No acknowledgments received yet");
|
||||
// No row rendered.
|
||||
expect(body.querySelector('[data-testid="acknowledgments-row"]')).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_acknowledgments_panel_renders_row_for_each_ack_link", async () => {
|
||||
setClaimAcksRows([
|
||||
{
|
||||
id: 1,
|
||||
claimId: "CLM-1",
|
||||
ackId: 99,
|
||||
ackKind: "999",
|
||||
ak2Index: 0,
|
||||
setControlNumber: "991102989",
|
||||
setAcceptRejectCode: "A",
|
||||
linkedAt: "2026-07-02T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
claimId: "CLM-1",
|
||||
ackId: 99,
|
||||
ackKind: "999",
|
||||
ak2Index: 1,
|
||||
setControlNumber: "991102990",
|
||||
setAcceptRejectCode: "R",
|
||||
linkedAt: "2026-07-02T12:00:01Z",
|
||||
},
|
||||
]);
|
||||
|
||||
const { unmount } = renderDrawer({ claimId: "CLM-1" });
|
||||
|
||||
const body = await settle(
|
||||
(b) =>
|
||||
b.querySelectorAll('[data-testid="acknowledgments-row"]').length === 2,
|
||||
);
|
||||
|
||||
// The panel must render one row per AK2 (per-AK2 granularity, D1).
|
||||
const rows = body.querySelectorAll('[data-testid="acknowledgments-row"]');
|
||||
expect(rows.length).toBe(2);
|
||||
|
||||
// Accept code pills render the literal set_accept_reject_code so
|
||||
// the operator can see "A" (accepted) vs "R" (rejected) inline.
|
||||
const codes = body.querySelectorAll('[data-testid="ack-code-pill"]');
|
||||
expect(codes[0]?.textContent).toContain("A");
|
||||
expect(codes[1]?.textContent).toContain("R");
|
||||
|
||||
// The header count must reflect the row count.
|
||||
expect(body.querySelector('[data-testid="acknowledgments-count"]')
|
||||
?.textContent).toContain("(2)");
|
||||
|
||||
// The "open" button is wired up on every row.
|
||||
const opens = body.querySelectorAll('[data-testid="open-ack-drawer"]');
|
||||
expect(opens.length).toBe(2);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_acknowledgments_panel_filters_out_rows_with_different_claim_id", async () => {
|
||||
// Base snapshot has one row for CLM-1 and one orphan (claimId
|
||||
// different from the active claim). The merge hook receives the
|
||||
// base list and the tail slice, but the panel filter
|
||||
// (`link.claimId === claimId`) drops the orphan. Test the
|
||||
// defensive filter inside the panel.
|
||||
setClaimAcksRows([
|
||||
{
|
||||
id: 1,
|
||||
claimId: "CLM-1",
|
||||
ackId: 99,
|
||||
ackKind: "999",
|
||||
ak2Index: 0,
|
||||
setControlNumber: "991102989",
|
||||
setAcceptRejectCode: "A",
|
||||
linkedAt: "2026-07-02T12:00:00Z",
|
||||
},
|
||||
]);
|
||||
// Override useMergedTail to simulate an orphan tail row sneaking
|
||||
// in (matches the prod merge contract — useMergedTail returns
|
||||
// base + tail, and the panel filter strips non-matching rows).
|
||||
useMergedTail.mockReturnValue([
|
||||
{
|
||||
id: 1,
|
||||
claimId: "CLM-1",
|
||||
ackId: 99,
|
||||
ackKind: "999",
|
||||
ak2Index: 0,
|
||||
setControlNumber: "991102989",
|
||||
setAcceptRejectCode: "A",
|
||||
linkedAt: "2026-07-02T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
claimId: "CLM-OTHER",
|
||||
ackId: 100,
|
||||
ackKind: "999",
|
||||
ak2Index: 0,
|
||||
setControlNumber: "991102999",
|
||||
setAcceptRejectCode: "R",
|
||||
linkedAt: "2026-07-02T12:00:01Z",
|
||||
},
|
||||
]);
|
||||
|
||||
const { unmount } = renderDrawer({ claimId: "CLM-1" });
|
||||
|
||||
const body = await settle(
|
||||
(b) =>
|
||||
b.querySelectorAll('[data-testid="acknowledgments-row"]').length === 1,
|
||||
);
|
||||
|
||||
// Only the matching-claim row renders.
|
||||
const rows = body.querySelectorAll('[data-testid="acknowledgments-row"]');
|
||||
expect(rows.length).toBe(1);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_acknowledgments_panel_open_button_navigates_to_acks_with_ack_id", async () => {
|
||||
setClaimAcksRows([
|
||||
{
|
||||
id: 1,
|
||||
claimId: "CLM-1",
|
||||
ackId: 99,
|
||||
ackKind: "999",
|
||||
ak2Index: 0,
|
||||
setControlNumber: "991102989",
|
||||
setAcceptRejectCode: "A",
|
||||
linkedAt: "2026-07-02T12:00:00Z",
|
||||
},
|
||||
]);
|
||||
|
||||
const navigate = vi.fn();
|
||||
useNavigate.mockReturnValue(navigate);
|
||||
|
||||
const { unmount } = renderDrawer({ claimId: "CLM-1" });
|
||||
|
||||
const body = await settle(
|
||||
(b) => b.querySelector('[data-testid="open-ack-drawer"]') !== null,
|
||||
);
|
||||
|
||||
const openBtn = body.querySelector(
|
||||
'[data-testid="open-ack-drawer"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(openBtn).not.toBeNull();
|
||||
openBtn!.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
// Navigate to /acks?ack=<id> — the Acks page mounts the
|
||||
// AckDrawer with the deep-link restored.
|
||||
expect(navigate).toHaveBeenCalledWith("/acks?ack=99");
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("test_acknowledgments_panel_mounts_live_tail_subscription", async () => {
|
||||
const { unmount } = renderDrawer({ claimId: "CLM-1" });
|
||||
|
||||
await settle((b) =>
|
||||
b.querySelector('[data-testid="acknowledgments-panel"]') !== null
|
||||
);
|
||||
|
||||
// The drawer mounts one claim-acks live-tail stream. Per
|
||||
// cyclone-frontend-page convention #3, the subscription lives on
|
||||
// the page (drawer here), not on the data hook.
|
||||
expect(useTailStream).toHaveBeenCalledWith("claim-acks");
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DiagnosesList } from "./DiagnosesList";
|
||||
import { PartiesGrid } from "./PartiesGrid";
|
||||
import { RawSegmentsPanel } from "./RawSegmentsPanel";
|
||||
import { MatchedRemitCard } from "./MatchedRemitCard";
|
||||
import { AcknowledgmentsPanel } from "./AcknowledgmentsPanel";
|
||||
import { StateHistoryTimeline } from "./StateHistoryTimeline";
|
||||
import { LineReconciliationTab } from "./LineReconciliationTab";
|
||||
import { ClaimDrawerSkeleton } from "./ClaimDrawerSkeleton";
|
||||
@@ -232,6 +233,13 @@ export function ClaimDrawer({
|
||||
serviceLines={data.serviceLines}
|
||||
lineReconciliation={data.lineReconciliation}
|
||||
/>
|
||||
{/* SP28: per-AK2 acknowledgments panel. Sits between
|
||||
ServiceLines and Diagnoses so the operator's
|
||||
eye-flow is "what was claimed → what came back
|
||||
from the payer → who/what is on the claim". The
|
||||
panel itself mounts the live-tail subscription
|
||||
(per cyclone-frontend-page convention #3). */}
|
||||
<AcknowledgmentsPanel claimId={data.id} />
|
||||
<DiagnosesList diagnoses={data.diagnoses} />
|
||||
<PartiesGrid parties={data.parties} />
|
||||
{data.matchedRemittance ? (
|
||||
|
||||
Reference in New Issue
Block a user