diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue index d01f4d5..8125130 100644 --- a/src/components/EventOverlay.vue +++ b/src/components/EventOverlay.vue @@ -44,131 +44,7 @@ const store = useCalendarStore() const dragState = ref(null) const justDragged = ref(false) -// Generate repeat occurrences for a specific date -function generateRepeatOccurrencesForDate(targetDateStr) { - const occurrences = [] - - // Get all events from the store and check for repeating ones - for (const [, eventList] of store.events) { - for (const baseEvent of eventList) { - if (!baseEvent.isRepeating || baseEvent.repeat === 'none') { - continue - } - - const targetDate = new Date(fromLocalString(targetDateStr)) - const baseStartDate = new Date(fromLocalString(baseEvent.startDate)) - const baseEndDate = new Date(fromLocalString(baseEvent.endDate)) - const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000)) - - if (baseEvent.repeat === 'weeks') { - const repeatWeekdays = baseEvent.repeatWeekdays - if (targetDate < baseStartDate) continue - const maxOccurrences = - baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10) - if (maxOccurrences === 0) continue - const interval = baseEvent.repeatInterval || 1 - const msPerDay = 24 * 60 * 60 * 1000 - - // Determine if targetDate lies within some occurrence span. We look backwards up to spanDays to find a start day. - let occStart = null - for (let back = 0; back <= spanDays; back++) { - const cand = new Date(targetDate) - cand.setDate(cand.getDate() - back) - if (cand < baseStartDate) break - const daysDiff = Math.floor((cand - baseStartDate) / msPerDay) - const weeksDiff = Math.floor(daysDiff / 7) - if (weeksDiff % interval !== 0) continue - if (repeatWeekdays[cand.getDay()]) { - // candidate start must produce span covering targetDate - const candEnd = new Date(cand) - candEnd.setDate(candEnd.getDate() + spanDays) - if (targetDate <= candEnd) { - occStart = cand - break - } - } - } - if (!occStart) continue - // Skip base occurrence if this is within its span (base already physically stored) - if (occStart.getTime() === baseStartDate.getTime()) continue - // Compute occurrence index (number of previous start days) - let occIdx = 0 - const cursor = new Date(baseStartDate) - while (cursor < occStart && occIdx < maxOccurrences) { - const cDaysDiff = Math.floor((cursor - baseStartDate) / msPerDay) - const cWeeksDiff = Math.floor(cDaysDiff / 7) - if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++ - cursor.setDate(cursor.getDate() + 1) - } - if (occIdx >= maxOccurrences) continue - const occEnd = new Date(occStart) - occEnd.setDate(occStart.getDate() + spanDays) - const occStartStr = toLocalString(occStart) - const occEndStr = toLocalString(occEnd) - occurrences.push({ - ...baseEvent, - id: `${baseEvent.id}_repeat_${occIdx}_${occStart.getDay()}`, - startDate: occStartStr, - endDate: occEndStr, - isRepeatOccurrence: true, - repeatIndex: occIdx, - }) - continue - } else { - // Handle other repeat types (months) - let intervalsPassed = 0 - const timeDiff = targetDate - baseStartDate - if (baseEvent.repeat === 'months') { - intervalsPassed = - (targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 + - (targetDate.getMonth() - baseStartDate.getMonth()) - } else { - continue - } - const interval = baseEvent.repeatInterval || 1 - if (intervalsPassed < 0 || intervalsPassed % interval !== 0) continue - - // Check a few occurrences around the target date - const maxOccurrences = - baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10) - if (maxOccurrences === 0) continue - const i = intervalsPassed - if (i >= maxOccurrences) continue - const currentStart = new Date(baseStartDate) - currentStart.setMonth(baseStartDate.getMonth() + i) - const currentEnd = new Date(currentStart) - currentEnd.setDate(currentStart.getDate() + spanDays) - // If target day lies within base (i===0) we skip because base is stored already - if (i === 0) { - // only skip if targetDate within base span - if (targetDate >= baseStartDate && targetDate <= baseEndDate) continue - } - const currentStartStr = toLocalString(currentStart) - const currentEndStr = toLocalString(currentEnd) - if (currentStartStr <= targetDateStr && targetDateStr <= currentEndStr) { - occurrences.push({ - ...baseEvent, - id: `${baseEvent.id}_repeat_${i}`, - startDate: currentStartStr, - endDate: currentEndStr, - isRepeatOccurrence: true, - repeatIndex: i, - }) - } - } - } - } - - return occurrences -} - -// Extract original event ID from repeat occurrence ID -function getOriginalEventId(eventId) { - if (typeof eventId === 'string' && eventId.includes('_repeat_')) { - return eventId.split('_repeat_')[0] - } - return eventId -} +// (legacy helpers removed) // Handle event click function handleEventClick(span) { @@ -470,38 +346,159 @@ const eventSpans = computed(() => { const spans = [] const weekEvents = new Map() - // Collect events from all days in this week, including repeat occurrences + // Collect stored base events props.week.days.forEach((day, dayIndex) => { - // Get base events for this day - day.events.forEach((event) => { - if (!weekEvents.has(event.id)) { - weekEvents.set(event.id, { - ...event, - startIdx: dayIndex, - endIdx: dayIndex, - }) - } else { - const existing = weekEvents.get(event.id) - existing.endIdx = dayIndex - } - }) - - // Generate repeat occurrences for this day - const repeatOccurrences = generateRepeatOccurrencesForDate(day.date) - repeatOccurrences.forEach((event) => { - if (!weekEvents.has(event.id)) { - weekEvents.set(event.id, { - ...event, - startIdx: dayIndex, - endIdx: dayIndex, - }) - } else { - const existing = weekEvents.get(event.id) - existing.endIdx = dayIndex - } + day.events.forEach((ev) => { + if (!weekEvents.has(ev.id)) { + weekEvents.set(ev.id, { ...ev, startIdx: dayIndex, endIdx: dayIndex }) + } else weekEvents.get(ev.id).endIdx = dayIndex }) }) + // Generate virtual repeats numerically + const weekStart = fromLocalString(props.week.days[0].date) + const weekEnd = fromLocalString(props.week.days[6].date) + const weekStartTime = weekStart.getTime() + const weekEndTime = weekEnd.getTime() + const DAY_MS = 86400000 + + // All repeating base events + const baseEvents = [] + const seen = new Set() + for (const [, list] of store.events) { + for (const ev of list) { + if (ev.isRepeating && !seen.has(ev.id)) { + seen.add(ev.id) + baseEvents.push(ev) + } + } + } + + for (const base of baseEvents) { + if (!base.isRepeating || base.repeat === 'none') continue + const baseStart = fromLocalString(base.startDate) + const baseEnd = fromLocalString(base.endDate) + const spanDays = Math.floor((baseEnd - baseStart) / DAY_MS) + const maxOccurrences = + base.repeatCount === 'unlimited' ? Infinity : parseInt(base.repeatCount, 10) + if (maxOccurrences === 0) continue + + if (base.repeat === 'weeks') { + const pattern = base.repeatWeekdays || [] + const interval = base.repeatInterval || 1 + if (!pattern.some(Boolean)) continue + // Align base block start to week (Sunday=0) + const baseBlockStart = new Date(baseStart) + baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) + // Search window + const searchStart = new Date(weekStart) + searchStart.setDate(searchStart.getDate() - 7) // one block back for early-week carries + const searchEnd = new Date(weekEnd) + searchEnd.setDate(searchEnd.getDate() + 7) // one block forward for late-week upcoming + const startBlocks = Math.floor((searchStart - baseBlockStart) / (7 * DAY_MS)) + const endBlocks = Math.floor((searchEnd - baseBlockStart) / (7 * DAY_MS)) + for (let b = Math.max(0, startBlocks); b <= endBlocks; b++) { + if (b % interval !== 0) continue + const blockStart = new Date(baseBlockStart) + blockStart.setDate(baseBlockStart.getDate() + b * 7) + for (let dow = 0; dow < 7; dow++) { + if (!pattern[dow]) continue + const cand = new Date(blockStart) + cand.setDate(blockStart.getDate() + dow) + if (cand < baseStart) continue + const isBase = cand.getTime() === baseStart.getTime() + const candStartTime = cand.getTime() + const candEndTime = candStartTime + spanDays * DAY_MS + const overlaps = candStartTime <= weekEndTime && candEndTime >= weekStartTime + if (!isBase && overlaps) { + let occIdx = 0 + const cursor = new Date(baseStart) + while (cursor < cand && occIdx < maxOccurrences) { + const weeksFromBase = Math.floor((cursor - baseBlockStart) / (7 * DAY_MS)) + if ( + weeksFromBase % interval === 0 && + pattern[cursor.getDay()] && + cursor.getTime() !== baseStart.getTime() + ) { + occIdx++ + } + cursor.setDate(cursor.getDate() + 1) + } + if (occIdx >= maxOccurrences && isFinite(maxOccurrences)) break + const occStartStr = toLocalString(cand) + const occEnd = new Date(cand) + occEnd.setDate(occEnd.getDate() + spanDays) + const occEndStr = toLocalString(occEnd) + let startIdx = -1 + let endIdx = -1 + props.week.days.forEach((d, idx) => { + if (startIdx === -1 && d.date >= occStartStr && d.date <= occEndStr) startIdx = idx + if (d.date >= occStartStr && d.date <= occEndStr) endIdx = idx + }) + const id = `${base.id}_repeat_${occIdx}_${cand.getDay()}` + if ((startIdx !== -1 || endIdx !== -1) && !weekEvents.has(id)) { + weekEvents.set(id, { + ...base, + id, + startDate: occStartStr, + endDate: occEndStr, + isRepeatOccurrence: true, + repeatIndex: occIdx, + startIdx: startIdx === -1 ? 0 : startIdx, + endIdx: endIdx === -1 ? 6 : endIdx, + }) + } + } + } + } + } else if (base.repeat === 'months') { + const interval = base.repeatInterval || 1 + const baseDay = baseStart.getDate() + const startMonthIndex = baseStart.getFullYear() * 12 + baseStart.getMonth() + const endMonthIndex = weekEnd.getFullYear() * 12 + weekEnd.getMonth() + for (let mi = startMonthIndex; mi <= endMonthIndex + 1; mi++) { + // +1 to catch overlap spilling in + const diff = mi - startMonthIndex + if (diff === 0) continue // base occurrence already stored + if (diff % interval !== 0) continue + if (diff > maxOccurrences && isFinite(maxOccurrences)) break + const y = Math.floor(mi / 12) + const m = mi % 12 + const daysInMonth = new Date(y, m + 1, 0).getDate() + const dom = Math.min(baseDay, daysInMonth) + const cand = new Date(y, m, dom) + if (cand < baseStart) continue + const candEnd = new Date(cand) + const spanDays = Math.floor((baseEnd - baseStart) / DAY_MS) + candEnd.setDate(candEnd.getDate() + spanDays) + const candStartStr = toLocalString(cand) + const candEndStr = toLocalString(candEnd) + const overlaps = cand.getTime() <= weekEndTime && candEnd.getTime() >= weekStartTime + if (!overlaps) continue + let startIdx = -1 + let endIdx = -1 + props.week.days.forEach((d, idx) => { + if (startIdx === -1 && d.date >= candStartStr && d.date <= candEndStr) startIdx = idx + if (d.date >= candStartStr && d.date <= candEndStr) endIdx = idx + }) + if (startIdx === -1 && endIdx === -1) continue + const id = `${base.id}_repeat_${diff}` + if (!weekEvents.has(id)) { + weekEvents.set(id, { + ...base, + id, + startDate: candStartStr, + endDate: candEndStr, + isRepeatOccurrence: true, + repeatIndex: diff, + startIdx: startIdx === -1 ? 0 : startIdx, + endIdx: endIdx === -1 ? 6 : endIdx, + }) + } + } + } + } + // Convert to array and sort const eventArray = Array.from(weekEvents.values()) eventArray.sort((a, b) => { diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index 8b08ac7..b4633c8 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -193,8 +193,14 @@ export const useCalendarStore = defineStore('calendar', { } } } else if (base.repeat === 'months') { - newStart = new Date(oldStart) - newStart.setMonth(newStart.getMonth() + 1) + // 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) } else { // Unknown pattern: delete entire series this.deleteEvent(baseId)