- Doc status click-to-update: SWO and PA cycle through states and persist to Supabase - Status legend added to worklist header - Empty state with file-open prompt for first-time users - Device normalizer expanded for broader CGM variant coverage - Warm cream text applied globally for secondary/muted UI elements - Drag-drop CSV import support - CORS root cause fixed via Railway ALLOWED_ORIGINS env var - DOCS_REQUIRED badge: RESUPPLY_READY patients with open cascade items now show amber Docs Required instead of green Clear to Ship, preventing premature shipment Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
17 KiB
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)
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_statustable 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(afterload_confirmed_visits_for_orgat ~line 220) -
Step 1: Add
upsert_doc_statusfunction
Insert after the load_confirmed_visits_for_org function:
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_statusandload_doc_statuses_for_orgare in the same file asupsert_confirmed_visitandload_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):
from core.persistence import (
persist_upload, get_or_create_org,
upsert_confirmed_visit, load_confirmed_visits_for_org,
)
Replace with:
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
DocStatusRequestPydantic model
Find class ConfirmVisitRequest and add this BEFORE it:
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):
confirmed_visits = load_confirmed_visits_for_org(org_id) if org_id else {}
Add the doc_statuses load on the next line:
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:
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:
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_outto accept and apply saved doc statuses
Find the _to_record_out function definition and add the saved_doc_statuses parameter and override logic.
Find:
def _to_record_out(
r,
record: Optional[object] = None,
confirmed_visit_date=None,
) -> RecordOut:
Replace signature with:
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):
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:
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-statusendpoint
Add after the /api/confirm-visit endpoint:
@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
updateDocStatusfunction
Append to api.js after the confirmVisit function:
/**
* 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
updateDocStatusanduseAuth
At the top of WorklistTable.jsx, find:
import { useState, useEffect, useRef, Fragment } from "react";
import { useTheme } from "../useTheme";
Add the auth import and api import:
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:
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
getTokenfrom Clerk inside WorklistTable component
In the WorklistTable function body (after the useTheme line), add:
const { getToken } = useAuth();
- Step 4: Replace
DocItemwith an updatable version
Find and replace the entire DocItem function:
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 (
<div className="flex items-start gap-[6px]">
<span className="text-[var(--text-warm)] w-[140px] shrink-0">{label}:</span>
{isClickable ? (
<button
onClick={handleCycle}
disabled={saving}
title={saving ? "Saving…" : `Click to update ${label} status`}
className={`font-medium ${color} underline-offset-2 hover:underline cursor-pointer disabled:opacity-50 bg-transparent border-none p-0 font-inherit text-left`}
style={{ fontSize: "inherit" }}
>
{saving ? "Saving…" : (value || "Unknown")}
</button>
) : (
<span className={`font-medium ${color}`}>{value || "Unknown"}</span>
)}
</div>
);
}
- 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 <DocItem> calls. Find:
<DocItem label="SWO" value={r.doc_state?.swo} />
and:
<DocItem label="Prior Authorization" value={r.doc_state?.pa} />
Replace all four <DocItem> calls in the expanded section with:
<DocItem
label="SWO"
value={localDocStates[r.patient_id]?.swo ?? r.doc_state?.swo}
patientId={r.patient_id}
docType="swo"
cycle={SWO_CYCLE}
onUpdated={(type, label) => setLocalDocStates(prev => ({
...prev,
[r.patient_id]: { ...prev[r.patient_id], [type]: label }
}))}
getToken={getToken}
/>
<DocItem
label="Qualifying Visit"
value={r.doc_state?.visit}
/>
<DocItem
label="PECOS Enrollment"
value={r.doc_state?.pecos}
/>
<DocItem
label="Prior Authorization"
value={localDocStates[r.patient_id]?.pa ?? r.doc_state?.pa}
patientId={r.patient_id}
docType="pa"
cycle={PA_CYCLE}
onUpdated={(type, label) => setLocalDocStates(prev => ({
...prev,
[r.patient_id]: { ...prev[r.patient_id], [type]: label }
}))}
getToken={getToken}
/>
<DocItem
label="Diagnosis on File"
value={r.doc_state?.diagnosis}
/>
- Step 6: Add
localDocStatesstate to the WorklistTable component
In the component body, after const [toast, setToast] = useState(...), add:
const [localDocStates, setLocalDocStates] = useState({});
Reset it when records change — add to the existing useEffect that watches records:
useEffect(() => {
setLocalRecords(records);
setLocalDocStates({});
}, [records]);
Task 6: Deploy
- Step 1: Deploy backend to Railway
cd /Users/sttil-solutions/projects/signal
railway up --detach
- Step 2: Verify health
curl -s https://signal-api-production-91c2.up.railway.app/health
Expected: {"status":"ok",...}
- Step 3: Test in browser
- Import any test CSV
- Expand a patient row
- Click "Pending" next to SWO — it should cycle to "Requested"
- Click again — should cycle to "On File"
- 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.