Files
apps/calendar/desktop/calendar.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

416 lines
18 KiB
JavaScript

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)