// schedule.jsx — Day / Week / Month views wired to /api/appointments. const { useState: useStateS, useMemo: useMemoS, useEffect: useEffectS } = React; function startOfDay(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; } function startOfWeek(d) { // ISO-style: week starts on Monday. const x = startOfDay(d); const day = x.getDay(); // 0..6, Sun..Sat const diff = (day === 0 ? -6 : 1 - day); x.setDate(x.getDate() + diff); return x; } function startOfMonth(d) { const x = startOfDay(d); x.setDate(1); return x; } function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; } function sameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } function dateInputValue(d) { const x = d ? new Date(d) : new Date(); if (isNaN(x)) return ""; const yyyy = x.getFullYear(); const mm = String(x.getMonth() + 1).padStart(2, "0"); const dd = String(x.getDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } function timeInputValue(d) { const x = d ? new Date(d) : new Date(); if (isNaN(x)) return "09:00"; return `${String(x.getHours()).padStart(2, "0")}:${String(x.getMinutes()).padStart(2, "0")}`; } function slotDateFromPointer(baseDay, event, hours) { const rect = event.currentTarget.getBoundingClientRect(); const y = Math.max(0, Math.min(rect.height, event.clientY - rect.top)); const minutesFromStart = Math.round((y / (64 / 60)) / 15) * 15; const startHour = hours && hours.length ? hours[0] : 0; const maxMinutes = Math.max(0, ((hours && hours.length ? hours.length : 24) * 60) - 15); const clamped = Math.min(maxMinutes, minutesFromStart); const next = new Date(baseDay); next.setHours(startHour, 0, 0, 0); next.setMinutes(next.getMinutes() + clamped); return next; } function Schedule({ onOpenEncounter, initialPatient }) { const store = window.MSStore.useStore(); const t = window.MSStore.useT(); const [view, setView] = useStateS("day"); const [providerFilter, setProviderFilter] = useStateS("all"); const [cursor, setCursor] = useStateS(() => startOfDay(new Date())); const [bookingOpen, setBookingOpen] = useStateS(false); const [bookingDraft, setBookingDraft] = useStateS(null); const account = store.activeAccount || {}; const bizStart = account.business_start_hour != null ? account.business_start_hour : 9; const bizEnd = account.business_end_hour != null ? account.business_end_hour : 18; const HOURS = Array.from({length: 24}, (_, i) => i); // 24-hour calendar const providers = (store.providers || []); const apps = (store.appointments || []).filter(a => providerFilter === "all" || a.providerId === providerFilter); const visibleProviders = providers.filter(p => providerFilter === "all" || p.id === providerFilter); function shiftCursor(delta) { const next = new Date(cursor); if (view === "day") next.setDate(next.getDate() + delta); else if (view === "week") next.setDate(next.getDate() + delta * 7); else next.setMonth(next.getMonth() + delta); if (view === "week") setCursor(startOfWeek(next)); else if (view === "month") setCursor(startOfMonth(next)); else setCursor(startOfDay(next)); } function goToday() { const today = new Date(); if (view === "week") setCursor(startOfWeek(today)); else if (view === "month") setCursor(startOfMonth(today)); else setCursor(startOfDay(today)); } function setViewKeepAnchor(next) { setView(next); if (next === "week") setCursor(c => startOfWeek(c)); else if (next === "month") setCursor(c => startOfMonth(c)); else setCursor(c => startOfDay(c)); } function openBooking(draft) { setBookingDraft(draft || null); setBookingOpen(true); } useEffectS(() => { if (!initialPatient) return; openBooking({ patient: initialPatient }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialPatient && initialPatient.id]); // Current header label / sub let titleDate, headerSub; if (view === "day") { titleDate = cursor.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { month: "long", day: "numeric", year: "numeric" }); headerSub = cursor.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { weekday: "long", month: "long", day: "numeric" }); } else if (view === "week") { const end = addDays(cursor, 6); titleDate = `${cursor.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { month: "short", day: "numeric" })} – ${end.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { month: "short", day: "numeric", year: "numeric" })}`; headerSub = t("sched.weekOf", { date: cursor.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { weekday: "long", month: "long", day: "numeric" }) }); } else { titleDate = cursor.toLocaleDateString(window.MSi18n && window.MSi18n.getLang() === "es" ? "es-ES" : "en-US", { month: "long", year: "numeric" }); headerSub = titleDate; } // Day-bucket appointments (used by week + day for counts) const dayApps = apps.filter(a => { const t = new Date(a.scheduledAt); if (isNaN(t)) return false; if (view === "day") return sameDay(t, cursor); if (view === "week") return t >= cursor && t < addDays(cursor, 7); return t >= cursor && t < new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1); }); return (

{t("sched.title")}

{headerSub} · {t("sched.sub", { n: dayApps.length })}
{titleDate}
{providers.map(p => ( ))}
{visibleProviders.length === 0 ? (
{t("sched.noProviders")}
) : view === "day" ? ( ) : view === "week" ? ( ) : ( { setView("day"); setCursor(startOfDay(d)); }}/> )}
{bookingOpen && ( { setBookingOpen(false); setBookingDraft(null); }} /> )}
); } function BookingDrawer({ cursor, providers, initialDraft, onClose }) { const store = window.MSStore.useStore(); const t = window.MSStore.useT(); const draftDate = initialDraft && initialDraft.scheduledAt ? new Date(initialDraft.scheduledAt) : null; const initialDate = dateInputValue(draftDate || cursor || new Date()); const defaultProvider = (providers && providers[0] && providers[0].id) || ""; const initialPatient = initialDraft && initialDraft.patient; const initialPatientId = initialPatient && initialPatient.id || ""; const [patientQuery, setPatientQuery] = useStateS(initialPatient ? `${initialPatient.first || ""} ${initialPatient.last || ""}`.trim() : ""); const [patientOptions, setPatientOptions] = useStateS([]); const [patientLoading, setPatientLoading] = useStateS(false); const [form, setForm] = useStateS({ patientMode: initialPatient ? "existing" : "existing", selectedPatientId: initialPatientId, patientName: initialPatient ? `${initialPatient.first || ""} ${initialPatient.last || ""}`.trim() : "", patientPhone: initialPatient && initialPatient.phone || "", patientEmail: initialPatient && initialPatient.email || "", date: initialDate, time: draftDate ? timeInputValue(draftDate) : "09:00", duration: "30", providerId: (initialDraft && initialDraft.providerId) || defaultProvider, reason: "", notes: "", }); const [pending, setPending] = useStateS(false); const [error, setError] = useStateS(""); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); function applySelectedPatient(patientId) { const p = patientOptions.find(p => (p.id || p.name) === patientId); const name = p ? (p.name || `${p.first || ""} ${p.last || ""}`.trim()) : ""; setForm(f => ({ ...f, selectedPatientId: patientId, patientName: name, patientPhone: p ? (p.phone || "") : "", patientEmail: p ? (p.email || "") : "", })); if (name) setPatientQuery(name); } useEffectS(() => { let cancelled = false; setPatientLoading(true); store.searchCalendarPatients(patientQuery).then(rows => { if (cancelled) return; setPatientOptions(Array.isArray(rows) ? rows : []); }).catch(() => { if (!cancelled) setPatientOptions([]); }).finally(() => { if (!cancelled) setPatientLoading(false); }); return () => { cancelled = true; }; }, [patientQuery, store.version]); useEffectS(() => { if (!form.providerId && providers && providers[0]) { set("providerId", providers[0].id); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [providers && providers.length]); async function submit() { if (pending) return; const patientName = form.patientName.trim(); if (!patientName) { setError(t("sched.errors.patient")); return; } if (!form.date || !form.time) { setError(t("sched.errors.time")); return; } setPending(true); setError(""); try { const scheduledAt = new Date(`${form.date}T${form.time}:00`).toISOString(); const assignedProvider = form.providerId || (providers && providers[0] && providers[0].id) || ""; await store.createAppointment({ patient_name: patientName, patient_phone: form.patientPhone.trim(), patient_email: form.patientEmail.trim(), doctor_id: assignedProvider, scheduled_at: scheduledAt, duration_minutes: Number(form.duration) || 30, reason: form.reason.trim(), notes: form.notes.trim(), status: "confirmed", }); onClose(); } catch (e) { setError(e && e.message || t("sched.errors.create")); } finally { setPending(false); } } return (
e.target === e.currentTarget && onClose()}>
{t("sched.bookTitle")}
{form.patientMode === "existing" ? (
{ setPatientQuery(e.target.value); setForm(f => ({...f, selectedPatientId: "", patientName: ""})); }} placeholder="Search patients from database"/> {!patientLoading && patientQuery.trim() && patientOptions.length === 0 && ( )}
) : ( set("patientName", e.target.value)} placeholder={t("sched.patientNamePh")}/> )}
set("patientPhone", e.target.value)}/>
set("patientEmail", e.target.value)}/>
set("date", e.target.value)}/>
set("time", e.target.value)}/>
set("reason", e.target.value)} placeholder={t("sched.reasonPh")}/>