init
This commit is contained in:
415
calendar/desktop/calendar.js
Normal file
415
calendar/desktop/calendar.js
Normal file
@@ -0,0 +1,415 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user