416 lines
18 KiB
JavaScript
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)
|