Somewhat broken refactoring to aid event repeat handling.

This commit is contained in:
Leo Vasanko 2025-08-22 21:42:54 -06:00
parent d794758b54
commit 02442f5135
7 changed files with 249 additions and 270 deletions

View File

@ -15,9 +15,9 @@ const handleCreateEvent = (eventData) => {
}
}
const handleEditEvent = (eventInstanceId) => {
const handleEditEvent = (eventClickPayload) => {
if (eventDialog.value) {
eventDialog.value.openEditDialog(eventInstanceId)
eventDialog.value.openEditDialog(eventClickPayload)
}
}

View File

@ -6,7 +6,7 @@ const props = defineProps({
const emit = defineEmits(['event-click'])
const handleEventClick = (eventId) => {
emit('event-click', eventId)
emit('event-click', { id: eventId, instanceId: eventId, occurrenceIndex: 0 })
}
</script>

View File

@ -144,54 +144,90 @@ function createWeek(virtualWeek) {
// 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)
}
if (calendarStore.events) {
for (const ev of calendarStore.events.values()) {
if (ev.isRepeating) repeatingBases.push(ev)
}
}
for (let i = 0; i < 7; i++) {
const dateStr = toLocalString(cur)
const storedEvents = calendarStore.events.get(dateStr) || []
const storedEvents = []
const idSet = calendarStore.dates.get(dateStr)
if (idSet) {
// Support Set or Array; ignore unexpected shapes
if (idSet instanceof Set) {
idSet.forEach((id) => {
const ev = calendarStore.events.get(id)
if (ev) storedEvents.push(ev)
})
} else if (Array.isArray(idSet)) {
for (const id of idSet) {
const ev = calendarStore.events.get(id)
if (ev) storedEvents.push(ev)
}
} else if (typeof idSet === 'object' && idSet !== null) {
// If mistakenly hydrated as plain object {id:true,...}
for (const id of Object.keys(idSet)) {
const ev = calendarStore.events.get(id)
if (ev) storedEvents.push(ev)
}
}
}
// 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
// Determine sequential occurrence index: base event = 0, first repeat = 1, etc.
let recurrenceIndex = 0
try {
if (base.repeat === 'weeks') {
const pattern = base.repeatWeekdays || []
const baseDate = new Date(base.startDate + 'T00:00:00')
const interval = base.repeatInterval || 1
const baseStart = new Date(base.startDate + 'T00:00:00')
const baseEnd = new Date(base.endDate + '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++
const WEEK_MS = 7 * 86400000
const baseBlockStart = new Date(baseStart)
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
function isAligned(d) {
const blk = new Date(d)
blk.setDate(d.getDate() - d.getDay())
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
return diff % interval === 0
}
if (cur.toDateString() === target.toDateString()) recurrenceIndex = matched
// Count valid occurrences after base end and before target
let count = 0
const cursor = new Date(baseEnd)
cursor.setDate(cursor.getDate() + 1)
while (cursor < target) {
if (pattern[cursor.getDay()] && isAligned(cursor)) count++
cursor.setDate(cursor.getDate() + 1)
}
// Target itself is guaranteed valid (occursOnDate passed), so its index is count+1
recurrenceIndex = count + 1
} else if (base.repeat === 'months') {
const baseDate = new Date(base.startDate + 'T00:00:00')
const baseStart = new Date(base.startDate + 'T00:00:00')
const target = new Date(dateStr + 'T00:00:00')
const interval = base.repeatInterval || 1
const diffMonths =
(target.getFullYear() - baseDate.getFullYear()) * 12 +
(target.getMonth() - baseDate.getMonth())
recurrenceIndex = diffMonths // matches existing monthly logic semantics
(target.getFullYear() - baseStart.getFullYear()) * 12 +
(target.getMonth() - baseStart.getMonth())
// diffMonths should be multiple of interval; sequential index = diffMonths/interval
recurrenceIndex = diffMonths / interval
}
} catch {}
} catch {
recurrenceIndex = 0
}
dayEvents.push({
...base,
id: base.id + '_v_' + dateStr,
startDate: dateStr,
endDate: dateStr,
_recurrenceIndex: recurrenceIndex,
_baseId: base.id,
})
}
}
@ -399,8 +435,8 @@ const handleDayTouchEnd = (dateStr) => {
}
}
const handleEventClick = (eventInstanceId) => {
emit('edit-event', eventInstanceId)
const handleEventClick = (payload) => {
emit('edit-event', payload)
}
// Handle year change emitted from CalendarHeader: scroll to computed target position

View File

@ -3,10 +3,18 @@ import CalendarDay from './CalendarDay.vue'
import EventOverlay from './EventOverlay.vue'
const props = defineProps({
week: Object
week: Object,
})
const emit = defineEmits(['day-mousedown', 'day-mouseenter', 'day-mouseup', 'day-touchstart', 'day-touchmove', 'day-touchend', 'event-click'])
const emit = defineEmits([
'day-mousedown',
'day-mouseenter',
'day-mouseup',
'day-touchstart',
'day-touchmove',
'day-touchend',
'event-click',
])
const handleDayMouseDown = (dateStr) => {
emit('day-mousedown', dateStr)
@ -32,21 +40,18 @@ const handleDayTouchEnd = (dateStr) => {
emit('day-touchend', dateStr)
}
const handleEventClick = (eventId) => {
emit('event-click', eventId)
const handleEventClick = (payload) => {
emit('event-click', payload)
}
</script>
<template>
<div
class="week-row"
:style="{ top: `${props.week.top}px` }"
>
<div class="week-row" :style="{ top: `${props.week.top}px` }">
<div class="week-label">W{{ props.week.weekNumber }}</div>
<div class="days-grid">
<CalendarDay
v-for="day in props.week.days"
:key="day.date"
<CalendarDay
v-for="day in props.week.days"
:key="day.date"
:day="day"
@mousedown="handleDayMouseDown(day.date)"
@mouseenter="handleDayMouseEnter(day.date)"
@ -56,10 +61,7 @@ const handleEventClick = (eventId) => {
@touchend="handleDayTouchEnd(day.date)"
@event-click="handleEventClick"
/>
<EventOverlay
:week="props.week"
@event-click="handleEventClick"
/>
<EventOverlay :week="props.week" @event-click="handleEventClick" />
</div>
</div>
</template>
@ -98,8 +100,8 @@ const handleEventClick = (eventId) => {
}
/* Fixed heights for cells and labels (from cells.css) */
.week-row :deep(.cell),
.week-label {
height: var(--cell-h);
.week-row :deep(.cell),
.week-label {
height: var(--cell-h);
}
</style>

View File

@ -149,90 +149,56 @@ function openCreateDialog(selectionData = null) {
})
}
function openEditDialog(eventInstanceId) {
function openEditDialog(payload) {
occurrenceContext.value = null
let baseId = eventInstanceId
let occurrenceIndex = 0
if (!payload) return
// Payload expected: { id: baseId, instanceId, occurrenceIndex }
const baseId = payload.id
let occurrenceIndex = payload.occurrenceIndex || 0
let weekday = null
let occurrenceDate = null
// Support legacy synthetic id pattern: baseId_repeat_<index>[_<weekday>]
if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) {
const [bid, suffix] = eventInstanceId.split('_repeat_')
baseId = bid
const parts = suffix.split('_')
occurrenceIndex = parseInt(parts[0], 10) || 0
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)
if (!event) return
// Derive occurrence date for repeat occurrences if not already determined above
if (
!occurrenceDate &&
event.isRepeating &&
((event.repeat === 'weeks' && occurrenceIndex >= 0) ||
(event.repeat === 'months' && occurrenceIndex > 0))
) {
// Compute occurrenceDate based on occurrenceIndex rather than parsing instanceId
if (event.isRepeating) {
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
const repeatWeekdaysLocal = event.repeatWeekdays || []
const baseDate = new Date(event.startDate + 'T00:00:00')
let cur = new Date(baseDate)
let matched = -1
let safety = 0
while (matched < occurrenceIndex && safety < 10000) {
const pattern = event.repeatWeekdays || []
const baseStart = new Date(event.startDate + 'T00:00:00')
const baseEnd = new Date(event.endDate + 'T00:00:00')
if (occurrenceIndex === 0) {
occurrenceDate = baseStart
weekday = baseStart.getDay()
} else {
// Count valid repeat occurrences (pattern + interval alignment) AFTER the base span
const interval = event.repeatInterval || 1
const WEEK_MS = 7 * 86400000
const baseBlockStart = new Date(baseStart)
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
function isAligned(d) {
const blk = new Date(d)
blk.setDate(d.getDate() - d.getDay())
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
return diff % interval === 0
}
let cur = new Date(baseEnd)
cur.setDate(cur.getDate() + 1)
if (repeatWeekdaysLocal[cur.getDay()]) matched++
safety++
let found = 0 // number of repeat occurrences found so far
let safety = 0
while (found < occurrenceIndex && safety < 20000) {
if (pattern[cur.getDay()] && isAligned(cur)) {
found++
if (found === occurrenceIndex) break
}
cur.setDate(cur.getDate() + 1)
safety++
}
occurrenceDate = cur
weekday = cur.getDay()
}
occurrenceDate = cur
} else if (event.repeat === 'months') {
const cur = new Date(event.startDate + 'T00:00:00')
} else if (event.repeat === 'months' && occurrenceIndex >= 0) {
const baseDate = new Date(event.startDate + 'T00:00:00')
const cur = new Date(baseDate)
cur.setMonth(cur.getMonth() + occurrenceIndex)
occurrenceDate = cur
}
@ -250,7 +216,7 @@ function openEditDialog(eventInstanceId) {
eventSaved.value = false
// Build occurrence context: treat any occurrenceIndex > 0 as a specific occurrence (weekday only relevant for weekly)
if (event.isRepeating) {
if (event.repeat === 'weeks' && weekday != null && occurrenceIndex >= 0) {
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
} else if (event.repeat === 'months' && occurrenceIndex > 0) {
occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate }
@ -282,19 +248,16 @@ function updateEventInStore() {
// For simple property updates (title, color, repeat), update all instances directly
// This avoids the expensive remove/re-add cycle
for (const [, eventList] of calendarStore.events) {
for (const event of eventList) {
if (event.id === editingEventId.value) {
event.title = title.value
event.colorId = colorId.value
event.repeat = repeat.value
event.repeatInterval = recurrenceInterval.value
event.repeatWeekdays = buildStoreWeekdayPattern()
event.repeatCount =
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none'
}
}
if (calendarStore.events?.has(editingEventId.value)) {
const event = calendarStore.events.get(editingEventId.value)
event.title = title.value
event.colorId = colorId.value
event.repeat = repeat.value
event.repeatInterval = recurrenceInterval.value
event.repeatWeekdays = buildStoreWeekdayPattern()
event.repeatCount =
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none'
}
}

View File

@ -5,7 +5,8 @@
:key="span.id"
class="event-span"
:class="[`event-color-${span.colorId}`]"
:data-recurrence="span._recurrenceIndex != null ? span._recurrenceIndex : undefined"
:data-id="span.id"
:data-n="span._recurrenceIndex != null ? span._recurrenceIndex : 0"
:style="{
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
gridRow: `${span.row}`,
@ -74,23 +75,26 @@ const eventSpans = computed(() => {
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
}
function handleEventClick(span) {
if (justDragged.value) return
emit('event-click', span.id)
// Emit composite payload: base id (without virtual marker), instance id, occurrence index (data-n)
const idStr = span.id
const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_')
const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr
emit('event-click', {
id: baseId,
instanceId: span.id,
occurrenceIndex: span._recurrenceIndex != null ? span._recurrenceIndex : 0,
})
}
function handleEventPointerDown(span, event) {
if (event.target.classList.contains('resize-handle')) return
event.stopPropagation()
const baseId = extractBaseId(span.id)
const isVirtual = baseId !== span.id
const idStr = span.id
const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_')
const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr
const isVirtual = hasVirtualMarker
startLocalDrag(
{
id: baseId,
@ -109,8 +113,10 @@ function handleEventPointerDown(span, event) {
function handleResizePointerDown(span, mode, event) {
event.stopPropagation()
const baseId = extractBaseId(span.id)
const isVirtual = baseId !== span.id
const idStr = span.id
const hasVirtualMarker = typeof idStr === 'string' && idStr.includes('_v_')
const baseId = hasVirtualMarker ? idStr.slice(0, idStr.lastIndexOf('_v_')) : idStr
const isVirtual = hasVirtualMarker
startLocalDrag(
{
id: baseId,

View File

@ -8,7 +8,8 @@ export const useCalendarStore = defineStore('calendar', {
state: () => ({
today: toLocalString(new Date()),
now: new Date().toISOString(), // store as ISO string
events: new Map(), // Map of date strings to arrays of events
events: new Map(), // id -> event object (primary)
dates: new Map(), // dateStr -> Set of event ids
weekend: getLocaleWeekendDays(),
config: {
select_days: 1000,
@ -134,41 +135,25 @@ export const useCalendarStore = defineStore('calendar', {
isRepeating: eventData.repeat && eventData.repeat !== 'none',
}
const startDate = new Date(fromLocalString(event.startDate))
const endDate = new Date(fromLocalString(event.endDate))
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const dateStr = toLocalString(d)
if (!this.events.has(dateStr)) {
this.events.set(dateStr, [])
}
this.events.get(dateStr).push({ ...event, isSpanning: startDate < endDate })
}
// No physical expansion; repeats are virtual
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
this._indexEventDates(event.id)
return event.id
},
getEventById(id) {
for (const [, list] of this.events) {
const found = list.find((e) => e.id === id)
if (found) return found
}
return null
return this.events.get(id) || null
},
selectEventColorId(startDateStr, endDateStr) {
const colorCounts = [0, 0, 0, 0, 0, 0, 0, 0]
const startDate = new Date(fromLocalString(startDateStr))
const endDate = new Date(fromLocalString(endDateStr))
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const dateStr = toLocalString(d)
const dayEvents = this.events.get(dateStr) || []
for (const event of dayEvents) {
if (event.colorId >= 0 && event.colorId < 8) {
colorCounts[event.colorId]++
}
}
// Count events whose ranges overlap at least one day in selected span
for (const ev of this.events.values()) {
const evStart = fromLocalString(ev.startDate)
const evEnd = fromLocalString(ev.endDate)
if (evEnd < startDate || evStart > endDate) continue
if (ev.colorId >= 0 && ev.colorId < 8) colorCounts[ev.colorId]++
}
let minCount = colorCounts[0]
@ -185,17 +170,15 @@ export const useCalendarStore = defineStore('calendar', {
},
deleteEvent(eventId) {
const datesToCleanup = []
for (const [dateStr, eventList] of this.events) {
const eventIndex = eventList.findIndex((event) => event.id === eventId)
if (eventIndex !== -1) {
eventList.splice(eventIndex, 1)
if (eventList.length === 0) {
datesToCleanup.push(dateStr)
}
if (!this.events.has(eventId)) return
// Remove id from all date sets
for (const [dateStr, set] of this.dates) {
if (set.has(eventId)) {
set.delete(eventId)
if (set.size === 0) this.dates.delete(dateStr)
}
}
datesToCleanup.forEach((dateStr) => this.events.delete(dateStr))
this.events.delete(eventId)
},
deleteSingleOccurrence(ctx) {
@ -219,24 +202,35 @@ export const useCalendarStore = defineStore('calendar', {
ctx.occurrenceDate.getDate(),
)
} else {
// Fallback: derive from occurrenceIndex (legacy path)
// Fallback: derive from sequential occurrenceIndex (base=0, first repeat=1)
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) {
const baseEnd = new Date(base.endDate + 'T00:00:00')
if (occurrenceIndex === 0) {
targetDate = baseStart
} else {
let cur = new Date(baseEnd)
cur.setDate(cur.getDate() + 1)
const blockStart = new Date(cur)
blockStart.setDate(cur.getDate() - cur.getDay())
let found = 0
let safety = 0
const WEEK_MS = 7 * 86400000
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++
function isAligned(d) {
const blk = new Date(d)
blk.setDate(d.getDate() - d.getDay())
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
return diff % interval === 0
}
while (found < occurrenceIndex && safety < 50000) {
if (pattern[cur.getDay()] && isAligned(cur)) {
found++
if (found === occurrenceIndex) break
}
cur.setDate(cur.getDate() + 1)
safety++
}
targetDate = cur
}
targetDate = cur
}
if (!targetDate) return
@ -311,13 +305,11 @@ export const useCalendarStore = defineStore('calendar', {
// 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
// Sequential index: base=0, first repeat=1
if (occurrenceIndex <= 0) return // base deletion handled elsewhere
// Count prior occurrences to KEEP (indices 0 .. occurrenceIndex-1) => occurrenceIndex total
const originalCountRaw = base.repeatCount
const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1
// Truncate base series to keep only priorOccurrences
const priorOccurrences = occurrenceIndex
this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences)
// Compute span days for multiday events
const spanDays = Math.round(
@ -333,10 +325,10 @@ export const useCalendarStore = defineStore('calendar', {
remainingCount = String(rem)
}
}
// Next occurrence after deleted one is at occurrenceIndex + interval months from base
// Next occurrence after deleted one is at (occurrenceIndex + 1)*interval months from base
const baseStart = fromLocalString(base.startDate)
const nextStart = new Date(baseStart)
nextStart.setMonth(nextStart.getMonth() + occurrenceIndex + interval)
nextStart.setMonth(nextStart.getMonth() + (occurrenceIndex + 1) * interval)
const nextEnd = new Date(nextStart)
nextEnd.setDate(nextEnd.getDate() + spanDays)
const nextStartStr = toLocalString(nextStart)
@ -477,49 +469,43 @@ export const useCalendarStore = defineStore('calendar', {
},
_snapshotBaseEvent(eventId) {
// Return a shallow snapshot of any instance for metadata
for (const [, eventList] of this.events) {
const e = eventList.find((x) => x.id === eventId)
if (e) return { ...e }
}
return null
const ev = this.events.get(eventId)
return ev ? { ...ev } : null
},
_removeEventFromAllDatesById(eventId) {
for (const [dateStr, list] of this.events) {
for (let i = list.length - 1; i >= 0; i--) {
if (list[i].id === eventId) {
list.splice(i, 1)
}
for (const [dateStr, set] of this.dates) {
if (set.has(eventId)) {
set.delete(eventId)
if (set.size === 0) this.dates.delete(dateStr)
}
if (list.length === 0) this.events.delete(dateStr)
}
},
_addEventToDateRangeWithId(eventId, baseData, startDate, endDate) {
const s = fromLocalString(startDate)
const e = fromLocalString(endDate)
const multi = startDate < endDate
const payload = {
// Update base data
this.events.set(eventId, {
...baseData,
id: eventId,
startDate,
endDate,
isSpanning: multi,
}
// Normalize single-day time fields
if (!multi) {
if (!payload.startTime) payload.startTime = '09:00'
if (!payload.durationMinutes) payload.durationMinutes = 60
} else {
payload.startTime = null
payload.durationMinutes = null
}
isSpanning: startDate < endDate,
})
this._indexEventDates(eventId)
},
_indexEventDates(eventId) {
const ev = this.events.get(eventId)
if (!ev) return
// remove old date references first
for (const [, set] of this.dates) set.delete(eventId)
const s = fromLocalString(ev.startDate)
const e = fromLocalString(ev.endDate)
const cur = new Date(s)
while (cur <= e) {
const dateStr = toLocalString(cur)
if (!this.events.has(dateStr)) this.events.set(dateStr, [])
this.events.get(dateStr).push({ ...payload })
if (!this.dates.has(dateStr)) this.dates.set(dateStr, new Set())
this.dates.get(dateStr).add(ev.id)
cur.setDate(cur.getDate() + 1)
}
},
@ -528,7 +514,7 @@ export const useCalendarStore = defineStore('calendar', {
// Adjust start/end range of a base event (non-generated) and reindex occurrences
setEventRange(eventId, newStartStr, newEndStr, { mode = 'auto' } = {}) {
const snapshot = this._findEventInAnyList(eventId)
const snapshot = this.events.get(eventId)
if (!snapshot) return
// Calculate current duration in days (inclusive)
const prevStart = new Date(fromLocalString(snapshot.startDate))
@ -588,7 +574,7 @@ export const useCalendarStore = defineStore('calendar', {
// Split a repeating series at a specific occurrence date and move that occurrence (and future) to new range
splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) {
const base = this._findEventInAnyList(baseId)
const base = this.events.get(baseId)
if (!base || !base.isRepeating) return
const originalCountRaw = base.repeatCount
const spanDays = Math.max(
@ -616,7 +602,7 @@ export const useCalendarStore = defineStore('calendar', {
const blk = new Date(d)
blk.setDate(d.getDate() - d.getDay())
const diff = Math.floor((blk - blockStartBase) / WEEK_MS)
return diff % interval === 0
return diff % interval === 0
}
const cursor = new Date(baseStart)
while (cursor < occurrenceDate) {
@ -680,7 +666,7 @@ export const useCalendarStore = defineStore('calendar', {
// Split a repeating series at a given occurrence index; returns new series id
splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) {
const base = this._findEventInAnyList(baseId)
const base = this.events.get(baseId)
if (!base || !base.isRepeating) return null
// Capture original repeatCount BEFORE truncation
const originalCountRaw = base.repeatCount
@ -715,42 +701,21 @@ export const useCalendarStore = defineStore('calendar', {
},
_terminateRepeatSeriesAtIndex(baseId, index) {
// Cap repeatCount to 'index' total occurrences (0-based index means index occurrences before split)
for (const [, list] of this.events) {
for (const ev of list) {
if (ev.id === baseId && ev.isRepeating) {
if (ev.repeatCount === 'unlimited') {
ev.repeatCount = String(index)
} else {
const rc = parseInt(ev.repeatCount, 10)
if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index))
}
}
}
const ev = this.events.get(baseId)
if (!ev || !ev.isRepeating) return
if (ev.repeatCount === 'unlimited') {
ev.repeatCount = String(index)
} else {
const rc = parseInt(ev.repeatCount, 10)
if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index))
}
},
_findEventInAnyList(eventId) {
for (const [, eventList] of this.events) {
const found = eventList.find((e) => e.id === eventId)
if (found) return found
}
return null
},
// _findEventInAnyList removed (direct map access)
_addEventToDateRange(event) {
const startDate = fromLocalString(event.startDate)
const endDate = fromLocalString(event.endDate)
const cur = new Date(startDate)
while (cur <= endDate) {
const dateStr = toLocalString(cur)
if (!this.events.has(dateStr)) {
this.events.set(dateStr, [])
}
this.events.get(dateStr).push({ ...event, isSpanning: event.startDate < event.endDate })
cur.setDate(cur.getDate() + 1)
}
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
this._indexEventDates(event.id)
},
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
@ -758,15 +723,22 @@ export const useCalendarStore = defineStore('calendar', {
persist: {
key: 'calendar-store',
storage: localStorage,
paths: ['today', 'events', 'config'],
// Persist new structures; keep legacy 'events' only for transitional restore if needed
paths: ['today', 'config', 'events', 'dates'],
serializer: {
serialize(value) {
return JSON.stringify(value, (_k, v) =>
v instanceof Map ? { __map: true, data: [...v] } : v,
)
return JSON.stringify(value, (_k, v) => {
if (v instanceof Map) return { __map: true, data: [...v] }
if (v instanceof Set) return { __set: true, data: [...v] }
return v
})
},
deserialize(value) {
return JSON.parse(value, (_k, v) => (v && v.__map ? new Map(v.data) : v))
return JSON.parse(value, (_k, v) => {
if (v && v.__map) return new Map(v.data)
if (v && v.__set) return new Set(v.data)
return v
})
},
},
},