Fix monthly repeats on events with day 29-31 not showing up on shorter months. Improve event repeat handling performance.
This commit is contained in:
parent
47976eef88
commit
26b2e983ed
@ -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,37 +346,158 @@ 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,
|
||||
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
|
||||
})
|
||||
} 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
|
||||
// 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())
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user