From 25cd785e62e9569dedc0cf77baef8e23e761e872 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 20 Jun 2026 08:44:20 -0600 Subject: [PATCH] docs(sp4): per-claim detail drawer design spec + gitignore superpowers dir --- .gitignore | 3 + .../2026-06-20-cyclone-claim-drawer-design.md | 257 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-20-cyclone-claim-drawer-design.md diff --git a/.gitignore b/.gitignore index f8bbcbb..e1108dc 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ claims_output/ # Worktrees (subagent-driven development) .worktrees/ + +# Brainstorm session artifacts (visual companion mockups, events, server state) +.superpowers/ diff --git a/docs/superpowers/specs/2026-06-20-cyclone-claim-drawer-design.md b/docs/superpowers/specs/2026-06-20-cyclone-claim-drawer-design.md new file mode 100644 index 0000000..30dfad4 --- /dev/null +++ b/docs/superpowers/specs/2026-06-20-cyclone-claim-drawer-design.md @@ -0,0 +1,257 @@ +# Sub-project 4 — Per-Claim Detail Drawer: Design Spec + +**Date:** 2026-06-20 +**Status:** Draft (awaiting user review) +**Branch:** `claim-drawer` +**Aesthetic direction:** Modern (geometric sans + bold borders + electric blue accent) + +## 1. Scope + +A right-side slide-over drawer that opens when the operator clicks any claim row on `/claims`. The drawer shows the full context for one claim in a self-contained panel: header, validation issues, service lines, diagnoses, parties (provider/subscriber/payer), raw segments, matched remittance (if any), and state history timeline. + +Sequential navigation: `j`/`k` or `↑`/`↓` steps between claims with the drawer staying open. URL syncs to `?claim={id}` so the drawer state is shareable and bookmarkable. + +## 2. Out of scope (deferred to future sub-projects) + +- **Resubmit / edit actions** — needs backend write endpoints; deferred to a later SP4 phase. +- **Notes / comments on claims** — separate feature, no DB schema today. +- **Bulk actions** — single-operator tool; no multi-select needed. +- **Auto-advance to next denied claim after closing drawer** — nice-to-have, not v1. +- **Remittance detail drawer** — covered by SP3's `/api/remittances/{id}` and a follow-up if needed. +- **The other 4 SP4 features from the README** — batch diff view, NDJSON streaming UI, advanced filters + saved sets, keyboard cheatsheet for the rest of the app. The cheatsheet entry for THIS drawer ships; the rest of the app's keyboard nav is deferred. + +## 3. Locked decisions + +### 3.1 Component decomposition + +The drawer is composed of 1 root component + 10 leaf components, each with one purpose. This keeps individual files small and tests focused. + +| Component | Responsibility | +|-----------|---------------| +| `ClaimDrawer.tsx` | Root: renders `Sheet`, fetches detail via `useClaimDetail`, handles j/k keyboard nav, manages URL sync | +| `ClaimDrawerHeader.tsx` | Claim ID, state badge, total amount, close button | +| `ValidationPanel.tsx` | Errors + warnings grouped by rule; per-rule counts | +| `ServiceLinesTable.tsx` | All service lines with procedure, modifiers, charge, units, date | +| `DiagnosesList.tsx` | HI segment codes with qualifiers | +| `PartiesGrid.tsx` | 3 cards: Billing Provider, Subscriber, Payer | +| `RawSegmentsPanel.tsx` | Collapsible `
` with full segment list (debug aid) | +| `MatchedRemitCard.tsx` | Conditional; shows matched remittance summary + "View remittance →" link | +| `StateHistoryTimeline.tsx` | Vertical timeline of `ActivityEvent` rows for this claim | +| `ClaimDrawerSkeleton.tsx` | Loading state with shimmering placeholders | +| `ClaimDrawerError.tsx` | 404 / network error / retry | + +All live under `src/components/ClaimDrawer/`. Each is a leaf component with explicit props (no context drilling beyond `claim: ClaimDetail`). + +### 3.2 Backend endpoint: `GET /api/claims/{claim_id}` + +New endpoint returning the full claim detail. Read-only. + +**Response shape:** +```json +{ + "id": "C-12345", + "batchId": "8f4a...", + "state": "submitted", + "stateLabel": "Submitted", + "billedAmount": 100.00, + "patientName": "John Doe", + "providerNpi": "1234567890", + "providerName": "Test Provider", + "payerName": "COHCPF", + "payerId": "SKCO0", + "submissionDate": "2026-06-11T00:00:00Z", + "serviceDateFrom": "2026-06-11", + "serviceDateTo": "2026-06-11", + "parsedAt": "2026-06-11T12:00:00Z", + "diagnoses": [{"code": "Z00", "qualifier": "ABK"}], + "serviceLines": [ + {"lineNumber": 1, "procedureQualifier": "HC", "procedureCode": "99213", "modifiers": [], "charge": 80.00, "units": 1.0, "unitType": "UN", "serviceDate": "2026-06-11"} + ], + "parties": { + "billingProvider": {"name": "...", "npi": "...", "taxId": "...", "address": {...}}, + "subscriber": {"firstName": "...", "lastName": "...", "memberId": "...", "dob": "...", "gender": "..."}, + "payer": {"name": "...", "id": "..."} + }, + "validation": { + "passed": false, + "errors": [{"rule": "R050_diagnosis_present", "severity": "error", "message": "..."}], + "warnings": [] + }, + "rawSegments": [["ISA", ...], ["CLM", ...]], + "matchedRemittance": null, + "stateHistory": [ + {"kind": "claim_submitted", "ts": "2026-06-11T12:00:00Z", "batchId": "8f4a..."}, + {"kind": "manual_match", "ts": "2026-06-12T10:00:00Z", "remittanceId": "R-9876"} + ] +} +``` + +**Implementation:** +- Add `CycloneStore.get_claim_detail(claim_id) -> dict | None` to `backend/src/cyclone/store.py` +- Reads from `claims.raw_json` (already populated by SP1 ingest) → deserializes to `ClaimOutput` → maps to UI shape +- Queries `activity_events WHERE claim_id = ?` ordered by `ts DESC` for `stateHistory` +- If `claims.matched_remittance_id` is set, looks up the remittance and embeds a summary (`{id, totalPaid, status, ackDate}`) +- 404 if claim not found + +### 3.3 Visual design tokens + +```css +:root { + /* Surface */ + --surface: #FAFAF7; /* cream-white drawer background */ + --surface-dim: #6B6B6B22; /* list dim overlay when drawer open */ + + /* Ink */ + --ink-primary: #0A0A0A; + --ink-secondary: #6B6B6B; + --ink-tertiary: #A0A0A0; + + /* Borders (the brutalist accent) */ + --border-heavy: #1A1A1A; /* 2px section dividers */ + --border-default: #E5E5E2; /* 1px hairline borders */ + + /* Accent */ + --accent: #1D4ED8; /* electric blue */ + --accent-hover: #1E40AF; + --focus-ring: #1D4ED8; + + /* Semantic */ + --error: #DC2626; + --error-bg: #FEF2F2; + --success: #15803D; + --success-bg: #F0FDF4; + --warning: #B45309; + --warning-bg: #FFFBEB; +} +``` + +**Type stack:** +- Display / labels / body: **Inter Tight** (400, 500, 600, 700, 800 weights) via Google Fonts or local WOFF2 +- Monospace (IDs, amounts, codes): **JetBrains Mono** (400, 600) — same family as SP3 Industrial direction +- Section labels: 10px uppercase, letter-spacing 0.18em, font-weight 700, color `--ink-secondary` + +**Layout:** +- Drawer width: **560px** on desktop (`≥1024px`), **420px** on tablet (768-1023px), **full-screen** sheet on `<768px` +- Section dividers: 2px solid `--border-heavy` (signature Modern touch) +- Inner padding: 24px sides, 20px vertical per section +- Big numbers: 32px font for the total charge; 24px for service-line charges; 18px for diagnosis codes + +### 3.4 Interaction patterns + +**Open:** +- Click any row on `/claims` (whole row is the click target except the row checkbox if present). +- URL updates to `?claim={id}` via `history.replaceState` (no navigation, no re-fetch of list). +- Drawer slides in from right, 240ms ease-out. List dimmed to 60% opacity via overlay. + +**Sequential navigation:** +- `j` or `↓` → next claim in the current sort order. +- `k` or `↑` → previous claim. +- Wraps: from last claim, `j` goes to first. +- New claim detail fetches in background; previous detail stays visible during fetch (no flash of skeleton). +- URL updates as you navigate (`?claim={newId}`). + +**Close:** +- `ESC` key, X button, click on the dimmed overlay. +- URL strips the `?claim=` param. + +**Loading:** +- Skeleton with shimmering placeholders matching section heights (~8-12 skeleton blocks). +- Fetch expected ~200-400ms on local backend; user perceives ~instant. + +**Errors:** +- 404 (claim ID in URL but no row): "Claim not found" + Close button + a hint to refresh the list. +- Network error: "Couldn't reach the server" + Retry button. +- Both inline in the drawer, not a toast (drawer is the focused surface). + +**Keyboard cheatsheet:** +- `?` toggles a small centered overlay (the drawer is dimmed further). +- Lists: `j` next, `k` prev, `↑`/`↓` same as j/k, `esc` close, `?` toggle this help. +- Dismisses on any other key or click outside. + +### 3.5 URL state contract + +- `/claims` → list, no drawer. +- `/claims?claim=C-12345` → list with drawer open for C-12345. +- Browser back/forward navigates through claim selections; Esc closes and pops the URL state. +- Deep link works: opening `/claims?claim=C-12345` directly loads the list AND opens the drawer for that claim. +- If the claim ID in the URL doesn't exist (was deleted, stale link), the drawer shows the 404 state. + +### 3.6 Frontend file layout + +``` +src/ +├── components/ +│ └── ClaimDrawer/ +│ ├── ClaimDrawer.tsx +│ ├── ClaimDrawerHeader.tsx +│ ├── ValidationPanel.tsx +│ ├── ServiceLinesTable.tsx +│ ├── DiagnosesList.tsx +│ ├── PartiesGrid.tsx +│ ├── RawSegmentsPanel.tsx +│ ├── MatchedRemitCard.tsx +│ ├── StateHistoryTimeline.tsx +│ ├── ClaimDrawerSkeleton.tsx +│ ├── ClaimDrawerError.tsx +│ └── index.ts # barrel export +├── hooks/ +│ ├── useClaimDetail.ts # fetches GET /api/claims/{id} +│ ├── useDrawerKeyboard.ts # j/k/esc handlers +│ └── useDrawerUrlState.ts # ?claim= sync +├── lib/ +│ └── api.ts # +api.getClaimDetail(id), +api.getClaimHistory(id) (optional split) +└── pages/ + ├── Claims.tsx # wire click + drawer render + └── Claims.test.tsx # +new tests +``` + +### 3.7 Testing targets + +**Frontend (Vitest + React Testing Library):** +- `ClaimDrawerHeader.test.tsx` — renders ID, state badge, total; close button calls callback +- `ValidationPanel.test.tsx` — empty state, error-only, warning-only, both; grouping by rule +- `ServiceLinesTable.test.tsx` — empty, single line, multiple lines; procedure code formatting +- `DiagnosesList.test.tsx` — empty, single, multiple; qualifier shown when present +- `PartiesGrid.test.tsx` — all 3 cards; missing fields gracefully omitted +- `RawSegmentsPanel.test.tsx` — collapsed by default; toggle expands +- `MatchedRemitCard.test.tsx` — null state (no "View" button); populated state with link +- `StateHistoryTimeline.test.tsx` — empty, single event, multiple events; kind-color mapping +- `ClaimDrawer.test.tsx` — full lifecycle: open, fetch detail, display sections, j/k nav, esc close, URL sync +- `Claims.test.tsx` — click row opens drawer, deep link opens drawer, esc closes, URL clears + +**Backend (pytest):** +- `test_get_claim_detail_happy_path` — full claim with all relations +- `test_get_claim_detail_not_found_returns_404` +- `test_get_claim_detail_state_history_includes_manual_match` +- `test_get_claim_detail_matched_remittance_summary_inlined` + +**Target counts:** ~8 new frontend tests, ~4 new backend tests. Existing 10 frontend + 409 backend tests must all stay green. + +### 3.8 Smoke test (end-of-SP4) + +1. Start backend (`backend/.venv/bin/python -m cyclone serve`). +2. Start frontend (`npm run dev`). +3. Open `http://localhost:5173/claims`. +4. Click first claim row → drawer opens with all sections populated. +5. Press `k` → drawer updates to previous claim; URL updates. +6. Press `j` repeatedly → wrap behavior works. +7. Press `?` → cheatsheet shows. +8. Press `esc` → drawer closes; URL strips `?claim=`. +9. Open `http://localhost:5173/claims?claim=` directly → drawer opens on page load. +10. Commit empty `smoke: end-to-end SP4 (claim drawer) flow passes`. + +## 4. Risk areas + +1. **Backend payload weight.** A claim with 50 service lines and 50 raw segments is ~50KB JSON. The drawer endpoint loads on every j/k press. Cache the parsed `ClaimOutput` in the React Query / SWR cache so navigation between claims is instant after the first fetch. +2. **ActivityEvent volume.** A claim with 20 manual_match/unmatch events is verbose. Default `stateHistory` to last 50; expose `?limit=N` if the operator wants more. +3. **Keyboard nav collisions.** `j`/`k` collide with browser find-in-page. Acceptable tradeoff for an internal tool — operator can `esc` out of the drawer to use browser search. Documented in the cheatsheet. +4. **Drawer + mobile.** Full-screen sheet on mobile means the list is hidden. Operator on mobile loses context. Acceptable for v1 (operator is on desktop per project context). +5. **URL sync race condition.** User opens drawer, navigates to claim 5, then refreshes. URL has `?claim=5` but cache has claim 1's detail. Page load must honor the URL claim, not the cache. The `useDrawerUrlState` hook reads the URL on mount and triggers the fetch. + +## 5. Migration / rollout + +- Single feature branch `claim-drawer` cut from `main` (parent: post-SP3 merge, `a1dc8a5`). +- Worktree at `.worktrees/claim-drawer/`, following SP1-3 pattern. +- No DB migrations (uses existing tables: `claims`, `batches`, `activity_events`, `remittances`). +- No new dependencies (Radix `Dialog` / `Sheet` from shadcn/ui — already in package.json). +- Merge to main after smoke passes; cleanup worktree + branch.