Recurrent deletion bugfixes.
This commit is contained in:
		| @@ -142,9 +142,59 @@ function createWeek(virtualWeek) { | |||||||
|   let monthToLabel = null |   let monthToLabel = null | ||||||
|   let labelYear = null |   let labelYear = null | ||||||
|  |  | ||||||
|  |   // Precollect unique repeating base events once (avoid nested loops for each day) | ||||||
|  |   const repeatingBases = [] | ||||||
|  |   const seen = new Set() | ||||||
|  |   for (const [, list] of calendarStore.events) { | ||||||
|  |     for (const ev of list) { | ||||||
|  |       if (ev.isRepeating && !seen.has(ev.id)) { | ||||||
|  |         seen.add(ev.id) | ||||||
|  |         repeatingBases.push(ev) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   for (let i = 0; i < 7; i++) { |   for (let i = 0; i < 7; i++) { | ||||||
|     const dateStr = toLocalString(cur) |     const dateStr = toLocalString(cur) | ||||||
|     const eventsForDay = calendarStore.events.get(dateStr) || [] |     const storedEvents = calendarStore.events.get(dateStr) || [] | ||||||
|  |     // Build day events starting with stored (base/spanning) then virtual occurrences | ||||||
|  |     const dayEvents = [...storedEvents] | ||||||
|  |     for (const base of repeatingBases) { | ||||||
|  |       // Skip if the base itself already on this date (already in storedEvents) | ||||||
|  |       if (dateStr >= base.startDate && dateStr <= base.endDate) continue | ||||||
|  |       if (calendarStore.occursOnDate(base, dateStr)) { | ||||||
|  |         // Determine occurrence index (0 = first repeat after base) for weekly / monthly | ||||||
|  |         let recurrenceIndex = 0 | ||||||
|  |         try { | ||||||
|  |           if (base.repeat === 'weeks') { | ||||||
|  |             const pattern = base.repeatWeekdays || [] | ||||||
|  |             const baseDate = new Date(base.startDate + 'T00:00:00') | ||||||
|  |             const target = new Date(dateStr + 'T00:00:00') | ||||||
|  |             let matched = -1 | ||||||
|  |             const cur = new Date(baseDate) | ||||||
|  |             while (cur < target && matched < 100000) { | ||||||
|  |               cur.setDate(cur.getDate() + 1) | ||||||
|  |               if (pattern[cur.getDay()]) matched++ | ||||||
|  |             } | ||||||
|  |             if (cur.toDateString() === target.toDateString()) recurrenceIndex = matched | ||||||
|  |           } else if (base.repeat === 'months') { | ||||||
|  |             const baseDate = new Date(base.startDate + 'T00:00:00') | ||||||
|  |             const target = new Date(dateStr + 'T00:00:00') | ||||||
|  |             const diffMonths = | ||||||
|  |               (target.getFullYear() - baseDate.getFullYear()) * 12 + | ||||||
|  |               (target.getMonth() - baseDate.getMonth()) | ||||||
|  |             recurrenceIndex = diffMonths // matches existing monthly logic semantics | ||||||
|  |           } | ||||||
|  |         } catch {} | ||||||
|  |         dayEvents.push({ | ||||||
|  |           ...base, | ||||||
|  |           id: base.id + '_v_' + dateStr, | ||||||
|  |           startDate: dateStr, | ||||||
|  |           endDate: dateStr, | ||||||
|  |           _recurrenceIndex: recurrenceIndex, | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     const dow = cur.getDay() |     const dow = cur.getDay() | ||||||
|     const isFirst = cur.getDate() === 1 |     const isFirst = cur.getDate() === 1 | ||||||
|  |  | ||||||
| @@ -177,7 +227,7 @@ function createWeek(virtualWeek) { | |||||||
|         selection.value.dayCount > 0 && |         selection.value.dayCount > 0 && | ||||||
|         dateStr >= selection.value.startDate && |         dateStr >= selection.value.startDate && | ||||||
|         dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1), |         dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1), | ||||||
|       events: eventsForDay, |       events: dayEvents, | ||||||
|     }) |     }) | ||||||
|     cur.setDate(cur.getDate() + 1) |     cur.setDate(cur.getDate() + 1) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -155,6 +155,8 @@ function openEditDialog(eventInstanceId) { | |||||||
|   let occurrenceIndex = 0 |   let occurrenceIndex = 0 | ||||||
|   let weekday = null |   let weekday = null | ||||||
|   let occurrenceDate = null |   let occurrenceDate = null | ||||||
|  |  | ||||||
|  |   // Support legacy synthetic id pattern: baseId_repeat_<index>[_<weekday>] | ||||||
|   if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) { |   if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) { | ||||||
|     const [bid, suffix] = eventInstanceId.split('_repeat_') |     const [bid, suffix] = eventInstanceId.split('_repeat_') | ||||||
|     baseId = bid |     baseId = bid | ||||||
| @@ -162,10 +164,57 @@ function openEditDialog(eventInstanceId) { | |||||||
|     occurrenceIndex = parseInt(parts[0], 10) || 0 |     occurrenceIndex = parseInt(parts[0], 10) || 0 | ||||||
|     if (parts.length > 1) weekday = parseInt(parts[1], 10) |     if (parts.length > 1) weekday = parseInt(parts[1], 10) | ||||||
|   } |   } | ||||||
|  |   // Support new virtual id pattern: baseId_v_YYYY-MM-DD | ||||||
|  |   else if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_v_')) { | ||||||
|  |     const splitIndex = eventInstanceId.lastIndexOf('_v_') | ||||||
|  |     if (splitIndex !== -1) { | ||||||
|  |       baseId = eventInstanceId.slice(0, splitIndex) | ||||||
|  |       const dateStr = eventInstanceId.slice(splitIndex + 3) | ||||||
|  |       occurrenceDate = new Date(dateStr + 'T00:00:00') | ||||||
|  |       // Derive occurrenceIndex based on event's repeat pattern | ||||||
|  |       const eventForIndex = calendarStore.getEventById(baseId) | ||||||
|  |       if (eventForIndex?.isRepeating) { | ||||||
|  |         if (eventForIndex.repeat === 'weeks') { | ||||||
|  |           const pattern = eventForIndex.repeatWeekdays || [] | ||||||
|  |           const baseDate = new Date(eventForIndex.startDate + 'T00:00:00') | ||||||
|  |           // Count matching weekdays after base until reaching occurrenceDate | ||||||
|  |           let cur = new Date(baseDate) | ||||||
|  |           let matched = -1 // first match after base increments this to 0 | ||||||
|  |           while (cur < occurrenceDate && matched < 100000) { | ||||||
|  |             cur.setDate(cur.getDate() + 1) | ||||||
|  |             if (pattern[cur.getDay()]) matched++ | ||||||
|  |           } | ||||||
|  |           if (cur.toDateString() === occurrenceDate.toDateString()) { | ||||||
|  |             occurrenceIndex = matched | ||||||
|  |             weekday = occurrenceDate.getDay() | ||||||
|  |           } else { | ||||||
|  |             // Fallback: treat as base click if something went wrong | ||||||
|  |             occurrenceIndex = 0 | ||||||
|  |             weekday = null | ||||||
|  |             occurrenceDate = null | ||||||
|  |           } | ||||||
|  |         } else if (eventForIndex.repeat === 'months') { | ||||||
|  |           const baseDate = new Date(eventForIndex.startDate + 'T00:00:00') | ||||||
|  |           const diffMonths = | ||||||
|  |             (occurrenceDate.getFullYear() - baseDate.getFullYear()) * 12 + | ||||||
|  |             (occurrenceDate.getMonth() - baseDate.getMonth()) | ||||||
|  |           const interval = eventForIndex.repeatInterval || 1 | ||||||
|  |           // occurrenceIndex for monthly logic: diff in months (first after base is 1 * interval) | ||||||
|  |           if (diffMonths > 0 && diffMonths % interval === 0) { | ||||||
|  |             occurrenceIndex = diffMonths // matches store deletion expectation | ||||||
|  |           } else { | ||||||
|  |             occurrenceDate = null | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const event = calendarStore.getEventById(baseId) |   const event = calendarStore.getEventById(baseId) | ||||||
|   if (!event) return |   if (!event) return | ||||||
|   // Derive occurrence date for repeat occurrences (occurrenceIndex > 0 means not the base) |   // Derive occurrence date for repeat occurrences if not already determined above | ||||||
|   if ( |   if ( | ||||||
|  |     !occurrenceDate && | ||||||
|     event.isRepeating && |     event.isRepeating && | ||||||
|     ((event.repeat === 'weeks' && occurrenceIndex >= 0) || |     ((event.repeat === 'weeks' && occurrenceIndex >= 0) || | ||||||
|       (event.repeat === 'months' && occurrenceIndex > 0)) |       (event.repeat === 'months' && occurrenceIndex > 0)) | ||||||
| @@ -173,8 +222,6 @@ function openEditDialog(eventInstanceId) { | |||||||
|     if (event.repeat === 'weeks' && occurrenceIndex >= 0) { |     if (event.repeat === 'weeks' && occurrenceIndex >= 0) { | ||||||
|       const repeatWeekdaysLocal = event.repeatWeekdays || [] |       const repeatWeekdaysLocal = event.repeatWeekdays || [] | ||||||
|       const baseDate = new Date(event.startDate + 'T00:00:00') |       const baseDate = new Date(event.startDate + 'T00:00:00') | ||||||
|       // occurrenceIndex counts prior occurrences AFTER base; |  | ||||||
|       // For occurrenceIndex = 0 we want first matching day after base. |  | ||||||
|       let cur = new Date(baseDate) |       let cur = new Date(baseDate) | ||||||
|       let matched = -1 |       let matched = -1 | ||||||
|       let safety = 0 |       let safety = 0 | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|       :key="span.id" |       :key="span.id" | ||||||
|       class="event-span" |       class="event-span" | ||||||
|       :class="[`event-color-${span.colorId}`]" |       :class="[`event-color-${span.colorId}`]" | ||||||
|  |       :data-recurrence="span._recurrenceIndex != null ? span._recurrenceIndex : undefined" | ||||||
|       :style="{ |       :style="{ | ||||||
|         gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`, |         gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`, | ||||||
|         gridRow: `${span.row}`, |         gridRow: `${span.row}`, | ||||||
| @@ -24,54 +25,81 @@ | |||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
| import { computed, ref } from 'vue' | import { computed, ref } from 'vue' | ||||||
| import { useCalendarStore } from '@/stores/CalendarStore' | import { useCalendarStore } from '@/stores/CalendarStore' | ||||||
| import { toLocalString, fromLocalString, daysInclusive, addDaysStr } from '@/utils/date' | import { daysInclusive, addDaysStr } from '@/utils/date' | ||||||
|  |  | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   week: { |   week: { type: Object, required: true }, | ||||||
|     type: Object, |  | ||||||
|     required: true, |  | ||||||
|   }, |  | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const emit = defineEmits(['event-click']) | const emit = defineEmits(['event-click']) | ||||||
| const store = useCalendarStore() | const store = useCalendarStore() | ||||||
|  |  | ||||||
| // Local drag state | // Drag state | ||||||
| const dragState = ref(null) | const dragState = ref(null) | ||||||
| const justDragged = ref(false) | const justDragged = ref(false) | ||||||
|  |  | ||||||
| // (legacy helpers removed) | // Consolidate already-provided day.events into contiguous spans (no recurrence generation) | ||||||
|  | const eventSpans = computed(() => { | ||||||
|  |   const weekEvents = new Map() | ||||||
|  |   props.week.days.forEach((day, dayIndex) => { | ||||||
|  |     day.events.forEach((ev) => { | ||||||
|  |       const key = ev.id | ||||||
|  |       if (!weekEvents.has(key)) { | ||||||
|  |         weekEvents.set(key, { ...ev, startIdx: dayIndex, endIdx: dayIndex }) | ||||||
|  |       } else { | ||||||
|  |         const ref = weekEvents.get(key) | ||||||
|  |         ref.endIdx = Math.max(ref.endIdx, dayIndex) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |   const arr = Array.from(weekEvents.values()) | ||||||
|  |   arr.sort((a, b) => { | ||||||
|  |     const spanA = a.endIdx - a.startIdx | ||||||
|  |     const spanB = b.endIdx - b.startIdx | ||||||
|  |     if (spanA !== spanB) return spanB - spanA | ||||||
|  |     if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx | ||||||
|  |     return String(a.id).localeCompare(String(b.id)) | ||||||
|  |   }) | ||||||
|  |   // Assign non-overlapping rows | ||||||
|  |   const rowsLastEnd = [] | ||||||
|  |   arr.forEach((ev) => { | ||||||
|  |     let row = 0 | ||||||
|  |     while (row < rowsLastEnd.length && !(ev.startIdx > rowsLastEnd[row])) row++ | ||||||
|  |     if (row === rowsLastEnd.length) rowsLastEnd.push(-1) | ||||||
|  |     rowsLastEnd[row] = ev.endIdx | ||||||
|  |     ev.row = row + 1 | ||||||
|  |   }) | ||||||
|  |   return arr | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | function extractBaseId(eventId) { | ||||||
|  |   if (typeof eventId !== 'string') return eventId | ||||||
|  |   if (eventId.includes('_repeat_')) return eventId.split('_repeat_')[0] | ||||||
|  |   if (eventId.includes('_v_')) return eventId.slice(0, eventId.lastIndexOf('_v_')) | ||||||
|  |   return eventId | ||||||
|  | } | ||||||
|  |  | ||||||
| // Handle event click |  | ||||||
| function handleEventClick(span) { | function handleEventClick(span) { | ||||||
|   if (justDragged.value) return |   if (justDragged.value) return | ||||||
|   // Emit the actual span id (may include repeat suffix) so edit dialog knows occurrence context |  | ||||||
|   emit('event-click', span.id) |   emit('event-click', span.id) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Handle event pointer down for dragging |  | ||||||
| function handleEventPointerDown(span, event) { | function handleEventPointerDown(span, event) { | ||||||
|   // Don't start drag if clicking on resize handle |  | ||||||
|   if (event.target.classList.contains('resize-handle')) return |   if (event.target.classList.contains('resize-handle')) return | ||||||
|  |  | ||||||
|   event.stopPropagation() |   event.stopPropagation() | ||||||
|   // Do not preventDefault here to allow click unless drag threshold is passed |   const baseId = extractBaseId(span.id) | ||||||
|  |   const isVirtual = baseId !== span.id | ||||||
|   // Get the date under the pointer |  | ||||||
|   const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget) |  | ||||||
|   const anchorDate = hit ? hit.date : span.startDate |  | ||||||
|  |  | ||||||
|   startLocalDrag( |   startLocalDrag( | ||||||
|     { |     { | ||||||
|       id: span.id, |       id: baseId, | ||||||
|  |       originalId: span.id, | ||||||
|  |       isVirtual, | ||||||
|       mode: 'move', |       mode: 'move', | ||||||
|       pointerStartX: event.clientX, |       pointerStartX: event.clientX, | ||||||
|       pointerStartY: event.clientY, |       pointerStartY: event.clientY, | ||||||
|       anchorDate, |       anchorDate: span.startDate, | ||||||
|       startDate: span.startDate, |       startDate: span.startDate, | ||||||
|       endDate: span.endDate, |       endDate: span.endDate, | ||||||
|     }, |     }, | ||||||
| @@ -79,13 +107,15 @@ function handleEventPointerDown(span, event) { | |||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Handle resize handle pointer down |  | ||||||
| function handleResizePointerDown(span, mode, event) { | function handleResizePointerDown(span, mode, event) { | ||||||
|   event.stopPropagation() |   event.stopPropagation() | ||||||
|   // Start drag from the current edge; anchorDate not needed for resize |   const baseId = extractBaseId(span.id) | ||||||
|  |   const isVirtual = baseId !== span.id | ||||||
|   startLocalDrag( |   startLocalDrag( | ||||||
|     { |     { | ||||||
|       id: span.id, |       id: baseId, | ||||||
|  |       originalId: span.id, | ||||||
|  |       isVirtual, | ||||||
|       mode, |       mode, | ||||||
|       pointerStartX: event.clientX, |       pointerStartX: event.clientX, | ||||||
|       pointerStartY: event.clientY, |       pointerStartY: event.clientY, | ||||||
| @@ -97,94 +127,6 @@ function handleResizePointerDown(span, mode, event) { | |||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get date under pointer coordinates |  | ||||||
| function getDateUnderPointer(clientX, clientY, targetEl) { |  | ||||||
|   // First try to find a day cell directly under the pointer |  | ||||||
|   let element = document.elementFromPoint(clientX, clientY) |  | ||||||
|  |  | ||||||
|   // If we hit an event element, temporarily hide it and try again |  | ||||||
|   const hiddenElements = [] |  | ||||||
|   while (element && element.classList.contains('event-span')) { |  | ||||||
|     element.style.pointerEvents = 'none' |  | ||||||
|     hiddenElements.push(element) |  | ||||||
|     element = document.elementFromPoint(clientX, clientY) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Restore pointer events for hidden elements |  | ||||||
|   hiddenElements.forEach((el) => (el.style.pointerEvents = 'auto')) |  | ||||||
|  |  | ||||||
|   if (element) { |  | ||||||
|     // Look for a day cell with data-date attribute |  | ||||||
|     const dayElement = element.closest('[data-date]') |  | ||||||
|     if (dayElement && dayElement.dataset.date) { |  | ||||||
|       return { date: dayElement.dataset.date } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Also check if we're over a week element and can calculate position |  | ||||||
|     const weekElement = element.closest('.week-row') |  | ||||||
|     if (weekElement) { |  | ||||||
|       const rect = weekElement.getBoundingClientRect() |  | ||||||
|       const relativeX = clientX - rect.left |  | ||||||
|       const dayWidth = rect.width / 7 |  | ||||||
|       const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth))) |  | ||||||
|  |  | ||||||
|       const daysGrid = weekElement.querySelector('.days-grid') |  | ||||||
|       if (daysGrid && daysGrid.children[dayIndex]) { |  | ||||||
|         const dayEl = daysGrid.children[dayIndex] |  | ||||||
|         const date = dayEl?.dataset?.date |  | ||||||
|         if (date) return { date } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Fallback: try to find the week overlay and calculate position |  | ||||||
|   const overlayEl = targetEl?.closest('.week-overlay') |  | ||||||
|   const weekElement = overlayEl ? overlayEl.parentElement : null |  | ||||||
|   if (!weekElement) { |  | ||||||
|     // If we're outside this week, try to find any week element under the pointer |  | ||||||
|     const allWeekElements = document.querySelectorAll('.week-row') |  | ||||||
|     let bestWeek = null |  | ||||||
|     let bestDistance = Infinity |  | ||||||
|  |  | ||||||
|     for (const week of allWeekElements) { |  | ||||||
|       const rect = week.getBoundingClientRect() |  | ||||||
|       if (clientY >= rect.top && clientY <= rect.bottom) { |  | ||||||
|         const distance = Math.abs(clientY - (rect.top + rect.height / 2)) |  | ||||||
|         if (distance < bestDistance) { |  | ||||||
|           bestDistance = distance |  | ||||||
|           bestWeek = week |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (bestWeek) { |  | ||||||
|       const rect = bestWeek.getBoundingClientRect() |  | ||||||
|       const relativeX = clientX - rect.left |  | ||||||
|       const dayWidth = rect.width / 7 |  | ||||||
|       const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth))) |  | ||||||
|  |  | ||||||
|       const daysGrid = bestWeek.querySelector('.days-grid') |  | ||||||
|       if (daysGrid && daysGrid.children[dayIndex]) { |  | ||||||
|         const dayEl = daysGrid.children[dayIndex] |  | ||||||
|         const date = dayEl?.dataset?.date |  | ||||||
|         if (date) return { date } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return null |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const rect = weekElement.getBoundingClientRect() |  | ||||||
|   const relativeX = clientX - rect.left |  | ||||||
|   const dayWidth = rect.width / 7 |  | ||||||
|   const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth))) |  | ||||||
|  |  | ||||||
|   if (props.week.days[dayIndex]) { |  | ||||||
|     return { date: props.week.days[dayIndex].date } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return null |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Local drag handling | // Local drag handling | ||||||
| function startLocalDrag(init, evt) { | function startLocalDrag(init, evt) { | ||||||
|   const spanDays = daysInclusive(init.startDate, init.endDate) |   const spanDays = daysInclusive(init.startDate, init.endDate) | ||||||
| @@ -292,250 +234,12 @@ function normalizeDateOrder(aStr, bStr) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function applyRangeDuringDrag(st, startDate, endDate) { | function applyRangeDuringDrag(st, startDate, endDate) { | ||||||
|   let ev = store.getEventById(st.id) |   // If dragging a virtual occurrence, map to base move without changing recurrence pattern mid-series. | ||||||
|   let isRepeatOccurrence = false |   // We disallow resizing individual virtual occurrences; treat as move of whole series anchor. | ||||||
|   let baseId = st.id |   if (st.isVirtual && st.mode !== 'move') return | ||||||
|   let repeatIndex = 0 |   store.setEventRange(st.id, startDate, endDate, { mode: st.mode }) | ||||||
|   let grabbedWeekday = null |  | ||||||
|  |  | ||||||
|   // If not found (repeat occurrences aren't stored) parse synthetic id |  | ||||||
|   if (!ev && typeof st.id === 'string' && st.id.includes('_repeat_')) { |  | ||||||
|     const [bid, suffix] = st.id.split('_repeat_') |  | ||||||
|     baseId = bid |  | ||||||
|     ev = store.getEventById(baseId) |  | ||||||
|     if (ev) { |  | ||||||
|       const parts = suffix.split('_') |  | ||||||
|       repeatIndex = parseInt(parts[0], 10) || 0 |  | ||||||
|       grabbedWeekday = parts.length > 1 ? parseInt(parts[1], 10) : null |  | ||||||
|       isRepeatOccurrence = repeatIndex >= 0 |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!ev) return |  | ||||||
|  |  | ||||||
|   const mode = st.mode === 'resize-left' || st.mode === 'resize-right' ? st.mode : 'move' |  | ||||||
|   if (isRepeatOccurrence) { |  | ||||||
|     if (repeatIndex === 0) { |  | ||||||
|       store.setEventRange(baseId, startDate, endDate, { mode }) |  | ||||||
|     } else { |  | ||||||
|       if (!st.splitNewBaseId) { |  | ||||||
|         const newId = store.splitRepeatSeries( |  | ||||||
|           baseId, |  | ||||||
|           repeatIndex, |  | ||||||
|           startDate, |  | ||||||
|           endDate, |  | ||||||
|           grabbedWeekday, |  | ||||||
|         ) |  | ||||||
|         if (newId) { |  | ||||||
|           st.splitNewBaseId = newId |  | ||||||
|           st.id = newId |  | ||||||
|           st.startDate = startDate |  | ||||||
|           st.endDate = endDate |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         store.setEventRange(st.splitNewBaseId, startDate, endDate, { mode }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } else { |  | ||||||
|     store.setEventRange(st.id, startDate, endDate, { mode }) |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Calculate event spans for this week |  | ||||||
| const eventSpans = computed(() => { |  | ||||||
|   const spans = [] |  | ||||||
|   const weekEvents = new Map() |  | ||||||
|  |  | ||||||
|   // Collect stored base events |  | ||||||
|   props.week.days.forEach((day, 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) => { |  | ||||||
|     // Sort by span length (longer first) |  | ||||||
|     const spanA = a.endIdx - a.startIdx |  | ||||||
|     const spanB = b.endIdx - b.startIdx |  | ||||||
|     if (spanA !== spanB) return spanB - spanA |  | ||||||
|  |  | ||||||
|     // Then by start position |  | ||||||
|     if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx |  | ||||||
|  |  | ||||||
|     // Then by start time if available |  | ||||||
|     const timeA = a.startTime ? timeToMinutes(a.startTime) : 0 |  | ||||||
|     const timeB = b.startTime ? timeToMinutes(b.startTime) : 0 |  | ||||||
|     if (timeA !== timeB) return timeA - timeB |  | ||||||
|  |  | ||||||
|     // Fallback to ID |  | ||||||
|     return String(a.id).localeCompare(String(b.id)) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   // Assign rows to avoid overlaps |  | ||||||
|   const rowsLastEnd = [] |  | ||||||
|   eventArray.forEach((event) => { |  | ||||||
|     let placedRow = 0 |  | ||||||
|     while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) { |  | ||||||
|       placedRow++ |  | ||||||
|     } |  | ||||||
|     if (placedRow === rowsLastEnd.length) { |  | ||||||
|       rowsLastEnd.push(-1) |  | ||||||
|     } |  | ||||||
|     rowsLastEnd[placedRow] = event.endIdx |  | ||||||
|     event.row = placedRow + 1 |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   return eventArray |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| function timeToMinutes(timeStr) { | function timeToMinutes(timeStr) { | ||||||
|   if (!timeStr) return 0 |   if (!timeStr) return 0 | ||||||
|   const [hours, minutes] = timeStr.split(':').map(Number) |   const [hours, minutes] = timeStr.split(':').map(Number) | ||||||
|   | |||||||
| @@ -25,6 +25,77 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   actions: { |   actions: { | ||||||
|  |     // Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences. | ||||||
|  |     occursOnDate(event, dateStr) { | ||||||
|  |       if (!event || !event.isRepeating || event.repeat === 'none') return false | ||||||
|  |       // Quick bounds: event cannot occur before its base start | ||||||
|  |       if (dateStr < event.startDate) return false | ||||||
|  |       // For multi-day spanning events, we treat start date as anchor; UI handles span painting separately. | ||||||
|  |       if (event.repeat === 'weeks') { | ||||||
|  |         const pattern = event.repeatWeekdays || [] | ||||||
|  |         if (!pattern.some(Boolean)) return false | ||||||
|  |         // Day of week must match | ||||||
|  |         const d = fromLocalString(dateStr) | ||||||
|  |         const dow = d.getDay() | ||||||
|  |         if (!pattern[dow]) return false | ||||||
|  |         // Compute week distance blocks respecting interval by counting ISO weeks since anchor Monday of base. | ||||||
|  |         const baseStart = fromLocalString(event.startDate) | ||||||
|  |         // If date is before base anchor weekday match, ensure anchor alignment | ||||||
|  |         // Count days since base start; ensure that number of matching weekdays encountered equals occurrence index < repeatCount | ||||||
|  |         // Optimized approach: approximate max occurrences cap first. | ||||||
|  |         const interval = event.repeatInterval || 1 | ||||||
|  |         // Check if date resides in a week block that aligns with interval | ||||||
|  |         const baseBlockStart = new Date(baseStart) | ||||||
|  |         baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) | ||||||
|  |         const currentBlockStart = new Date(d) | ||||||
|  |         currentBlockStart.setDate(d.getDate() - d.getDay()) | ||||||
|  |         const WEEK_MS = 7 * 86400000 | ||||||
|  |         const blocksDiff = Math.floor((currentBlockStart - baseBlockStart) / WEEK_MS) | ||||||
|  |         if (blocksDiff < 0 || blocksDiff % interval !== 0) return false | ||||||
|  |         // Count occurrences up to this date to enforce repeatCount finite limits | ||||||
|  |         if (event.repeatCount !== 'unlimited') { | ||||||
|  |           const targetTime = d.getTime() | ||||||
|  |           let occs = 0 | ||||||
|  |           const cursor = new Date(baseStart) | ||||||
|  |           const limit = parseInt(event.repeatCount, 10) | ||||||
|  |           const safetyLimit = Math.min(limit + 1, 10000) | ||||||
|  |           while (cursor.getTime() <= targetTime && occs < safetyLimit) { | ||||||
|  |             if (pattern[cursor.getDay()]) { | ||||||
|  |               if (cursor.getTime() === targetTime) { | ||||||
|  |                 // This is the occurrence. Validate occs < limit | ||||||
|  |                 return occs < limit | ||||||
|  |               } | ||||||
|  |               occs++ | ||||||
|  |             } | ||||||
|  |             cursor.setDate(cursor.getDate() + 1) | ||||||
|  |           } | ||||||
|  |           return false | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |       } else if (event.repeat === 'months') { | ||||||
|  |         const baseStart = fromLocalString(event.startDate) | ||||||
|  |         const d = fromLocalString(dateStr) | ||||||
|  |         const diffMonths = | ||||||
|  |           (d.getFullYear() - baseStart.getFullYear()) * 12 + (d.getMonth() - baseStart.getMonth()) | ||||||
|  |         if (diffMonths < 0) return false | ||||||
|  |         const interval = event.repeatInterval || 1 | ||||||
|  |         if (diffMonths % interval !== 0) return false | ||||||
|  |         // Check day match (clamped for shorter months). Base day might exceed target month length. | ||||||
|  |         const baseDay = baseStart.getDate() | ||||||
|  |         const daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() | ||||||
|  |         const effectiveDay = Math.min(baseDay, daysInMonth) | ||||||
|  |         if (d.getDate() !== effectiveDay) return false | ||||||
|  |         if (event.repeatCount !== 'unlimited') { | ||||||
|  |           const limit = parseInt(event.repeatCount, 10) | ||||||
|  |           if (isNaN(limit)) return false | ||||||
|  |           // Base is occurrence 0; diffMonths/interval gives occurrence index | ||||||
|  |           const occurrenceIndex = diffMonths / interval | ||||||
|  |           return occurrenceIndex < limit | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |       } | ||||||
|  |       return false | ||||||
|  |     }, | ||||||
|     updateCurrentDate() { |     updateCurrentDate() { | ||||||
|       const d = new Date() |       const d = new Date() | ||||||
|       this.now = d.toISOString() |       this.now = d.toISOString() | ||||||
| @@ -133,28 +204,96 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|       if (!base || !base.isRepeating) return |       if (!base || !base.isRepeating) return | ||||||
|       // WEEKLY SERIES ------------------------------------------------------ |       // WEEKLY SERIES ------------------------------------------------------ | ||||||
|       if (base.repeat === 'weeks') { |       if (base.repeat === 'weeks') { | ||||||
|         // Strategy: split series around the target occurrence, omitting it. |         const interval = base.repeatInterval || 1 | ||||||
|         const remaining = |         const pattern = base.repeatWeekdays || [] | ||||||
|           base.repeatCount === 'unlimited' |         if (!pattern.some(Boolean)) return | ||||||
|             ? 'unlimited' |         // Preserve original count before any truncation | ||||||
|             : String(Math.max(0, parseInt(base.repeatCount, 10) - (occurrenceIndex + 1))) |         const originalCountRaw = base.repeatCount | ||||||
|         // Keep occurrences before the deleted one |  | ||||||
|         this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex) |         // Determine target occurrence date | ||||||
|         if (remaining === '0') return |         let targetDate = null | ||||||
|         // Find date of next occurrence (first after deleted) |         if (ctx.occurrenceDate instanceof Date) { | ||||||
|         const startDate = new Date(base.startDate + 'T00:00:00') |           targetDate = new Date( | ||||||
|         let idx = 0 |             ctx.occurrenceDate.getFullYear(), | ||||||
|         let cur = new Date(startDate) |             ctx.occurrenceDate.getMonth(), | ||||||
|         while (idx <= occurrenceIndex && idx < 10000) { |             ctx.occurrenceDate.getDate(), | ||||||
|  |           ) | ||||||
|  |         } else { | ||||||
|  |           // Fallback: derive from occurrenceIndex (legacy path) | ||||||
|  |           const baseStart = new Date(base.startDate + 'T00:00:00') | ||||||
|  |           let cur = new Date(baseStart) | ||||||
|  |           let matched = -1 | ||||||
|  |           let safety = 0 | ||||||
|  |           while (matched < occurrenceIndex && safety < 20000) { | ||||||
|             cur.setDate(cur.getDate() + 1) |             cur.setDate(cur.getDate() + 1) | ||||||
|           if (base.repeatWeekdays && base.repeatWeekdays[cur.getDay()]) idx++ |             const blockStart = new Date(cur) | ||||||
|  |             blockStart.setDate(cur.getDate() - cur.getDay()) | ||||||
|  |             const baseBlockStart = new Date(baseStart) | ||||||
|  |             baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) | ||||||
|  |             const WEEK_MS = 7 * 86400000 | ||||||
|  |             const blocksDiff = Math.floor((blockStart - baseBlockStart) / WEEK_MS) | ||||||
|  |             const aligned = blocksDiff % interval === 0 | ||||||
|  |             if (aligned && pattern[cur.getDay()]) matched++ | ||||||
|  |             safety++ | ||||||
|           } |           } | ||||||
|         const nextStartStr = toLocalString(cur) |           targetDate = cur | ||||||
|         // Preserve multi‑day span if any |         } | ||||||
|  |         if (!targetDate) return | ||||||
|  |  | ||||||
|  |         // Count occurrences BEFORE target (always include the base occurrence as first) | ||||||
|  |         const baseStart = new Date(base.startDate + 'T00:00:00') | ||||||
|  |         const baseBlockStart = new Date(baseStart) | ||||||
|  |         baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay()) | ||||||
|  |         const WEEK_MS = 7 * 86400000 | ||||||
|  |         function isAligned(d) { | ||||||
|  |           const block = new Date(d) | ||||||
|  |           block.setDate(d.getDate() - d.getDay()) | ||||||
|  |           const diff = Math.floor((block - baseBlockStart) / WEEK_MS) | ||||||
|  |           return diff % interval === 0 | ||||||
|  |         } | ||||||
|  |         // Start with 1 (base occurrence) if target is after base; if target IS base (should not happen here) leave 0 | ||||||
|  |         let countBefore = targetDate > baseStart ? 1 : 0 | ||||||
|  |         let probe = new Date(baseStart) | ||||||
|  |         probe.setDate(probe.getDate() + 1) // start counting AFTER base | ||||||
|  |         let safety2 = 0 | ||||||
|  |         while (probe < targetDate && safety2 < 50000) { | ||||||
|  |           if (pattern[probe.getDay()] && isAligned(probe)) countBefore++ | ||||||
|  |           probe.setDate(probe.getDate() + 1) | ||||||
|  |           safety2++ | ||||||
|  |         } | ||||||
|  |         // Terminate original series to keep only occurrences before target | ||||||
|  |         this._terminateRepeatSeriesAtIndex(baseId, countBefore) | ||||||
|  |  | ||||||
|  |         // Calculate remaining occurrences for new series using ORIGINAL total | ||||||
|  |         let remainingCount = 'unlimited' | ||||||
|  |         if (originalCountRaw !== 'unlimited') { | ||||||
|  |           const originalTotal = parseInt(originalCountRaw, 10) | ||||||
|  |           if (!isNaN(originalTotal)) { | ||||||
|  |             const rem = originalTotal - countBefore - 1 // kept + deleted | ||||||
|  |             if (rem <= 0) return // nothing left to continue | ||||||
|  |             remainingCount = String(rem) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Continuation starts at NEXT valid occurrence (matching weekday & aligned block) | ||||||
|  |         let continuationStart = new Date(targetDate) | ||||||
|  |         let searchSafety = 0 | ||||||
|  |         let foundNext = false | ||||||
|  |         while (searchSafety < 50000) { | ||||||
|  |           continuationStart.setDate(continuationStart.getDate() + 1) | ||||||
|  |           if (pattern[continuationStart.getDay()] && isAligned(continuationStart)) { | ||||||
|  |             foundNext = true | ||||||
|  |             break | ||||||
|  |           } | ||||||
|  |           searchSafety++ | ||||||
|  |         } | ||||||
|  |         if (!foundNext) return // no remaining occurrences | ||||||
|  |  | ||||||
|         const spanDays = Math.round( |         const spanDays = Math.round( | ||||||
|           (fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000), |           (fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000), | ||||||
|         ) |         ) | ||||||
|         const nextEnd = new Date(fromLocalString(nextStartStr)) |         const nextStartStr = toLocalString(continuationStart) | ||||||
|  |         const nextEnd = new Date(continuationStart) | ||||||
|         nextEnd.setDate(nextEnd.getDate() + spanDays) |         nextEnd.setDate(nextEnd.getDate() + spanDays) | ||||||
|         const nextEndStr = toLocalString(nextEnd) |         const nextEndStr = toLocalString(nextEnd) | ||||||
|         this.createEvent({ |         this.createEvent({ | ||||||
| @@ -163,7 +302,8 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|           endDate: nextEndStr, |           endDate: nextEndStr, | ||||||
|           colorId: base.colorId, |           colorId: base.colorId, | ||||||
|           repeat: 'weeks', |           repeat: 'weeks', | ||||||
|           repeatCount: remaining, |           repeatInterval: interval, | ||||||
|  |           repeatCount: remainingCount, | ||||||
|           repeatWeekdays: base.repeatWeekdays, |           repeatWeekdays: base.repeatWeekdays, | ||||||
|         }) |         }) | ||||||
|         return |         return | ||||||
| @@ -175,6 +315,7 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|         if (occurrenceIndex <= 0) return // base itself handled elsewhere |         if (occurrenceIndex <= 0) return // base itself handled elsewhere | ||||||
|         if (occurrenceIndex % interval !== 0) return // should not happen (synthetic id only for valid occurrences) |         if (occurrenceIndex % interval !== 0) return // should not happen (synthetic id only for valid occurrences) | ||||||
|         // Count prior occurrences (including base) before the deleted one |         // Count prior occurrences (including base) before the deleted one | ||||||
|  |         const originalCountRaw = base.repeatCount | ||||||
|         const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1 |         const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1 | ||||||
|         // Truncate base series to keep only priorOccurrences |         // Truncate base series to keep only priorOccurrences | ||||||
|         this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences) |         this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences) | ||||||
| @@ -184,8 +325,8 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|         ) |         ) | ||||||
|         // Remaining occurrences after deletion |         // Remaining occurrences after deletion | ||||||
|         let remainingCount = 'unlimited' |         let remainingCount = 'unlimited' | ||||||
|         if (base.repeatCount !== 'unlimited') { |         if (originalCountRaw !== 'unlimited') { | ||||||
|           const total = parseInt(base.repeatCount, 10) |           const total = parseInt(originalCountRaw, 10) | ||||||
|           if (!isNaN(total)) { |           if (!isNaN(total)) { | ||||||
|             const rem = total - priorOccurrences - 1 // subtract kept + deleted |             const rem = total - priorOccurrences - 1 // subtract kept + deleted | ||||||
|             if (rem <= 0) return // nothing left |             if (rem <= 0) return // nothing left | ||||||
| @@ -214,7 +355,13 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|  |  | ||||||
|     deleteFromOccurrence(ctx) { |     deleteFromOccurrence(ctx) { | ||||||
|       const { baseId, occurrenceIndex } = ctx |       const { baseId, occurrenceIndex } = ctx | ||||||
|       this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex) |       const base = this.getEventById(baseId) | ||||||
|  |       if (!base || !base.isRepeating) return | ||||||
|  |       // We want to keep occurrences up to and including the selected one; that becomes new repeatCount. | ||||||
|  |       // occurrenceIndex here represents the number of repeats AFTER the base (weekly: 0 = first repeat; monthly: diffMonths) | ||||||
|  |       // Total kept occurrences = base (1) + occurrenceIndex | ||||||
|  |       const keptTotal = 1 + Math.max(0, occurrenceIndex) | ||||||
|  |       this._terminateRepeatSeriesAtIndex(baseId, keptTotal) | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     deleteFirstOccurrence(baseId) { |     deleteFirstOccurrence(baseId) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko