// ─── Calendar Page ─────────────────────────────────────────────────────────── const STATUS_APPT = { scheduled: { color: 'blue', label: 'Scheduled' }, completed: { color: 'green', label: 'Completed' }, cancelled: { color: 'red', label: 'Cancelled' }, rescheduled: { color: 'amber', label: 'Rescheduled' }, }; function ApptModal({ appt, onClose, onSave }) { const isEdit = !!appt; const now = new Date(); const pad = n => String(n).padStart(2, '0'); const defaultStart = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate()+1)}T10:00`; const defaultEnd = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate()+1)}T11:00`; const initForm = isEdit ? { title: appt.title || '', contact_name: appt.contact_name || '', contact_phone: appt.contact_phone || '', scheduled_start: new Date(appt.scheduled_start).toISOString().slice(0, 16), scheduled_end: appt.scheduled_end ? new Date(appt.scheduled_end).toISOString().slice(0, 16) : '', notes: appt.notes || '', status: appt.status || 'scheduled' } : { title: '', contact_name: '', contact_phone: '', scheduled_start: defaultStart, scheduled_end: defaultEnd, notes: '', status: 'scheduled' }; const [form, setForm] = React.useState(initForm); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(''); const set = k => v => setForm(p => ({ ...p, [k]: v })); const handleSave = async () => { setSaving(true); setError(''); try { const res = await fetch(isEdit ? `/api/appointments/${appt.id}` : '/api/appointments', { method: isEdit ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || data.detail || 'Failed to save'); onSave(data.appointment || data, isEdit); onClose(); } catch (e) { setError(e.message); } finally { setSaving(false); } }; return (
e.stopPropagation()}>
{isEdit ? 'Edit Appointment' : 'New Appointment'}
{isEdit && (
Status
)} {error &&
{error}
}
Cancel {saving ? 'Saving…' : (isEdit ? 'Save Changes' : 'Create Appointment')}
); } function CalendarPage() { const [view, setView] = React.useState('Month'); const [editingAppt, setEditingAppt] = React.useState(null); const [showModal, setShowModal] = React.useState(false); const [appts, setAppts] = React.useState([]); const [loading, setLoading] = React.useState(true); const [curDate, setCurDate] = React.useState(new Date()); React.useEffect(() => { fetch('/api/appointments') .then(r => r.json()) .then(data => { setAppts(Array.isArray(data) ? data : []); setLoading(false); }) .catch(() => { setLoading(false); }); }, []); const curMonth = new Date(curDate.getFullYear(), curDate.getMonth(), 1); const monthName = curMonth.toLocaleString('en-US', { month: 'long', year: 'numeric' }); const weekName = `Week of ${new Date(curDate.setDate(curDate.getDate() - curDate.getDay())).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; const dayName = curDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); const firstDay = curMonth.getDay(); const daysInMonth = new Date(curMonth.getFullYear(), curMonth.getMonth() + 1, 0).getDate(); const cells = Array(firstDay).fill(null).concat(Array.from({ length: daysInMonth }, (_, i) => i + 1)); while (cells.length % 7 !== 0) cells.push(null); const today = new Date(); const getApptForDay = (d, m, y) => appts.filter(a => { const ad = new Date(a.scheduled_start); return ad.getDate() === d && ad.getMonth() === m && ad.getFullYear() === y; }).sort((a, b) => new Date(a.scheduled_start) - new Date(b.scheduled_start)); const upcoming = [...appts] .filter(a => a.status === 'scheduled' && new Date(a.scheduled_start) >= new Date()) .sort((a, b) => new Date(a.scheduled_start) - new Date(b.scheduled_start)) .slice(0, 20); const prevRange = () => { if (view === 'Month') setCurDate(new Date(curDate.getFullYear(), curDate.getMonth() - 1, 1)); else if (view === 'Week') setCurDate(new Date(curDate.getFullYear(), curDate.getMonth(), curDate.getDate() - 7)); else setCurDate(new Date(curDate.getFullYear(), curDate.getMonth(), curDate.getDate() - 1)); }; const nextRange = () => { if (view === 'Month') setCurDate(new Date(curDate.getFullYear(), curDate.getMonth() + 1, 1)); else if (view === 'Week') setCurDate(new Date(curDate.getFullYear(), curDate.getMonth(), curDate.getDate() + 7)); else setCurDate(new Date(curDate.getFullYear(), curDate.getMonth(), curDate.getDate() + 1)); }; const handleDayClick = (day) => { if (!day) return; setCurDate(new Date(curMonth.getFullYear(), curMonth.getMonth(), day)); setView('Day'); }; const renderApptBlock = (a) => (
{ e.stopPropagation(); setEditingAppt(a); setShowModal(true); }} style={{ cursor: 'pointer', background: 'rgba(90,126,245,0.2)', borderRadius: 4, padding: '4px 6px', fontSize: 11, color: '#a0b0f0', marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', borderLeft: `2px solid ${STATUS_APPT[a.status]?.color === 'green' ? '#34d399' : (STATUS_APPT[a.status]?.color === 'red' ? '#f87171' : '#5a7ef5')}` }}> {new Date(a.scheduled_start).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }).replace(' ', '')} {a.contact_name} - {a.title}
); return (
{ setEditingAppt(null); setShowModal(true); }}>+ New Appointment} /> {loading ? : (
{view === 'Month' ? monthName : (view === 'Week' ? weekName : dayName)}
{view === 'Month' && ( <>
{['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].map(d => (
{d}
))}
{cells.map((day, i) => { const dayAppts = day ? getApptForDay(day, curMonth.getMonth(), curMonth.getFullYear()) : []; const isToday = day && today.getMonth() === curMonth.getMonth() && today.getFullYear() === curMonth.getFullYear() && day === today.getDate(); return (
handleDayClick(day)} style={{ cursor: day ? 'pointer' : 'default', minHeight: 90, padding: 4, borderRadius: 8, background: isToday ? 'rgba(90,126,245,0.08)' : 'transparent', border: isToday ? '1px solid rgba(90,126,245,0.3)' : '1px solid transparent', transition: 'background 0.2s' }}> {day &&
{day}
} {dayAppts.slice(0, 3).map((a) => renderApptBlock(a))} {dayAppts.length > 3 &&
+{dayAppts.length - 3} more
}
); })}
)} {view === 'Week' && (
{Array.from({length: 7}).map((_, i) => { const startOfWeek = new Date(curDate); startOfWeek.setDate(curDate.getDate() - curDate.getDay() + i); const isToday = startOfWeek.toDateString() === today.toDateString(); const dayAppts = getApptForDay(startOfWeek.getDate(), startOfWeek.getMonth(), startOfWeek.getFullYear()); return (
{ setCurDate(startOfWeek); setView('Day'); }}> {['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][i]} {startOfWeek.getDate()}
{dayAppts.map(a => renderApptBlock(a))}
); })}
)} {view === 'Day' && (
{getApptForDay(curDate.getDate(), curDate.getMonth(), curDate.getFullYear()).length === 0 ? (
No appointments for this day.
) : getApptForDay(curDate.getDate(), curDate.getMonth(), curDate.getFullYear()).map(a => (
{ setEditingAppt(a); setShowModal(true); }} style={{ cursor: 'pointer', background: 'rgba(255,255,255,0.03)', borderLeft: `3px solid ${STATUS_APPT[a.status]?.color === 'green' ? '#34d399' : (STATUS_APPT[a.status]?.color === 'red' ? '#f87171' : '#5a7ef5')}`, borderRadius: 8, padding: '16px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{a.title}
{a.contact_name} · {a.contact_phone}
{a.notes &&
{a.notes}
}
{new Date(a.scheduled_start).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true })}
{STATUS_APPT[a.status]?.label || a.status}
))}
)}
{upcoming.length === 0 ? (
No upcoming appointments.
) : upcoming.map(a => (
{ setEditingAppt(a); setShowModal(true); }} style={{ cursor: 'pointer', padding: '12px 18px', borderBottom: '1px solid rgba(255,255,255,0.04)', transition: 'background 0.2s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{a.title}
{STATUS_APPT[a.status]?.label || a.status}
{a.contact_name} · {a.contact_phone}
{new Date(a.scheduled_start).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true })}
))}
)} {showModal && ( { setShowModal(false); setEditingAppt(null); }} onSave={(appt, isEdit) => { if (isEdit) { setAppts(p => p.map(a => a.id === appt.id ? appt : a)); } else { setAppts(p => [appt, ...p]); } }} /> )}
); } Object.assign(window, { CalendarPage });