diff --git a/CLAUDE.md b/CLAUDE.md index 4d75064..c88ac8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,20 +64,20 @@ and stays predictable. Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Projects/2026-05-18-pilot-readiness-whitepaper.md` -### Checklist Status (as of 2026-06-09) — 65% pilot-ready +### Checklist Status (as of 2026-06-10) — 85% pilot-ready (11/13) | Checklist Item | Status | |---|---| | App hosted behind stable URL | PASS — https://signal-ui-xi.vercel.app (deployed 2026-05-29) | | Demo login / controlled access working | PASS | -| Synthetic sample data loads end to end | NOT VERIFIED as Gaboro org | +| Synthetic sample data loads end to end | PASS — verified as Gaboro DME org 2026-06-10 | | 25 CSV variants pass ingestion tests | PASS — 50 new files generated (PA + NJ sets), 100% normalizer pass rate 2026-06-07 | -| Import flow: mapping, validation, warnings | PARTIAL — CSVImport exists, full mapping review unconfirmed | +| Import flow: mapping, validation, warnings | PASS — mapping review step built 2026-06-10; confidence display, override dropdown, unmapped column notice, required-missing notice all implemented in CSVImport.jsx | | Report generation calculates from backend | PASS | | Each status has reason + recommended action | PASS — visit date proxy bug fixed 2026-06-09; `_resolve_visit_date()` implements full priority chain (CSV column > Supabase confirmed > shipment-30d estimate) | -| Export CSV works and opens cleanly | NOT VERIFIED end to end | -| Placeholder content removed | PARTIAL — StatCard undefined variables fixed 2026-06-09; Sidebar + sweep in progress | -| Browser smoke test passes | NOT RUN | +| Export CSV works and opens cleanly | PASS — verified end to end 2026-06-10 | +| Placeholder content removed | PARTIAL — StatCard undefined variables fixed 2026-06-09; no empty-state guidance or status legend for first-time users | +| Browser smoke test passes | PASS — CSV import, all filter tabs, Confirm Visit, Export all verified 2026-06-10; sidebar count sync fixed | | Data handling rules documented | PASS — privacy-policy.md + data-handling.md written 2026-06-07; minimum necessary fields, CSV disposition policy, retention, and payer_rules.json cadence all documented | | Real PHI blocked | PARTIAL — architecture confirmed; Supabase plan tier (Pro vs Team) not yet verified | | Pilot success metrics written down | PASS | diff --git a/python-backend/api/main.py b/python-backend/api/main.py index 35b2a29..a089ad9 100644 --- a/python-backend/api/main.py +++ b/python-backend/api/main.py @@ -115,25 +115,27 @@ DEVICE_DISPLAY = { } FLAG_LABELS = { - "SUPPLY_LAPSED": "Docs Required", - "VISIT_REQUIRED": "Escalate", - "TRANSFER_PENDING": "New Patient", - "RENEWAL_CRITICAL": "Escalate", - "RENEWAL_ELEVATED": "Confirm Appointment", - "RENEWAL_SOON": "Begin Outreach", - "RESUPPLY_READY": "Clear to Ship", - "ACTIVE": "On Track", + "SUPPLY_LAPSED": "Docs Required", + "VISIT_REQUIRED": "Escalate", + "TRANSFER_PENDING": "New Patient", + "RENEWAL_CRITICAL": "Escalate", + "RENEWAL_ELEVATED": "Confirm Appointment", + "RENEWAL_SOON": "Begin Outreach", + "RESUPPLY_READY": "Clear to Ship", + "ACTIVE": "On Track", + "NO_RECENT_SHIPMENT": "No Recent Shipment", } FLAG_ACTIONS = { - "SUPPLY_LAPSED": "Pull patient file — verify documentation is current before initiating any new order", - "VISIT_REQUIRED": "Contact prescriber's office and patient directly — ask: 'Was there a visit in the past 6 months to evaluate this patient's CGM dosing needs?' No shipment until visit is confirmed and documented", - "TRANSFER_PENDING": "Obtain full documentation set (SWO, PECOS, PA, diagnosis) — Signal cannot assess status until records are on file", - "RENEWAL_CRITICAL": "Contact prescriber's office and patient directly — ask: 'Was there a visit in the past 6 months to evaluate CGM dosing needs?' Appointment must be on calendar within 45 days", - "RENEWAL_ELEVATED": "Follow up with prescriber's office — ask: 'Was there a visit in the past 6 months to evaluate CGM dosing needs?' Confirm appointment is scheduled", - "RENEWAL_SOON": "Contact prescriber's office — ask: 'Was there a visit in the past 6 months to evaluate this patient's CGM dosing needs?' Schedule evaluation now if not yet completed", - "RESUPPLY_READY": "Initiate resupply order — patient is within the 30-day refill window", - "ACTIVE": "No action needed — documentation current, visit more than 90 days out", + "SUPPLY_LAPSED": "Pull patient file — verify documentation is current before initiating any new order", + "VISIT_REQUIRED": "Contact prescriber's office and patient directly — ask: 'Was there a visit in the past 6 months to evaluate this patient's CGM dosing needs?' No shipment until visit is confirmed and documented", + "TRANSFER_PENDING": "Obtain full documentation set (SWO, PECOS, PA, diagnosis) — Signal cannot assess status until records are on file", + "RENEWAL_CRITICAL": "Contact prescriber's office and patient directly — ask: 'Was there a visit in the past 6 months to evaluate CGM dosing needs?' Appointment must be on calendar within 45 days", + "RENEWAL_ELEVATED": "Follow up with prescriber's office — ask: 'Was there a visit in the past 6 months to evaluate CGM dosing needs?' Confirm appointment is scheduled", + "RENEWAL_SOON": "Contact prescriber's office — ask: 'Was there a visit in the past 6 months to evaluate this patient's CGM dosing needs?' Schedule evaluation now if not yet completed", + "RESUPPLY_READY": "Initiate resupply order — patient is within the 30-day refill window", + "ACTIVE": "No action needed — documentation current, visit more than 90 days out", + "NO_RECENT_SHIPMENT": "Verify status with prescriber office — confirm patient is still active", } @@ -208,6 +210,8 @@ def _build_reason(flag_val: str, days_until_end: int, days_until_visit: Optional return f"Coverage ends in {days_until_end} {unit}. Patient is within resupply window — initiate shipment now." if flag_val == "TRANSFER_PENDING": return "Patient transferred from prior supplier. Verify all documentation before proceeding." + if flag_val == "NO_RECENT_SHIPMENT": + return "No shipment on file in 12+ months. Contact the prescriber office to confirm the patient is still active and has not transferred care." unit = "day" if days_until_end == 1 else "days" return f"Coverage on track. Resupply window opens in approximately {days_until_end} {unit}." @@ -280,6 +284,7 @@ def _compute_stats(records: list[RecordOut]) -> dict: "renewal_soon": flags.count("RENEWAL_SOON"), "resupply_ready": flags.count("RESUPPLY_READY"), "active": flags.count("ACTIVE"), + "no_recent_shipment": flags.count("NO_RECENT_SHIPMENT"), "prescriber_action": ( flags.count("SUPPLY_LAPSED") + flags.count("VISIT_REQUIRED") + flags.count("RENEWAL_CRITICAL") + flags.count("RENEWAL_ELEVATED") diff --git a/python-backend/core/coverage_calculator.py b/python-backend/core/coverage_calculator.py index 8889882..4cf2478 100644 --- a/python-backend/core/coverage_calculator.py +++ b/python-backend/core/coverage_calculator.py @@ -12,14 +12,16 @@ PHI CONTRACT: signature or data structure in this file. Flag types emitted: - SUPPLY_LAPSED — supply cycle ended, no new shipment - VISIT_REQUIRED — visit date past due, no confirmed new visit - TRANSFER_PENDING — transferred patient, all docs need verification - RENEWAL_CRITICAL — <= 45 days to next visit due - RENEWAL_ELEVATED — <= 60 days to next visit due - RENEWAL_SOON — <= 90 days to next visit due - RESUPPLY_READY — within refill window, visit not yet urgent - ACTIVE — all clear + SUPPLY_LAPSED — supply cycle ended, no new shipment + VISIT_REQUIRED — visit date past due, no confirmed new visit + TRANSFER_PENDING — transferred patient, all docs need verification + RENEWAL_CRITICAL — <= 45 days to next visit due + RENEWAL_ELEVATED — <= 60 days to next visit due + RENEWAL_SOON — <= 90 days to next visit due + RESUPPLY_READY — within refill window, visit not yet urgent + ACTIVE — all clear + NO_RECENT_SHIPMENT — last shipment 12+ months ago, no confirmed active care; + verify patient status with prescriber office """ import json @@ -38,14 +40,15 @@ PAYER_RULES_PATH = Path(__file__).parent.parent / "config" / "payer_rules.json" class CoverageFlag(str, Enum): - SUPPLY_LAPSED = "SUPPLY_LAPSED" # supply cycle ended, no new shipment - VISIT_REQUIRED = "VISIT_REQUIRED" # visit date past due, no confirmed new visit - TRANSFER_PENDING = "TRANSFER_PENDING" # transferred patient, all docs need verification - RENEWAL_CRITICAL = "RENEWAL_CRITICAL" # <= 45 days to next visit due - RENEWAL_ELEVATED = "RENEWAL_ELEVATED" # <= 60 days to next visit due - RENEWAL_SOON = "RENEWAL_SOON" # <= 90 days to next visit due - RESUPPLY_READY = "RESUPPLY_READY" # within refill window, visit not yet urgent - ACTIVE = "ACTIVE" # all clear + SUPPLY_LAPSED = "SUPPLY_LAPSED" # supply cycle ended, no new shipment + VISIT_REQUIRED = "VISIT_REQUIRED" # visit date past due, no confirmed new visit + TRANSFER_PENDING = "TRANSFER_PENDING" # transferred patient, all docs need verification + RENEWAL_CRITICAL = "RENEWAL_CRITICAL" # <= 45 days to next visit due + RENEWAL_ELEVATED = "RENEWAL_ELEVATED" # <= 60 days to next visit due + RENEWAL_SOON = "RENEWAL_SOON" # <= 90 days to next visit due + RESUPPLY_READY = "RESUPPLY_READY" # within refill window, visit not yet urgent + ACTIVE = "ACTIVE" # all clear + NO_RECENT_SHIPMENT = "NO_RECENT_SHIPMENT" # last shipment 12+ months ago, no confirmed active care @dataclass(frozen=True) @@ -300,7 +303,31 @@ def calculate_coverage( if flag == CoverageFlag.ACTIVE and days_until_end <= refill_window_days: flag = CoverageFlag.RESUPPLY_READY - priority = _compute_priority(flag, days_until_visit) + # Catch-all: patient shows no active-care signals and last shipment was 12+ months ago. + # Only applies when no higher-priority flag has fired (flag is ACTIVE or RESUPPLY_READY). + # A confirmed visit date within the last 12 months overrides this check, treating the + # patient as still active even if the shipment record is old. + NO_RECENT_SHIPMENT_DAYS = 365 + days_since_shipment = (today - record.shipment_date).days + confirmed_visit_within_12m = ( + confirmed_visit_date is not None + and (today - confirmed_visit_date).days < NO_RECENT_SHIPMENT_DAYS + ) + csv_visit_within_12m = ( + record.csv_visit_date is not None + and (today - record.csv_visit_date).days < NO_RECENT_SHIPMENT_DAYS + ) + if ( + flag in (CoverageFlag.ACTIVE, CoverageFlag.RESUPPLY_READY) + and days_since_shipment >= NO_RECENT_SHIPMENT_DAYS + and not confirmed_visit_within_12m + and not csv_visit_within_12m + ): + flag = CoverageFlag.NO_RECENT_SHIPMENT + days_until_visit = None + priority = 5 # lowest — drops to bottom of worklist sort + else: + priority = _compute_priority(flag, days_until_visit) return CoverageResult( patient_id=record.patient_id, @@ -344,7 +371,7 @@ def calculate_batch( result = calculate_coverage(record, as_of=as_of, confirmed_visit_date=confirmed_date) results.append(result) except ValueError as exc: - logger.warning("Skipping record for patient_id hash — %s", exc) + logger.warning("Skipping record for patient_id hash: %s", exc) results.sort(key=lambda r: r.priority_score, reverse=True) return results diff --git a/python-backend/core/doc_state_machine.py b/python-backend/core/doc_state_machine.py index 343e2a8..8393bb9 100644 --- a/python-backend/core/doc_state_machine.py +++ b/python-backend/core/doc_state_machine.py @@ -66,22 +66,18 @@ def _build_cascade( cascade = [] if not visit_confirmed: - cascade.append("Qualifying visit not confirmed") - if swo in ("Pending", "Expired"): - cascade.append("SWO validity at risk") - if payer_type in _PA_REQUIRED and pa in ("Pending", "Required — Not Started"): - cascade.append("PA renewal cannot proceed") - cascade.append("Next shipment blocked") + cascade.append("Visit date not on file") + cascade.append("Device shipment at risk") return cascade if swo in ("Pending", "Expired"): - cascade.append("SWO missing or expired") - if payer_type in _PECOS_REQUIRED and pecos not in ("Verified",): - cascade.append("PECOS enrollment at risk") - cascade.append("Next shipment blocked") + cascade.append("SWO not on file") + cascade.append("Device shipment at risk") + return cascade if diagnosis == "Missing": - cascade.append("Diagnosis documentation missing — required for all payers") + cascade.append("Diagnosis not on file") + cascade.append("Device shipment at risk") return cascade diff --git a/signal-ui/src/App.jsx b/signal-ui/src/App.jsx index 05ec988..af4c801 100644 --- a/signal-ui/src/App.jsx +++ b/signal-ui/src/App.jsx @@ -8,9 +8,10 @@ import CSVImport from "./components/CSVImport"; import CSVExport from "./components/CSVExport"; import ThemeToggle from "./components/ThemeToggle"; import Toast from "./components/Toast"; +import EmptyState from "./components/EmptyState"; import Privacy from "./Privacy"; import { showToast } from "./lib/toast"; -import { uploadToBackend, apiRecordToLocal } from "./lib/api"; +import { apiRecordToLocal } from "./lib/api"; import { parseCSV, processBatch } from "./lib/coverage"; function AppInner() { @@ -36,16 +37,16 @@ function AppInner() { const atRiskCount = supplyLapsed + visitRequired + renewalCritical; const actionNeededCount = renewalSoon + renewalElevated + transferPending; - const handleResults = useCallback(async (file) => { + // handleResults receives (data, file) from CSVImport after the upload (and optional + // mapping review) step is complete. data is the backend API response, or null if + // the backend was unreachable, in which case we fall back to local processing. + const handleResults = useCallback((data, file) => { const label = new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); - const token = await getToken().catch(() => null); - const data = await uploadToBackend(file, token); - if (data) { const results = data.records.map(apiRecordToLocal); setRecords(results); @@ -70,7 +71,7 @@ function AppInner() { showToast(msg); }; reader.readAsText(file); - }, [getToken]); + }, []); return (
| + Original CSV header + | ++ Detected Signal field + | + {showConfidence && ( ++ Confidence + | + )} + {reviewState.rows.some((r) => r.exampleValue !== null) && ( ++ Example value + | + )} ++ Override + | +
|---|---|---|---|---|
| + {row.rawHeader} + | + + {/* Detected Signal field */} ++ {FIELD_LABELS[row.canonical] ?? row.canonical} + | + + {/* Confidence */} + {showConfidence && ( ++ {confidenceLabel(row.confidence)} + | + )} + + {/* Example value */} + {reviewState.rows.some((r) => r.exampleValue !== null) && ( ++ {row.exampleValue ?? ""} + | + )} + + {/* Override dropdown */} ++ + | +
+ Import your order management export to get started. +
+ +|
-
+
{r.patient_id}
{i === 0 && (
@@ -221,7 +291,7 @@ export default function WorklistTable({ records, activeFilter, onFilterChange })
|