import { useState, useEffect, Fragment } from "react"; import { useAuth } from "@clerk/react"; import { VerdictBadge } from "./Badge"; import StatusLegend from "./StatusLegend"; import ConfirmVisitModal from "./ConfirmVisitModal"; import { updateDocStatus } from "../lib/api"; // P6 patient-nested worklist (Kisa-approved mockup, 2026-07-07). // One row per patient carrying the rollup verdict; expands into device // coverage lines with their doc checklists and rule citations. Green is // earned only from the readiness verdict. Tabs count PATIENTS. // The five status labels are locked; "Data gap" is a filter + chip, never // a sixth label (Kisa Decision E). const DOC_LABELS = { swo: "SWO", visit: "Qualifying Visit", pecos: "PECOS Enrollment", pa: "Prior Authorization", diagnosis: "Diagnosis on File", }; // doc_type -> the canonical CSV field whose mapping decides whether an // absent item is a FILE gap (column never mapped) or a PATIENT gap. const DOC_COLUMN = { swo: "csv_swo_status", visit: "csv_visit_date", pecos: "csv_pecos_verified", pa: "csv_pa_status", diagnosis: "csv_diagnosis_on_file", }; const SWO_CYCLE = ["pending", "requested", "on_file"]; const PA_CYCLE = ["not_required", "requested", "approved", "denied"]; const DOC_STATUS_LABELS = { pending: "Pending", requested: "Requested", on_file: "On File", not_required: "Not Required", approved: "Approved", denied: "Denied", }; // Timing hints are descriptive phrases, never status labels (locked sets). const TIMING_HINTS = { SUPPLY_LAPSED: "Supply lapsed", VISIT_REQUIRED: "Visit overdue", RENEWAL_CRITICAL: "Visit renewal critical", RENEWAL_ELEVATED: "Visit renewal due soon", RENEWAL_SOON: "Visit renewal approaching", RESUPPLY_READY: "In resupply window", ACTIVE: "Timing on track", NO_RECENT_SHIPMENT: "No recent shipment", TRANSFER_PENDING: "Transfer pending", }; const TABS = [ { key: "all", label: "All Patients", color: "#5A7E7E" }, { key: "At Risk", label: "At Risk", color: "#CC2222" }, { key: "Action Needed", label: "Action Needed", color: "#CB6B20" }, { key: "On Track", label: "On Track", color: "#2EA3A3" }, { key: "Clear to Ship", label: "Clear to Ship", color: "#22C55E", check: true }, { key: "Plan Type Needed", label: "Plan Type Needed", color: "#9B8EC4" }, ]; function itemIsFileGap(item, mappedFields) { // REQUIRED only: NOT_EVALUATED items (plan type unknown) belong to the // Plan Type Needed treatment, never double-flagged as data gaps. return ( item.input_state === "ABSENT" && item.required_state === "REQUIRED" && !(DOC_COLUMN[item.doc_type] in mappedFields) ); } function patientFileGaps(p, mappedFields) { const gaps = new Set(); for (const line of p.lines || []) { for (const item of line.doc_items || []) { if (itemIsFileGap(item, mappedFields)) gaps.add(item.doc_type); } } return [...gaps]; } function lineToEcho(line) { const by = {}; (line.doc_items || []).forEach((i) => { by[i.doc_type] = i.value; }); return { device_type: line.device_type, gradeable: line.gradeable, plan_type: line.plan_type, shipment_date: line.shipment_date || null, csv_swo_status: by.swo || null, csv_visit_date: by.visit || null, csv_pecos_verified: by.pecos || null, csv_pa_status: by.pa || null, csv_diagnosis_on_file: by.diagnosis || null, }; } function lineToModalRecord(p, line) { const by = {}; (line.doc_items || []).forEach((i) => { by[i.doc_type] = i.value; }); return { patient_id: p.patient_id, last_shipment_date: line.shipment_date, payer: line.payer || "", device_type: line.device_type, quantity: line.total_quantity || 1, plan_type: line.plan_type, csv_visit_date: by.visit || null, csv_swo_status: by.swo || null, csv_pecos_verified: by.pecos || null, csv_pa_status: by.pa || null, csv_diagnosis_on_file: by.diagnosis || null, _echo_lines: (p.lines || []).map(lineToEcho), }; } // Merge a P4 recompute response ({lines, rollup_status}) into local state. // Line identity is (device_type, plan_type) — a patient can hold the same // device under two plan types (typed + unknown), so device alone is not a key. function mergeRecompute(p, recomputed) { if (!recomputed?.lines) return p; const lineKey = (l) => `${l.device_type}:${l.plan_type ?? ""}`; const byKey = {}; recomputed.lines.forEach((l) => { byKey[lineKey(l)] = l; }); return { ...p, rollup_status: recomputed.rollup_status ?? p.rollup_status, lines: p.lines.map((line) => { const upd = byKey[lineKey(line)]; return upd ? { ...line, line_status: upd.line_status, doc_items: upd.doc_items ?? line.doc_items } : line; }), }; } export default function PatientWorklist({ data, onImportClick }) { const { getToken } = useAuth(); const [patients, setPatients] = useState(data.patients || []); const [tab, setTab] = useState("all"); const [refine, setRefine] = useState(null); // null | "gaps" | "outreach" const [expanded, setExpanded] = useState(null); // patient_id const [confirming, setConfirming] = useState(null); // {record, patientId} const [showAllSkipped, setShowAllSkipped] = useState(false); useEffect(() => { setPatients(data.patients || []); setExpanded(null); }, [data]); const mappedFields = data.mapping_summary?.mapped || {}; const skippedReasons = data.skipped_reasons || []; const skippedCount = data.skipped || 0; const gradeable = patients.filter((p) => p.rollup_status); const gapPatients = patients.filter( (p) => patientFileGaps(p, mappedFields).length > 0 ); const outreachPatients = patients.filter( (p) => ["At Risk", "Action Needed"].includes(p.rollup_status) && patientFileGaps(p, mappedFields).length === 0 ); // Single derivation for all counts: always from the local patients state. const counts = { all: patients.length }; TABS.slice(1).forEach((t) => { counts[t.key] = gradeable.filter((p) => p.rollup_status === t.key).length; }); let visible = tab === "all" ? patients : patients.filter((p) => p.rollup_status === tab); if (refine === "gaps") { visible = visible.filter((p) => patientFileGaps(p, mappedFields).length > 0); } else if (refine === "outreach") { visible = visible.filter((p) => outreachPatients.includes(p)); } function downloadSkipped() { const blob = new Blob( ["Signal — rows not processed\n\n" + skippedReasons.join("\n") + "\n"], { type: "text/plain;charset=utf-8;" } ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `signal-not-processed-${new Date().toISOString().slice(0, 10)}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function applyRecompute(patientId, recomputed) { setPatients((prev) => prev.map((p) => (p.patient_id === patientId ? mergeRecompute(p, recomputed) : p)) ); } const ungraded = patients.length - gradeable.length; return (
{/* Header + patient tabs */}
Outreach Worklist
{patients.length} patients · {gradeable.length} gradeable {ungraded > 0 && ` · ${ungraded} informational only`} {skippedCount > 0 && ` · ${skippedCount} rows not processed`}
{TABS.map((t) => { const count = counts[t.key] ?? 0; const pct = t.key !== "all" && gradeable.length > 0 ? Math.round((count / gradeable.length) * 100) : null; const isActive = tab === t.key; const indicatorColor = isActive ? "white" : t.color; return ( ); })}
{/* Refine pills — separates fix-the-file work from outreach work */}
Refine: Data-gap patients keep their honest status; this separates fix-the-file work from call-the-prescriber work
{/* Not-processed strip — rows Signal never scores and never hides */} {skippedCount > 0 && (
{skippedCount} row{skippedCount !== 1 ? "s" : ""} not processed {" "} — Signal never scores a row whose required information was not provided, and never hides one:
    {(showAllSkipped ? skippedReasons : skippedReasons.slice(0, 3)).map((r, i) => (
  • {r}
  • ))}
{skippedReasons.length > 3 && !showAllSkipped && ( )}
)} {/* Patient table */} {["", "Patient", "Device(s)", "Payer", "Status", "Last Resupply", "Actions"].map((h, i) => ( ))} {visible.map((p) => { const isExpanded = expanded === p.patient_id; const gaps = patientFileGaps(p, mappedFields); const isPTN = p.rollup_status === "Plan Type Needed"; const capped = p.plan_type_needed && !isPTN; const lastShip = (p.lines || []) .map((l) => l.shipment_date) .filter(Boolean) .sort() .pop(); return ( setExpanded(isExpanded ? null : p.patient_id)} > {isExpanded && (p.lines || []).map((line) => ( ))} ); })} {visible.length === 0 && ( )}
{h}
{isExpanded ? "▼" : "▶"} {p.patient_id} {(p.lines || []).map((l) => l.device_display).join(", ")} {(p.lines || [])[0]?.payer || "—"} {capped && (
Capped: includes a Plan Type Needed line
)} {gaps.length > 0 && !isPTN && (
Data gap — {gaps.map((g) => DOC_LABELS[g]).join(", ")} column not provided in the file
)} {!p.rollup_status && (
Non-CGM device — displayed, not graded, excluded from counts
)}
{lastShip || "—"} e.stopPropagation()}> {isPTN && ( )} {gaps.length > 0 && !isPTN && ( ◇ Fix the file, not the phone )}
applyRecompute(p.patient_id, rec)} onConfirmVisit={() => setConfirming({ record: lineToModalRecord(p, line), patientId: p.patient_id }) } />
No patients match this filter.
{/* Footer */}
PHI-safe — patient names and DOBs never stored. Crosswalk: patient_id only. {visible.length} of {patients.length} patients shown
{confirming && ( setConfirming(null)} onConfirmed={(patientId, updated) => applyRecompute(patientId, updated)} /> )}
); } function DeviceLine({ patient, line, mappedFields, getToken, onRecomputed, onConfirmVisit }) { const items = line.doc_items || []; const visitItem = items.find((i) => i.doc_type === "visit"); const showConfirmVisit = line.gradeable && visitItem && !visitItem.satisfied; return (
{line.device_display} {line.plan_type || "Plan type needed"} {line.timing_flag && ( {TIMING_HINTS[line.timing_flag] || line.timing_flag} )} {line.order_numbers?.length ? `Orders: ${line.order_numbers.join(", ")} · ` : ""} {line.hcpcs_codes?.length ? `HCPCS: ${line.hcpcs_codes.join(", ")} · ` : ""} qty {line.total_quantity} {line.prior_shipment_count > 0 && ` · ${line.prior_shipment_count} prior shipment${line.prior_shipment_count !== 1 ? "s" : ""}`}
{line.gradeable && items.length > 0 ? (
{items.map((item) => ( ))} {showConfirmVisit && ( )}
) : (
Non-CGM device — no readiness actions.
)}
); } function ChecklistItem({ patient, line, item, fileGap, getToken, onRecomputed }) { const [saving, setSaving] = useState(false); const cyclable = item.doc_type === "swo" ? SWO_CYCLE : item.doc_type === "pa" ? PA_CYCLE : null; const icon = fileGap ? { glyph: "◇", cls: "text-[#6B4F9A]" } : item.satisfied ? item.required_state === "NOT_REQUIRED" && item.input_state === "ABSENT" ? { glyph: "—", cls: "text-[var(--text-warm)]" } : { glyph: "✓", cls: "text-[#166534]" } : item.quality === "BAD" ? { glyph: "✗", cls: "text-[#CC2222]" } : { glyph: "✗", cls: "text-[#CB6B20]" }; const valueText = fileGap ? "Column not mapped" : item.value || (item.required_state === "NOT_REQUIRED" ? "Not required" : "Not on file"); async function handleCycle() { if (!cyclable || saving || fileGap) return; const currentKey = Object.entries(DOC_STATUS_LABELS).find(([, v]) => v === item.value)?.[0]; const idx = currentKey ? cyclable.indexOf(currentKey) : -1; const nextKey = cyclable[(idx + 1) % cyclable.length]; setSaving(true); try { const token = getToken ? await getToken().catch(() => null) : null; const result = await updateDocStatus( patient.patient_id, item.doc_type, nextKey, null, null, token, line.device_type, (patient.lines || []).map(lineToEcho) ); if (result?.lines) onRecomputed(result); } finally { setSaving(false); } } return (
{icon.glyph}
{DOC_LABELS[item.doc_type]}
{cyclable && !fileGap ? ( ) : (
{valueText}
)}
{item.rule_id ? `${item.rule_id}` : ""} {fileGap ? " · resolves at re-import or mapping review" : ""}
); }