# Doc Status Updates Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Allow billing staff to update SWO status and PA status directly in Signal's expanded patient row, persisted in Supabase, loaded on every CSV import. **Architecture:** Mirror the existing `confirmed_visits` pattern exactly — a new `doc_status` Supabase table, two new persistence functions (`upsert_doc_status` / `load_doc_statuses_for_org`), one new API endpoint (`PUT /api/doc-status`), and inline clickable status chips in the WorklistTable expanded row for SWO and PA only (PECOS and Diagnosis are informational — no staff update in Signal). **Tech Stack:** FastAPI (Python backend), Supabase (postgres via supabase-py), React/Tailwind (signal-ui), Vite dev server --- ## Status Values (locked — do not change) | Doc type | Allowed values | Good | In-progress | Bad | |---|---|---|---|---| | `swo` | `pending`, `requested`, `on_file` | `on_file` | `requested` | `pending` | | `pa` | `not_required`, `requested`, `approved`, `denied` | `not_required`, `approved` | `requested` | `denied` | Display labels: - `pending` → "Pending" - `requested` → "Requested" - `on_file` → "On File" - `not_required` → "Not Required" - `approved` → "Approved" - `denied` → "Denied" Colors (reuse existing DocItem pattern): - Good → `text-[#1A8040]` - In-progress / requested → `text-[#CB6B20]` - Bad → `text-[#CC2222]` --- ## File Map | File | Action | What changes | |---|---|---| | `python-backend/core/persistence.py` | Modify | Add `upsert_doc_status`, `load_doc_statuses_for_org` | | `python-backend/api/main.py` | Modify | Add `DocStatusRequest` model, `PUT /api/doc-status` endpoint, load doc_statuses in upload response | | `signal-ui/src/lib/api.js` | Modify | Add `updateDocStatus` function | | `signal-ui/src/components/WorklistTable.jsx` | Modify | `DocItem` becomes clickable for SWO and PA; add update handler and local state | No new files required. No Supabase migration file needed — the table is created via Supabase dashboard SQL (instructions in Task 1). --- ## Task 1: Create Supabase `doc_status` table **Files:** None (SQL runs in Supabase dashboard) - [ ] **Step 1: Run this SQL in Supabase SQL editor** (Dashboard → SQL Editor → New query) ```sql create table if not exists doc_status ( id uuid primary key default gen_random_uuid(), org_id uuid not null references organizations(id) on delete cascade, patient_id_hash text not null, doc_type text not null check (doc_type in ('swo', 'pa')), status text not null, updated_at timestamptz not null default now(), unique (org_id, patient_id_hash, doc_type) ); -- RLS: org members can only see their own org's data alter table doc_status enable row level security; create policy "org_access" on doc_status using (org_id in ( select id from organizations where clerk_org_id = (current_setting('request.jwt.claims', true)::json->>'org_id') )); ``` - [ ] **Step 2: Verify in Supabase Table Editor** that `doc_status` table appears with columns: id, org_id, patient_id_hash, doc_type, status, updated_at --- ## Task 2: Backend persistence functions **Files:** - Modify: `python-backend/core/persistence.py` (after `load_confirmed_visits_for_org` at ~line 220) - [ ] **Step 1: Add `upsert_doc_status` function** Insert after the `load_confirmed_visits_for_org` function: ```python def upsert_doc_status( org_id: str, patient_id_hash: str, doc_type: str, status: str, ) -> bool: """ Insert or update a doc status (swo or pa) for a patient. Returns True on success, False if Supabase unavailable. """ client = get_client() if not client: return False try: client.table("doc_status").upsert({ "org_id": org_id, "patient_id_hash": patient_id_hash, "doc_type": doc_type, "status": status, "updated_at": "now()", }, on_conflict="org_id,patient_id_hash,doc_type").execute() return True except Exception as e: logger.error(f"Failed to upsert doc_status: {e}") return False def load_doc_statuses_for_org(org_id: str) -> dict: """ Load all SWO and PA statuses for an org. Returns dict mapping patient_id_hash -> {doc_type: status}. Example: {"abc123": {"swo": "on_file", "pa": "approved"}} """ client = get_client() if not client: return {} try: result = client.table("doc_status") \ .select("patient_id_hash,doc_type,status") \ .eq("org_id", org_id) \ .execute() out: dict = {} for row in (result.data or []): h = row["patient_id_hash"] if h not in out: out[h] = {} out[h][row["doc_type"]] = row["status"] return out except Exception as e: logger.error(f"Failed to load doc_statuses: {e}") return {} ``` - [ ] **Step 2: Verify import** — `upsert_doc_status` and `load_doc_statuses_for_org` are in the same file as `upsert_confirmed_visit` and `load_confirmed_visits_for_org`. No new imports needed. --- ## Task 3: Backend API endpoint + upload integration **Files:** - Modify: `python-backend/api/main.py` - [ ] **Step 1: Import new persistence functions** Find the existing import line (around line 20): ```python from core.persistence import ( persist_upload, get_or_create_org, upsert_confirmed_visit, load_confirmed_visits_for_org, ) ``` Replace with: ```python from core.persistence import ( persist_upload, get_or_create_org, upsert_confirmed_visit, load_confirmed_visits_for_org, upsert_doc_status, load_doc_statuses_for_org, ) ``` - [ ] **Step 2: Add `DocStatusRequest` Pydantic model** Find `class ConfirmVisitRequest` and add this BEFORE it: ```python class DocStatusRequest(BaseModel): patient_id: str doc_type: str # "swo" or "pa" status: str # see locked values in plan VALID_DOC_STATUSES = { "swo": {"pending", "requested", "on_file"}, "pa": {"not_required", "requested", "approved", "denied"}, } ``` - [ ] **Step 3: Load doc statuses in the upload endpoint** In the `/api/upload` endpoint, find the block that loads confirmed visits (around line 392): ```python confirmed_visits = load_confirmed_visits_for_org(org_id) if org_id else {} ``` Add the doc_statuses load on the next line: ```python confirmed_visits = load_confirmed_visits_for_org(org_id) if org_id else {} doc_statuses = load_doc_statuses_for_org(org_id) if org_id else {} ``` - [ ] **Step 4: Pass doc_statuses into `_to_record_out`** Find the `_to_record_out` call in the upload endpoint: ```python out = [ _to_record_out( r, record=record_lookup.get(r.patient_id), confirmed_visit_date=confirmed_visits.get( hashlib.sha256(r.patient_id.encode()).hexdigest() ), ) for r in results ] ``` Replace with: ```python out = [ _to_record_out( r, record=record_lookup.get(r.patient_id), confirmed_visit_date=confirmed_visits.get( hashlib.sha256(r.patient_id.encode()).hexdigest() ), saved_doc_statuses=doc_statuses.get( hashlib.sha256(r.patient_id.encode()).hexdigest(), {} ), ) for r in results ] ``` - [ ] **Step 5: Update `_to_record_out` to accept and apply saved doc statuses** Find the `_to_record_out` function definition and add the `saved_doc_statuses` parameter and override logic. Find: ```python def _to_record_out( r, record: Optional[object] = None, confirmed_visit_date=None, ) -> RecordOut: ``` Replace signature with: ```python def _to_record_out( r, record: Optional[object] = None, confirmed_visit_date=None, saved_doc_statuses: dict | None = None, ) -> RecordOut: ``` Then, just before `doc_state_out = DocStateOut(...)` is built, find where `doc` is computed and after `compute_doc_state` is called, add the override: Find the block that builds `doc_state_out` (around line 268-290): ```python doc = compute_doc_state( ... ) doc_state_out = DocStateOut( swo=doc.swo, visit=doc.visit, pecos=doc.pecos, pa=doc.pa, diagnosis=doc.diagnosis, ) ``` Replace the `doc_state_out` build with: ```python doc = compute_doc_state( ... ) # Apply any staff-saved overrides from Supabase doc_status table _saved = saved_doc_statuses or {} _status_labels = { "pending": "Pending", "requested": "Requested", "on_file": "On File", "not_required": "Not Required", "approved": "Approved", "denied": "Denied", } swo_display = _status_labels.get(_saved.get("swo", ""), doc.swo) pa_display = _status_labels.get(_saved.get("pa", ""), doc.pa) doc_state_out = DocStateOut( swo=swo_display, visit=doc.visit, pecos=doc.pecos, pa=pa_display, diagnosis=doc.diagnosis, ) ``` - [ ] **Step 6: Add `PUT /api/doc-status` endpoint** Add after the `/api/confirm-visit` endpoint: ```python @app.put("/api/doc-status") async def update_doc_status( body: DocStatusRequest, claims: dict = Depends(require_auth), ): """ Store a staff-updated SWO or PA status for a patient. Persists across future CSV imports for this org/patient. """ if body.doc_type not in VALID_DOC_STATUSES: raise HTTPException(status_code=400, detail=f"Invalid doc_type: {body.doc_type}. Must be 'swo' or 'pa'.") if body.status not in VALID_DOC_STATUSES[body.doc_type]: valid = ", ".join(sorted(VALID_DOC_STATUSES[body.doc_type])) raise HTTPException(status_code=400, detail=f"Invalid status '{body.status}' for {body.doc_type}. Valid: {valid}.") clerk_org_id = claims.get("o", {}).get("id") if isinstance(claims.get("o"), dict) else None org_id = get_or_create_org(clerk_org_id=clerk_org_id) if not org_id: raise HTTPException(status_code=503, detail="Organization not found.") patient_hash = hashlib.sha256(body.patient_id.encode()).hexdigest() success = upsert_doc_status(org_id, patient_hash, body.doc_type, body.status) if not success: raise HTTPException(status_code=503, detail="Failed to save doc status.") return {"patient_id": body.patient_id, "doc_type": body.doc_type, "status": body.status} ``` --- ## Task 4: Frontend API helper **Files:** - Modify: `signal-ui/src/lib/api.js` - [ ] **Step 1: Add `updateDocStatus` function** Append to `api.js` after the `confirmVisit` function: ```javascript /** * Update SWO or PA status for a patient. * @param {string} patientId * @param {"swo"|"pa"} docType * @param {string} status * @param {string|null} token - Clerk JWT */ export async function updateDocStatus(patientId, docType, status, token = null) { const authHeader = token ? { "Authorization": `Bearer ${token}` } : API_KEY ? { "X-API-Key": API_KEY } : {}; try { const resp = await fetch(`${BACKEND_URL}/api/doc-status`, { method: "PUT", headers: { ...authHeader, "Content-Type": "application/json" }, body: JSON.stringify({ patient_id: patientId, doc_type: docType, status }), }); if (!resp.ok) return null; return resp.json(); } catch { return null; } } ``` --- ## Task 5: Frontend — clickable SWO and PA status chips **Files:** - Modify: `signal-ui/src/components/WorklistTable.jsx` - [ ] **Step 1: Add import for `updateDocStatus` and `useAuth`** At the top of WorklistTable.jsx, find: ```javascript import { useState, useEffect, useRef, Fragment } from "react"; import { useTheme } from "../useTheme"; ``` Add the auth import and api import: ```javascript import { useState, useEffect, useRef, Fragment } from "react"; import { useTheme } from "../useTheme"; import { updateDocStatus } from "../lib/api"; import { useAuth } from "@clerk/react"; ``` - [ ] **Step 2: Add SWO and PA cycle maps** After the existing `CONFIRM_VISIT_FLAGS` constant, add: ```javascript const SWO_CYCLE = ["pending", "requested", "on_file"]; const PA_CYCLE = ["not_required", "requested", "approved", "denied"]; const DOC_STATUS_LABELS = { pending: "Pending", requested: "Requested", on_file: "On File", not_required: "Not Required", approved: "Approved", denied: "Denied", }; ``` - [ ] **Step 3: Add `getToken` from Clerk inside WorklistTable component** In the `WorklistTable` function body (after the `useTheme` line), add: ```javascript const { getToken } = useAuth(); ``` - [ ] **Step 4: Replace `DocItem` with an updatable version** Find and replace the entire `DocItem` function: ```javascript function DocItem({ label, value, patientId, docType, cycle, onUpdated, getToken }) { const [saving, setSaving] = useState(false); const isGood = value && ( value === "On File" || value === "Verified" || value === "Not Required" || value.startsWith("Confirmed") || value.startsWith("Approved") || value === "N/A" ); const isBad = value && ( value === "Missing" || value === "Not Verified" || value === "Expired" || value === "Denied" || value === "Required — Not Started" || value === "Pending" ); const color = isGood ? "text-[#1A8040]" : isBad ? "text-[#CC2222]" : "text-[#CB6B20]"; // Find the internal key for the current display value const currentKey = Object.entries(DOC_STATUS_LABELS).find(([, v]) => v === value)?.[0]; const handleCycle = async () => { if (!cycle || !patientId || saving) return; const idx = currentKey ? cycle.indexOf(currentKey) : -1; const nextKey = cycle[(idx + 1) % cycle.length]; const nextLabel = DOC_STATUS_LABELS[nextKey]; setSaving(true); const token = getToken ? await getToken().catch(() => null) : null; const result = await updateDocStatus(patientId, docType, nextKey, token); setSaving(false); if (result && onUpdated) onUpdated(docType, nextLabel); }; const isClickable = !!cycle && !!patientId; return (
{label}: {isClickable ? ( ) : ( {value || "Unknown"} )}
); } ``` - [ ] **Step 5: Update the expanded row to pass update props to DocItem** Find the expanded row section in the component (the Documentation Checklist section). It renders `` calls. Find: ```javascript ``` and: ```javascript ``` Replace all four `` calls in the expanded section with: ```javascript setLocalDocStates(prev => ({ ...prev, [r.patient_id]: { ...prev[r.patient_id], [type]: label } }))} getToken={getToken} /> setLocalDocStates(prev => ({ ...prev, [r.patient_id]: { ...prev[r.patient_id], [type]: label } }))} getToken={getToken} /> ``` - [ ] **Step 6: Add `localDocStates` state to the WorklistTable component** In the component body, after `const [toast, setToast] = useState(...)`, add: ```javascript const [localDocStates, setLocalDocStates] = useState({}); ``` Reset it when records change — add to the existing `useEffect` that watches records: ```javascript useEffect(() => { setLocalRecords(records); setLocalDocStates({}); }, [records]); ``` --- ## Task 6: Deploy - [ ] **Step 1: Deploy backend to Railway** ```bash cd /Users/sttil-solutions/projects/signal railway up --detach ``` - [ ] **Step 2: Verify health** ```bash curl -s https://signal-api-production-91c2.up.railway.app/health ``` Expected: `{"status":"ok",...}` - [ ] **Step 3: Test in browser** 1. Import any test CSV 2. Expand a patient row 3. Click "Pending" next to SWO — it should cycle to "Requested" 4. Click again — should cycle to "On File" 5. Re-import the same CSV — SWO should now show "On File" (loaded from Supabase) - [ ] **Step 4: Update CLAUDE.md pilot readiness checklist** Mark "Placeholder content removed" as PASS (both empty state and status legend now done). Count gives 13/13.