diff --git a/src/App.vue b/src/App.vue index 8d02976..439bde5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,9 +15,9 @@ const handleCreateEvent = (eventData) => { } } -const handleEditEvent = (eventInstanceId) => { +const handleEditEvent = (eventClickPayload) => { if (eventDialog.value) { - eventDialog.value.openEditDialog(eventInstanceId) + eventDialog.value.openEditDialog(eventClickPayload) } } diff --git a/src/components/CalendarDay.vue b/src/components/CalendarDay.vue index b68648b..b5251b3 100644 --- a/src/components/CalendarDay.vue +++ b/src/components/CalendarDay.vue @@ -6,7 +6,7 @@ const props = defineProps({ const emit = defineEmits(['event-click']) const handleEventClick = (eventId) => { - emit('event-click', eventId) + emit('event-click', { id: eventId, instanceId: eventId, occurrenceIndex: 0 }) } diff --git a/src/components/CalendarView.vue b/src/components/CalendarView.vue index c80bc24..b9bfb8d 100644 --- a/src/components/CalendarView.vue +++ b/src/components/CalendarView.vue @@ -144,54 +144,90 @@ function createWeek(virtualWeek) { // 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) - } + if (calendarStore.events) { + for (const ev of calendarStore.events.values()) { + if (ev.isRepeating) repeatingBases.push(ev) } } for (let i = 0; i < 7; i++) { const dateStr = toLocalString(cur) - const storedEvents = calendarStore.events.get(dateStr) || [] + const storedEvents = [] + const idSet = calendarStore.dates.get(dateStr) + if (idSet) { + // Support Set or Array; ignore unexpected shapes + if (idSet instanceof Set) { + idSet.forEach((id) => { + const ev = calendarStore.events.get(id) + if (ev) storedEvents.push(ev) + }) + } else if (Array.isArray(idSet)) { + for (const id of idSet) { + const ev = calendarStore.events.get(id) + if (ev) storedEvents.push(ev) + } + } else if (typeof idSet === 'object' && idSet !== null) { + // If mistakenly hydrated as plain object {id:true,...} + for (const id of Object.keys(idSet)) { + const ev = calendarStore.events.get(id) + if (ev) storedEvents.push(ev) + } + } + } // 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 + // Determine sequential occurrence index: base event = 0, first repeat = 1, etc. let recurrenceIndex = 0 try { if (base.repeat === 'weeks') { const pattern = base.repeatWeekdays || [] - const baseDate = new Date(base.startDate + 'T00:00:00') + const interval = base.repeatInterval || 1 + const baseStart = new Date(base.startDate + 'T00:00:00') + const baseEnd = new Date(base.endDate + '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++ + const WEEK_MS = 7 * 86400000 + const baseBlockStart = new Date(baseStart) + baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) + function isAligned(d) { + const blk = new Date(d) + blk.setDate(d.getDate() - d.getDay()) + const diff = Math.floor((blk - baseBlockStart) / WEEK_MS) + return diff % interval === 0 } - if (cur.toDateString() === target.toDateString()) recurrenceIndex = matched + // Count valid occurrences after base end and before target + let count = 0 + const cursor = new Date(baseEnd) + cursor.setDate(cursor.getDate() + 1) + while (cursor < target) { + if (pattern[cursor.getDay()] && isAligned(cursor)) count++ + cursor.setDate(cursor.getDate() + 1) + } + // Target itself is guaranteed valid (occursOnDate passed), so its index is count+1 + recurrenceIndex = count + 1 } else if (base.repeat === 'months') { - const baseDate = new Date(base.startDate + 'T00:00:00') + const baseStart = new Date(base.startDate + 'T00:00:00') const target = new Date(dateStr + 'T00:00:00') + const interval = base.repeatInterval || 1 const diffMonths = - (target.getFullYear() - baseDate.getFullYear()) * 12 + - (target.getMonth() - baseDate.getMonth()) - recurrenceIndex = diffMonths // matches existing monthly logic semantics + (target.getFullYear() - baseStart.getFullYear()) * 12 + + (target.getMonth() - baseStart.getMonth()) + // diffMonths should be multiple of interval; sequential index = diffMonths/interval + recurrenceIndex = diffMonths / interval } - } catch {} + } catch { + recurrenceIndex = 0 + } dayEvents.push({ ...base, id: base.id + '_v_' + dateStr, startDate: dateStr, endDate: dateStr, _recurrenceIndex: recurrenceIndex, + _baseId: base.id, }) } } @@ -399,8 +435,8 @@ const handleDayTouchEnd = (dateStr) => { } } -const handleEventClick = (eventInstanceId) => { - emit('edit-event', eventInstanceId) +const handleEventClick = (payload) => { + emit('edit-event', payload) } // Handle year change emitted from CalendarHeader: scroll to computed target position diff --git a/src/components/CalendarWeek.vue b/src/components/CalendarWeek.vue index ded3d9c..49205b8 100644 --- a/src/components/CalendarWeek.vue +++ b/src/components/CalendarWeek.vue @@ -3,10 +3,18 @@ import CalendarDay from './CalendarDay.vue' import EventOverlay from './EventOverlay.vue' const props = defineProps({ - week: Object + week: Object, }) -const emit = defineEmits(['day-mousedown', 'day-mouseenter', 'day-mouseup', 'day-touchstart', 'day-touchmove', 'day-touchend', 'event-click']) +const emit = defineEmits([ + 'day-mousedown', + 'day-mouseenter', + 'day-mouseup', + 'day-touchstart', + 'day-touchmove', + 'day-touchend', + 'event-click', +]) const handleDayMouseDown = (dateStr) => { emit('day-mousedown', dateStr) @@ -32,21 +40,18 @@ const handleDayTouchEnd = (dateStr) => { emit('day-touchend', dateStr) } -const handleEventClick = (eventId) => { - emit('event-click', eventId) +const handleEventClick = (payload) => { + emit('event-click', payload) } @@ -98,8 +100,8 @@ const handleEventClick = (eventId) => { } /* Fixed heights for cells and labels (from cells.css) */ -.week-row :deep(.cell), -.week-label { - height: var(--cell-h); +.week-row :deep(.cell), +.week-label { + height: var(--cell-h); } diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue index 330e47b..d150ae5 100644 --- a/src/components/EventDialog.vue +++ b/src/components/EventDialog.vue @@ -149,90 +149,56 @@ function openCreateDialog(selectionData = null) { }) } -function openEditDialog(eventInstanceId) { +function openEditDialog(payload) { occurrenceContext.value = null - let baseId = eventInstanceId - let occurrenceIndex = 0 + if (!payload) return + // Payload expected: { id: baseId, instanceId, occurrenceIndex } + const baseId = payload.id + let occurrenceIndex = payload.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 - const parts = suffix.split('_') - 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 if not already determined above - if ( - !occurrenceDate && - event.isRepeating && - ((event.repeat === 'weeks' && occurrenceIndex >= 0) || - (event.repeat === 'months' && occurrenceIndex > 0)) - ) { + // Compute occurrenceDate based on occurrenceIndex rather than parsing instanceId + if (event.isRepeating) { if (event.repeat === 'weeks' && occurrenceIndex >= 0) { - const repeatWeekdaysLocal = event.repeatWeekdays || [] - const baseDate = new Date(event.startDate + 'T00:00:00') - let cur = new Date(baseDate) - let matched = -1 - let safety = 0 - while (matched < occurrenceIndex && safety < 10000) { + const pattern = event.repeatWeekdays || [] + const baseStart = new Date(event.startDate + 'T00:00:00') + const baseEnd = new Date(event.endDate + 'T00:00:00') + if (occurrenceIndex === 0) { + occurrenceDate = baseStart + weekday = baseStart.getDay() + } else { + // Count valid repeat occurrences (pattern + interval alignment) AFTER the base span + const interval = event.repeatInterval || 1 + const WEEK_MS = 7 * 86400000 + const baseBlockStart = new Date(baseStart) + baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) + function isAligned(d) { + const blk = new Date(d) + blk.setDate(d.getDate() - d.getDay()) + const diff = Math.floor((blk - baseBlockStart) / WEEK_MS) + return diff % interval === 0 + } + let cur = new Date(baseEnd) cur.setDate(cur.getDate() + 1) - if (repeatWeekdaysLocal[cur.getDay()]) matched++ - safety++ + let found = 0 // number of repeat occurrences found so far + let safety = 0 + while (found < occurrenceIndex && safety < 20000) { + if (pattern[cur.getDay()] && isAligned(cur)) { + found++ + if (found === occurrenceIndex) break + } + cur.setDate(cur.getDate() + 1) + safety++ + } + occurrenceDate = cur + weekday = cur.getDay() } - occurrenceDate = cur - } else if (event.repeat === 'months') { - const cur = new Date(event.startDate + 'T00:00:00') + } else if (event.repeat === 'months' && occurrenceIndex >= 0) { + const baseDate = new Date(event.startDate + 'T00:00:00') + const cur = new Date(baseDate) cur.setMonth(cur.getMonth() + occurrenceIndex) occurrenceDate = cur } @@ -250,7 +216,7 @@ function openEditDialog(eventInstanceId) { eventSaved.value = false // Build occurrence context: treat any occurrenceIndex > 0 as a specific occurrence (weekday only relevant for weekly) if (event.isRepeating) { - if (event.repeat === 'weeks' && weekday != null && occurrenceIndex >= 0) { + if (event.repeat === 'weeks' && occurrenceIndex >= 0) { occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate } } else if (event.repeat === 'months' && occurrenceIndex > 0) { occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate } @@ -282,19 +248,16 @@ function updateEventInStore() { // For simple property updates (title, color, repeat), update all instances directly // This avoids the expensive remove/re-add cycle - for (const [, eventList] of calendarStore.events) { - for (const event of eventList) { - if (event.id === editingEventId.value) { - event.title = title.value - event.colorId = colorId.value - event.repeat = repeat.value - event.repeatInterval = recurrenceInterval.value - event.repeatWeekdays = buildStoreWeekdayPattern() - event.repeatCount = - recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value) - event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none' - } - } + if (calendarStore.events?.has(editingEventId.value)) { + const event = calendarStore.events.get(editingEventId.value) + event.title = title.value + event.colorId = colorId.value + event.repeat = repeat.value + event.repeatInterval = recurrenceInterval.value + event.repeatWeekdays = buildStoreWeekdayPattern() + event.repeatCount = + recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value) + event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none' } } diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue index a87ffb2..903c448 100644 --- a/src/components/EventOverlay.vue +++ b/src/components/EventOverlay.vue @@ -5,7 +5,8 @@ :key="span.id" class="event-span" :class="[`event-color-${span.colorId}`]" - :data-recurrence="span._recurrenceIndex != null ? span._recurrenceIndex : undefined" + :data-id="span.id" + :data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0" :style="{ gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`, gridRow: `${span.row}`, @@ -74,23 +75,26 @@ const eventSpans = computed(() => { return arr }) -function extractBaseId(eventId) { - if (typeof eventId !== 'string') return eventId - if (eventId.includes('_repeat_')) return eventId.split('_repeat_')[0] - if (eventId.includes('_v_')) return eventId.slice(0, eventId.lastIndexOf('_v_')) - return eventId -} - function handleEventClick(span) { if (justDragged.value) return - emit('event-click', span.id) + // Emit composite payload: base id (without virtual marker), instance id, occurrence index (data-n) + const idStr = span.id + const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_') + const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr + emit('event-click', { + id: baseId, + instanceId: span.id, + occurrenceIndex: span._recurrenceIndex != null ? span._recurrenceIndex : 0, + }) } function handleEventPointerDown(span, event) { if (event.target.classList.contains('resize-handle')) return event.stopPropagation() - const baseId = extractBaseId(span.id) - const isVirtual = baseId !== span.id + const idStr = span.id + const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_') + const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr + const isVirtual = hasVirtualMarker startLocalDrag( { id: baseId, @@ -109,8 +113,10 @@ function handleEventPointerDown(span, event) { function handleResizePointerDown(span, mode, event) { event.stopPropagation() - const baseId = extractBaseId(span.id) - const isVirtual = baseId !== span.id + const idStr = span.id + const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_') + const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr + const isVirtual = hasVirtualMarker startLocalDrag( { id: baseId, diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index e0f3a84..09f05e6 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -8,7 +8,8 @@ export const useCalendarStore = defineStore('calendar', { state: () => ({ today: toLocalString(new Date()), now: new Date().toISOString(), // store as ISO string - events: new Map(), // Map of date strings to arrays of events + events: new Map(), // id -> event object (primary) + dates: new Map(), // dateStr -> Set of event ids weekend: getLocaleWeekendDays(), config: { select_days: 1000, @@ -134,41 +135,25 @@ export const useCalendarStore = defineStore('calendar', { isRepeating: eventData.repeat && eventData.repeat !== 'none', } - const startDate = new Date(fromLocalString(event.startDate)) - const endDate = new Date(fromLocalString(event.endDate)) - - for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { - const dateStr = toLocalString(d) - if (!this.events.has(dateStr)) { - this.events.set(dateStr, []) - } - this.events.get(dateStr).push({ ...event, isSpanning: startDate < endDate }) - } - // No physical expansion; repeats are virtual + this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate }) + this._indexEventDates(event.id) return event.id }, getEventById(id) { - for (const [, list] of this.events) { - const found = list.find((e) => e.id === id) - if (found) return found - } - return null + return this.events.get(id) || null }, selectEventColorId(startDateStr, endDateStr) { const colorCounts = [0, 0, 0, 0, 0, 0, 0, 0] const startDate = new Date(fromLocalString(startDateStr)) const endDate = new Date(fromLocalString(endDateStr)) - - for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { - const dateStr = toLocalString(d) - const dayEvents = this.events.get(dateStr) || [] - for (const event of dayEvents) { - if (event.colorId >= 0 && event.colorId < 8) { - colorCounts[event.colorId]++ - } - } + // Count events whose ranges overlap at least one day in selected span + for (const ev of this.events.values()) { + const evStart = fromLocalString(ev.startDate) + const evEnd = fromLocalString(ev.endDate) + if (evEnd < startDate || evStart > endDate) continue + if (ev.colorId >= 0 && ev.colorId < 8) colorCounts[ev.colorId]++ } let minCount = colorCounts[0] @@ -185,17 +170,15 @@ export const useCalendarStore = defineStore('calendar', { }, deleteEvent(eventId) { - const datesToCleanup = [] - for (const [dateStr, eventList] of this.events) { - const eventIndex = eventList.findIndex((event) => event.id === eventId) - if (eventIndex !== -1) { - eventList.splice(eventIndex, 1) - if (eventList.length === 0) { - datesToCleanup.push(dateStr) - } + if (!this.events.has(eventId)) return + // Remove id from all date sets + for (const [dateStr, set] of this.dates) { + if (set.has(eventId)) { + set.delete(eventId) + if (set.size === 0) this.dates.delete(dateStr) } } - datesToCleanup.forEach((dateStr) => this.events.delete(dateStr)) + this.events.delete(eventId) }, deleteSingleOccurrence(ctx) { @@ -219,24 +202,35 @@ export const useCalendarStore = defineStore('calendar', { ctx.occurrenceDate.getDate(), ) } else { - // Fallback: derive from occurrenceIndex (legacy path) + // Fallback: derive from sequential occurrenceIndex (base=0, first repeat=1) const baseStart = new Date(base.startDate + 'T00:00:00') - let cur = new Date(baseStart) - let matched = -1 - let safety = 0 - while (matched < occurrenceIndex && safety < 20000) { + const baseEnd = new Date(base.endDate + 'T00:00:00') + if (occurrenceIndex === 0) { + targetDate = baseStart + } else { + let cur = new Date(baseEnd) cur.setDate(cur.getDate() + 1) - const blockStart = new Date(cur) - blockStart.setDate(cur.getDate() - cur.getDay()) + let found = 0 + let safety = 0 + const WEEK_MS = 7 * 86400000 const baseBlockStart = new Date(baseStart) baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) - const WEEK_MS = 7 * 86400000 - const blocksDiff = Math.floor((blockStart - baseBlockStart) / WEEK_MS) - const aligned = blocksDiff % interval === 0 - if (aligned && pattern[cur.getDay()]) matched++ - safety++ + function isAligned(d) { + const blk = new Date(d) + blk.setDate(d.getDate() - d.getDay()) + const diff = Math.floor((blk - baseBlockStart) / WEEK_MS) + return diff % interval === 0 + } + while (found < occurrenceIndex && safety < 50000) { + if (pattern[cur.getDay()] && isAligned(cur)) { + found++ + if (found === occurrenceIndex) break + } + cur.setDate(cur.getDate() + 1) + safety++ + } + targetDate = cur } - targetDate = cur } if (!targetDate) return @@ -311,13 +305,11 @@ export const useCalendarStore = defineStore('calendar', { // MONTHLY SERIES ----------------------------------------------------- if (base.repeat === 'months') { const interval = base.repeatInterval || 1 - // occurrenceIndex is the diff in months from base (1-based for first recurrence) - if (occurrenceIndex <= 0) return // base itself handled elsewhere - if (occurrenceIndex % interval !== 0) return // should not happen (synthetic id only for valid occurrences) - // Count prior occurrences (including base) before the deleted one + // Sequential index: base=0, first repeat=1 + if (occurrenceIndex <= 0) return // base deletion handled elsewhere + // Count prior occurrences to KEEP (indices 0 .. occurrenceIndex-1) => occurrenceIndex total const originalCountRaw = base.repeatCount - const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1 - // Truncate base series to keep only priorOccurrences + const priorOccurrences = occurrenceIndex this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences) // Compute span days for multi‑day events const spanDays = Math.round( @@ -333,10 +325,10 @@ export const useCalendarStore = defineStore('calendar', { remainingCount = String(rem) } } - // Next occurrence after deleted one is at occurrenceIndex + interval months from base + // Next occurrence after deleted one is at (occurrenceIndex + 1)*interval months from base const baseStart = fromLocalString(base.startDate) const nextStart = new Date(baseStart) - nextStart.setMonth(nextStart.getMonth() + occurrenceIndex + interval) + nextStart.setMonth(nextStart.getMonth() + (occurrenceIndex + 1) * interval) const nextEnd = new Date(nextStart) nextEnd.setDate(nextEnd.getDate() + spanDays) const nextStartStr = toLocalString(nextStart) @@ -477,49 +469,43 @@ export const useCalendarStore = defineStore('calendar', { }, _snapshotBaseEvent(eventId) { - // Return a shallow snapshot of any instance for metadata - for (const [, eventList] of this.events) { - const e = eventList.find((x) => x.id === eventId) - if (e) return { ...e } - } - return null + const ev = this.events.get(eventId) + return ev ? { ...ev } : null }, _removeEventFromAllDatesById(eventId) { - for (const [dateStr, list] of this.events) { - for (let i = list.length - 1; i >= 0; i--) { - if (list[i].id === eventId) { - list.splice(i, 1) - } + for (const [dateStr, set] of this.dates) { + if (set.has(eventId)) { + set.delete(eventId) + if (set.size === 0) this.dates.delete(dateStr) } - if (list.length === 0) this.events.delete(dateStr) } }, _addEventToDateRangeWithId(eventId, baseData, startDate, endDate) { - const s = fromLocalString(startDate) - const e = fromLocalString(endDate) - const multi = startDate < endDate - const payload = { + // Update base data + this.events.set(eventId, { ...baseData, id: eventId, startDate, endDate, - isSpanning: multi, - } - // Normalize single-day time fields - if (!multi) { - if (!payload.startTime) payload.startTime = '09:00' - if (!payload.durationMinutes) payload.durationMinutes = 60 - } else { - payload.startTime = null - payload.durationMinutes = null - } + isSpanning: startDate < endDate, + }) + this._indexEventDates(eventId) + }, + + _indexEventDates(eventId) { + const ev = this.events.get(eventId) + if (!ev) return + // remove old date references first + for (const [, set] of this.dates) set.delete(eventId) + const s = fromLocalString(ev.startDate) + const e = fromLocalString(ev.endDate) const cur = new Date(s) while (cur <= e) { const dateStr = toLocalString(cur) - if (!this.events.has(dateStr)) this.events.set(dateStr, []) - this.events.get(dateStr).push({ ...payload }) + if (!this.dates.has(dateStr)) this.dates.set(dateStr, new Set()) + this.dates.get(dateStr).add(ev.id) cur.setDate(cur.getDate() + 1) } }, @@ -528,7 +514,7 @@ export const useCalendarStore = defineStore('calendar', { // Adjust start/end range of a base event (non-generated) and reindex occurrences setEventRange(eventId, newStartStr, newEndStr, { mode = 'auto' } = {}) { - const snapshot = this._findEventInAnyList(eventId) + const snapshot = this.events.get(eventId) if (!snapshot) return // Calculate current duration in days (inclusive) const prevStart = new Date(fromLocalString(snapshot.startDate)) @@ -588,7 +574,7 @@ export const useCalendarStore = defineStore('calendar', { // Split a repeating series at a specific occurrence date and move that occurrence (and future) to new range splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) { - const base = this._findEventInAnyList(baseId) + const base = this.events.get(baseId) if (!base || !base.isRepeating) return const originalCountRaw = base.repeatCount const spanDays = Math.max( @@ -616,7 +602,7 @@ export const useCalendarStore = defineStore('calendar', { const blk = new Date(d) blk.setDate(d.getDate() - d.getDay()) const diff = Math.floor((blk - blockStartBase) / WEEK_MS) - return diff % interval === 0 + return diff % interval === 0 } const cursor = new Date(baseStart) while (cursor < occurrenceDate) { @@ -680,7 +666,7 @@ export const useCalendarStore = defineStore('calendar', { // Split a repeating series at a given occurrence index; returns new series id splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) { - const base = this._findEventInAnyList(baseId) + const base = this.events.get(baseId) if (!base || !base.isRepeating) return null // Capture original repeatCount BEFORE truncation const originalCountRaw = base.repeatCount @@ -715,42 +701,21 @@ export const useCalendarStore = defineStore('calendar', { }, _terminateRepeatSeriesAtIndex(baseId, index) { - // Cap repeatCount to 'index' total occurrences (0-based index means index occurrences before split) - for (const [, list] of this.events) { - for (const ev of list) { - if (ev.id === baseId && ev.isRepeating) { - if (ev.repeatCount === 'unlimited') { - ev.repeatCount = String(index) - } else { - const rc = parseInt(ev.repeatCount, 10) - if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index)) - } - } - } + const ev = this.events.get(baseId) + if (!ev || !ev.isRepeating) return + if (ev.repeatCount === 'unlimited') { + ev.repeatCount = String(index) + } else { + const rc = parseInt(ev.repeatCount, 10) + if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index)) } }, - _findEventInAnyList(eventId) { - for (const [, eventList] of this.events) { - const found = eventList.find((e) => e.id === eventId) - if (found) return found - } - return null - }, + // _findEventInAnyList removed (direct map access) _addEventToDateRange(event) { - const startDate = fromLocalString(event.startDate) - const endDate = fromLocalString(event.endDate) - const cur = new Date(startDate) - - while (cur <= endDate) { - const dateStr = toLocalString(cur) - if (!this.events.has(dateStr)) { - this.events.set(dateStr, []) - } - this.events.get(dateStr).push({ ...event, isSpanning: event.startDate < event.endDate }) - cur.setDate(cur.getDate() + 1) - } + this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate }) + this._indexEventDates(event.id) }, // NOTE: legacy dynamic getEventById for synthetic occurrences removed. @@ -758,15 +723,22 @@ export const useCalendarStore = defineStore('calendar', { persist: { key: 'calendar-store', storage: localStorage, - paths: ['today', 'events', 'config'], + // Persist new structures; keep legacy 'events' only for transitional restore if needed + paths: ['today', 'config', 'events', 'dates'], serializer: { serialize(value) { - return JSON.stringify(value, (_k, v) => - v instanceof Map ? { __map: true, data: [...v] } : v, - ) + return JSON.stringify(value, (_k, v) => { + if (v instanceof Map) return { __map: true, data: [...v] } + if (v instanceof Set) return { __set: true, data: [...v] } + return v + }) }, deserialize(value) { - return JSON.parse(value, (_k, v) => (v && v.__map ? new Map(v.data) : v)) + return JSON.parse(value, (_k, v) => { + if (v && v.__map) return new Map(v.data) + if (v && v.__set) return new Set(v.data) + return v + }) }, }, },