From 29246af591db4c59aef4bbd8af945c27843c233b Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 25 Aug 2025 22:04:04 -0600 Subject: [PATCH] Recurrent/weekday input fixes. Refactored event store to use recur map rather than separate properties. --- src/components/EventDialog.vue | 110 ++++++++++++++---------- src/components/EventOverlay.vue | 12 +-- src/components/WeekdaySelector.vue | 61 ++++++++++--- src/plugins/virtualWeeks.js | 8 +- src/stores/CalendarStore.js | 133 ++++++++++++++++------------- src/utils/date.js | 59 ++++++++----- 6 files changed, 235 insertions(+), 148 deletions(-) diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue index 813ad6f..8e5777b 100644 --- a/src/components/EventDialog.vue +++ b/src/components/EventDialog.vue @@ -29,6 +29,7 @@ const dialogMode = ref('create') // 'create' or 'edit' const editingEventId = ref(null) const unsavedCreateId = ref(null) const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate } +const initialWeekday = ref(null) const title = computed({ get() { if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) { @@ -63,10 +64,13 @@ function getStartingWeekday(selectionData = null) { } const fallbackWeekdays = computed(() => { - const startingDay = getStartingWeekday() - const fallback = [false, false, false, false, false, false, false] - fallback[startingDay] = true - return fallback + let weekday = initialWeekday.value + if (weekday == null) { + weekday = getStartingWeekday() + } + const fb = [false, false, false, false, false, false, false] + fb[weekday] = true + return fb }) // Maps UI frequency display (including years) to store frequency (weeks/months only) @@ -140,14 +144,24 @@ const selectedColor = computed({ const repeatCountBinding = computed({ get() { if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) { - const rc = calendarStore.events.get(editingEventId.value).repeatCount + const ev = calendarStore.events.get(editingEventId.value) + const rc = ev.recur?.count ?? 'unlimited' return rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0 } return recurrenceOccurrences.value }, set(v) { if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) { - calendarStore.events.get(editingEventId.value).repeatCount = v === 0 ? 'unlimited' : String(v) + const ev = calendarStore.events.get(editingEventId.value) + if (!ev.recur && v !== 0) { + ev.recur = { + freq: recurrenceFrequency.value, + interval: recurrenceInterval.value, + count: 'unlimited', + weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null, + } + } + if (ev.recur) ev.recur.count = v === 0 ? 'unlimited' : String(v) calendarStore.touchEvents() } recurrenceOccurrences.value = v @@ -178,14 +192,7 @@ const repeat = computed({ }) function buildStoreWeekdayPattern() { - let sunFirst = [...recurrenceWeekdays.value] - - if (!sunFirst.some(Boolean)) { - const startingDay = getStartingWeekday() - sunFirst[startingDay] = true - } - - return sunFirst + return [...recurrenceWeekdays.value] } function loadWeekdayPatternFromStore(storePattern) { @@ -222,6 +229,7 @@ function openCreateDialog(selectionData = null) { } occurrenceContext.value = null + initialWeekday.value = null dialogMode.value = 'create' recurrenceEnabled.value = false recurrenceInterval.value = 1 @@ -234,17 +242,23 @@ function openCreateDialog(selectionData = null) { const startingDay = getStartingWeekday({ start, end }) recurrenceWeekdays.value[startingDay] = true + initialWeekday.value = startingDay editingEventId.value = calendarStore.createEvent({ title: '', startDate: start, endDate: end, colorId: colorId.value, - repeat: repeat.value, - repeatInterval: recurrenceInterval.value, - repeatCount: - recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value), - repeatWeekdays: buildStoreWeekdayPattern(), + recur: + recurrenceEnabled.value && repeat.value !== 'none' + ? { + freq: recurrenceFrequency.value, + interval: recurrenceInterval.value, + count: + recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value), + weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null, + } + : null, }) unsavedCreateId.value = editingEventId.value @@ -277,6 +291,7 @@ function openEditDialog(payload) { unsavedCreateId.value = null } occurrenceContext.value = null + initialWeekday.value = null if (!payload) return const baseId = payload.id @@ -287,16 +302,16 @@ function openEditDialog(payload) { const event = calendarStore.getEventById(baseId) if (!event) return - if (event.isRepeating) { - if (event.repeat === 'weeks' && occurrenceIndex >= 0) { - const pattern = event.repeatWeekdays || [] + if (event.recur) { + if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) { + const pattern = event.recur.weekdays || [] const baseStart = fromLocalString(event.startDate, DEFAULT_TZ) const baseEnd = fromLocalString(event.endDate, DEFAULT_TZ) if (occurrenceIndex === 0) { occurrenceDate = baseStart weekday = baseStart.getDay() } else { - const interval = event.repeatInterval || 1 + const interval = event.recur.interval || 1 const WEEK_MS = 7 * 86400000 const baseBlockStart = getMondayOfISOWeek(baseStart) function isAligned(d) { @@ -318,22 +333,24 @@ function openEditDialog(payload) { occurrenceDate = cur weekday = cur.getDay() } - } else if (event.repeat === 'months' && occurrenceIndex >= 0) { + } else if (event.recur.freq === 'months' && occurrenceIndex >= 0) { const baseDate = fromLocalString(event.startDate, DEFAULT_TZ) occurrenceDate = addMonths(baseDate, occurrenceIndex) } } dialogMode.value = 'edit' editingEventId.value = baseId - loadWeekdayPatternFromStore(event.repeatWeekdays) - repeat.value = event.repeat // triggers setter mapping into recurrence state - if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval + loadWeekdayPatternFromStore(event.recur?.weekdays) + initialWeekday.value = + weekday != null ? weekday : fromLocalString(event.startDate, DEFAULT_TZ).getDay() + repeat.value = event.recur ? event.recur.freq : 'none' + if (event.recur?.interval) recurrenceInterval.value = event.recur.interval // Set UI display frequency based on loaded data - if (event.repeat === 'weeks') { + if (event.recur?.freq === 'weeks') { uiDisplayFrequency.value = 'weeks' - } else if (event.repeat === 'months') { - if (event.repeatInterval && event.repeatInterval % 12 === 0 && event.repeatInterval >= 12) { + } else if (event.recur?.freq === 'months') { + if (event.recur.interval && event.recur.interval % 12 === 0 && event.recur.interval >= 12) { uiDisplayFrequency.value = 'years' } else { uiDisplayFrequency.value = 'months' @@ -342,15 +359,15 @@ function openEditDialog(payload) { uiDisplayFrequency.value = 'weeks' } - const rc = event.repeatCount ?? 'unlimited' + const rc = event.recur?.count ?? 'unlimited' recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0 colorId.value = event.colorId eventSaved.value = false - if (event.isRepeating) { - if (event.repeat === 'weeks' && occurrenceIndex >= 0) { + if (event.recur) { + if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) { occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate } - } else if (event.repeat === 'months' && occurrenceIndex > 0) { + } else if (event.recur.freq === 'months' && occurrenceIndex > 0) { occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate } } } @@ -379,12 +396,17 @@ function updateEventInStore() { if (calendarStore.events?.has(editingEventId.value)) { const event = calendarStore.events.get(editingEventId.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 (recurrenceEnabled.value && repeat.value !== 'none') { + event.recur = { + freq: recurrenceFrequency.value, + interval: recurrenceInterval.value, + count: + recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value), + weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null, + } + } else { + event.recur = null + } calendarStore.touchEvents() } } @@ -468,11 +490,9 @@ const isRepeatingBaseEdit = computed(() => isRepeatingEdit.value && !occurrenceC const isLastOccurrence = computed(() => { if (!occurrenceContext.value || !editingEventId.value) return false const event = calendarStore.getEventById(editingEventId.value) - if (!event || !event.isRepeating) return false - - if (event.repeatCount === 'unlimited' || recurrenceOccurrences.value === 0) return false - - const totalCount = parseInt(event.repeatCount, 10) || 0 + if (!event || !event.recur) return false + if (event.recur.count === 'unlimited' || recurrenceOccurrences.value === 0) return false + const totalCount = parseInt(event.recur.count, 10) || 0 return occurrenceContext.value.occurrenceIndex === totalCount - 1 }) const formattedOccurrenceShort = computed(() => { diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue index 1c7614f..2b15949 100644 --- a/src/components/EventOverlay.vue +++ b/src/components/EventOverlay.vue @@ -176,11 +176,11 @@ function startLocalDrag(init, evt) { const baseEv = store.getEventById(init.id) if ( baseEv && - baseEv.isRepeating && - baseEv.repeat === 'weeks' && - Array.isArray(baseEv.repeatWeekdays) + baseEv.recur && + baseEv.recur.freq === 'weeks' && + Array.isArray(baseEv.recur.weekdays) ) { - originalPattern = [...baseEv.repeatWeekdays] + originalPattern = [...baseEv.recur.weekdays] } } catch {} } @@ -286,8 +286,8 @@ function onDragPointerMove(e) { const shift = currentWeekday - st.originalWeekday const rotated = store._rotateWeekdayPattern([...st.originalPattern], shift) const ev = store.getEventById(st.id) - if (ev && ev.repeat === 'weeks') { - ev.repeatWeekdays = rotated + if (ev && ev.recur && ev.recur.freq === 'weeks') { + ev.recur.weekdays = rotated store.touchEvents() } } catch {} diff --git a/src/components/WeekdaySelector.vue b/src/components/WeekdaySelector.vue index 601e7ae..ef35aa4 100644 --- a/src/components/WeekdaySelector.vue +++ b/src/components/WeekdaySelector.vue @@ -33,7 +33,7 @@ diff --git a/src/plugins/virtualWeeks.js b/src/plugins/virtualWeeks.js index ad6dcbf..83c6131 100644 --- a/src/plugins/virtualWeeks.js +++ b/src/plugins/virtualWeeks.js @@ -65,14 +65,14 @@ export function createVirtualWeekManager({ const repeatingBases = [] if (calendarStore.events) { for (const ev of calendarStore.events.values()) { - if (ev.isRepeating) repeatingBases.push(ev) + if (ev.recur) repeatingBases.push(ev) } } const collectEventsForDate = (dateStr, curDateObj) => { const storedEvents = [] for (const ev of calendarStore.events.values()) { - if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) { + if (!ev.recur && dateStr >= ev.startDate && dateStr <= ev.endDate) { storedEvents.push(ev) } } @@ -289,7 +289,7 @@ export function createVirtualWeekManager({ if (!visibleWeeks.value.length) return const repeatingBases = [] if (calendarStore.events) { - for (const ev of calendarStore.events.values()) if (ev.isRepeating) repeatingBases.push(ev) + for (const ev of calendarStore.events.values()) if (ev.recur) repeatingBases.push(ev) } const selStart = selection.value.startDate const selCount = selection.value.dayCount @@ -303,7 +303,7 @@ export function createVirtualWeekManager({ // Rebuild events list for this day const storedEvents = [] for (const ev of calendarStore.events.values()) { - if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) { + if (!ev.recur && dateStr >= ev.startDate && dateStr <= ev.endDate) { storedEvents.push(ev) } } diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index 1a27bca..763a978 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -137,11 +137,17 @@ export const useCalendarStore = defineStore('calendar', { eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate), startTime: singleDay ? eventData.startTime || '09:00' : null, durationMinutes: singleDay ? eventData.durationMinutes || 60 : null, - repeat: ['weeks', 'months'].includes(eventData.repeat) ? eventData.repeat : 'none', - repeatInterval: eventData.repeatInterval || 1, - repeatCount: eventData.repeatCount || 'unlimited', - repeatWeekdays: eventData.repeatWeekdays, - isRepeating: eventData.repeat && eventData.repeat !== 'none', + recur: + eventData.recur && ['weeks', 'months'].includes(eventData.recur.freq) + ? { + freq: eventData.recur.freq, + interval: eventData.recur.interval || 1, + count: eventData.recur.count ?? 'unlimited', + weekdays: Array.isArray(eventData.recur.weekdays) + ? [...eventData.recur.weekdays] + : null, + } + : null, } this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate }) this.notifyEventsChanged() @@ -181,12 +187,12 @@ export const useCalendarStore = defineStore('calendar', { deleteFirstOccurrence(baseId) { const base = this.getEventById(baseId) if (!base) return - if (!base.isRepeating) { + if (!base.recur) { this.deleteEvent(baseId) return } const numericCount = - base.repeatCount === 'unlimited' ? Infinity : parseInt(base.repeatCount, 10) + base.recur.count === 'unlimited' ? Infinity : parseInt(base.recur.count, 10) if (numericCount <= 1) { this.deleteEvent(baseId) return @@ -205,7 +211,7 @@ export const useCalendarStore = defineStore('calendar', { ) base.startDate = nextStartStr base.endDate = newEndStr - if (numericCount !== Infinity) base.repeatCount = String(Math.max(1, numericCount - 1)) + if (numericCount !== Infinity) base.recur.count = String(Math.max(1, numericCount - 1)) this.events.set(baseId, { ...base, isSpanning: base.startDate < base.endDate }) this.notifyEventsChanged() }, @@ -215,7 +221,7 @@ export const useCalendarStore = defineStore('calendar', { if (occurrenceIndex == null) return const base = this.getEventById(baseId) if (!base) return - if (!base.isRepeating) { + if (!base.recur) { if (occurrenceIndex === 0) this.deleteEvent(baseId) return } @@ -224,7 +230,8 @@ export const useCalendarStore = defineStore('calendar', { return } const snapshot = { ...base } - base.repeatCount = occurrenceIndex + snapshot.recur = snapshot.recur ? { ...snapshot.recur } : null + base.recur.count = occurrenceIndex const nextStartStr = getOccurrenceDate(snapshot, occurrenceIndex + 1, DEFAULT_TZ) if (!nextStartStr) return const durationDays = Math.max( @@ -236,7 +243,7 @@ export const useCalendarStore = defineStore('calendar', { ) const newEndStr = toLocalString(addDays(fromLocalString(nextStartStr), durationDays)) const originalNumeric = - snapshot.repeatCount === 'unlimited' ? Infinity : parseInt(snapshot.repeatCount, 10) + snapshot.recur.count === 'unlimited' ? Infinity : parseInt(snapshot.recur.count, 10) let remainingCount = 'unlimited' if (originalNumeric !== Infinity) { const rem = originalNumeric - (occurrenceIndex + 1) @@ -248,10 +255,14 @@ export const useCalendarStore = defineStore('calendar', { startDate: nextStartStr, endDate: newEndStr, colorId: snapshot.colorId, - repeat: snapshot.repeat, - repeatInterval: snapshot.repeatInterval, - repeatCount: remainingCount, - repeatWeekdays: snapshot.repeatWeekdays, + recur: snapshot.recur + ? { + freq: snapshot.recur.freq, + interval: snapshot.recur.interval, + count: remainingCount, + weekdays: snapshot.recur.weekdays ? [...snapshot.recur.weekdays] : null, + } + : null, }) this.notifyEventsChanged() }, @@ -259,7 +270,7 @@ export const useCalendarStore = defineStore('calendar', { deleteFromOccurrence(ctx) { const { baseId, occurrenceIndex } = ctx const base = this.getEventById(baseId) - if (!base || !base.isRepeating) return + if (!base || !base.recur) return if (occurrenceIndex === 0) { this.deleteEvent(baseId) return @@ -288,15 +299,15 @@ export const useCalendarStore = defineStore('calendar', { if ( rotatePattern && (mode === 'move' || mode === 'resize-left') && - snapshot.isRepeating && - snapshot.repeat === 'weeks' && - Array.isArray(snapshot.repeatWeekdays) + snapshot.recur && + snapshot.recur.freq === 'weeks' && + Array.isArray(snapshot.recur.weekdays) ) { const oldDow = prevStart.getDay() const newDow = newStart.getDay() const shift = newDow - oldDow if (shift !== 0) { - snapshot.repeatWeekdays = this._rotateWeekdayPattern(snapshot.repeatWeekdays, shift) + snapshot.recur.weekdays = this._rotateWeekdayPattern(snapshot.recur.weekdays, shift) } } this.events.set(eventId, { ...snapshot, isSpanning: snapshot.startDate < snapshot.endDate }) @@ -305,8 +316,8 @@ export const useCalendarStore = defineStore('calendar', { splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) { const base = this.events.get(baseId) - if (!base || !base.isRepeating) return - const originalCountRaw = base.repeatCount + if (!base || !base.recur) return + const originalCountRaw = base.recur.count const occurrenceDate = fromLocalString(occurrenceDateStr, DEFAULT_TZ) const baseStart = fromLocalString(base.startDate, DEFAULT_TZ) // If series effectively has <=1 occurrence, treat as simple move (no split) and flatten @@ -317,9 +328,8 @@ export const useCalendarStore = defineStore('calendar', { } if (totalOccurrences <= 1) { // Flatten to non-repeating if not already - if (base.isRepeating) { - base.repeat = 'none' - base.isRepeating = false + if (base.recur) { + base.recur = null this.events.set(baseId, { ...base }) } this.setEventRange(baseId, newStartStr, newEndStr, { mode: 'move', rotatePattern: true }) @@ -330,9 +340,9 @@ export const useCalendarStore = defineStore('calendar', { return baseId } let keptOccurrences = 0 - if (base.repeat === 'weeks') { - const interval = base.repeatInterval || 1 - const pattern = base.repeatWeekdays || [] + if (base.recur.freq === 'weeks') { + const interval = base.recur.interval || 1 + const pattern = base.recur.weekdays || [] if (!pattern.some(Boolean)) return const WEEK_MS = 7 * 86400000 const blockStartBase = getMondayOfISOWeek(baseStart) @@ -346,11 +356,11 @@ export const useCalendarStore = defineStore('calendar', { if (pattern[cursor.getDay()] && isAligned(cursor)) keptOccurrences++ cursor = addDays(cursor, 1) } - } else if (base.repeat === 'months') { + } else if (base.recur.freq === 'months') { const diffMonths = (occurrenceDate.getFullYear() - baseStart.getFullYear()) * 12 + (occurrenceDate.getMonth() - baseStart.getMonth()) - const interval = base.repeatInterval || 1 + const interval = base.recur.interval || 1 if (diffMonths <= 0 || diffMonths % interval !== 0) return keptOccurrences = diffMonths } else { @@ -359,7 +369,12 @@ export const useCalendarStore = defineStore('calendar', { this._terminateRepeatSeriesAtIndex(baseId, keptOccurrences) // After truncation compute base kept count const truncated = this.events.get(baseId) - if (truncated && truncated.repeatCount && truncated.repeatCount !== 'unlimited') { + if ( + truncated && + truncated.recur && + truncated.recur.count && + truncated.recur.count !== 'unlimited' + ) { // keptOccurrences already reflects number before split; adjust not needed further } let remainingCount = 'unlimited' @@ -371,13 +386,13 @@ export const useCalendarStore = defineStore('calendar', { remainingCount = String(rem) } } - let repeatWeekdays = base.repeatWeekdays - if (base.repeat === 'weeks' && Array.isArray(base.repeatWeekdays)) { + let weekdays = base.recur.weekdays + if (base.recur.freq === 'weeks' && Array.isArray(base.recur.weekdays)) { const origWeekday = occurrenceDate.getDay() const newWeekday = fromLocalString(newStartStr, DEFAULT_TZ).getDay() const shift = newWeekday - origWeekday if (shift !== 0) { - repeatWeekdays = this._rotateWeekdayPattern(base.repeatWeekdays, shift) + weekdays = this._rotateWeekdayPattern(base.recur.weekdays, shift) } } const newId = this.createEvent({ @@ -385,29 +400,29 @@ export const useCalendarStore = defineStore('calendar', { startDate: newStartStr, endDate: newEndStr, colorId: base.colorId, - repeat: base.repeat, - repeatInterval: base.repeatInterval, - repeatCount: remainingCount, - repeatWeekdays, + recur: { + freq: base.recur.freq, + interval: base.recur.interval, + count: remainingCount, + weekdays, + }, }) // Flatten base if single occurrence now - if (truncated && truncated.isRepeating) { + if (truncated && truncated.recur) { const baseCountNum = - truncated.repeatCount === 'unlimited' ? Infinity : parseInt(truncated.repeatCount, 10) + truncated.recur.count === 'unlimited' ? Infinity : parseInt(truncated.recur.count, 10) if (baseCountNum <= 1) { - truncated.repeat = 'none' - truncated.isRepeating = false + truncated.recur = null this.events.set(baseId, { ...truncated }) } } // Flatten new if single occurrence only const newly = this.events.get(newId) - if (newly && newly.isRepeating) { + if (newly && newly.recur) { const newCountNum = - newly.repeatCount === 'unlimited' ? Infinity : parseInt(newly.repeatCount, 10) + newly.recur.count === 'unlimited' ? Infinity : parseInt(newly.recur.count, 10) if (newCountNum <= 1) { - newly.repeat = 'none' - newly.isRepeating = false + newly.recur = null this.events.set(newId, { ...newly }) } } @@ -417,8 +432,8 @@ export const useCalendarStore = defineStore('calendar', { splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr) { const base = this.events.get(baseId) - if (!base || !base.isRepeating) return null - const originalCountRaw = base.repeatCount + if (!base || !base.recur) return null + const originalCountRaw = base.recur.count this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex) let newSeriesCount = 'unlimited' if (originalCountRaw !== 'unlimited') { @@ -433,21 +448,25 @@ export const useCalendarStore = defineStore('calendar', { startDate: newStartStr, endDate: newEndStr, colorId: base.colorId, - repeat: base.repeat, - repeatInterval: base.repeatInterval, - repeatCount: newSeriesCount, - repeatWeekdays: base.repeatWeekdays, + recur: base.recur + ? { + freq: base.recur.freq, + interval: base.recur.interval, + count: newSeriesCount, + weekdays: base.recur.weekdays ? [...base.recur.weekdays] : null, + } + : null, }) }, _terminateRepeatSeriesAtIndex(baseId, index) { const ev = this.events.get(baseId) - if (!ev || !ev.isRepeating) return - if (ev.repeatCount === 'unlimited') { - ev.repeatCount = String(index) + if (!ev || !ev.recur) return + if (ev.recur.count === 'unlimited') { + ev.recur.count = String(index) } else { - const rc = parseInt(ev.repeatCount, 10) - if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index)) + const rc = parseInt(ev.recur.count, 10) + if (!isNaN(rc)) ev.recur.count = String(Math.min(rc, index)) } this.notifyEventsChanged() }, diff --git a/src/utils/date.js b/src/utils/date.js index d6e5f7b..c5d7c17 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -81,9 +81,14 @@ function countPatternDaysInInterval(startDate, endDate, patternArr) { } // Recurrence: Weekly ------------------------------------------------------ +function _getRecur(event) { + return event && event.recur ? event.recur : null +} + function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat !== 'weeks') return null - const pattern = event.repeatWeekdays || [] + const recur = _getRecur(event) + if (!recur || recur.freq !== 'weeks') return null + const pattern = recur.weekdays || [] if (!pattern.some(Boolean)) return null const target = fromLocalString(dateStr, timeZone) @@ -93,7 +98,7 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { const dow = dateFns.getDay(target) if (!pattern[dow]) return null // target not active - const interval = event.repeatInterval || 1 + const interval = recur.interval || 1 const baseBlockStart = getMondayOfISOWeek(baseStart, timeZone) const currentBlockStart = getMondayOfISOWeek(target, timeZone) // Number of weeks between block starts (each block start is a Monday) @@ -106,8 +111,9 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { // Same ISO week as base: count pattern days from baseStart up to target (inclusive) if (weekDiff === 0) { let n = countPatternDaysInInterval(baseStart, target, pattern) - 1 - if (!baseCountsAsPattern) n += 1 // Shift indices so base occurrence stays 0 - return n < 0 || n >= event.repeatCount ? null : n + if (!baseCountsAsPattern) n += 1 + const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10) + return n < 0 || n >= maxCount ? null : n } const baseWeekEnd = dateFns.addDays(baseBlockStart, 6) @@ -120,42 +126,48 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { const currentWeekCount = countPatternDaysInInterval(currentBlockStart, target, pattern) let n = firstWeekCount + middleWeeksCount + currentWeekCount - 1 if (!baseCountsAsPattern) n += 1 - return n >= event.repeatCount ? null : n + const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10) + return n >= maxCount ? null : n } // Recurrence: Monthly ----------------------------------------------------- function getMonthlyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat !== 'months') return null + const recur = _getRecur(event) + if (!recur || recur.freq !== 'months') return null const baseStart = fromLocalString(event.startDate, timeZone) const d = fromLocalString(dateStr, timeZone) const diffMonths = dateFns.differenceInCalendarMonths(d, baseStart) if (diffMonths < 0) return null - const interval = event.repeatInterval || 1 + const interval = recur.interval || 1 if (diffMonths % interval !== 0) return null const baseDay = dateFns.getDate(baseStart) const effectiveDay = Math.min(baseDay, dateFns.getDaysInMonth(d)) if (dateFns.getDate(d) !== effectiveDay) return null const n = diffMonths / interval - return n >= event.repeatCount ? null : n + const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10) + return n >= maxCount ? null : n } function getOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat === 'none') return null + const recur = _getRecur(event) + if (!recur) return null if (dateStr < event.startDate) return null - if (event.repeat === 'weeks') return getWeeklyOccurrenceIndex(event, dateStr, timeZone) - if (event.repeat === 'months') return getMonthlyOccurrenceIndex(event, dateStr, timeZone) + if (recur.freq === 'weeks') return getWeeklyOccurrenceIndex(event, dateStr, timeZone) + if (recur.freq === 'months') return getMonthlyOccurrenceIndex(event, dateStr, timeZone) return null } // Reverse lookup: given a recurrence index (0-based) return the occurrence start date string. // Returns null if the index is out of range or the event is not repeating. function getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat !== 'weeks') return null + const recur = _getRecur(event) + if (!recur || recur.freq !== 'weeks') return null if (occurrenceIndex < 0 || !Number.isInteger(occurrenceIndex)) return null - if (event.repeatCount !== 'unlimited' && occurrenceIndex >= event.repeatCount) return null - const pattern = event.repeatWeekdays || [] + const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10) + if (occurrenceIndex >= maxCount) return null + const pattern = recur.weekdays || [] if (!pattern.some(Boolean)) return null - const interval = event.repeatInterval || 1 + const interval = recur.interval || 1 const baseStart = fromLocalString(event.startDate, timeZone) if (occurrenceIndex === 0) return toLocalString(baseStart, timeZone) const baseWeekMonday = getMondayOfISOWeek(baseStart, timeZone) @@ -192,10 +204,12 @@ function getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) } function getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat !== 'months') return null + const recur = _getRecur(event) + if (!recur || recur.freq !== 'months') return null if (occurrenceIndex < 0 || !Number.isInteger(occurrenceIndex)) return null - if (event.repeatCount !== 'unlimited' && occurrenceIndex >= event.repeatCount) return null - const interval = event.repeatInterval || 1 + const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10) + if (occurrenceIndex >= maxCount) return null + const interval = recur.interval || 1 const baseStart = fromLocalString(event.startDate, timeZone) const targetMonthOffset = occurrenceIndex * interval const monthDate = dateFns.addMonths(baseStart, targetMonthOffset) @@ -208,9 +222,10 @@ function getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) } function getOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) { - if (!event?.isRepeating || event.repeat === 'none') return null - if (event.repeat === 'weeks') return getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone) - if (event.repeat === 'months') return getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone) + const recur = _getRecur(event) + if (!recur) return null + if (recur.freq === 'weeks') return getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone) + if (recur.freq === 'months') return getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone) return null }