- Doc status click-to-update: SWO and PA cycle through states and persist to Supabase - Status legend added to worklist header - Empty state with file-open prompt for first-time users - Device normalizer expanded for broader CGM variant coverage - Warm cream text applied globally for secondary/muted UI elements - Drag-drop CSV import support - CORS root cause fixed via Railway ALLOWED_ORIGINS env var - DOCS_REQUIRED badge: RESUPPLY_READY patients with open cascade items now show amber Docs Required instead of green Clear to Ship, preventing premature shipment Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
186 lines
7.5 KiB
JavaScript
186 lines
7.5 KiB
JavaScript
import { useState } from "react";
|
|
import { useAuth } from "@clerk/react";
|
|
import { confirmVisit } from "../lib/api";
|
|
|
|
export default function ConfirmVisitModal({ record, onClose, onConfirmed, onSaveSuccess }) {
|
|
const { getToken } = useAuth();
|
|
const [visitDate, setVisitDate] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
const [step, setStep] = useState("entry"); // "entry" | "confirm"
|
|
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const minDate = "2020-01-01";
|
|
|
|
// Real shipment date from the backend record. The legacy reconstruction from
|
|
// coverage_end_date is a fallback for records loaded before last_shipment_date
|
|
// existed; it never fabricates today's date.
|
|
function computeShipmentDate() {
|
|
if (record.last_shipment_date) return record.last_shipment_date;
|
|
if (record.coverage_end_date && record.days_until_coverage_end !== undefined) {
|
|
const coverageEnd = new Date(record.coverage_end_date);
|
|
const shipmentMs = coverageEnd.getTime() - record.days_until_coverage_end * 86400000;
|
|
return new Date(shipmentMs).toISOString().split("T")[0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function handleValidateAndAdvance() {
|
|
setError("");
|
|
if (!visitDate) {
|
|
setError("Please enter a visit date.");
|
|
return;
|
|
}
|
|
if (visitDate > today) {
|
|
setError("Visit date cannot be in the future.");
|
|
return;
|
|
}
|
|
// Validate: visit must be within 6 months (183 days, matching the backend)
|
|
// before the order/shipment date
|
|
const shipmentDateStr = computeShipmentDate();
|
|
if (!shipmentDateStr) {
|
|
setError("This record has no shipment date on file. Re-import the CSV and try again.");
|
|
return;
|
|
}
|
|
const shipmentDateObj = new Date(shipmentDateStr + "T00:00:00");
|
|
const visitDateObj = new Date(visitDate + "T00:00:00");
|
|
const sixMonthsBeforeShipment = new Date(shipmentDateObj.getTime() - 183 * 86400000);
|
|
if (visitDateObj < sixMonthsBeforeShipment) {
|
|
setError("Visit date must be within 6 months of the order date.");
|
|
return;
|
|
}
|
|
setStep("confirm");
|
|
}
|
|
|
|
async function handleConfirmAndSave() {
|
|
setSaving(true);
|
|
try {
|
|
const token = await getToken().catch(() => null);
|
|
const updated = await confirmVisit({
|
|
patient_id: record.patient_id,
|
|
confirmed_date: visitDate,
|
|
shipment_date: computeShipmentDate(),
|
|
payer: record.payer,
|
|
device_type: record.device_type,
|
|
quantity: record.quantity || 1,
|
|
component: record.component || "sensor",
|
|
// Echo doc status fields so the backend recompute preserves
|
|
// checklist state instead of regressing it to defaults
|
|
csv_visit_date: record.csv_visit_date || null,
|
|
csv_swo_status: record.csv_swo_status || null,
|
|
csv_pecos_verified: record.csv_pecos_verified || null,
|
|
csv_pa_status: record.csv_pa_status || null,
|
|
csv_diagnosis_on_file: record.csv_diagnosis_on_file || null,
|
|
csv_transfer_from: record.csv_transfer_from || null,
|
|
order_number: record.order_number || null,
|
|
hcpcs: record.hcpcs || null,
|
|
}, token);
|
|
onConfirmed(record.patient_id, updated);
|
|
onSaveSuccess?.({ patient_id: record.patient_id });
|
|
onClose();
|
|
} catch (e) {
|
|
setError(e.message || "Failed to save. Please try again.");
|
|
setStep("entry");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
const formattedVisitDate = visitDate
|
|
? new Date(visitDate + "T00:00:00").toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })
|
|
: "";
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
|
|
<div
|
|
className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[12px] shadow-xl p-6 w-[420px] max-w-[95vw]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{step === "entry" ? (
|
|
<>
|
|
<div className="font-heading font-bold text-[15px] text-[var(--text-heading)] mb-1">
|
|
Confirm Qualifying Visit
|
|
</div>
|
|
<div className="text-[12px] text-[var(--text-warm)] mb-4">
|
|
Patient ID: <span className="font-mono font-semibold text-[var(--text-secondary)]">{record.patient_id}</span>
|
|
</div>
|
|
|
|
<label className="block text-[12px] font-semibold text-[var(--text-secondary)] mb-1">
|
|
Qualifying visit date, confirmed with prescriber office or from SWO on file
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={visitDate}
|
|
max={today}
|
|
min={minDate}
|
|
onChange={(e) => setVisitDate(e.target.value)}
|
|
className="w-full border border-[var(--border-color)] rounded-[6px] px-3 py-2 text-[13px] bg-[var(--bg-elevated)] text-[var(--text-primary)] mb-2 focus:outline-none focus:border-[var(--brand)]"
|
|
/>
|
|
<div className="text-[10.5px] text-[var(--text-warm)] mb-4">
|
|
Contact prescriber office to confirm. Do not use patient-reported date.
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-[11px] text-[#CC2222] mb-3 px-2 py-1 bg-red-50 rounded border border-red-200">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 justify-end">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-[12px] rounded-md border border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--brand)] cursor-pointer"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleValidateAndAdvance}
|
|
className="px-4 py-2 text-[12px] rounded-md bg-[var(--brand)] text-white font-semibold hover:opacity-90 cursor-pointer"
|
|
>
|
|
Save Visit Date
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="font-heading font-bold text-[15px] text-[var(--text-heading)] mb-4">
|
|
One last look
|
|
</div>
|
|
|
|
<div className="text-[13px] text-[var(--text-secondary)] mb-1">
|
|
Patient ID: <span className="font-mono font-semibold">{record.patient_id}</span>
|
|
</div>
|
|
<div className="text-[13px] text-[var(--text-secondary)] mb-4">
|
|
Visit date: <span className="font-semibold">{formattedVisitDate}</span>
|
|
</div>
|
|
<div className="text-[11.5px] text-[var(--text-warm)] mb-5">
|
|
Confirm this is the verified date from the prescriber office.
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-[11px] text-[#CC2222] mb-3 px-2 py-1 bg-red-50 rounded border border-red-200">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 justify-end">
|
|
<button
|
|
onClick={() => { setStep("entry"); setError(""); }}
|
|
className="px-4 py-2 text-[12px] rounded-md border border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--brand)] cursor-pointer"
|
|
>
|
|
Go back
|
|
</button>
|
|
<button
|
|
onClick={handleConfirmAndSave}
|
|
disabled={saving}
|
|
className="px-4 py-2 text-[12px] rounded-md bg-[var(--brand)] text-white font-semibold hover:opacity-90 cursor-pointer disabled:opacity-50"
|
|
>
|
|
{saving ? "Saving..." : "Confirm and save"}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|