docs(sp4): per-claim detail drawer design spec + gitignore superpowers dir
This commit is contained in:
@@ -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 `<details>` 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=<known-id>` 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.
|
||||
Reference in New Issue
Block a user