import { useState, useEffect, useRef, Fragment } from "react"; import { useTheme } from "../useTheme"; import { useAuth } from "@clerk/react"; import Badge from "./Badge"; import DocStatusBar from "./DocStatusBar"; import ConfirmVisitModal from "./ConfirmVisitModal"; import StatusLegend from "./StatusLegend"; import { updateDocStatus } from "../lib/api"; function daysLabel(days, flag) { if (days === undefined || days === null) return ; if (days < 0) return Expired {Math.abs(days)}d ago; // RESUPPLY_READY: days left means window closing, not a problem — show as teal urgency not red alert if (flag === "RESUPPLY_READY") { if (days <= 7) return {days} days; if (days <= 30) return {days} days; return {days} days; } if (days <= 7) return {days} days; if (days <= 30) return {days} days; return {days} days; } function scoreClass(priority) { if (priority >= 1000) return "text-[#B00000]"; if (priority >= 500) return "text-[var(--accent-text)]"; if (priority >= 200) return "text-[var(--text-secondary)]"; return "text-[var(--text-warm)]"; } const FILTERS = [ { key: "all", label: "All", color: null, check: false }, { key: "at-risk", label: "At Risk", color: "#E53E3E", check: false }, { key: "action-needed", label: "Action Needed", color: "#F59E0B", check: false }, { key: "RESUPPLY_READY", label: "Clear to Ship", color: "#22C55E", check: true }, { key: "ACTIVE", label: "On Track", color: "#2EA3A3", check: false }, ]; 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", }; const AT_RISK_FLAGS = ["SUPPLY_LAPSED", "VISIT_REQUIRED", "RENEWAL_CRITICAL"]; const ACTION_NEEDED_FLAGS = ["RENEWAL_ELEVATED", "RENEWAL_SOON", "TRANSFER_PENDING"]; const HIGH_PRIORITY_FLAGS = ["SUPPLY_LAPSED", "VISIT_REQUIRED", "RENEWAL_CRITICAL", "TRANSFER_PENDING"]; const CONFIRM_VISIT_FLAGS = ["VISIT_REQUIRED", "RENEWAL_CRITICAL", "RENEWAL_ELEVATED", "RENEWAL_SOON"]; export default function WorklistTable({ records, activeFilter, onFilterChange, onVisitConfirmed }) { const { dark } = useTheme(); const { getToken } = useAuth(); const [expandedRow, setExpandedRow] = useState(() => localStorage.getItem("signal_expanded_row") || null); const [activePatientId, setActivePatientId] = useState(null); const [confirmingRecord, setConfirmingRecord] = useState(null); const [localRecords, setLocalRecords] = useState(records); const [toast, setToast] = useState({ visible: false, message: "" }); const [localDocStates, setLocalDocStates] = useState({}); const rowRefs = useRef({}); useEffect(() => { setLocalRecords(records); setLocalDocStates({}); }, [records]); function showToast(message) { setToast({ visible: true, message }); setTimeout(() => setToast({ visible: false, message: "" }), 5000); } function handleSaveSuccess({ patient_id: savedPatientId }) { setActivePatientId(prev => (prev === savedPatientId ? null : prev)); const EXCLUDED_FLAGS = ["ACTIVE", "NO_RECENT_SHIPMENT"]; const next = localRecords .filter(r => !EXCLUDED_FLAGS.includes(r.flag) && r.patient_id !== savedPatientId) .sort((a, b) => (b.priority_score || b.priority || 0) - (a.priority_score || a.priority || 0))[0]; if (next) { const label = next.reason || next.flag || ""; showToast(`Done. Next: Patient ${next.patient_id}: ${label}`); } else { showToast("Done. Your list is clear."); } } function handleVisitConfirmed(patientId, updatedRecord) { setLocalRecords(prev => prev.map(r => r.patient_id === patientId ? { ...r, ...updatedRecord } : r) .sort((a, b) => (b.priority_score || b.priority || 0) - (a.priority_score || a.priority || 0)) ); onVisitConfirmed?.(patientId, updatedRecord); } const filtered = activeFilter === "all" ? localRecords : activeFilter === "at-risk" ? localRecords.filter((r) => AT_RISK_FLAGS.includes(r.flag)) : activeFilter === "action-needed" ? localRecords.filter((r) => ACTION_NEEDED_FLAGS.includes(r.flag)) : localRecords.filter((r) => r.flag === activeFilter); const todayStr = new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); const total = localRecords.length; const tabCounts = { all: total, "at-risk": localRecords.filter(r => AT_RISK_FLAGS.includes(r.flag)).length, "action-needed": localRecords.filter(r => ACTION_NEEDED_FLAGS.includes(r.flag)).length, RESUPPLY_READY: localRecords.filter(r => r.flag === "RESUPPLY_READY" && !(r.cascade?.length > 0)).length, ACTIVE: localRecords.filter(r => r.flag === "ACTIVE").length, }; return (
{/* Header */}
Outreach Worklist
{localRecords.length} patients · sorted by priority score · {todayStr}
{FILTERS.map((f) => { const count = tabCounts[f.key] ?? 0; const pct = total > 0 && f.key !== "all" ? Math.round((count / total) * 100) : null; const isActive = activeFilter === f.key; const indicatorColor = isActive ? "white" : f.color; return ( ); })}
{/* Working-on banner */} {activePatientId && (
{ const targetKey = Object.keys(rowRefs.current).find(k => k.startsWith(activePatientId + "-")); if (targetKey) { setExpandedRow(targetKey); localStorage.setItem("signal_expanded_row", targetKey); rowRefs.current[targetKey]?.scrollIntoView({ behavior: "smooth", block: "center" }); } }} > Working on Patient {activePatientId}, click to return
)} {/* Table */} {["Patient ID", "Device", "Payer", "Days Left", "Status", "Priority", "Action"].map(h => ( ))} {filtered.map((r, i) => { const hp = HIGH_PRIORITY_FLAGS.includes(r.flag); const isStale = r.flag === "NO_RECENT_SHIPMENT"; const rowKey = r.patient_id + "-" + i; const isExpanded = expandedRow === rowKey; // NO_RECENT_SHIPMENT rows never show Confirm Visit — those patients need prescriber // verification first, not a visit date entry. CONFIRM_VISIT_FLAGS does not include // NO_RECENT_SHIPMENT, so the estimated-visit fallback check is also gated. const showConfirmVisit = !isStale && (CONFIRM_VISIT_FLAGS.includes(r.flag) || r.visit_date_confidence === "estimated"); // Support both API shape (days_until_coverage_end) and legacy local shape (daysUntilEnd) const daysLeft = r.days_until_coverage_end ?? r.daysUntilEnd; const priority = r.priority_score ?? r.priority; const deviceDisplay = r.device_display || r.device_type; // RESUPPLY_READY with open cascade items means timing is fine but docs are not. // Show amber "Docs Required" badge instead of green "Clear to Ship". const effectiveFlag = (r.flag === "RESUPPLY_READY" && r.cascade?.length > 0) ? "DOCS_REQUIRED" : r.flag; return ( { rowRefs.current[rowKey] = el; }} className={`border-b border-[var(--border-subtle)] transition-colors cursor-pointer hover:bg-[var(--row-hover)] ${ isStale ? "opacity-50" : hp ? dark ? "bg-[rgba(224,104,48,0.09)] hover:bg-[rgba(203,107,32,0.14)]" : "bg-[rgba(224,96,40,0.05)] hover:bg-[rgba(203,107,32,0.14)]" : "" }`} onClick={() => { if (isExpanded) { // Collapsing — clear active if this is the active patient setActivePatientId(prev => (prev === r.patient_id ? null : prev)); setExpandedRow(null); localStorage.removeItem("signal_expanded_row"); } else { // Expanding — mark as active if showConfirmVisit if (showConfirmVisit) { setActivePatientId(r.patient_id); } setExpandedRow(rowKey); localStorage.setItem("signal_expanded_row", rowKey); } }} > {/* Expanded row — cascade + doc checklist */} {isExpanded && ( )} ); })} {filtered.length === 0 && ( )}
{h}
{r.patient_id}
{i === 0 && (
★ TOP PRIORITY
)} {r.visit_date_confidence === "estimated" && (
Estimated visit date
)}
{deviceDisplay} {r.payer} {daysLabel(daysLeft, r.flag)}
{r.doc_state && }
{r.reason && (
{r.reason}
)} {r.flag === "ACTIVE" && r.next_visit_due_date && (
Next visit due: {new Date(r.next_visit_due_date + "T00:00:00").toLocaleDateString("en-US", { month: "short", year: "numeric" })}
)}
{priority} e.stopPropagation()}> setConfirmingRecord(r)} onExpand={(e) => { e.stopPropagation(); if (isExpanded) { setActivePatientId(prev => (prev === r.patient_id ? null : prev)); setExpandedRow(null); localStorage.removeItem("signal_expanded_row"); } else { if (showConfirmVisit) setActivePatientId(r.patient_id); setExpandedRow(rowKey); localStorage.setItem("signal_expanded_row", rowKey); } }} />
{r.cascade && r.cascade.length > 0 && (
Documentation Actions Required
    {r.cascade.map((item, idx) => { const isDenied = /denied/i.test(item); const color = isDenied ? "#CC2222" : "#CB6B20"; return (
  • {item}
  • ); })}
)} {r.doc_state && (
Documentation Checklist
setLocalDocStates(prev => ({ ...prev, [r.patient_id]: { ...prev[r.patient_id], [type]: label } }))} /> setLocalDocStates(prev => ({ ...prev, [r.patient_id]: { ...prev[r.patient_id], [type]: label } }))} />
)} {!r.doc_state && !r.cascade?.length && (
No documentation details available. Upload a CSV with doc columns to see the full checklist.
)}
No patients match this filter.
{/* Footer */}
PHI-safe — patient names and DOBs never stored. Crosswalk: patient_id only. {activeFilter === "all" ? `${localRecords.length} patients · all results shown` : `${filtered.length} of ${localRecords.length} · filtered`}
{/* Confirm Visit Modal */} {confirmingRecord && ( setConfirmingRecord(null)} onConfirmed={handleVisitConfirmed} onSaveSuccess={handleSaveSuccess} /> )} {/* Next-priority toast */} {toast.visible && (
{toast.message}
)}
); } function ActionCell({ flag, cascade, showConfirmVisit, onConfirmVisit, onExpand }) { const hasCascade = cascade && cascade.length > 0; const accentBtn = "bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[10px] py-[4px] rounded-md text-[11px] cursor-pointer font-body whitespace-nowrap transition-all hover:bg-[rgba(203,107,32,0.1)] self-start"; const mutedBtn = "bg-transparent border border-[var(--border-color)] text-[var(--text-secondary)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:border-[var(--brand)] hover:text-[var(--brand)]"; if (flag === "RESUPPLY_READY") { return (
Ready to ship
); } if (hasCascade) { return (
{showConfirmVisit ? ( ) : ( )}
); } if (showConfirmVisit) { return ( ); } if (flag === "TRANSFER_PENDING") { return ; } if (flag === "NO_RECENT_SHIPMENT" || flag === "SUPPLY_LAPSED") { return ; } return null; } function DocItem({ label, value, patientId, docType, cycle, getToken, onUpdated }) { const [saving, setSaving] = useState(false); const isGood = value && ( value === "On File" || value === "Verified" || value === "Not Required" || value.startsWith("Confirmed") || value.startsWith("Approved") || value === "N/A" ); const isBad = value && ( value === "Missing" || value === "Not Verified" || value === "Expired" || value === "Denied" || value === "Required — Not Started" || value === "Pending" ); const color = isGood ? "text-[#1A8040]" : isBad ? "text-[#CC2222]" : "text-[#CB6B20]"; const currentKey = Object.entries(DOC_STATUS_LABELS).find(([, v]) => v === value)?.[0]; const handleCycle = async () => { if (!cycle || !patientId || saving) return; const idx = currentKey ? cycle.indexOf(currentKey) : -1; const nextKey = cycle[(idx + 1) % cycle.length]; const nextLabel = DOC_STATUS_LABELS[nextKey]; setSaving(true); const token = getToken ? await getToken().catch(() => null) : null; const result = await updateDocStatus(patientId, docType, nextKey, null, null, token); setSaving(false); if (result && onUpdated) onUpdated(docType, nextLabel); }; return (
{label}: {cycle ? ( ) : ( {value || "Unknown"} )}
); }