From 4529d0c4125108ae85894ef0577b8889d55ecaa7 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 22 Aug 2025 21:08:14 -0600 Subject: [PATCH] Recurrent deletion bugfixes. --- src/components/CalendarView.vue | 54 ++++- src/components/EventDialog.vue | 53 +++- src/components/EventOverlay.vue | 414 +++++--------------------------- src/stores/CalendarStore.js | 191 +++++++++++++-- 4 files changed, 330 insertions(+), 382 deletions(-) diff --git a/src/components/CalendarView.vue b/src/components/CalendarView.vue index 6cad89f..c80bc24 100644 --- a/src/components/CalendarView.vue +++ b/src/components/CalendarView.vue @@ -142,9 +142,59 @@ function createWeek(virtualWeek) { let monthToLabel = null let labelYear = null + // Precollect unique repeating base events once (avoid nested loops for each day) + const repeatingBases = [] + const seen = new Set() + for (const [, list] of calendarStore.events) { + for (const ev of list) { + if (ev.isRepeating && !seen.has(ev.id)) { + seen.add(ev.id) + repeatingBases.push(ev) + } + } + } + for (let i = 0; i < 7; i++) { const dateStr = toLocalString(cur) - const eventsForDay = calendarStore.events.get(dateStr) || [] + const storedEvents = calendarStore.events.get(dateStr) || [] + // Build day events starting with stored (base/spanning) then virtual occurrences + const dayEvents = [...storedEvents] + for (const base of repeatingBases) { + // Skip if the base itself already on this date (already in storedEvents) + if (dateStr >= base.startDate && dateStr <= base.endDate) continue + if (calendarStore.occursOnDate(base, dateStr)) { + // Determine occurrence index (0 = first repeat after base) for weekly / monthly + let recurrenceIndex = 0 + try { + if (base.repeat === 'weeks') { + const pattern = base.repeatWeekdays || [] + const baseDate = new Date(base.startDate + 'T00:00:00') + const target = new Date(dateStr + 'T00:00:00') + let matched = -1 + const cur = new Date(baseDate) + while (cur < target && matched < 100000) { + cur.setDate(cur.getDate() + 1) + if (pattern[cur.getDay()]) matched++ + } + if (cur.toDateString() === target.toDateString()) recurrenceIndex = matched + } else if (base.repeat === 'months') { + const baseDate = new Date(base.startDate + 'T00:00:00') + const target = new Date(dateStr + 'T00:00:00') + const diffMonths = + (target.getFullYear() - baseDate.getFullYear()) * 12 + + (target.getMonth() - baseDate.getMonth()) + recurrenceIndex = diffMonths // matches existing monthly logic semantics + } + } catch {} + dayEvents.push({ + ...base, + id: base.id + '_v_' + dateStr, + startDate: dateStr, + endDate: dateStr, + _recurrenceIndex: recurrenceIndex, + }) + } + } const dow = cur.getDay() const isFirst = cur.getDate() === 1 @@ -177,7 +227,7 @@ function createWeek(virtualWeek) { selection.value.dayCount > 0 && dateStr >= selection.value.startDate && dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1), - events: eventsForDay, + events: dayEvents, }) cur.setDate(cur.getDate() + 1) } diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue index 81a334f..330e47b 100644 --- a/src/components/EventDialog.vue +++ b/src/components/EventDialog.vue @@ -155,6 +155,8 @@ function openEditDialog(eventInstanceId) { let occurrenceIndex = 0 let weekday = null let occurrenceDate = null + + // Support legacy synthetic id pattern: baseId_repeat_[_] if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) { const [bid, suffix] = eventInstanceId.split('_repeat_') baseId = bid @@ -162,10 +164,57 @@ function openEditDialog(eventInstanceId) { occurrenceIndex = parseInt(parts[0], 10) || 0 if (parts.length > 1) weekday = parseInt(parts[1], 10) } + // Support new virtual id pattern: baseId_v_YYYY-MM-DD + else if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_v_')) { + const splitIndex = eventInstanceId.lastIndexOf('_v_') + if (splitIndex !== -1) { + baseId = eventInstanceId.slice(0, splitIndex) + const dateStr = eventInstanceId.slice(splitIndex + 3) + occurrenceDate = new Date(dateStr + 'T00:00:00') + // Derive occurrenceIndex based on event's repeat pattern + const eventForIndex = calendarStore.getEventById(baseId) + if (eventForIndex?.isRepeating) { + if (eventForIndex.repeat === 'weeks') { + const pattern = eventForIndex.repeatWeekdays || [] + const baseDate = new Date(eventForIndex.startDate + 'T00:00:00') + // Count matching weekdays after base until reaching occurrenceDate + let cur = new Date(baseDate) + let matched = -1 // first match after base increments this to 0 + while (cur < occurrenceDate && matched < 100000) { + cur.setDate(cur.getDate() + 1) + if (pattern[cur.getDay()]) matched++ + } + if (cur.toDateString() === occurrenceDate.toDateString()) { + occurrenceIndex = matched + weekday = occurrenceDate.getDay() + } else { + // Fallback: treat as base click if something went wrong + occurrenceIndex = 0 + weekday = null + occurrenceDate = null + } + } else if (eventForIndex.repeat === 'months') { + const baseDate = new Date(eventForIndex.startDate + 'T00:00:00') + const diffMonths = + (occurrenceDate.getFullYear() - baseDate.getFullYear()) * 12 + + (occurrenceDate.getMonth() - baseDate.getMonth()) + const interval = eventForIndex.repeatInterval || 1 + // occurrenceIndex for monthly logic: diff in months (first after base is 1 * interval) + if (diffMonths > 0 && diffMonths % interval === 0) { + occurrenceIndex = diffMonths // matches store deletion expectation + } else { + occurrenceDate = null + } + } + } + } + } + const event = calendarStore.getEventById(baseId) if (!event) return - // Derive occurrence date for repeat occurrences (occurrenceIndex > 0 means not the base) + // Derive occurrence date for repeat occurrences if not already determined above if ( + !occurrenceDate && event.isRepeating && ((event.repeat === 'weeks' && occurrenceIndex >= 0) || (event.repeat === 'months' && occurrenceIndex > 0)) @@ -173,8 +222,6 @@ function openEditDialog(eventInstanceId) { if (event.repeat === 'weeks' && occurrenceIndex >= 0) { const repeatWeekdaysLocal = event.repeatWeekdays || [] const baseDate = new Date(event.startDate + 'T00:00:00') - // occurrenceIndex counts prior occurrences AFTER base; - // For occurrenceIndex = 0 we want first matching day after base. let cur = new Date(baseDate) let matched = -1 let safety = 0 diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue index 8125130..3f0d918 100644 --- a/src/components/EventOverlay.vue +++ b/src/components/EventOverlay.vue @@ -5,6 +5,7 @@ :key="span.id" class="event-span" :class="[`event-color-${span.colorId}`]" + :data-recurrence="span._recurrenceIndex != null ? span._recurrenceIndex : undefined" :style="{ gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`, gridRow: `${span.row}`, @@ -24,54 +25,81 @@ -