Recurrent event handling bugfixes.

This commit is contained in:
Leo Vasanko 2025-08-22 20:34:56 -06:00
parent d46aaa6106
commit 1257fba211
2 changed files with 115 additions and 50 deletions

View File

@ -44,7 +44,7 @@ const fallbackWeekdays = computed(() => {
return fallback return fallback
}) })
// Repeat mapping uses 'weeks' | 'months' | 'none' directly (legacy 'weekly'/'monthly' accepted on load) // Repeat mapping uses 'weeks' | 'months' | 'none' directly
const repeat = computed({ const repeat = computed({
get() { get() {
if (!recurrenceEnabled.value) return 'none' if (!recurrenceEnabled.value) return 'none'
@ -56,8 +56,8 @@ const repeat = computed({
return return
} }
recurrenceEnabled.value = true recurrenceEnabled.value = true
if (val === 'weeks' || val === 'weekly') recurrenceFrequency.value = 'weeks' if (val === 'weeks') recurrenceFrequency.value = 'weeks'
else if (val === 'months' || val === 'monthly') recurrenceFrequency.value = 'months' else if (val === 'months') recurrenceFrequency.value = 'months'
}, },
}) })
@ -164,18 +164,31 @@ function openEditDialog(eventInstanceId) {
} }
const event = calendarStore.getEventById(baseId) const event = calendarStore.getEventById(baseId)
if (!event) return if (!event) return
// Derive occurrence date if weekly occurrence // Derive occurrence date for repeat occurrences (occurrenceIndex > 0 means not the base)
if (weekday != null) { if (
// Recompute occurrence date: iterate days accumulating selected weekdays event.isRepeating &&
const repeatWeekdaysLocal = event.repeatWeekdays ((event.repeat === 'weeks' && occurrenceIndex >= 0) ||
let idx = 0 (event.repeat === 'months' && occurrenceIndex > 0))
let cur = new Date(event.startDate + 'T00:00:00') ) {
while (idx < occurrenceIndex && idx < 10000) { if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
// safety bound const repeatWeekdaysLocal = event.repeatWeekdays || []
cur.setDate(cur.getDate() + 1) const baseDate = new Date(event.startDate + 'T00:00:00')
if (repeatWeekdaysLocal[cur.getDay()]) idx++ // 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
while (matched < occurrenceIndex && safety < 10000) {
cur.setDate(cur.getDate() + 1)
if (repeatWeekdaysLocal[cur.getDay()]) matched++
safety++
}
occurrenceDate = cur
} else if (event.repeat === 'months') {
const cur = new Date(event.startDate + 'T00:00:00')
cur.setMonth(cur.getMonth() + occurrenceIndex)
occurrenceDate = cur
} }
occurrenceDate = cur
} }
dialogMode.value = 'edit' dialogMode.value = 'edit'
editingEventId.value = baseId editingEventId.value = baseId
@ -188,8 +201,13 @@ function openEditDialog(eventInstanceId) {
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0 recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
colorId.value = event.colorId colorId.value = event.colorId
eventSaved.value = false eventSaved.value = false
if (event.isRepeating && occurrenceIndex >= 0 && weekday != null) { // Build occurrence context: treat any occurrenceIndex > 0 as a specific occurrence (weekday only relevant for weekly)
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate } if (event.isRepeating) {
if (event.repeat === 'weeks' && weekday != null && occurrenceIndex >= 0) {
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
} else if (event.repeat === 'months' && occurrenceIndex > 0) {
occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate }
}
} }
showDialog.value = true showDialog.value = true

View File

@ -55,12 +55,8 @@ export const useCalendarStore = defineStore('calendar', {
eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate), eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
startTime: singleDay ? eventData.startTime || '09:00' : null, startTime: singleDay ? eventData.startTime || '09:00' : null,
durationMinutes: singleDay ? eventData.durationMinutes || 60 : null, durationMinutes: singleDay ? eventData.durationMinutes || 60 : null,
repeat: // Normalized repeat value: only 'weeks', 'months', or 'none'
(eventData.repeat === 'weekly' repeat: ['weeks', 'months'].includes(eventData.repeat) ? eventData.repeat : 'none',
? 'weeks'
: eventData.repeat === 'monthly'
? 'months'
: eventData.repeat) || 'none',
repeatInterval: eventData.repeatInterval || 1, repeatInterval: eventData.repeatInterval || 1,
repeatCount: eventData.repeatCount || 'unlimited', repeatCount: eventData.repeatCount || 'unlimited',
repeatWeekdays: eventData.repeatWeekdays, repeatWeekdays: eventData.repeatWeekdays,
@ -134,35 +130,86 @@ export const useCalendarStore = defineStore('calendar', {
deleteSingleOccurrence(ctx) { deleteSingleOccurrence(ctx) {
const { baseId, occurrenceIndex } = ctx const { baseId, occurrenceIndex } = ctx
const base = this.getEventById(baseId) const base = this.getEventById(baseId)
if (!base || base.repeat !== 'weekly') return if (!base || !base.isRepeating) return
if (!base || base.repeat !== 'weeks') return // WEEKLY SERIES ------------------------------------------------------
// Strategy: clone pattern into two: reduce base repeatCount to exclude this occurrence; create new series for remainder excluding this one if (base.repeat === 'weeks') {
// Simpler: convert base to explicit exception by creating a one-off non-repeating event? For now: create exception by inserting a blocking dummy? Instead just split by shifting repeatCount logic: decrement repeatCount and create a new series starting after this single occurrence. // Strategy: split series around the target occurrence, omitting it.
// Implementation (approx): terminate at occurrenceIndex; create a new series starting next occurrence with remaining occurrences. const remaining =
const remaining = base.repeatCount === 'unlimited'
base.repeatCount === 'unlimited' ? 'unlimited'
? 'unlimited' : String(Math.max(0, parseInt(base.repeatCount, 10) - (occurrenceIndex + 1)))
: String(Math.max(0, parseInt(base.repeatCount, 10) - (occurrenceIndex + 1))) // Keep occurrences before the deleted one
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex) this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
if (remaining === '0') return if (remaining === '0') return
// Find date of next occurrence // Find date of next occurrence (first after deleted)
const startDate = new Date(base.startDate + 'T00:00:00') const startDate = new Date(base.startDate + 'T00:00:00')
let idx = 0 let idx = 0
let cur = new Date(startDate) let cur = new Date(startDate)
while (idx <= occurrenceIndex && idx < 10000) { while (idx <= occurrenceIndex && idx < 10000) {
cur.setDate(cur.getDate() + 1) cur.setDate(cur.getDate() + 1)
if (base.repeatWeekdays[cur.getDay()]) idx++ if (base.repeatWeekdays && base.repeatWeekdays[cur.getDay()]) idx++
}
const nextStartStr = toLocalString(cur)
// Preserve multiday span if any
const spanDays = Math.round(
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
)
const nextEnd = new Date(fromLocalString(nextStartStr))
nextEnd.setDate(nextEnd.getDate() + spanDays)
const nextEndStr = toLocalString(nextEnd)
this.createEvent({
title: base.title,
startDate: nextStartStr,
endDate: nextEndStr,
colorId: base.colorId,
repeat: 'weeks',
repeatCount: remaining,
repeatWeekdays: base.repeatWeekdays,
})
return
}
// 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
const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1
// Truncate base series to keep only priorOccurrences
this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences)
// Compute span days for multiday events
const spanDays = Math.round(
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
)
// Remaining occurrences after deletion
let remainingCount = 'unlimited'
if (base.repeatCount !== 'unlimited') {
const total = parseInt(base.repeatCount, 10)
if (!isNaN(total)) {
const rem = total - priorOccurrences - 1 // subtract kept + deleted
if (rem <= 0) return // nothing left
remainingCount = String(rem)
}
}
// Next occurrence after deleted one is at occurrenceIndex + interval months from base
const baseStart = fromLocalString(base.startDate)
const nextStart = new Date(baseStart)
nextStart.setMonth(nextStart.getMonth() + occurrenceIndex + interval)
const nextEnd = new Date(nextStart)
nextEnd.setDate(nextEnd.getDate() + spanDays)
const nextStartStr = toLocalString(nextStart)
const nextEndStr = toLocalString(nextEnd)
this.createEvent({
title: base.title,
startDate: nextStartStr,
endDate: nextEndStr,
colorId: base.colorId,
repeat: 'months',
repeatInterval: interval,
repeatCount: remainingCount,
})
} }
const nextStartStr = toLocalString(cur)
this.createEvent({
title: base.title,
startDate: nextStartStr,
endDate: nextStartStr,
colorId: base.colorId,
repeat: 'weeks',
repeatCount: remaining,
repeatWeekdays: base.repeatWeekdays,
})
}, },
deleteFromOccurrence(ctx) { deleteFromOccurrence(ctx) {