Signal/signal-ui/src/components/ConfirmVisitModal.jsx
Kisa 6181de74ed feat: patient-nested worklist — the true-green display (plan 02, P6)
The worklist now shows one row per patient carrying the rollup verdict,
expanding into device coverage lines with doc checklists and rule citations.
Green is earned only from the readiness verdict. Kisa-approved design
(mockup v2, decisions A/B/C/D/E locked 2026-07-07): purple Plan Type Needed
badge + Map-the-plan-type CTA; ◇ column-not-mapped chips; not-processed
strip with download; Data-gaps filter (honest labels, no sixth status);
extended legend; per-line work-queue export (backend lines[] support,
audit-logged). Tabs count patients. SWO/PA cycling is device-scoped and
returns the re-rolled patient through the P4 echo contract; Confirm Visit
fans out across the patient's echoed lines. Legacy table remains at
?legacy=1 (its DOCS_REQUIRED stopgap intentionally kept while it exists —
literal deviation from the plan's remove-in-same-change line, logged).
Independent audit: SHIP; its MEDIUM (line-identity merge collision) and two
LOWs fixed in-commit. 171 backend tests green; frontend builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 06:37:04 -04:00

191 lines
7.9 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,
// P6 nested view: plan type + the patient's echoed lines so the
// response carries the re-rolled patient (visit fans out to all
// lines). Legacy records lack these — behavior unchanged.
...(record.plan_type ? { plan_type: record.plan_type } : {}),
...(record._echo_lines ? { lines: record._echo_lines } : {}),
}, 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>
);
}