Signal/docs/superpowers/plans/2026-06-13-doc-status-updates.md
Kisa 1d95bb2ab2 feat: doc status persistence, empty state, status legend, and Clear to Ship guard
- 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>
2026-06-14 05:09:06 -04:00

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_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:

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 importupsert_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):

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 DocStatusRequest Pydantic 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_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:

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-status endpoint

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 updateDocStatus function

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 updateDocStatus and useAuth

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 getToken from Clerk inside WorklistTable component

In the WorklistTable function body (after the useTheme line), add:

  const { getToken } = useAuth();
  • Step 4: Replace DocItem with 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 localDocStates state 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
  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.