// patients.jsx — Patient list (grouped by appt) + full-page chart view // Wired to /api/patients, /api/patients/profile, POST /api/patients, // PUT /api/patients/update via the store. const { useState: usePSt, useMemo: usePMemo, useCallback: usePCb, useEffect: usePEf } = React; // ── Helpers ────────────────────────────────────────────────────────────────── function daysSince(iso) { if (!iso) return 999; const d = new Date(iso); if (isNaN(d)) return 999; return Math.round((Date.now() - d.getTime()) / 86400000); } // ── VitalSpark (inline sparkline) ─────────────────────────────────────────── function VitalSparkline({ values, warn, width = 56, height = 24 }) { if (!values || values.length === 0) { return ( ); } const max = Math.max(...values), min = Math.min(...values); const range = max - min || 1; const pts = values.map((v, i) => { const x = values.length === 1 ? width / 2 : (i / (values.length - 1)) * width; const y = height - ((v - min) / range) * (height - 4) - 2; return `${x},${y}`; }).join(" "); const color = warn ? "var(--accent-danger)" : "var(--primary)"; const lastX = values.length === 1 ? width / 2 : width; const lastY = height - ((values[values.length - 1] - min) / range) * (height - 4) - 2; return ( ); } // ── PatientRow ─────────────────────────────────────────────────────────────── function PatientRow({ p, onOpen, todayAppt, lastReason, isFavorite, onToggleFavorite }) { const t = window.MSStore.useT(); const open = () => onOpen(p.id); return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); } }} >
{p.avatar}
{p.first} {p.last} {isFavorite ? <> {t("pat.favorite")} : (p.age ? <>{p.age}{p.sex || ""} : (p.phone || p.email || ""))}
{p.lastVisit ? new Date(p.lastVisit).toLocaleDateString("en-US", {month: "short", day: "numeric"}) : "—"} {lastReason || (p.total_visits ? `${p.total_visits} visit${p.total_visits === 1 ? "" : "s"}` : "No visits yet")}
); } // ── Add patient modal ──────────────────────────────────────────────────────── function AddPatientModal({ onClose, onAdd }) { const [form, setForm] = usePSt({ first: "", last: "", dob: "", sex: "F", phone: "", email: "", insurance: "", guardian: "", flags: "", chronic: "", }); const [pending, setPending] = usePSt(false); const [error, setError] = usePSt(""); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const valid = form.first.trim() && form.last.trim(); async function handleSubmit() { if (!valid || pending) return; setError(""); setPending(true); try { const fullName = `${form.first.trim()} ${form.last.trim()}`.trim(); let age = ""; if (form.dob) { const d = new Date(form.dob); if (!isNaN(d)) { const ageNum = Math.floor((Date.now() - d.getTime()) / (365.25 * 86400000)); if (ageNum >= 0 && ageNum < 130) age = String(ageNum); } } await onAdd({ name: fullName, age, phone: form.phone, email: form.email }); onClose(); } catch (e) { setError(e && e.message || "Could not add patient."); } finally { setPending(false); } } return (
e.target === e.currentTarget && onClose()}>
Add patient
Demographics
set("first", e.target.value)}/>
set("last", e.target.value)}/>
set("dob", e.target.value)}/>
Contact
set("phone", e.target.value)}/>
set("email", e.target.value)}/>
{error && (
{error}
)}
); } // ── Edit patient drawer ───────────────────────────────────────────────────── function PatientEditDrawer({ p, onClose, onSave }) { const [form, setForm] = usePSt({ age: p.age || "", phone: p.phone || "", email: p.email || "", }); const [pending, setPending] = usePSt(false); const [error, setError] = usePSt(""); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); async function submit() { setError(""); setPending(true); try { await onSave(form); onClose(); } catch (e) { setError(e && e.message || "Could not save changes."); } finally { setPending(false); } } return (
e.target === e.currentTarget && onClose()}>
Edit patient
Patient · {p.first} {p.last}
set("age", e.target.value)} placeholder="38"/>
set("phone", e.target.value)}/>
set("email", e.target.value)}/>
Renaming a patient is handled at the next encounter — the directory is keyed by the recorded name.
{error && (
{error}
)}
); } function VitalsEditDrawer({ patient, vitals, onClose, onSave }) { const [form, setForm] = usePSt({ bp: vitals.bp || "", hr: vitals.hr || "", temp: vitals.temp || "", spo2: vitals.spo2 || "", weight: vitals.weight || "", }); const [pending, setPending] = usePSt(false); const [error, setError] = usePSt(""); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); async function submit() { setPending(true); setError(""); try { await onSave(form); onClose(); } catch (e) { setError(e && e.message || "Could not save vitals."); } finally { setPending(false); } } return (
e.target === e.currentTarget && onClose()}>
Edit vitals
{patient.first} {patient.last}
set("bp", e.target.value)} placeholder="120/80"/>
set("hr", e.target.value)} placeholder="72"/>
set("temp", e.target.value)} placeholder="98.6 F"/>
set("spo2", e.target.value)} placeholder="98"/>
set("weight", e.target.value)} placeholder="160 lb"/>
{!vitals.session_id &&
Vitals can be saved after the patient has at least one real encounter.
} {error &&
{error}
}
); } // ── PatientChart (full-page) ──────────────────────────────────────────────── function buildVitalTrend(history) { const points = []; for (const h of (history || []).slice().reverse()) { const bp = h.vitals && h.vitals.bp; if (bp) { const sys = parseInt(String(bp).split("/")[0], 10); if (!isNaN(sys)) points.push(sys); } } if (points.length >= 2) { return { label: "BP (sys)", values: points, unit: "mmHg sys", warn: false }; } return null; } function PatientChart({ patient, onBack, onOpenEncounter, onSchedule, isFavorite, onToggleFavorite }) { const store = window.MSStore.useStore(); const t = window.MSStore.useT(); const [tab, setTab] = usePSt("overview"); const [editing, setEditing] = usePSt(false); const [editingVitals, setEditingVitals] = usePSt(false); const [deleting, setDeleting] = usePSt(false); const [deleteError, setDeleteError] = usePSt(""); const [profile, setProfile] = usePSt(null); const [profileError, setProfileError] = usePSt(null); const [loading, setLoading] = usePSt(true); usePEf(() => { let cancelled = false; setLoading(true); setProfileError(null); store.getPatientProfile(patient.id).then(data => { if (cancelled) return; setProfile(data); setLoading(false); }).catch(e => { if (cancelled) return; setProfileError(e && e.message || "Could not load patient"); setLoading(false); }); return () => { cancelled = true; }; }, [patient.id]); const upcoming = (store.appointments || []).filter(a => a.patientName && a.patientName.toLowerCase() === (`${patient.first} ${patient.last}`).toLowerCase()); const vitals = (profile && profile.latest_vitals) || {}; const aggregated = (profile && profile.aggregated) || { diagnoses: [], medications: [], allergies: [] }; const history = (profile && profile.history) || []; const vitalTrend = buildVitalTrend(history); async function handleSave(form) { await store.updatePatient(patient.id, { age: form.age || "", phone: form.phone || "", email: form.email || "", }); const fresh = await store.getPatientProfile(patient.id); setProfile(fresh); } async function handleSaveVitals(form) { await store.updateSessionVitals(vitals.session_id, form); const fresh = await store.getPatientProfile(patient.id); setProfile(fresh); } async function handleDelete() { if (!window.confirm(`Delete ${patient.first} ${patient.last}'s patient record and visit history?`)) return; setDeleting(true); setDeleteError(""); try { await store.deletePatient(patient.id); onBack(); } catch (e) { setDeleteError(e && e.message || "Could not delete patient."); } finally { setDeleting(false); } } return (
{editing && setEditing(false)} onSave={handleSave}/>} {editingVitals && setEditingVitals(false)} onSave={handleSaveVitals}/>} {/* Banner */}
{patient.avatar}

{patient.first} {patient.last}

{(aggregated.allergies || []).map(f => ⚠ {f})} {(aggregated.diagnoses || []).slice(0, 3).map(c => {c})}
{patient.age ? `${patient.age} yrs` : "Age unknown"} · {patient.phone || "—"} · {patient.email || "—"}
{patient.phone && } {patient.email && } {upcoming.length > 0 && ( )} {upcoming.length === 0 && ( )}
{deleteError &&
{deleteError}
} {/* Body */}
{/* Left rail */}
{t("chart.rail.vitals")} {vitals && vitals.session_id && · {t("chart.rail.latestVisit")}}
{[ { label: "BP", val: vitals.bp || "—" }, { label: "HR", val: vitals.hr ? vitals.hr + " bpm" : "—" }, { label: "Temp", val: vitals.temp || "—" }, { label: "SpO₂", val: vitals.spo2 || "—" }, { label: "Weight", val: vitals.weight || "—" }, ].map(v => (
{v.label}
{v.val}
))}
{(aggregated.diagnoses || []).length > 0 && (
{t("chart.rail.conditions")}
{aggregated.diagnoses.map(c => (
{c}
))}
)} {(aggregated.allergies || []).length > 0 && (
{t("chart.rail.alerts")}
{aggregated.allergies.map(f => (
{f}
))}
)}
{t("chart.rail.medications")}
{(aggregated.medications || []).length === 0 &&
{t("chart.rail.noMeds")}
} {(aggregated.medications || []).map(m => (
{m}
))}
{t("chart.rail.contact")}
{t("chart.rail.phone")} {patient.phone || "—"}
{t("chart.rail.email")} {patient.email || "—"}
{/* Main content */}
{["overview", "notes", "medications", "labs", "documents"].map(tabId => ( ))}
{loading && (
{t("chart.loading")}
)} {!loading && profileError && (
{profileError}
)} {!loading && !profileError && tab === "overview" && (
{vitalTrend && (
{t("chart.trendLabel", { label: vitalTrend.label })} {t("chart.trendSub", { n: vitalTrend.values.length })}
{vitalTrend.values[vitalTrend.values.length - 1]} {vitalTrend.unit}
)}
{t("chart.history")} {t("chart.historyCount", { n: history.length })}
{history.length === 0 && (
{t("chart.noVisits")}
)} {history.map((v, i) => { const date = (v.started_at || v.date || "").slice(0, 10); const reason = (v.diagnoses && v.diagnoses[0]) || v.chief_complaint || "Visit"; return ( ); })}
)} {!loading && !profileError && tab === "notes" && (
{history.length === 0 && (
{t("chart.noNotes")}
)} {history.map((v, i) => (
{(v.diagnoses && v.diagnoses[0]) || "Visit"} {(v.started_at || "").slice(0, 10)}
{v.session_id && }
{v.chief_complaint ? <>{t("chart.chiefComplaint")}{v.chief_complaint} : {t("chart.noChiefComplaint")}}
))}
)} {!loading && !profileError && tab === "medications" && ( (aggregated.medications || []).length === 0 ? (
{t("chart.rail.noMeds")}
) : (
{(aggregated.medications || []).map(m => (
{m}
))}
) )} {!loading && !profileError && tab === "labs" && (
{t("chart.noLabs")}
)} {!loading && !profileError && tab === "documents" && (
{t("chart.noDocs")}
)}
); } // ── Patients (main component) ──────────────────────────────────────────────── function Patients({ onOpenEncounter, onSchedulePatient, focusId }) { const store = window.MSStore.useStore(); const t = window.MSStore.useT(); const [adding, setAdding] = usePSt(false); const [q, setQ] = usePSt(""); const [chartId, setChartId] = usePSt(focusId || null); const [sortFilter, setSortFilter] = usePSt("all"); const [favoriteIds, setFavoriteIds] = usePSt(() => { try { return JSON.parse(localStorage.getItem("medsync-favorite-patients") || "[]"); } catch (_) { return []; } }); const favoriteSet = usePMemo(() => new Set(favoriteIds), [favoriteIds]); const toggleFavorite = usePCb((id) => { setFavoriteIds(prev => { const next = prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]; try { localStorage.setItem("medsync-favorite-patients", JSON.stringify(next)); } catch (_) {} return next; }); }, []); const todayAppointments = usePMemo(() => { const map = {}; const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today.getTime() + 86400000); for (const a of (store.appointments || [])) { const t = new Date(a.scheduledAt); if (isNaN(t)) continue; if (t >= today && t < tomorrow && a.patientName) { const k = a.patientName.toLowerCase(); if (!map[k] || t < new Date(map[k].scheduledAt)) map[k] = a; } } return map; }, [store.appointments]); const filtered = usePMemo(() => { let list = [...(store.patients || [])]; if (q) { const ql = q.toLowerCase(); list = list.filter(p => `${p.first} ${p.last}`.toLowerCase().includes(ql) || (p.phone || "").includes(q) || (p.email || "").toLowerCase().includes(ql) ); } if (sortFilter === "favorites") list = list.filter(p => favoriteSet.has(p.id)); if (sortFilter === "today") list = list.filter(p => todayAppointments[(`${p.first} ${p.last}`).toLowerCase()]); return list; }, [q, sortFilter, store.patients, todayAppointments, favoriteSet]); const grouped = usePMemo(() => { const groups = [[], [], []]; for (const p of filtered) { const todayAppt = todayAppointments[(`${p.first} ${p.last}`).toLowerCase()]; let g; if (todayAppt) g = 0; else g = daysSince(p.lastVisit) <= 30 ? 1 : 2; groups[g].push(p); } return groups; }, [filtered, todayAppointments]); const openChart = usePCb((id) => setChartId(id), []); const backToList = usePCb(() => setChartId(null), []); if (chartId) { const patient = (store.patients || []).find(p => p.id === chartId); if (!patient) { return (
Patient not found
); } return ; } async function handleAdd(form) { await store.createPatient(form); } const totalPatients = (store.patients || []).length; const isLoading = store.loading && store.loading.patients; const groupLabels = [t("pat.groupToday"), t("pat.groupRecent"), t("pat.groupEarlier")]; return (
{adding && setAdding(false)} onAdd={handleAdd}/>}

{t("pat.title")}

{isLoading ? t("pat.loading") : t("pat.count", { n: totalPatients })} {(store.errors && store.errors.patients) && {store.errors.patients}}
{/* Toolbar */}
setQ(e.target.value)} placeholder={t("pat.searchPh")}/>
{[["all", t("common.all")], ["today", t("common.today")], ["favorites", t("pat.favorites")]].map(([v, label]) => ( ))}
{/* Column headers */}
{t("pat.col.patient")}
{t("pat.col.lastVisit")}
{/* Grouped rows */}
{grouped.map((group, gi) => group.length === 0 ? null : (
{groupLabels[gi]}
{group.map(p => { const todayAppt = todayAppointments[(`${p.first} ${p.last}`).toLowerCase()]; const lastReason = (p.raw && p.raw.diagnoses && p.raw.diagnoses[0]) || (p.total_visits ? `${p.total_visits} visit${p.total_visits === 1 ? "" : "s"}` : null); return ; })}
))} {filtered.length === 0 && !isLoading && (
{q ? "No patients match your search" : "No patients yet — click Add patient to create one."}
)}
); } window.Patients = Patients;