fix: demo readiness — filter flags, auth, active display, sidebar org, visit validation

- coverage.js: correct 7-tier flag names matching backend (was OUT_OF_COVERAGE etc.)
- Sidebar: dynamic org name from Clerk useOrganization (was hardcoded "Demo Supplier")
- App.jsx: fix undefined escalateCount/outreachCount crash
- Privacy.jsx: replace personal email with privacy@sttilsolutions.com
- api.js + ConfirmVisitModal: Confirm Visit uses Clerk Bearer token (was 401 on Vercel)
- WorklistTable: ACTIVE rows show next visit due date
- main.py: server-side 6-month qualifying visit validation
- CLAUDE.md: pilot readiness checklist updated to 65%
- pitch/legal/gaboro-nda.md: STTIL city + Robert Robinson email filled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kisa 2026-06-10 00:27:17 -04:00
parent 34e6c08c14
commit d197c529ce
10 changed files with 124 additions and 49 deletions

View file

@ -64,7 +64,7 @@ and stays predictable.
Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Projects/2026-05-18-pilot-readiness-whitepaper.md` Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Projects/2026-05-18-pilot-readiness-whitepaper.md`
### Checklist Status (as of 2026-06-07) — 45-50% pilot-ready ### Checklist Status (as of 2026-06-09) — 65% pilot-ready
| Checklist Item | Status | | Checklist Item | Status |
|---|---| |---|---|
@ -74,9 +74,9 @@ Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Project
| 25 CSV variants pass ingestion tests | PASS — 50 new files generated (PA + NJ sets), 100% normalizer pass rate 2026-06-07 | | 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 | PARTIAL — CSVImport exists, full mapping review unconfirmed |
| Report generation calculates from backend | PASS | | Report generation calculates from backend | PASS |
| Each status has reason + recommended action | PARTIAL — payer bug fixed; visit date still uses shipment proxy | | 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 | | Export CSV works and opens cleanly | NOT VERIFIED end to end |
| Placeholder content removed | NOT VERIFIED | | Placeholder content removed | PARTIAL — StatCard undefined variables fixed 2026-06-09; Sidebar + sweep in progress |
| Browser smoke test passes | NOT RUN | | Browser smoke test passes | NOT RUN |
| 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 | | 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 | | Real PHI blocked | PARTIAL — architecture confirmed; Supabase plan tier (Pro vs Team) not yet verified |

View file

@ -3,8 +3,8 @@
**Effective Date:** June __, 2026 **Effective Date:** June __, 2026
**Between:** **Between:**
- **STTIL Solutions LLC** ("STTIL"), a limited liability company, with its principal place of business in [City, State] - **STTIL Solutions LLC** ("STTIL"), a limited liability company, with its principal place of business in Jacksonville, Florida
- **Gaboro DME** ("Gaboro"), with its principal place of business in [City, State] - **Gaboro DME** ("Gaboro"), with its principal place of business in Philadelphia, Pennsylvania
Each a "Party" and together the "Parties." Each a "Party" and together the "Parties."
@ -118,4 +118,4 @@ By: ___________________________
Name: Robert Robinson Name: Robert Robinson
Title: Co-Founder and Managing Partner Title: Co-Founder and Managing Partner
Date: _________________________ Date: _________________________
Email: ___________________________ Email: robertr@gaboromed.com

View file

@ -469,6 +469,20 @@ async def confirm_visit(
if confirmed > date_type.today(): if confirmed > date_type.today():
raise HTTPException(status_code=400, detail="Confirmed date cannot be in the future.") raise HTTPException(status_code=400, detail="Confirmed date cannot be in the future.")
# Validate: visit must be within 6 months (183 days) before the order/shipment date.
# 183 days = ~6 months, consistent with CMS CGM qualifying visit window.
from datetime import timedelta as _td
six_months_before = shipment - _td(days=183)
if confirmed < six_months_before:
raise HTTPException(
status_code=400,
detail=(
f"Visit date {confirmed.isoformat()} is more than 6 months before "
f"the order date {shipment.isoformat()}. A qualifying visit must fall "
f"within the 6-month window before each supply order."
),
)
# Get org # Get org
clerk_org_id = claims.get("o", {}).get("id") if isinstance(claims.get("o"), dict) else None 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) org_id = get_or_create_org(clerk_org_id=clerk_org_id)

View file

@ -29,8 +29,12 @@ function AppInner() {
const transferPending = records.filter((r) => r.flag === "TRANSFER_PENDING").length; const transferPending = records.filter((r) => r.flag === "TRANSFER_PENDING").length;
const refill = records.filter((r) => r.flag === "RESUPPLY_READY").length; const refill = records.filter((r) => r.flag === "RESUPPLY_READY").length;
const okCount = records.filter((r) => r.flag === "ACTIVE").length; const okCount = records.filter((r) => r.flag === "ACTIVE").length;
// escalateCount = VISIT_REQUIRED (qualifying visit past due) + RENEWAL_CRITICAL (<=45 days)
const escalateCount = visitRequired + renewalCritical; const escalateCount = visitRequired + renewalCritical;
const outreachCount = renewalSoon + renewalElevated; // outreachCount = RENEWAL_ELEVATED (<=60d) + RENEWAL_SOON (<=90d)
const outreachCount = renewalElevated + renewalSoon;
const atRiskCount = supplyLapsed + visitRequired + renewalCritical;
const actionNeededCount = renewalSoon + renewalElevated + transferPending;
const handleResults = useCallback(async (file) => { const handleResults = useCallback(async (file) => {
const label = new Date().toLocaleDateString("en-US", { const label = new Date().toLocaleDateString("en-US", {
@ -60,7 +64,7 @@ function AppInner() {
const rows = parseCSV(e.target.result); const rows = parseCSV(e.target.result);
const { results, skipped } = processBatch(rows); const { results, skipped } = processBatch(rows);
setRecords(results); setRecords(results);
setImportLabel(`${file.name} · ${label} · local processing`); setImportLabel(`${file.name} · ${label}`);
let msg = `Loaded ${results.length} patient${results.length !== 1 ? "s" : ""} from ${file.name}`; let msg = `Loaded ${results.length} patient${results.length !== 1 ? "s" : ""} from ${file.name}`;
if (skipped.length) msg += ` · ${skipped.length} skipped`; if (skipped.length) msg += ` · ${skipped.length} skipped`;
showToast(msg); showToast(msg);
@ -72,9 +76,10 @@ function AppInner() {
<div className="flex w-full min-h-screen bg-[var(--bg-page)] text-[var(--text-primary)] transition-colors"> <div className="flex w-full min-h-screen bg-[var(--bg-page)] text-[var(--text-primary)] transition-colors">
{/* Sidebar */} {/* Sidebar */}
<Sidebar <Sidebar
supplyLapsedCount={supplyLapsed} atRiskCount={atRiskCount}
escalateCount={escalateCount} actionNeededCount={actionNeededCount}
clearToShipCount={refill} clearToShipCount={refill}
onTrackCount={okCount}
activeFilter={activeFilter} activeFilter={activeFilter}
onFilterChange={setActiveFilter} onFilterChange={setActiveFilter}
onImportClick={() => csvImportRef.current?.trigger()} onImportClick={() => csvImportRef.current?.trigger()}
@ -113,8 +118,8 @@ function AppInner() {
<StatCard <StatCard
priority priority
label="Needs Attention" label="Needs Attention"
value={supplyLapsed + escalateCount + transferPending} value={supplyLapsed + visitRequired + renewalCritical + transferPending}
sub={`${supplyLapsed} docs required · ${escalateCount} escalate · ${transferPending} new patient`} sub={`${supplyLapsed} docs required · ${visitRequired + renewalCritical} escalate · ${transferPending} new patient`}
/> />
<StatCard <StatCard
label="Outreach in Progress" label="Outreach in Progress"

View file

@ -130,10 +130,10 @@ export default function Privacy() {
<br /> <br />
Email:{" "} Email:{" "}
<a <a
href="mailto:kisasttil@gmail.com" href="mailto:privacy@sttilsolutions.com"
className="text-[var(--brand,#147A7A)] underline underline-offset-2" className="text-[var(--brand,#147A7A)] underline underline-offset-2"
> >
kisasttil@gmail.com privacy@sttilsolutions.com
</a> </a>
</p> </p>
</section> </section>
@ -143,10 +143,10 @@ export default function Privacy() {
<p className="text-[11px] text-[var(--text-muted,#7A9E9E)] leading-[1.6]"> <p className="text-[11px] text-[var(--text-muted,#7A9E9E)] leading-[1.6]">
Signal is operated by STTIL Solutions LLC.{" "} Signal is operated by STTIL Solutions LLC.{" "}
<a <a
href="https://signal.sttilsolutions.com" href="https://sttilsolutions.com/signal"
className="text-[var(--brand,#147A7A)] underline underline-offset-2" className="text-[var(--brand,#147A7A)] underline underline-offset-2"
> >
signal.sttilsolutions.com sttilsolutions.com/signal
</a> </a>
{" "}· We may update this policy from time to time. We will notify active customers of material {" "}· We may update this policy from time to time. We will notify active customers of material
changes by email. Continued use of Signal following notice constitutes acceptance. changes by email. Continued use of Signal following notice constitutes acceptance.

View file

@ -1,7 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { useAuth } from "@clerk/react";
import { confirmVisit } from "../lib/api"; import { confirmVisit } from "../lib/api";
export default function ConfirmVisitModal({ record, onClose, onConfirmed }) { export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
const { getToken } = useAuth();
const [visitDate, setVisitDate] = useState(""); const [visitDate, setVisitDate] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -29,9 +31,20 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
setError("Visit date cannot be in the future."); setError("Visit date cannot be in the future.");
return; return;
} }
// Validate: visit must be within 6 months before the order/shipment date
const shipmentDateStr = computeShipmentDate();
const shipmentDateObj = new Date(shipmentDateStr + "T00:00:00");
const visitDateObj = new Date(visitDate + "T00:00:00");
const sixMonthsBeforeShipment = new Date(shipmentDateObj);
sixMonthsBeforeShipment.setMonth(sixMonthsBeforeShipment.getMonth() - 6);
if (visitDateObj < sixMonthsBeforeShipment) {
setError("Visit date must be within 6 months of the order date. A visit older than 6 months does not satisfy the qualifying visit requirement.");
return;
}
setSaving(true); setSaving(true);
try { try {
const token = await getToken().catch(() => null);
const updated = await confirmVisit({ const updated = await confirmVisit({
patient_id: record.patient_id, patient_id: record.patient_id,
confirmed_date: visitDate, confirmed_date: visitDate,
@ -40,7 +53,7 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
device_type: record.device_type, device_type: record.device_type,
quantity: 1, quantity: 1,
component: record.component || "sensor", component: record.component || "sensor",
}); }, token);
onConfirmed(record.patient_id, updated); onConfirmed(record.patient_id, updated);
onClose(); onClose();
} catch (e) { } catch (e) {

View file

@ -1,11 +1,23 @@
import { useOrganization } from "@clerk/react";
export default function Sidebar({ export default function Sidebar({
supplyLapsedCount, atRiskCount,
escalateCount, actionNeededCount,
clearToShipCount, clearToShipCount,
onTrackCount,
activeFilter, activeFilter,
onFilterChange, onFilterChange,
onImportClick, onImportClick,
}) { }) {
const { organization } = useOrganization();
const orgName = organization?.name ?? "My Organization";
const initials = orgName
.split(" ")
.slice(0, 2)
.map((w) => w[0])
.join("")
.toUpperCase();
const navItems = [ const navItems = [
{ {
label: "All Patients", label: "All Patients",
@ -14,17 +26,17 @@ export default function Sidebar({
badge: null, badge: null,
}, },
{ {
label: "Docs Required", label: "At Risk",
key: "SUPPLY_LAPSED", key: "at-risk",
icon: "!", icon: "!",
badge: supplyLapsedCount, badge: atRiskCount,
badgeWarn: true, badgeWarn: true,
}, },
{ {
label: "Escalate", label: "Action Needed",
key: "escalate", key: "action-needed",
icon: "!", icon: "!",
badge: escalateCount, badge: actionNeededCount,
badgeWarn: true, badgeWarn: true,
}, },
{ {
@ -33,6 +45,12 @@ export default function Sidebar({
icon: "✓", icon: "✓",
badge: clearToShipCount, badge: clearToShipCount,
}, },
{
label: "On Track",
key: "ACTIVE",
icon: "✓",
badge: onTrackCount,
},
]; ];
return ( return (
@ -116,11 +134,11 @@ export default function Sidebar({
<div className="px-5 py-[14px] border-t border-[rgba(15,94,94,0.5)]"> <div className="px-5 py-[14px] border-t border-[rgba(15,94,94,0.5)]">
<div className="flex items-center gap-[10px]"> <div className="flex items-center gap-[10px]">
<div className="w-[30px] h-[30px] bg-[rgba(46,163,163,0.18)] border border-[rgba(46,163,163,0.3)] rounded-full flex items-center justify-center text-[11px] text-teal-400 font-semibold shrink-0"> <div className="w-[30px] h-[30px] bg-[rgba(46,163,163,0.18)] border border-[rgba(46,163,163,0.3)] rounded-full flex items-center justify-center text-[11px] text-teal-400 font-semibold shrink-0">
DS {initials}
</div> </div>
<div> <div>
<div className="text-xs font-medium text-warm-white"> <div className="text-xs font-medium text-warm-white">
Demo Supplier {orgName}
</div> </div>
<div className="text-[10px] text-teal-300 mt-[1px]"> <div className="text-[10px] text-teal-300 mt-[1px]">
Billing Staff Billing Staff

View file

@ -162,6 +162,11 @@ export default function WorklistTable({ records, activeFilter, onFilterChange })
{r.reason} {r.reason}
</div> </div>
)} )}
{r.flag === "ACTIVE" && r.next_visit_due_date && (
<div className="text-[10.5px] text-[var(--text-muted)] mt-[4px]">
Next visit due: {new Date(r.next_visit_due_date + "T00:00:00").toLocaleDateString("en-US", { month: "short", year: "numeric" })}
</div>
)}
</td> </td>
<td className="px-[22px] py-[13px] align-middle"> <td className="px-[22px] py-[13px] align-middle">
<span className={`font-mono text-[16px] font-medium ${scoreClass(priority)}`}> <span className={`font-mono text-[16px] font-medium ${scoreClass(priority)}`}>

View file

@ -92,8 +92,10 @@ export function apiRecordToLocal(r) {
* Confirm a qualifying visit date for a patient. * Confirm a qualifying visit date for a patient.
* Returns the updated RecordOut from the backend. * Returns the updated RecordOut from the backend.
*/ */
export async function confirmVisit(payload) { export async function confirmVisit(payload, token = null) {
const authHeader = API_KEY ? { "X-API-Key": API_KEY } : {}; const authHeader = token
? { "Authorization": `Bearer ${token}` }
: API_KEY ? { "X-API-Key": API_KEY } : {};
try { try {
const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, { const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, {
method: "POST", method: "POST",

View file

@ -7,24 +7,33 @@
*/ */
export const FLAG = { export const FLAG = {
OUT_OF_COVERAGE: "OUT_OF_COVERAGE", SUPPLY_LAPSED: "SUPPLY_LAPSED",
VISIT_DUE: "VISIT_DUE", VISIT_REQUIRED: "VISIT_REQUIRED",
REFILL_WINDOW: "REFILL_WINDOW", RENEWAL_CRITICAL: "RENEWAL_CRITICAL",
OK: "OK", RENEWAL_ELEVATED: "RENEWAL_ELEVATED",
RENEWAL_SOON: "RENEWAL_SOON",
RESUPPLY_READY: "RESUPPLY_READY",
ACTIVE: "ACTIVE",
}; };
const FLAG_LABELS = { const FLAG_LABELS = {
[FLAG.OUT_OF_COVERAGE]: "Supply Lapsed", [FLAG.SUPPLY_LAPSED]: "Supply Lapsed",
[FLAG.VISIT_DUE]: "Renewal Due", [FLAG.VISIT_REQUIRED]: "Visit Required",
[FLAG.REFILL_WINDOW]: "Resupply Ready", [FLAG.RENEWAL_CRITICAL]: "Renewal Due",
[FLAG.OK]: "Active", [FLAG.RENEWAL_ELEVATED]: "Renewal Elevated",
[FLAG.RENEWAL_SOON]: "Renewal Soon",
[FLAG.RESUPPLY_READY]: "Resupply Ready",
[FLAG.ACTIVE]: "Active",
}; };
const FLAG_ACTIONS = { const FLAG_ACTIONS = {
[FLAG.OUT_OF_COVERAGE]: "Contact Prescriber", [FLAG.SUPPLY_LAPSED]: "Contact Prescriber",
[FLAG.VISIT_DUE]: "Request Renewal", [FLAG.VISIT_REQUIRED]: "Schedule Visit",
[FLAG.REFILL_WINDOW]: "Initiate Resupply", [FLAG.RENEWAL_CRITICAL]: "Request Renewal",
[FLAG.OK]: "No action needed", [FLAG.RENEWAL_ELEVATED]: "Request Renewal",
[FLAG.RENEWAL_SOON]: "Plan Renewal",
[FLAG.RESUPPLY_READY]: "Initiate Resupply",
[FLAG.ACTIVE]: "No action needed",
}; };
/** Wear-day rules — mirrors payer_rules.json */ /** Wear-day rules — mirrors payer_rules.json */
@ -79,9 +88,12 @@ function getPayerConfig(payer) {
} }
function computePriority(flag, daysUntilEnd) { function computePriority(flag, daysUntilEnd) {
if (flag === FLAG.OUT_OF_COVERAGE) return 1000 + Math.abs(daysUntilEnd); if (flag === FLAG.SUPPLY_LAPSED) return 1000 + Math.abs(daysUntilEnd);
if (flag === FLAG.VISIT_DUE) return 500 + Math.max(0, 90 - daysUntilEnd); if (flag === FLAG.VISIT_REQUIRED) return 1500 + Math.abs(daysUntilEnd);
if (flag === FLAG.REFILL_WINDOW) return 200 + Math.max(0, 30 - daysUntilEnd); if (flag === FLAG.RENEWAL_CRITICAL) return 500 + Math.max(0, 90 - daysUntilEnd);
if (flag === FLAG.RENEWAL_ELEVATED) return 700 + Math.max(0, 90 - daysUntilEnd);
if (flag === FLAG.RENEWAL_SOON) return 400 + Math.max(0, 90 - daysUntilEnd);
if (flag === FLAG.RESUPPLY_READY) return 200 + Math.max(0, 30 - daysUntilEnd);
return 0; return 0;
} }
@ -109,13 +121,19 @@ export function calculateCoverage(record) {
let flag; let flag;
if (daysUntilEnd < 0) { if (daysUntilEnd < 0) {
flag = FLAG.OUT_OF_COVERAGE; flag = FLAG.SUPPLY_LAPSED;
} else if (daysUntilVisit !== null && daysUntilVisit <= 30) { } else if (daysUntilVisit !== null && daysUntilVisit < 0) {
flag = FLAG.VISIT_DUE; flag = FLAG.VISIT_REQUIRED;
} else if (daysUntilVisit !== null && daysUntilVisit <= 45) {
flag = FLAG.RENEWAL_CRITICAL;
} else if (daysUntilVisit !== null && daysUntilVisit <= 60) {
flag = FLAG.RENEWAL_ELEVATED;
} else if (daysUntilVisit !== null && daysUntilVisit <= 90) {
flag = FLAG.RENEWAL_SOON;
} else if (daysUntilEnd <= payerCfg.refillWindowDays) { } else if (daysUntilEnd <= payerCfg.refillWindowDays) {
flag = FLAG.REFILL_WINDOW; flag = FLAG.RESUPPLY_READY;
} else { } else {
flag = FLAG.OK; flag = FLAG.ACTIVE;
} }
return { return {