calendar/src/stores/CalendarStore.js
Leo Vasanko b4b14a695b Cleanup
2025-08-23 18:38:25 -06:00

800 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia'
import {
toLocalString,
fromLocalString,
getLocaleWeekendDays,
getMondayOfISOWeek,
getOccurrenceIndex,
} from '@/utils/date'
import {
initializeHolidays,
getHolidayForDate,
isHoliday,
getAvailableCountries,
getAvailableStates,
} from '@/utils/holidays'
const MIN_YEAR = 1900
const MAX_YEAR = 2100
export const useCalendarStore = defineStore('calendar', {
state: () => ({
today: toLocalString(new Date()),
now: new Date().toISOString(),
events: new Map(),
weekend: getLocaleWeekendDays(),
_holidayConfigSignature: null,
_holidaysInitialized: false,
config: {
select_days: 1000,
min_year: MIN_YEAR,
max_year: MAX_YEAR,
first_day: 1,
holidays: {
enabled: true,
country: 'auto',
state: null,
region: null,
},
},
}),
getters: {
// Basic configuration getters
minYear: () => MIN_YEAR,
maxYear: () => MAX_YEAR,
},
actions: {
// Initialize holidays based on current config
initializeHolidaysFromConfig() {
if (!this.config.holidays.enabled) {
return false
}
let country = this.config.holidays.country
if (country === 'auto') {
const locale = navigator.language || navigator.languages?.[0]
if (!locale) return false
const parts = locale.split('-')
if (parts.length < 2) return false
country = parts[parts.length - 1].toUpperCase()
}
if (country) {
return this.initializeHolidays(
country,
this.config.holidays.state,
this.config.holidays.region,
)
}
return false
},
occursOnDate(event, dateStr) {
return getOccurrenceIndex(event, dateStr) !== null
},
updateCurrentDate() {
const d = new Date()
this.now = d.toISOString()
const today = toLocalString(d)
if (this.today !== today) {
this.today = today
}
},
// Holiday management
initializeHolidays(country, state = null, region = null) {
let actualCountry = country
if (country === 'auto') {
const locale = navigator.language || navigator.languages?.[0]
if (!locale) return false
const parts = locale.split('-')
if (parts.length < 2) return false
actualCountry = parts[parts.length - 1].toUpperCase()
}
if (this.config.holidays.country !== 'auto') {
this.config.holidays.country = country
}
this.config.holidays.state = state
this.config.holidays.region = region
this._holidayConfigSignature = null
this._holidaysInitialized = false
return initializeHolidays(actualCountry, state, region)
},
_ensureHolidaysInitialized() {
if (!this.config.holidays.enabled) {
return false
}
let actualCountry = this.config.holidays.country
if (this.config.holidays.country === 'auto') {
const locale = navigator.language || navigator.languages?.[0]
if (!locale) return false
const parts = locale.split('-')
if (parts.length < 2) return false
actualCountry = parts[parts.length - 1].toUpperCase()
}
const configSignature = `${actualCountry}-${this.config.holidays.state}-${this.config.holidays.region}-${this.config.holidays.enabled}`
if (this._holidayConfigSignature !== configSignature || !this._holidaysInitialized) {
const success = initializeHolidays(
actualCountry,
this.config.holidays.state,
this.config.holidays.region,
)
if (success) {
this._holidayConfigSignature = configSignature
this._holidaysInitialized = true
}
return success
}
return this._holidaysInitialized
},
getHolidayForDate(dateStr) {
if (!this._ensureHolidaysInitialized()) {
return null
}
return getHolidayForDate(dateStr)
},
isHoliday(dateStr) {
if (!this._ensureHolidaysInitialized()) {
return false
}
return isHoliday(dateStr)
},
getAvailableCountries() {
return getAvailableCountries() || []
},
getAvailableStates(country) {
return getAvailableStates(country) || []
},
toggleHolidays() {
this.config.holidays.enabled = !this.config.holidays.enabled
},
// Event management
generateId() {
try {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return window.crypto.randomUUID()
}
} catch {}
return 'e-' + Math.random().toString(36).slice(2, 10) + '-' + Date.now().toString(36)
},
createEvent(eventData) {
const singleDay = eventData.startDate === eventData.endDate
const event = {
id: this.generateId(),
title: eventData.title,
startDate: eventData.startDate,
endDate: eventData.endDate,
colorId:
eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
startTime: singleDay ? eventData.startTime || '09:00' : null,
durationMinutes: singleDay ? eventData.durationMinutes || 60 : null,
// Normalized repeat value: only 'weeks', 'months', or 'none'
repeat: ['weeks', 'months'].includes(eventData.repeat) ? eventData.repeat : 'none',
repeatInterval: eventData.repeatInterval || 1,
repeatCount: eventData.repeatCount || 'unlimited',
repeatWeekdays: eventData.repeatWeekdays,
isRepeating: eventData.repeat && eventData.repeat !== 'none',
}
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
return event.id
},
getEventById(id) {
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))
// 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]
let selectedColor = 0
for (let colorId = 1; colorId < 8; colorId++) {
if (colorCounts[colorId] < minCount) {
minCount = colorCounts[colorId]
selectedColor = colorId
}
}
return selectedColor
},
deleteEvent(eventId) {
this.events.delete(eventId)
},
deleteSingleOccurrence(ctx) {
const { baseId, occurrenceIndex } = ctx
const base = this.getEventById(baseId)
if (!base || !base.isRepeating) return
// WEEKLY SERIES ------------------------------------------------------
if (base.repeat === 'weeks') {
// Special case: deleting the first occurrence (index 0) should shift the series forward
if (occurrenceIndex === 0) {
const baseStart = fromLocalString(base.startDate)
const baseEnd = fromLocalString(base.endDate)
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
const pattern = base.repeatWeekdays || []
if (!pattern.some(Boolean)) {
// No pattern to continue -> delete whole series
this.deleteEvent(baseId)
return
}
const interval = base.repeatInterval || 1
const WEEK_MS = 7 * 86400000
const baseBlockStart = getMondayOfISOWeek(baseStart)
const isAligned = (d) => {
const blk = getMondayOfISOWeek(d)
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
return diff % interval === 0
}
const probe = new Date(baseStart)
let safety = 0
let found = null
while (safety < 5000) {
probe.setDate(probe.getDate() + 1)
if (pattern[probe.getDay()] && isAligned(probe)) {
found = new Date(probe)
break
}
safety++
}
if (!found) {
// Nothing after first -> delete series
this.deleteEvent(baseId)
return
}
// Adjust repeat count
if (base.repeatCount !== 'unlimited') {
const rc = parseInt(base.repeatCount, 10)
if (!isNaN(rc)) {
const newRc = rc - 1
if (newRc <= 0) {
this.deleteEvent(baseId)
return
}
base.repeatCount = String(newRc)
}
}
const newEnd = new Date(found)
newEnd.setDate(newEnd.getDate() + spanDays)
base.startDate = toLocalString(found)
base.endDate = toLocalString(newEnd)
base.isSpanning = base.startDate < base.endDate
this.events.set(base.id, base)
return
}
const interval = base.repeatInterval || 1
const pattern = base.repeatWeekdays || []
if (!pattern.some(Boolean)) return
// Preserve original count before any truncation
const originalCountRaw = base.repeatCount
// Determine target occurrence date
let targetDate = null
if (ctx.occurrenceDate instanceof Date) {
targetDate = new Date(
ctx.occurrenceDate.getFullYear(),
ctx.occurrenceDate.getMonth(),
ctx.occurrenceDate.getDate(),
)
} else {
// Fallback: derive from sequential occurrenceIndex (base=0, first repeat=1)
const baseStart = new Date(base.startDate + 'T00:00:00')
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)
let found = 0
let safety = 0
const WEEK_MS = 7 * 86400000
const baseBlockStart = getMondayOfISOWeek(baseStart)
function isAligned(d) {
const blk = getMondayOfISOWeek(d)
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
}
}
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 = getMondayOfISOWeek(baseStart)
const WEEK_MS = 7 * 86400000
function isAligned(d) {
const block = getMondayOfISOWeek(d)
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(
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
)
const nextStartStr = toLocalString(continuationStart)
const nextEnd = new Date(continuationStart)
nextEnd.setDate(nextEnd.getDate() + spanDays)
const nextEndStr = toLocalString(nextEnd)
this.createEvent({
title: base.title,
startDate: nextStartStr,
endDate: nextEndStr,
colorId: base.colorId,
repeat: 'weeks',
repeatInterval: interval,
repeatCount: remainingCount,
repeatWeekdays: base.repeatWeekdays,
})
return
}
// MONTHLY SERIES -----------------------------------------------------
if (base.repeat === 'months') {
if (occurrenceIndex === 0) {
const baseStart = fromLocalString(base.startDate)
const baseEnd = fromLocalString(base.endDate)
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
const interval = base.repeatInterval || 1
const targetMonthIndex = baseStart.getMonth() + interval
const targetYear = baseStart.getFullYear() + Math.floor(targetMonthIndex / 12)
const targetMonth = targetMonthIndex % 12
const daysInTarget = new Date(targetYear, targetMonth + 1, 0).getDate()
const dom = Math.min(baseStart.getDate(), daysInTarget)
const newStart = new Date(targetYear, targetMonth, dom)
if (base.repeatCount !== 'unlimited') {
const rc = parseInt(base.repeatCount, 10)
if (!isNaN(rc)) {
const newRc = rc - 1
if (newRc <= 0) {
this.deleteEvent(baseId)
return
}
base.repeatCount = String(newRc)
}
}
const newEnd = new Date(newStart)
newEnd.setDate(newEnd.getDate() + spanDays)
base.startDate = toLocalString(newStart)
base.endDate = toLocalString(newEnd)
base.isSpanning = base.startDate < base.endDate
this.events.set(base.id, base)
return
}
const interval = base.repeatInterval || 1
// Sequential index: base=0, first repeat=1
if (occurrenceIndex <= 0) return // base deletion handled elsewhere
// Count prior occurrences to KEEP (indices 0 .. occurrenceIndex-1) => occurrenceIndex total
const originalCountRaw = base.repeatCount
const priorOccurrences = occurrenceIndex
this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences)
// Compute span days for multiday events
const spanDays = Math.round(
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
)
// Remaining occurrences after deletion
let remainingCount = 'unlimited'
if (originalCountRaw !== 'unlimited') {
const total = parseInt(originalCountRaw, 10)
if (!isNaN(total)) {
const rem = total - priorOccurrences - 1 // subtract kept + deleted
if (rem <= 0) return // nothing left
remainingCount = String(rem)
}
}
// 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 + 1) * interval)
const nextEnd = new Date(nextStart)
nextEnd.setDate(nextEnd.getDate() + spanDays)
const nextStartStr = toLocalString(nextStart)
const nextEndStr = toLocalString(nextEnd)
this.createEvent({
title: base.title,
startDate: nextStartStr,
endDate: nextEndStr,
colorId: base.colorId,
repeat: 'months',
repeatInterval: interval,
repeatCount: remainingCount,
})
}
},
deleteFromOccurrence(ctx) {
const { baseId, occurrenceIndex } = ctx
const base = this.getEventById(baseId)
if (!base || !base.isRepeating) return
// Special case: if deleting from the base occurrence (index 0), delete the entire series
if (occurrenceIndex === 0) {
this.deleteEvent(baseId)
return
}
const keptTotal = occurrenceIndex
this._terminateRepeatSeriesAtIndex(baseId, keptTotal)
},
deleteFirstOccurrence(baseId) {
const base = this.getEventById(baseId)
if (!base || !base.isRepeating) return
const oldStart = fromLocalString(base.startDate)
const oldEnd = fromLocalString(base.endDate)
const spanDays = Math.max(0, Math.round((oldEnd - oldStart) / (24 * 60 * 60 * 1000)))
let newStartDate = null
if (base.repeat === 'weeks') {
const pattern = base.repeatWeekdays || []
if (!pattern.some(Boolean)) {
// No valid pattern -> delete series
this.deleteEvent(baseId)
return
}
const interval = base.repeatInterval || 1
const baseBlockStart = getMondayOfISOWeek(oldStart)
const WEEK_MS = 7 * 86400000
const isAligned = (d) => {
const block = getMondayOfISOWeek(d)
const diff = Math.floor((block - baseBlockStart) / WEEK_MS)
return diff % interval === 0
}
// search forward for next valid weekday respecting interval alignment
const probe = new Date(oldStart)
let safety = 0
while (safety < 5000) {
probe.setDate(probe.getDate() + 1)
if (pattern[probe.getDay()] && isAligned(probe)) {
newStartDate = new Date(probe)
break
}
safety++
}
} else if (base.repeat === 'months') {
const interval = base.repeatInterval || 1
const y = oldStart.getFullYear()
const m = oldStart.getMonth()
const targetMonthIndex = m + interval
const targetYear = y + Math.floor(targetMonthIndex / 12)
const targetMonth = targetMonthIndex % 12
const daysInTargetMonth = new Date(targetYear, targetMonth + 1, 0).getDate()
const dom = Math.min(oldStart.getDate(), daysInTargetMonth)
newStartDate = new Date(targetYear, targetMonth, dom)
} else {
// Unsupported repeat type
this.deleteEvent(baseId)
return
}
if (!newStartDate) {
// No continuation; deleting first removes series
this.deleteEvent(baseId)
return
}
// Decrement repeatCount if limited
if (base.repeatCount !== 'unlimited') {
const rc = parseInt(base.repeatCount, 10)
if (!isNaN(rc)) {
const newRc = rc - 1
if (newRc <= 0) {
// After removing first occurrence there are none left
this.deleteEvent(baseId)
return
}
base.repeatCount = String(newRc)
}
}
const newEndDate = new Date(newStartDate)
newEndDate.setDate(newEndDate.getDate() + spanDays)
base.startDate = toLocalString(newStartDate)
base.endDate = toLocalString(newEndDate)
base.isSpanning = base.startDate < base.endDate
// Persist updated base event
this.events.set(base.id, base)
return base.id
},
// Adjust start/end range of a base event (non-generated) and reindex occurrences
setEventRange(eventId, newStartStr, newEndStr, { mode = 'auto' } = {}) {
const snapshot = this.events.get(eventId)
if (!snapshot) return
// Calculate current duration in days (inclusive)
const prevStart = new Date(fromLocalString(snapshot.startDate))
const prevEnd = new Date(fromLocalString(snapshot.endDate))
const prevDurationDays = Math.max(
0,
Math.round((prevEnd - prevStart) / (24 * 60 * 60 * 1000)),
)
const newStart = new Date(fromLocalString(newStartStr))
const newEnd = new Date(fromLocalString(newEndStr))
const proposedDurationDays = Math.max(
0,
Math.round((newEnd - newStart) / (24 * 60 * 60 * 1000)),
)
let finalDurationDays = prevDurationDays
if (mode === 'resize-left' || mode === 'resize-right') {
finalDurationDays = proposedDurationDays
}
snapshot.startDate = newStartStr
snapshot.endDate = toLocalString(
new Date(
new Date(fromLocalString(newStartStr)).setDate(
new Date(fromLocalString(newStartStr)).getDate() + finalDurationDays,
),
),
)
// Rotate weekly recurrence pattern when moving (not resizing) so selected weekdays track shift
if (
mode === 'move' &&
snapshot.isRepeating &&
snapshot.repeat === 'weeks' &&
Array.isArray(snapshot.repeatWeekdays)
) {
const oldDow = prevStart.getDay()
const newDow = newStart.getDay()
const shift = newDow - oldDow
if (shift !== 0) {
const rotated = [false, false, false, false, false, false, false]
for (let i = 0; i < 7; i++) {
if (snapshot.repeatWeekdays[i]) {
let ni = (i + shift) % 7
if (ni < 0) ni += 7
rotated[ni] = true
}
}
snapshot.repeatWeekdays = rotated
}
}
// Update the event directly
this.events.set(eventId, {
...snapshot,
startDate: snapshot.startDate,
endDate: snapshot.endDate,
isSpanning: snapshot.startDate < snapshot.endDate,
})
},
// 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.events.get(baseId)
if (!base || !base.isRepeating) return
const originalCountRaw = base.repeatCount
const spanDays = Math.max(
0,
Math.round(
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
),
)
const occurrenceDate = fromLocalString(occurrenceDateStr)
const baseStart = fromLocalString(base.startDate)
if (occurrenceDate <= baseStart) {
// Moving the base itself: just move entire series
this.setEventRange(baseId, newStartStr, newEndStr, { mode: 'move' })
return
}
let keptOccurrences = 0 // number of occurrences BEFORE the moved one
if (base.repeat === 'weeks') {
const interval = base.repeatInterval || 1
const pattern = base.repeatWeekdays || []
if (!pattern.some(Boolean)) return
const WEEK_MS = 7 * 86400000
const blockStartBase = getMondayOfISOWeek(baseStart)
function isAligned(d) {
const blk = getMondayOfISOWeek(d)
const diff = Math.floor((blk - blockStartBase) / WEEK_MS)
return diff % interval === 0
}
const cursor = new Date(baseStart)
while (cursor < occurrenceDate) {
if (pattern[cursor.getDay()] && isAligned(cursor)) keptOccurrences++
cursor.setDate(cursor.getDate() + 1)
}
} else if (base.repeat === 'months') {
const diffMonths =
(occurrenceDate.getFullYear() - baseStart.getFullYear()) * 12 +
(occurrenceDate.getMonth() - baseStart.getMonth())
const interval = base.repeatInterval || 1
if (diffMonths <= 0 || diffMonths % interval !== 0) return // invalid occurrence
keptOccurrences = diffMonths // base is occurrence 0; we keep all before diffMonths
} else {
// Unsupported repeat type
return
}
// Truncate original series to keptOccurrences
this._terminateRepeatSeriesAtIndex(baseId, keptOccurrences)
// Compute remaining occurrences count
let remainingCount = 'unlimited'
if (originalCountRaw !== 'unlimited') {
const total = parseInt(originalCountRaw, 10)
if (!isNaN(total)) {
const rem = total - keptOccurrences
if (rem <= 0) return
remainingCount = String(rem)
}
}
// Determine repeat-specific adjustments
let repeatWeekdays = base.repeatWeekdays
if (base.repeat === 'weeks' && Array.isArray(base.repeatWeekdays)) {
// Rotate pattern so that the moved occurrence weekday stays active relative to new anchor
const origWeekday = occurrenceDate.getDay()
const newWeekday = fromLocalString(newStartStr).getDay()
const shift = newWeekday - origWeekday
if (shift !== 0) {
const rotated = [false, false, false, false, false, false, false]
for (let i = 0; i < 7; i++) {
if (base.repeatWeekdays[i]) {
let ni = (i + shift) % 7
if (ni < 0) ni += 7
rotated[ni] = true
}
}
repeatWeekdays = rotated
}
}
// Create continuation series starting at newStartStr
this.createEvent({
title: base.title,
startDate: newStartStr,
endDate: newEndStr,
colorId: base.colorId,
repeat: base.repeat,
repeatInterval: base.repeatInterval,
repeatCount: remainingCount,
repeatWeekdays,
})
},
// Split a repeating series at a given occurrence index; returns new series id
splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) {
const base = this.events.get(baseId)
if (!base || !base.isRepeating) return null
// Capture original repeatCount BEFORE truncation
const originalCountRaw = base.repeatCount
// Truncate base to keep occurrences before split point (indices 0..occurrenceIndex-1)
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
// Compute new series repeatCount (remaining occurrences starting at occurrenceIndex)
let newSeriesCount = 'unlimited'
if (originalCountRaw !== 'unlimited') {
const originalNum = parseInt(originalCountRaw, 10)
if (!isNaN(originalNum)) {
const remaining = originalNum - occurrenceIndex
newSeriesCount = String(Math.max(1, remaining))
}
}
const newId = this.createEvent({
title: base.title,
startDate: newStartStr,
endDate: newEndStr,
colorId: base.colorId,
repeat: base.repeat,
repeatInterval: base.repeatInterval,
repeatCount: newSeriesCount,
repeatWeekdays: base.repeatWeekdays,
})
return newId
},
_terminateRepeatSeriesAtIndex(baseId, 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 removed (direct map access)
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
},
persist: {
key: 'calendar-store',
storage: localStorage,
// Persist only events map, no dates indexing
paths: ['today', 'config', 'events'],
serializer: {
serialize(value) {
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) => {
if (v && v.__map) return new Map(v.data)
if (v && v.__set) return new Set(v.data)
return v
})
},
},
},
})