diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index b6c4af3..d7fc5e5 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -259,6 +259,61 @@ export const useCalendarStore = defineStore('calendar', { if (!base || !base.isRepeating) return // WEEKLY SERIES ------------------------------------------------------ if (base.repeat === 'weeks') { + // Special case: deleting the first occurrence (index 0) should shift the series forward + if (occurrenceIndex === 0) { + const baseStart = fromLocalString(base.startDate) + const baseEnd = fromLocalString(base.endDate) + const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000))) + const pattern = base.repeatWeekdays || [] + if (!pattern.some(Boolean)) { + // No pattern to continue -> delete whole series + this.deleteEvent(baseId) + return + } + const interval = base.repeatInterval || 1 + const WEEK_MS = 7 * 86400000 + const baseBlockStart = getMondayOfISOWeek(baseStart) + const isAligned = (d) => { + const blk = getMondayOfISOWeek(d) + const diff = Math.floor((blk - baseBlockStart) / WEEK_MS) + return diff % interval === 0 + } + const probe = new Date(baseStart) + let safety = 0 + let found = null + while (safety < 5000) { + probe.setDate(probe.getDate() + 1) + if (pattern[probe.getDay()] && isAligned(probe)) { + found = new Date(probe) + break + } + safety++ + } + if (!found) { + // Nothing after first -> delete series + this.deleteEvent(baseId) + return + } + // Adjust repeat count + if (base.repeatCount !== 'unlimited') { + const rc = parseInt(base.repeatCount, 10) + if (!isNaN(rc)) { + const newRc = rc - 1 + if (newRc <= 0) { + this.deleteEvent(baseId) + return + } + base.repeatCount = String(newRc) + } + } + const newEnd = new Date(found) + newEnd.setDate(newEnd.getDate() + spanDays) + base.startDate = toLocalString(found) + base.endDate = toLocalString(newEnd) + base.isSpanning = base.startDate < base.endDate + this.events.set(base.id, base) + return + } const interval = base.repeatInterval || 1 const pattern = base.repeatWeekdays || [] if (!pattern.some(Boolean)) return @@ -372,6 +427,36 @@ export const useCalendarStore = defineStore('calendar', { } // MONTHLY SERIES ----------------------------------------------------- if (base.repeat === 'months') { + if (occurrenceIndex === 0) { + const baseStart = fromLocalString(base.startDate) + const baseEnd = fromLocalString(base.endDate) + const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000))) + const interval = base.repeatInterval || 1 + const targetMonthIndex = baseStart.getMonth() + interval + const targetYear = baseStart.getFullYear() + Math.floor(targetMonthIndex / 12) + const targetMonth = targetMonthIndex % 12 + const daysInTarget = new Date(targetYear, targetMonth + 1, 0).getDate() + const dom = Math.min(baseStart.getDate(), daysInTarget) + const newStart = new Date(targetYear, targetMonth, dom) + if (base.repeatCount !== 'unlimited') { + const rc = parseInt(base.repeatCount, 10) + if (!isNaN(rc)) { + const newRc = rc - 1 + if (newRc <= 0) { + this.deleteEvent(baseId) + return + } + base.repeatCount = String(newRc) + } + } + const newEnd = new Date(newStart) + newEnd.setDate(newEnd.getDate() + spanDays) + base.startDate = toLocalString(newStart) + base.endDate = toLocalString(newEnd) + base.isSpanning = base.startDate < base.endDate + this.events.set(base.id, base) + return + } const interval = base.repeatInterval || 1 // Sequential index: base=0, first repeat=1 if (occurrenceIndex <= 0) return // base deletion handled elsewhere @@ -434,47 +519,67 @@ export const useCalendarStore = defineStore('calendar', { deleteFirstOccurrence(baseId) { const base = this.getEventById(baseId) if (!base || !base.isRepeating) return - const oldStart = new Date(fromLocalString(base.startDate)) - const oldEnd = new Date(fromLocalString(base.endDate)) - const spanDays = Math.round((oldEnd - oldStart) / (24 * 60 * 60 * 1000)) - let newStart = null + const oldStart = fromLocalString(base.startDate) + const oldEnd = fromLocalString(base.endDate) + const spanDays = Math.max(0, Math.round((oldEnd - oldStart) / (24 * 60 * 60 * 1000))) - if (base.repeat === 'weeks' && base.repeatWeekdays) { + let newStartDate = null + + if (base.repeat === 'weeks') { + const pattern = base.repeatWeekdays || [] + if (!pattern.some(Boolean)) { + // No valid pattern -> delete series + this.deleteEvent(baseId) + return + } + const interval = base.repeatInterval || 1 + const baseBlockStart = getMondayOfISOWeek(oldStart) + const WEEK_MS = 7 * 86400000 + const isAligned = (d) => { + const block = getMondayOfISOWeek(d) + const diff = Math.floor((block - baseBlockStart) / WEEK_MS) + return diff % interval === 0 + } + // search forward for next valid weekday respecting interval alignment const probe = new Date(oldStart) - for (let i = 0; i < 14; i++) { - // search ahead up to 2 weeks + let safety = 0 + while (safety < 5000) { probe.setDate(probe.getDate() + 1) - if (base.repeatWeekdays[probe.getDay()]) { - newStart = new Date(probe) + if (pattern[probe.getDay()] && isAligned(probe)) { + newStartDate = new Date(probe) break } + safety++ } } else if (base.repeat === 'months') { - // Advance one month, clamping to last day if necessary - const o = oldStart - const nextMonthIndex = o.getMonth() + 1 - const y = o.getFullYear() + Math.floor(nextMonthIndex / 12) - const m = nextMonthIndex % 12 - const daysInTargetMonth = new Date(y, m + 1, 0).getDate() - const dom = Math.min(o.getDate(), daysInTargetMonth) - newStart = new Date(y, m, dom) + const interval = base.repeatInterval || 1 + const y = oldStart.getFullYear() + const m = oldStart.getMonth() + const targetMonthIndex = m + interval + const targetYear = y + Math.floor(targetMonthIndex / 12) + const targetMonth = targetMonthIndex % 12 + const daysInTargetMonth = new Date(targetYear, targetMonth + 1, 0).getDate() + const dom = Math.min(oldStart.getDate(), daysInTargetMonth) + newStartDate = new Date(targetYear, targetMonth, dom) } else { - // Unknown pattern: delete entire series + // Unsupported repeat type this.deleteEvent(baseId) return } - if (!newStart) { - // No subsequent occurrence -> delete entire series + if (!newStartDate) { + // No continuation; deleting first removes series this.deleteEvent(baseId) return } + // Decrement repeatCount if limited if (base.repeatCount !== 'unlimited') { const rc = parseInt(base.repeatCount, 10) if (!isNaN(rc)) { - const newRc = Math.max(0, rc - 1) - if (newRc === 0) { + const newRc = rc - 1 + if (newRc <= 0) { + // After removing first occurrence there are none left this.deleteEvent(baseId) return } @@ -482,65 +587,14 @@ export const useCalendarStore = defineStore('calendar', { } } - const newEnd = new Date(newStart) - newEnd.setDate(newEnd.getDate() + spanDays) - base.startDate = toLocalString(newStart) - base.endDate = toLocalString(newEnd) - // old occurrence expansion removed (series handled differently now) - const originalRepeatCount = base.repeatCount - // Always cap original series at the split occurrence index (occurrences 0..index-1) - // Keep its weekday pattern unchanged. - this._terminateRepeatSeriesAtIndex(baseId, index) - - let newRepeatCount = 'unlimited' - if (originalRepeatCount !== 'unlimited') { - const originalCount = parseInt(originalRepeatCount, 10) - if (!isNaN(originalCount)) { - const remaining = originalCount - index - // remaining occurrences go to new series; ensure at least 1 (the dragged occurrence itself) - newRepeatCount = remaining > 0 ? String(remaining) : '1' - } - } else { - // Original was unlimited: original now capped, new stays unlimited - newRepeatCount = 'unlimited' - } - - // Handle weekdays for weekly repeats - let newRepeatWeekdays = base.repeatWeekdays - if (base.repeat === 'weeks' && base.repeatWeekdays) { - const newStartDate = new Date(fromLocalString(startDate)) - let dayShift = 0 - if (grabbedWeekday != null) { - // Rotate so that the grabbed weekday maps to the new start weekday - dayShift = newStartDate.getDay() - grabbedWeekday - } else { - // Fallback: rotate by difference between new and original start weekday - const originalStartDate = new Date(fromLocalString(base.startDate)) - dayShift = newStartDate.getDay() - originalStartDate.getDay() - } - if (dayShift !== 0) { - const rotatedWeekdays = [false, false, false, false, false, false, false] - for (let i = 0; i < 7; i++) { - if (base.repeatWeekdays[i]) { - let nd = (i + dayShift) % 7 - if (nd < 0) nd += 7 - rotatedWeekdays[nd] = true - } - } - newRepeatWeekdays = rotatedWeekdays - } - } - - const newId = this.createEvent({ - title: base.title, - startDate, - endDate, - colorId: base.colorId, - repeat: base.repeat, - repeatCount: newRepeatCount, - repeatWeekdays: newRepeatWeekdays, - }) - return newId + const newEndDate = new Date(newStartDate) + newEndDate.setDate(newEndDate.getDate() + spanDays) + base.startDate = toLocalString(newStartDate) + base.endDate = toLocalString(newEndDate) + base.isSpanning = base.startDate < base.endDate + // Persist updated base event + this.events.set(base.id, base) + return base.id }, _snapshotBaseEvent(eventId) { diff --git a/src/utils/date.js b/src/utils/date.js index 6e9c5ba..ee7f501 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -103,24 +103,24 @@ function getWeeklyOccurrenceIndex(event, dateStr) { if (d.getTime() === baseStart.getTime()) { return 0 // Base occurrence is always index 0 } - + if (d < baseStart) { return null // Dates before base start in same week are not valid occurrences } - + let occurrenceIndex = 0 const cursor = new Date(baseStart) - + // Count the base occurrence first if (pattern[cursor.getDay()]) occurrenceIndex++ - + // Move to the next day and count until we reach the target cursor.setDate(cursor.getDate() + 1) while (cursor <= d) { if (pattern[cursor.getDay()]) occurrenceIndex++ cursor.setDate(cursor.getDate() + 1) } - + // Subtract 1 because we want the index, not the count occurrenceIndex-- @@ -139,16 +139,16 @@ function getWeeklyOccurrenceIndex(event, dateStr) { const firstWeekCursor = new Date(baseStart) const firstWeekEnd = new Date(baseBlockStart) firstWeekEnd.setDate(firstWeekEnd.getDate() + 6) // End of first week (Sunday) - + while (firstWeekCursor <= firstWeekEnd) { if (pattern[firstWeekCursor.getDay()]) firstWeekPatternDays++ firstWeekCursor.setDate(firstWeekCursor.getDate() + 1) } - + // For subsequent complete intervals, use the full pattern count const fullWeekdaysPerInterval = pattern.filter(Boolean).length const completeIntervals = blocksDiff / interval - + // First interval uses actual first week count, remaining intervals use full count let occurrenceIndex = firstWeekPatternDays + (completeIntervals - 1) * fullWeekdaysPerInterval