Signal/signal-ui/src/components/ConfirmVisitModal.jsx
Kisa 1d95bb2ab2 feat: doc status persistence, empty state, status legend, and Clear to Ship guard
- 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>
2026-06-14 05:09:06 -04:00

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>
);
}