import calendarUtil from "../calendarUtil.js" import "./DesktopToolbar.js" import "./DesktopSidebar.js" import "./DesktopMonthView.js" import "./Events/DesktopEventDetails.js" import "./Events/DesktopEventForm.js" import "./DesktopCalendarForm.js" css(` calendar- { font-family: 'Arial'; scrollbar-width: none; -ms-overflow-style: none; } `) class Calendar extends Shadow { constructor() { super() this.currentDate = new Date(); this.weekStartsOn = 0; this.calendars = (global.currentNetwork.data?.calendars ?? []).map(c => ({ ...c })) const storedCalIds = JSON.parse(localStorage.getItem(`calendarSelection_${global.profile.id}_${global.currentNetwork.id}`) || 'null') if (storedCalIds) { const restored = this.calendars.filter(c => storedCalIds.includes(c.id)) this.selectedCalendars = restored.length > 0 ? restored : [...this.calendars] } else { this.selectedCalendars = [...this.calendars] } this.events = (global.currentNetwork.data?.events ?? []).map(event => ({ ...event, time_start: new Date(event.time_start), time_end: new Date(event.time_end) })) } toggleCalendar(calendar) { const isSelected = this.selectedCalendars.some(c => c.id === calendar.id); if (isSelected && this.selectedCalendars.length === 1) return; if (isSelected) { this.selectedCalendars = this.selectedCalendars.filter(c => c.id !== calendar.id); } else { this.selectedCalendars = [...this.selectedCalendars, calendar]; } localStorage.setItem(`calendarSelection_${global.profile.id}_${global.currentNetwork.id}`, JSON.stringify(this.selectedCalendars.map(c => c.id))) this.rerender(); } render() { HStack(() => { DesktopSidebar( this.currentDate, this.calendars, this.selectedCalendars, this.events, this.weekStartsOn, { onSelectDate: (date) => this.goToDate(date), onToggleCalendar: (cal) => this.toggleCalendar(cal), onNewCalendar: () => { let formEl $("modal-").open(() => { formEl = DesktopCalendarForm(this.calendars, (calendar) => this.addCalendar(calendar)) }) $("modal-")._closeOverride = async () => { const saved = formEl ? await formEl.trySave() : false if (saved === null) return if (saved) this.addCalendar(saved) $("modal-").forceClose() } }, onEditCalendar: (cal) => this.openEditCalendarForm(cal) } ) VStack(() => { DesktopToolbar(this.currentDate, { goToPrevious: () => this.goToPrevious(), goToCurrent: () => this.goToCurrent(), goToNext: () => this.goToNext(), onNewEvent: () => { let formEl $("modal-").open(() => { formEl = DesktopEventForm(this.calendars, (event) => this.addEvent(event)) }) $("modal-")._closeOverride = () => formEl?.handleBack() } }) DesktopMonthView(this.selectedCalendars, this.events, this.currentDate, this.weekStartsOn, (event) => this.openEventDetails(event), (day, removeGhost) => { let formEl const onBack = () => { if (removeGhost) removeGhost() $("modal-").forceClose() } $("modal-").open(() => { formEl = DesktopEventForm(this.calendars, (event) => this.addEvent(event), null, null, onBack, day) }) $("modal-")._closeOverride = () => formEl?.handleBack() } ) .flex(1) .minHeight(0) .width(100, pct) .overflow("hidden") }) .flex(1) .height(100, pct) .overflow("hidden") }) .height(100, pct) .width(100, pct) .overflow("hidden") } openEventDetails(event) { $("modal-").open(() => DesktopEventDetails( this.calendars, event, (editResult) => { if (editResult?.scope) { this.handleEditResult(editResult) } else { this.updateEvent(editResult) } this.rerender() }, (deleteResult) => { this.handleDeleteResult(deleteResult) this.rerender() }, (evt) => this.openEditForm(evt) )) } openEditForm(event) { let formEl const goBack = () => { $("modal-").forceClose() const currentEvent = this.events.find(e => e.id === event.id) ?? event this.openEventDetails({ ...currentEvent }) } $("modal-").open(() => { formEl = DesktopEventForm( this.calendars, (editResult) => { if (editResult?.scope) { this.handleEditResult(editResult) this.rerender() const findId = editResult.scope === 'all' ? editResult.templateId : editResult.event.id const updatedEvt = this.events.find(e => e.id === findId) if (updatedEvt) setTimeout(() => this.openEventDetails(updatedEvt), 50) } else { this.updateEvent(editResult) this.openEventDetails(editResult) } }, event, (deleteResult) => { this.handleDeleteResult(deleteResult) this.rerender() }, goBack ) }) $("modal-")._closeOverride = () => formEl?.handleBack() } addEvent(event) { this.events = [...this.events, this.parseEvent(event)] global.currentNetwork.data.events = [...global.currentNetwork.data.events, event] this.rerender() } openEditCalendarForm(calendar) { let formEl $("modal-").open(() => { formEl = DesktopCalendarForm( this.calendars, (updated) => { this.updateCalendar(updated); $("modal-").forceClose() }, calendar, (deletedId) => this.deleteCalendar(deletedId), () => $("modal-").forceClose() ) }) $("modal-")._closeOverride = async () => { const saved = formEl ? await formEl.trySave() : null if (saved === null) return this.updateCalendar(saved) $("modal-").forceClose() } } addCalendar(calendar) { this.calendars = [...this.calendars, calendar] this.selectedCalendars = [...this.selectedCalendars, calendar] global.currentNetwork.data.calendars = [...global.currentNetwork.data.calendars, calendar] this.rerender() } updateCalendar(calendar) { this.calendars = this.calendars.map(c => c.id === calendar.id ? calendar : c) this.selectedCalendars = this.selectedCalendars.map(c => c.id === calendar.id ? calendar : c) global.currentNetwork.data.calendars = global.currentNetwork.data.calendars.map(c => c.id === calendar.id ? calendar : c) this.rerender() } deleteCalendar(id) { this.calendars = this.calendars.filter(c => c.id !== id) this.selectedCalendars = this.selectedCalendars.filter(c => c.id !== id) global.currentNetwork.data.calendars = global.currentNetwork.data.calendars.filter(c => c.id !== id) this.rerender() } updateEvent(event) { const parsed = this.parseEvent(event) this.events = this.events.map(e => e.id === parsed.id ? parsed : e) global.currentNetwork.data.events = global.currentNetwork.data.events.map(e => e.id === event.id ? event : e) this.rerender() } deleteEvent(id) { this.events = this.events.filter(e => e.id !== id) global.currentNetwork.data.events = global.currentNetwork.data.events.filter(e => e.id !== id) this.rerender() } handleEditResult({ scope, event: resultEvent, templateId, occurrenceDate }) { const event = { ...resultEvent, time_start: new Date(resultEvent.time_start), time_end: new Date(resultEvent.time_end) }; if (scope === 'all') { // Preserve end_date from old template — it may have been set by a 'this and future' split. const oldTemplate = this.events.find(e => e.id === templateId); const oldEndDate = oldTemplate?.recurrence?.end_date ?? null; const recurrence = event.recurrence ? { ...event.recurrence, end_date: event.recurrence.end_date ?? oldEndDate } : null; const mergedEvent = { ...event, recurrence }; this.events = this.events.map(e => e.id === templateId ? mergedEvent : e); global.currentNetwork.data.events = global.currentNetwork.data.events.map(e => e.id === templateId ? { ...resultEvent, recurrence } : e ); } else if (scope === 'single') { const alreadyExists = this.events.some(e => e.id === resultEvent.id); if (alreadyExists) { this.events = this.events.map(e => e.id === resultEvent.id ? event : e); global.currentNetwork.data.events = global.currentNetwork.data.events.map(e => e.id === resultEvent.id ? resultEvent : e); } else { this.events = [...this.events, event]; global.currentNetwork.data.events = [...global.currentNetwork.data.events, resultEvent]; } } else if (scope === 'future') { const capDate = occurrenceDate ? new Date(occurrenceDate) : null; if (capDate) { const oldTemplate = this.events.find(e => e.id === templateId); const oldEndDate = oldTemplate?.recurrence?.end_date ? new Date(oldTemplate.recurrence.end_date) : null; const baseRecurrence = event.recurrence ?? oldTemplate?.recurrence; const inheritedRecurrence = baseRecurrence ? { ...baseRecurrence, end_date: oldEndDate ? oldEndDate.toISOString() : null } : null; const newTemplateEvent = { ...event, recurrence: inheritedRecurrence }; const newTemplateRaw = { ...resultEvent, recurrence: inheritedRecurrence }; const descendantIds = new Set( this.events .filter(e => { if (!(e.parent_template_id === templateId && e.recurrence_id)) return false; const t = new Date(e.time_start); return t >= capDate && (!oldEndDate || t < oldEndDate); }) .map(e => e.id) ); const newId = resultEvent.id; const updateAndFilter = (arr) => arr.map(e => { if (e.id === templateId && e.recurrence) { return { ...e, recurrence: { ...e.recurrence, end_date: capDate.toISOString() } }; } if (e.recurrence_parent_id === templateId && e.recurrence_exception_date) { const exDate = new Date(e.recurrence_exception_date); if (exDate >= capDate && (!oldEndDate || exDate < oldEndDate)) { return { ...e, recurrence_parent_id: newId }; } } return e; }).filter(e => { if (e.recurrence_parent_id === templateId && e.recurrence_exception_date) { return new Date(e.recurrence_exception_date) < capDate; } if (descendantIds.has(e.id) || descendantIds.has(e.recurrence_parent_id)) { return false; } return true; }); this.events = [...updateAndFilter(this.events), newTemplateEvent]; global.currentNetwork.data.events = [...updateAndFilter(global.currentNetwork.data.events), newTemplateRaw]; } } } handleDeleteResult({ scope, templateId, occurrenceDate, overrideId }) { if (scope === 'all') { // Promote non-cancelled overrides (single-event edits) to standalone; remove cancelled placeholders and template const promoteOverrides = (arr) => arr .filter(e => e.id !== templateId && !(e.recurrence_parent_id === templateId && e.is_cancelled)) .map(e => e.recurrence_parent_id === templateId ? { ...e, recurrence_parent_id: null, recurrence_exception_date: null } : e ); this.events = promoteOverrides(this.events); global.currentNetwork.data.events = promoteOverrides(global.currentNetwork.data.events); } else if (scope === 'single') { if (overrideId) { this.events = this.events.map(e => e.id === overrideId ? { ...e, is_cancelled: true } : e); global.currentNetwork.data.events = global.currentNetwork.data.events.map(e => e.id === overrideId ? { ...e, is_cancelled: true } : e); } else if (occurrenceDate) { const occDate = new Date(occurrenceDate); const syntheticOverride = { id: `cancelled_${templateId}_${occurrenceDate}`, recurrence_parent_id: templateId, recurrence_exception_date: occDate, is_cancelled: true, time_start: occDate, time_end: occDate, calendars: [], all_day: false, }; this.events = [...this.events, syntheticOverride]; global.currentNetwork.data.events = [...global.currentNetwork.data.events, syntheticOverride]; } } else if (scope === 'future') { const capDate = occurrenceDate ? new Date(occurrenceDate) : null; if (capDate) { const oldTemplate = this.events.find(e => e.id === templateId); // Server does a full delete when capDate <= time_start (no occurrences would remain) if (oldTemplate && capDate <= new Date(oldTemplate.time_start)) { const promoteOverrides = (arr) => arr .filter(e => e.id !== templateId && !(e.recurrence_parent_id === templateId && e.is_cancelled)) .map(e => e.recurrence_parent_id === templateId ? { ...e, recurrence_parent_id: null, recurrence_exception_date: null } : e ); this.events = promoteOverrides(this.events); global.currentNetwork.data.events = promoteOverrides(global.currentNetwork.data.events); return; } const oldEndDate = oldTemplate?.recurrence?.end_date ? new Date(oldTemplate.recurrence.end_date) : null; const descendantIds = new Set( this.events .filter(e => { if (!(e.parent_template_id === templateId && e.recurrence_id)) return false; const t = new Date(e.time_start); return t >= capDate && (!oldEndDate || t < oldEndDate); }) .map(e => e.id) ); const updateAndFilter = (arr) => arr.map(e => { if (e.id === templateId && e.recurrence) { return { ...e, recurrence: { ...e.recurrence, end_date: capDate.toISOString() } }; } // Promote future non-cancelled overrides to standalone events if (e.recurrence_parent_id === templateId && e.recurrence_exception_date && new Date(e.recurrence_exception_date) >= capDate && !e.is_cancelled) { return { ...e, recurrence_parent_id: null, recurrence_exception_date: null }; } return e; }).filter(e => { // Remove future cancelled placeholders (meaningless without the series) if (e.recurrence_parent_id === templateId && e.recurrence_exception_date) { return new Date(e.recurrence_exception_date) < capDate; } if (descendantIds.has(e.id) || descendantIds.has(e.recurrence_parent_id)) { return false; } return true; }); this.events = updateAndFilter(this.events); global.currentNetwork.data.events = updateAndFilter(global.currentNetwork.data.events); } } } parseEvent(event) { return { ...event, time_start: new Date(event.time_start), time_end: new Date(event.time_end) } } goToPrevious() { this.currentDate = calendarUtil.addMonths(this.currentDate, -1); this.rerender(); } goToCurrent() { this.currentDate = new Date(); this.rerender(); } goToNext() { this.currentDate = calendarUtil.addMonths(this.currentDate, 1); this.rerender(); } goToDate(date) { if (calendarUtil.isSameMonth(this.currentDate, date)) return; this.currentDate = date; this.rerender(); } } register(Calendar)