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:
parent
34e6c08c14
commit
d197c529ce
10 changed files with 124 additions and 49 deletions
|
|
@ -64,7 +64,7 @@ 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-07) — 45-50% pilot-ready
|
||||
### Checklist Status (as of 2026-06-09) — 65% pilot-ready
|
||||
|
||||
| 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 |
|
||||
| Import flow: mapping, validation, warnings | PARTIAL — CSVImport exists, full mapping review unconfirmed |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
**Effective Date:** June __, 2026
|
||||
|
||||
**Between:**
|
||||
- **STTIL Solutions LLC** ("STTIL"), a limited liability company, with its principal place of business in [City, State]
|
||||
- **Gaboro DME** ("Gaboro"), 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 Philadelphia, Pennsylvania
|
||||
|
||||
Each a "Party" and together the "Parties."
|
||||
|
||||
|
|
@ -118,4 +118,4 @@ By: ___________________________
|
|||
Name: Robert Robinson
|
||||
Title: Co-Founder and Managing Partner
|
||||
Date: _________________________
|
||||
Email: ___________________________
|
||||
Email: robertr@gaboromed.com
|
||||
|
|
|
|||
|
|
@ -469,6 +469,20 @@ async def confirm_visit(
|
|||
if confirmed > date_type.today():
|
||||
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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,12 @@ function AppInner() {
|
|||
const transferPending = records.filter((r) => r.flag === "TRANSFER_PENDING").length;
|
||||
const refill = records.filter((r) => r.flag === "RESUPPLY_READY").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 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 label = new Date().toLocaleDateString("en-US", {
|
||||
|
|
@ -60,7 +64,7 @@ function AppInner() {
|
|||
const rows = parseCSV(e.target.result);
|
||||
const { results, skipped } = processBatch(rows);
|
||||
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}`;
|
||||
if (skipped.length) msg += ` · ${skipped.length} skipped`;
|
||||
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">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
supplyLapsedCount={supplyLapsed}
|
||||
escalateCount={escalateCount}
|
||||
atRiskCount={atRiskCount}
|
||||
actionNeededCount={actionNeededCount}
|
||||
clearToShipCount={refill}
|
||||
onTrackCount={okCount}
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={setActiveFilter}
|
||||
onImportClick={() => csvImportRef.current?.trigger()}
|
||||
|
|
@ -113,8 +118,8 @@ function AppInner() {
|
|||
<StatCard
|
||||
priority
|
||||
label="Needs Attention"
|
||||
value={supplyLapsed + escalateCount + transferPending}
|
||||
sub={`${supplyLapsed} docs required · ${escalateCount} escalate · ${transferPending} new patient`}
|
||||
value={supplyLapsed + visitRequired + renewalCritical + transferPending}
|
||||
sub={`${supplyLapsed} docs required · ${visitRequired + renewalCritical} escalate · ${transferPending} new patient`}
|
||||
/>
|
||||
<StatCard
|
||||
label="Outreach in Progress"
|
||||
|
|
|
|||
|
|
@ -130,10 +130,10 @@ export default function Privacy() {
|
|||
<br />
|
||||
Email:{" "}
|
||||
<a
|
||||
href="mailto:kisasttil@gmail.com"
|
||||
href="mailto:privacy@sttilsolutions.com"
|
||||
className="text-[var(--brand,#147A7A)] underline underline-offset-2"
|
||||
>
|
||||
kisasttil@gmail.com
|
||||
privacy@sttilsolutions.com
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -143,10 +143,10 @@ export default function Privacy() {
|
|||
<p className="text-[11px] text-[var(--text-muted,#7A9E9E)] leading-[1.6]">
|
||||
Signal is operated by STTIL Solutions LLC.{" "}
|
||||
<a
|
||||
href="https://signal.sttilsolutions.com"
|
||||
href="https://sttilsolutions.com/signal"
|
||||
className="text-[var(--brand,#147A7A)] underline underline-offset-2"
|
||||
>
|
||||
signal.sttilsolutions.com
|
||||
sttilsolutions.com/signal
|
||||
</a>
|
||||
{" "}· 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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { useAuth } from "@clerk/react";
|
||||
import { confirmVisit } from "../lib/api";
|
||||
|
||||
export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
||||
const { getToken } = useAuth();
|
||||
const [visitDate, setVisitDate] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -29,9 +31,20 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
|||
setError("Visit date cannot be in the future.");
|
||||
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);
|
||||
try {
|
||||
const token = await getToken().catch(() => null);
|
||||
const updated = await confirmVisit({
|
||||
patient_id: record.patient_id,
|
||||
confirmed_date: visitDate,
|
||||
|
|
@ -40,7 +53,7 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
|
|||
device_type: record.device_type,
|
||||
quantity: 1,
|
||||
component: record.component || "sensor",
|
||||
});
|
||||
}, token);
|
||||
onConfirmed(record.patient_id, updated);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
import { useOrganization } from "@clerk/react";
|
||||
|
||||
export default function Sidebar({
|
||||
supplyLapsedCount,
|
||||
escalateCount,
|
||||
atRiskCount,
|
||||
actionNeededCount,
|
||||
clearToShipCount,
|
||||
onTrackCount,
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
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 = [
|
||||
{
|
||||
label: "All Patients",
|
||||
|
|
@ -14,17 +26,17 @@ export default function Sidebar({
|
|||
badge: null,
|
||||
},
|
||||
{
|
||||
label: "Docs Required",
|
||||
key: "SUPPLY_LAPSED",
|
||||
label: "At Risk",
|
||||
key: "at-risk",
|
||||
icon: "!",
|
||||
badge: supplyLapsedCount,
|
||||
badge: atRiskCount,
|
||||
badgeWarn: true,
|
||||
},
|
||||
{
|
||||
label: "Escalate",
|
||||
key: "escalate",
|
||||
label: "Action Needed",
|
||||
key: "action-needed",
|
||||
icon: "!",
|
||||
badge: escalateCount,
|
||||
badge: actionNeededCount,
|
||||
badgeWarn: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -33,6 +45,12 @@ export default function Sidebar({
|
|||
icon: "✓",
|
||||
badge: clearToShipCount,
|
||||
},
|
||||
{
|
||||
label: "On Track",
|
||||
key: "ACTIVE",
|
||||
icon: "✓",
|
||||
badge: onTrackCount,
|
||||
},
|
||||
];
|
||||
|
||||
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="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">
|
||||
DS
|
||||
{initials}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-warm-white">
|
||||
Demo Supplier
|
||||
{orgName}
|
||||
</div>
|
||||
<div className="text-[10px] text-teal-300 mt-[1px]">
|
||||
Billing Staff
|
||||
|
|
|
|||
|
|
@ -162,6 +162,11 @@ export default function WorklistTable({ records, activeFilter, onFilterChange })
|
|||
{r.reason}
|
||||
</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 className="px-[22px] py-[13px] align-middle">
|
||||
<span className={`font-mono text-[16px] font-medium ${scoreClass(priority)}`}>
|
||||
|
|
|
|||
|
|
@ -92,8 +92,10 @@ export function apiRecordToLocal(r) {
|
|||
* Confirm a qualifying visit date for a patient.
|
||||
* Returns the updated RecordOut from the backend.
|
||||
*/
|
||||
export async function confirmVisit(payload) {
|
||||
const authHeader = API_KEY ? { "X-API-Key": API_KEY } : {};
|
||||
export async function confirmVisit(payload, token = null) {
|
||||
const authHeader = token
|
||||
? { "Authorization": `Bearer ${token}` }
|
||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -7,24 +7,33 @@
|
|||
*/
|
||||
|
||||
export const FLAG = {
|
||||
OUT_OF_COVERAGE: "OUT_OF_COVERAGE",
|
||||
VISIT_DUE: "VISIT_DUE",
|
||||
REFILL_WINDOW: "REFILL_WINDOW",
|
||||
OK: "OK",
|
||||
SUPPLY_LAPSED: "SUPPLY_LAPSED",
|
||||
VISIT_REQUIRED: "VISIT_REQUIRED",
|
||||
RENEWAL_CRITICAL: "RENEWAL_CRITICAL",
|
||||
RENEWAL_ELEVATED: "RENEWAL_ELEVATED",
|
||||
RENEWAL_SOON: "RENEWAL_SOON",
|
||||
RESUPPLY_READY: "RESUPPLY_READY",
|
||||
ACTIVE: "ACTIVE",
|
||||
};
|
||||
|
||||
const FLAG_LABELS = {
|
||||
[FLAG.OUT_OF_COVERAGE]: "Supply Lapsed",
|
||||
[FLAG.VISIT_DUE]: "Renewal Due",
|
||||
[FLAG.REFILL_WINDOW]: "Resupply Ready",
|
||||
[FLAG.OK]: "Active",
|
||||
[FLAG.SUPPLY_LAPSED]: "Supply Lapsed",
|
||||
[FLAG.VISIT_REQUIRED]: "Visit Required",
|
||||
[FLAG.RENEWAL_CRITICAL]: "Renewal Due",
|
||||
[FLAG.RENEWAL_ELEVATED]: "Renewal Elevated",
|
||||
[FLAG.RENEWAL_SOON]: "Renewal Soon",
|
||||
[FLAG.RESUPPLY_READY]: "Resupply Ready",
|
||||
[FLAG.ACTIVE]: "Active",
|
||||
};
|
||||
|
||||
const FLAG_ACTIONS = {
|
||||
[FLAG.OUT_OF_COVERAGE]: "Contact Prescriber",
|
||||
[FLAG.VISIT_DUE]: "Request Renewal",
|
||||
[FLAG.REFILL_WINDOW]: "Initiate Resupply",
|
||||
[FLAG.OK]: "No action needed",
|
||||
[FLAG.SUPPLY_LAPSED]: "Contact Prescriber",
|
||||
[FLAG.VISIT_REQUIRED]: "Schedule Visit",
|
||||
[FLAG.RENEWAL_CRITICAL]: "Request Renewal",
|
||||
[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 */
|
||||
|
|
@ -79,9 +88,12 @@ function getPayerConfig(payer) {
|
|||
}
|
||||
|
||||
function computePriority(flag, daysUntilEnd) {
|
||||
if (flag === FLAG.OUT_OF_COVERAGE) return 1000 + Math.abs(daysUntilEnd);
|
||||
if (flag === FLAG.VISIT_DUE) return 500 + Math.max(0, 90 - daysUntilEnd);
|
||||
if (flag === FLAG.REFILL_WINDOW) return 200 + Math.max(0, 30 - daysUntilEnd);
|
||||
if (flag === FLAG.SUPPLY_LAPSED) return 1000 + Math.abs(daysUntilEnd);
|
||||
if (flag === FLAG.VISIT_REQUIRED) return 1500 + Math.abs(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;
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +121,19 @@ export function calculateCoverage(record) {
|
|||
|
||||
let flag;
|
||||
if (daysUntilEnd < 0) {
|
||||
flag = FLAG.OUT_OF_COVERAGE;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit <= 30) {
|
||||
flag = FLAG.VISIT_DUE;
|
||||
flag = FLAG.SUPPLY_LAPSED;
|
||||
} else if (daysUntilVisit !== null && daysUntilVisit < 0) {
|
||||
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) {
|
||||
flag = FLAG.REFILL_WINDOW;
|
||||
flag = FLAG.RESUPPLY_READY;
|
||||
} else {
|
||||
flag = FLAG.OK;
|
||||
flag = FLAG.ACTIVE;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue