From fece94359466c8bf5b97a8a0ed29da4f4cd6b963 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 24 Aug 2025 09:08:20 -0600 Subject: [PATCH] Simplified CalendarStore --- src/components/CalendarView.vue | 8 +- src/stores/CalendarStore.js | 201 +++++++------------------------- 2 files changed, 46 insertions(+), 163 deletions(-) diff --git a/src/components/CalendarView.vue b/src/components/CalendarView.vue index d73a6d4..6ec4d5e 100644 --- a/src/components/CalendarView.vue +++ b/src/components/CalendarView.vue @@ -20,6 +20,7 @@ import { } from '@/utils/date' import { toLocalString, fromLocalString, DEFAULT_TZ } from '@/utils/date' import { addDays, differenceInCalendarDays } from 'date-fns' +import { getHolidayForDate } from '@/utils/holidays' const calendarStore = useCalendarStore() const viewport = ref(null) @@ -232,7 +233,12 @@ function createWeek(virtualWeek) { } // Get holiday info once per day - const holiday = calendarStore.getHolidayForDate(dateStr) + // Ensure holidays initialized lazily + let holiday = null + if (calendarStore.config.holidays.enabled) { + calendarStore._ensureHolidaysInitialized?.() + holiday = getHolidayForDate(dateStr) + } days.push({ date: dateStr, diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index 38fd544..c744ddc 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -4,18 +4,11 @@ import { fromLocalString, getLocaleWeekendDays, getMondayOfISOWeek, - getOccurrenceIndex, getOccurrenceDate, DEFAULT_TZ, } from '@/utils/date' import { differenceInCalendarDays, addDays } from 'date-fns' -import { - initializeHolidays, - getHolidayForDate, - isHoliday, - getAvailableCountries, - getAvailableStates, -} from '@/utils/holidays' +import { initializeHolidays, getAvailableCountries, getAvailableStates } from '@/utils/holidays' const MIN_YEAR = 1900 const MAX_YEAR = 2100 @@ -43,29 +36,23 @@ export const useCalendarStore = defineStore('calendar', { }), getters: { - // Basic configuration getters minYear: () => MIN_YEAR, maxYear: () => MAX_YEAR, }, actions: { - // Initialize holidays based on current config + _resolveCountry(code) { + if (!code || code !== 'auto') return code + const locale = navigator.language || navigator.languages?.[0] + if (!locale) return null + const parts = locale.split('-') + if (parts.length < 2) return null + return parts[parts.length - 1].toUpperCase() + }, + 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 (!this.config.holidays.enabled) return false + const country = this._resolveCountry(this.config.holidays.country) if (country) { return this.initializeHolidays( country, @@ -73,107 +60,55 @@ export const useCalendarStore = defineStore('calendar', { 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, DEFAULT_TZ) - if (this.today !== today) { - this.today = today - } + 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 - } + const actualCountry = this._resolveCountry(country) + 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( + if (!this.config.holidays.enabled) return false + const actualCountry = this._resolveCountry(this.config.holidays.country) + const sig = `${actualCountry}-${this.config.holidays.state}-${this.config.holidays.region}-${this.config.holidays.enabled}` + if (this._holidayConfigSignature !== sig || !this._holidaysInitialized) { + const ok = initializeHolidays( actualCountry, this.config.holidays.state, this.config.holidays.region, ) - if (success) { - this._holidayConfigSignature = configSignature + if (ok) { + this._holidayConfigSignature = sig this._holidaysInitialized = true } - return success + return ok } - 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') { @@ -194,14 +129,12 @@ export const useCalendarStore = defineStore('calendar', { 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 }, @@ -214,53 +147,42 @@ export const useCalendarStore = defineStore('calendar', { const colorCounts = [0, 0, 0, 0, 0, 0, 0, 0] const startDate = fromLocalString(startDateStr, DEFAULT_TZ) const endDate = fromLocalString(endDateStr, DEFAULT_TZ) - // 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 + for (let c = 1; c < 8; c++) { + if (colorCounts[c] < minCount) { + minCount = colorCounts[c] + selectedColor = c } } - return selectedColor }, deleteEvent(eventId) { - console.log('Deleting event', eventId) this.events.delete(eventId) }, - // Remove the first (base) occurrence of a repeating event by shifting anchor forward deleteFirstOccurrence(baseId) { - console.log('Deleting first occurrence', baseId) const base = this.getEventById(baseId) if (!base) return if (!base.isRepeating) { - // Simple (non-repeating) event: delete entirely this.deleteEvent(baseId) return } const numericCount = base.repeatCount === 'unlimited' ? Infinity : parseInt(base.repeatCount, 10) if (numericCount <= 1) { - // Only one occurrence (or invalid count) -> delete event this.deleteEvent(baseId) return } - // Get the next occurrence start date (index 1) const nextStartStr = getOccurrenceDate(base, 1, DEFAULT_TZ) if (!nextStartStr) { - // No next occurrence; remove event this.deleteEvent(baseId) return } @@ -271,39 +193,29 @@ export const useCalendarStore = defineStore('calendar', { addDays(fromLocalString(nextStartStr, DEFAULT_TZ), durationDays), DEFAULT_TZ, ) - // Mutate existing event instead of delete+recreate so references remain stable base.startDate = nextStartStr base.endDate = newEndStr - if (numericCount !== Infinity) { - base.repeatCount = String(Math.max(1, numericCount - 1)) - } + if (numericCount !== Infinity) base.repeatCount = String(Math.max(1, numericCount - 1)) this.events.set(baseId, { ...base, isSpanning: base.startDate < base.endDate }) }, - // Delete a specific occurrence (not the first) from a repeating series, splitting into two deleteSingleOccurrence(ctx) { - console.log('DeletesingleOccurrence') const { baseId, occurrenceIndex } = ctx || {} - if (occurrenceIndex === undefined || occurrenceIndex === null) return + if (occurrenceIndex == null) return const base = this.getEventById(baseId) if (!base) return if (!base.isRepeating) { - // Single non-repeating event deletion if (occurrenceIndex === 0) this.deleteEvent(baseId) return } if (occurrenceIndex === 0) { - // Delegate to specialized first-occurrence deletion this.deleteFirstOccurrence(baseId) return } - // Save copy before truncation for computing next occurrence date const snapshot = { ...base } - // Cap original series to occurrences before the deleted one base.repeatCount = occurrenceIndex const nextStartStr = getOccurrenceDate(snapshot, occurrenceIndex + 1, DEFAULT_TZ) - console.log('Deleting single', occurrenceIndex, nextStartStr) - if (!nextStartStr) return // no continuation + if (!nextStartStr) return const durationDays = Math.max( 0, differenceInCalendarDays( @@ -336,40 +248,30 @@ export const useCalendarStore = defineStore('calendar', { 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) + this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex) }, - // 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 = fromLocalString(snapshot.startDate, DEFAULT_TZ) const prevEnd = fromLocalString(snapshot.endDate, DEFAULT_TZ) const prevDurationDays = Math.max(0, differenceInCalendarDays(prevEnd, prevStart)) - const newStart = fromLocalString(newStartStr, DEFAULT_TZ) const newEnd = fromLocalString(newEndStr, DEFAULT_TZ) const proposedDurationDays = Math.max(0, differenceInCalendarDays(newEnd, newStart)) - let finalDurationDays = prevDurationDays - if (mode === 'resize-left' || mode === 'resize-right') { + if (mode === 'resize-left' || mode === 'resize-right') finalDurationDays = proposedDurationDays - } - snapshot.startDate = newStartStr snapshot.endDate = toLocalString( addDays(fromLocalString(newStartStr, DEFAULT_TZ), finalDurationDays), DEFAULT_TZ, ) - // Rotate weekly recurrence pattern when moving (not resizing) so selected weekdays track shift if ( mode === 'move' && snapshot.isRepeating && @@ -391,29 +293,20 @@ export const useCalendarStore = defineStore('calendar', { snapshot.repeatWeekdays = rotated } } - // Update the event directly - this.events.set(eventId, { - ...snapshot, - startDate: snapshot.startDate, - endDate: snapshot.endDate, - isSpanning: snapshot.startDate < snapshot.endDate, - }) + this.events.set(eventId, { ...snapshot, 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 - // spanDays not needed for splitting logic here post-refactor const occurrenceDate = fromLocalString(occurrenceDateStr, DEFAULT_TZ) const baseStart = fromLocalString(base.startDate, DEFAULT_TZ) 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 + let keptOccurrences = 0 if (base.repeat === 'weeks') { const interval = base.repeatInterval || 1 const pattern = base.repeatWeekdays || [] @@ -435,15 +328,12 @@ export const useCalendarStore = defineStore('calendar', { (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 + if (diffMonths <= 0 || diffMonths % interval !== 0) return + keptOccurrences = 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) @@ -453,10 +343,8 @@ export const useCalendarStore = defineStore('calendar', { 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, DEFAULT_TZ).getDay() const shift = newWeekday - origWeekday @@ -472,7 +360,6 @@ export const useCalendarStore = defineStore('calendar', { repeatWeekdays = rotated } } - // Create continuation series starting at newStartStr this.createEvent({ title: base.title, startDate: newStartStr, @@ -485,15 +372,11 @@ export const useCalendarStore = defineStore('calendar', { }) }, - // Split a repeating series at a given occurrence index; returns new series id - splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) { + splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr) { 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) @@ -502,7 +385,7 @@ export const useCalendarStore = defineStore('calendar', { newSeriesCount = String(Math.max(1, remaining)) } } - const newId = this.createEvent({ + return this.createEvent({ title: base.title, startDate: newStartStr, endDate: newEndStr, @@ -512,7 +395,6 @@ export const useCalendarStore = defineStore('calendar', { repeatCount: newSeriesCount, repeatWeekdays: base.repeatWeekdays, }) - return newId }, _terminateRepeatSeriesAtIndex(baseId, index) { @@ -525,15 +407,10 @@ export const useCalendarStore = defineStore('calendar', { 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) {