feat(inbox): candidates + unmatched rows drill to RemitDrawer or ClaimDrawer

This commit is contained in:
Tyler
2026-06-21 17:05:11 -06:00
parent 7427838292
commit 54440da2cd
2 changed files with 156 additions and 6 deletions
+42 -2
View File
@@ -13,10 +13,13 @@
// ---------------------------------------------------------------------------
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "@/lib/utils";
import { Lane, type LaneRow } from "@/components/inbox/Lane";
import { InboxHeader } from "@/components/inbox/InboxHeader";
import { BulkBar } from "@/components/inbox/BulkBar";
import { RemitDrawer } from "@/components/RemitDrawer";
import { useRemitDrawerUrlState } from "@/hooks/useRemitDrawerUrlState";
import { useInboxLanes } from "@/hooks/useInboxLanes";
import {
exportInboxCsvUrl,
@@ -40,6 +43,12 @@ function rowKey(row: LaneRow): string {
export default function Inbox() {
const { lanes, loading, error, refetch } = useInboxLanes();
// SP21 Phase 4 Task 4.4: drill-down from inbox rows. The hook reads
// `?remit=` off `window.location.search` so opening a row from the
// /inbox URL pushes `?remit=ID` onto /inbox itself (it doesn't
// navigate to /remittances). The drawer just opens in-place.
const navigate = useNavigate();
const { remitId, open, close } = useRemitDrawerUrlState();
const [selected, setSelected] = useState<Record<LaneKey, string[]>>({
rejected: [],
payer_rejected: [],
@@ -218,6 +227,22 @@ export default function Inbox() {
className="min-h-screen"
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
>
{/* SP21 Phase 4 Task 4.4: RemitDrawer mounts here so a row click
in the candidates / unmatched lanes drills into the parent
remit. The drawer portals into document.body (Radix Dialog),
so the surrounding dark surface is decorative — the drawer
overlays it when open. The hook reads `?remit=` off the URL,
so deep links restore the open remit on reload. */}
<RemitDrawer
remitId={remitId}
remits={[]}
onClose={close}
onNavigate={open}
onToggleHelp={() => {
// No cheatsheet on the inbox surface — `?` is a no-op
// here, but the prop is required by the drawer's contract.
}}
/>
<InboxHeader needEyesCount={needEyes} doneTodayCount={lanes.done_today.length} />
{/* Fold — a thin amber rule + italic serif annotation that
@@ -290,14 +315,29 @@ export default function Inbox() {
name="CANDIDATES"
accent="amber"
rows={lanes.candidates}
onRowClick={() => {}}
// Candidates are remits waiting for a claim match — drill
// straight into the RemitDrawer for the source remit.
onRowClick={(row) => open(row.id)}
onSelectionChange={(ids) => setLaneSelected("candidates", ids)}
/>
<Lane
name="UNMATCHED"
accent="ink-blue"
rows={lanes.unmatched}
onRowClick={() => {}}
// Unmatched is a mixed bucket (kind === "claim" | "remit" per
// the InboxClaimRow union). Defensive branch — today the
// backend only emits "claim" rows here, but the type allows
// both, and the next data-source change shouldn't require a
// page edit.
onRowClick={(row) => {
if (row.kind === "remit") {
open(row.id);
} else if (row.kind === "claim") {
navigate(
`/claims?claim=${encodeURIComponent(row.id)}`,
);
}
}}
onSelectionChange={(ids) => setLaneSelected("unmatched", ids)}
/>
<Lane