# 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 (
    <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:

```javascript
<DocItem label="SWO" value={r.doc_state?.swo} />
```
and:
```javascript
<DocItem label="Prior Authorization" value={r.doc_state?.pa} />
```

Replace all four `<DocItem>` calls in the expanded section with:

```javascript
<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:
```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.
