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`
|
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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)}`}>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue