// ============================================================ // MOORE AI v13 — CALENDAR TAB // Month view + Week view with time slots (like GHL/Google Cal) // ============================================================ const CalendarView = ({ calEvents, calView, setCalView, calDate, setCalDate, loadingTabs, fetchCalendar, calendars, selectedCalendarId, setSelectedCalendarId }) => { const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate(); const getFirstDayOfMonth = (year, month) => new Date(year, month, 1).getDay(); const year = calDate.getFullYear(); const month = calDate.getMonth(); const daysInMonth = getDaysInMonth(year, month); const firstDay = getFirstDayOfMonth(year, month); const today = new Date(); // Current time indicator const [nowMinutes, setNowMinutes] = React.useState(() => today.getHours() * 60 + today.getMinutes()); React.useEffect(() => { const t = setInterval(() => setNowMinutes(new Date().getHours() * 60 + new Date().getMinutes()), 60000); return () => clearInterval(t); }, []); // Week view: get the Sunday-starting week that contains calDate const getWeekStart = (d) => { const s = new Date(d); s.setDate(s.getDate() - s.getDay()); s.setHours(0, 0, 0, 0); return s; }; const weekStart = getWeekStart(calDate); const weekDays = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(weekStart.getDate() + i); return d; }); // Hours to display (6am – 9pm) const HOURS = Array.from({ length: 16 }, (_, i) => i + 6); const gridStart = HOURS[0]; const totalHours = HOURS.length; const PX_PER_HOUR = 64; const totalHeight = totalHours * PX_PER_HOUR; // Current time position in px const nowPx = ((nowMinutes / 60) - gridStart) / totalHours * totalHeight; const showNowLine = nowMinutes / 60 >= gridStart && nowMinutes / 60 <= gridStart + totalHours; // Which day column is today (for the red line) const todayColIndex = weekDays.findIndex(d => d.toDateString() === today.toDateString()); const eventsOnDay = (day) => calEvents.filter(e => { if (!e.startTime) return false; const d = new Date(e.startTime); return d.getFullYear() === year && d.getMonth() === month && d.getDate() === day; }); const eventsOnDate = (date) => calEvents.filter(e => { if (!e.startTime) return false; const d = new Date(e.startTime); return d.getFullYear() === date.getFullYear() && d.getMonth() === date.getMonth() && d.getDate() === date.getDate(); }); const monthName = calDate.toLocaleString('default', { month: 'long', year: 'numeric' }); const weekLabel = `${weekDays[0].toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${weekDays[6].toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const handleCalendarChange = async (id) => { setSelectedCalendarId(id); await API.saveSettings({ calendarId: id }); fetchCalendar(id); }; const prevPeriod = () => { const d = new Date(calDate); if (calView === 'week') d.setDate(d.getDate() - 7); else d.setMonth(d.getMonth() - 1); setCalDate(d); }; const nextPeriod = () => { const d = new Date(calDate); if (calView === 'week') d.setDate(d.getDate() + 7); else d.setMonth(d.getMonth() + 1); setCalDate(d); }; const fmtHour = (h) => h === 12 ? '12 PM' : h > 12 ? `${h - 12} PM` : `${h} AM`; return (
{/* Header */}
Schedule

Calendar

{/* Calendar picker — always show; "All" shows every calendar's events */} {/* View toggle */}
{['month', 'week'].map(v => ( ))}
{/* Nav */} {calView === 'week' ? weekLabel : monthName}
{loadingTabs.calendar ? (
) : calView === 'week' ? ( /* ===== WEEK VIEW ===== */
{/* Day headers */}
GMT
{weekDays.map((d, i) => { const isToday = d.toDateString() === today.toDateString(); return (
{dayNames[d.getDay()]}
{d.getDate()}
); })}
{/* Time grid */}
{/* Hour lines + labels */} {HOURS.map((hour, hi) => (
{hi > 0 ? fmtHour(hour) : ''}
{weekDays.map((d, di) => { const isToday = d.toDateString() === today.toDateString(); return (
); })} ))} {/* Current time red line */} {showNowLine && (
{todayColIndex >= 0 && (
)}
)} {/* Events — absolutely positioned per column */} {weekDays.map((d, di) => { const dayEvs = eventsOnDate(d); if (!dayEvs.length) return null; return ( {dayEvs.map((ev, ei) => { const start = new Date(ev.startTime); const end = ev.endTime ? new Date(ev.endTime) : new Date(start.getTime() + 60 * 60 * 1000); const startH = start.getHours() + start.getMinutes() / 60; const endH = end.getHours() + end.getMinutes() / 60; const topPx = Math.max(0, (startH - gridStart) / totalHours * totalHeight); const heightPx = Math.max(20, (endH - startH) / totalHours * totalHeight); const isBusy = !ev.contactId; // Calculate left/width based on grid column const colWidth = `calc((100% - 56px) / 7)`; const colLeft = `calc(56px + ${di} * (100% - 56px) / 7)`; return (
{isBusy ? ev.title || 'Busy' : ev.title}
{heightPx > 28 && (
{isBusy ? `${fmtTime(ev.startTime)} – ${ev.endTime ? fmtTime(ev.endTime) : ''}` : `${fmtTime(ev.startTime)}${ev.endTime ? ' – ' + fmtTime(ev.endTime) : ''}`}
)} {heightPx > 44 && !isBusy && ev.contactName && (
{ev.contactName}
)}
); })}
); })}
) : ( /* ===== MONTH VIEW ===== */
{dayNames.map(d => (
{d}
))}
{Array.from({ length: firstDay }).map((_, i) => (
))} {Array.from({ length: daysInMonth }).map((_, i) => { const day = i + 1; const dayEvents = eventsOnDay(day); const isToday = today.getDate() === day && today.getMonth() === month && today.getFullYear() === year; return (
0 ? 'has-event' : ''}`}>
{day}
{dayEvents.slice(0, 3).map((ev, ei) => { const isBusy = !ev.contactId; return (
{isBusy ? (ev.title || 'Busy') : `${fmtTime(ev.startTime)} ${ev.title}`}
); })} {dayEvents.length > 3 &&
+{dayEvents.length - 3} more
}
); })}
)} {/* Upcoming Appointments list — real appointments only */} {calEvents.filter(e => e.contactId).length > 0 && (
Upcoming Appointments {calEvents.filter(e => e.contactId).length} booked
{calEvents .filter(ev => ev.contactId && new Date(ev.startTime) >= new Date(today.setHours(0,0,0,0))) .sort((a, b) => new Date(a.startTime) - new Date(b.startTime)) .slice(0, 10) .map(ev => (
{ev.title}
{ev.contactName}
{fmtDate(ev.startTime)}
{fmtTime(ev.startTime)}{ev.endTime ? ` – ${fmtTime(ev.endTime)}` : ''}
{ev.status || 'scheduled'}
))}
)}
); };