// ============================================================
// 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 */}
{/* Calendar picker — always show; "All" shows every calendar's events */}
handleCalendarChange(e.target.value)}
className="bg-slate-900/60 border border-slate-700/60 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-blue-500/50 cursor-pointer">
All Calendars
{(calendars || []).map(c => (
{c.name}
))}
{/* View toggle */}
{['month', 'week'].map(v => (
setCalView(v)}
className={`px-3 py-2 text-xs font-bold mono uppercase tracking-wider transition-colors ${calView === v ? 'bg-blue-600/30 text-blue-400' : 'text-slate-500 hover:text-slate-300'}`}>
{v}
))}
{/* Nav */}
{calView === 'week' ? weekLabel : monthName}
setCalDate(new Date())} className="px-3 py-2 text-xs mono text-slate-400 hover:text-white bg-slate-900/60 border border-slate-800 rounded-xl transition-colors">Today
fetchCalendar(selectedCalendarId)} className="p-2 bg-slate-900/60 border border-slate-800 rounded-xl text-slate-400 hover:text-white transition-colors">
{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'}
))}
)}
);
};