A little better but still broken. I need easier tasks for a while.
This commit is contained in:
parent
02442f5135
commit
8caaf33cb9
@ -13,6 +13,9 @@ import {
|
|||||||
daysInclusive,
|
daysInclusive,
|
||||||
addDaysStr,
|
addDaysStr,
|
||||||
formatDateRange,
|
formatDateRange,
|
||||||
|
getMondayOfISOWeek,
|
||||||
|
getOccurrenceIndex,
|
||||||
|
getVirtualOccurrenceEndDate,
|
||||||
} from '@/utils/date'
|
} from '@/utils/date'
|
||||||
import { toLocalString, fromLocalString } from '@/utils/date'
|
import { toLocalString, fromLocalString } from '@/utils/date'
|
||||||
|
|
||||||
@ -153,25 +156,11 @@ function createWeek(virtualWeek) {
|
|||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const dateStr = toLocalString(cur)
|
const dateStr = toLocalString(cur)
|
||||||
const storedEvents = []
|
const storedEvents = []
|
||||||
const idSet = calendarStore.dates.get(dateStr)
|
|
||||||
if (idSet) {
|
// Find all non-repeating events that occur on this date
|
||||||
// Support Set or Array; ignore unexpected shapes
|
for (const ev of calendarStore.events.values()) {
|
||||||
if (idSet instanceof Set) {
|
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||||
idSet.forEach((id) => {
|
storedEvents.push(ev)
|
||||||
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
|
// Build day events starting with stored (base/spanning) then virtual occurrences
|
||||||
@ -179,56 +168,45 @@ function createWeek(virtualWeek) {
|
|||||||
for (const base of repeatingBases) {
|
for (const base of repeatingBases) {
|
||||||
// Skip if the base itself already on this date (already in storedEvents)
|
// Skip if the base itself already on this date (already in storedEvents)
|
||||||
if (dateStr >= base.startDate && dateStr <= base.endDate) continue
|
if (dateStr >= base.startDate && dateStr <= base.endDate) continue
|
||||||
if (calendarStore.occursOnDate(base, dateStr)) {
|
|
||||||
// Determine sequential occurrence index: base event = 0, first repeat = 1, etc.
|
// Check if any occurrence of this repeating event spans through this date
|
||||||
let recurrenceIndex = 0
|
const baseStart = fromLocalString(base.startDate)
|
||||||
try {
|
const baseEnd = fromLocalString(base.endDate)
|
||||||
if (base.repeat === 'weeks') {
|
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||||
const pattern = base.repeatWeekdays || []
|
const currentDate = fromLocalString(dateStr)
|
||||||
const interval = base.repeatInterval || 1
|
|
||||||
const baseStart = new Date(base.startDate + 'T00:00:00')
|
let occurrenceFound = false
|
||||||
const baseEnd = new Date(base.endDate + 'T00:00:00')
|
|
||||||
const target = new Date(dateStr + 'T00:00:00')
|
// Check dates going backwards to find an occurrence that might span to this date
|
||||||
const WEEK_MS = 7 * 86400000
|
for (let offset = 0; offset <= spanDays && !occurrenceFound; offset++) {
|
||||||
const baseBlockStart = new Date(baseStart)
|
const candidateStart = new Date(currentDate)
|
||||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
candidateStart.setDate(candidateStart.getDate() - offset)
|
||||||
function isAligned(d) {
|
const candidateStartStr = toLocalString(candidateStart)
|
||||||
const blk = new Date(d)
|
|
||||||
blk.setDate(d.getDate() - d.getDay())
|
const occurrenceIndex = getOccurrenceIndex(base, candidateStartStr)
|
||||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
if (occurrenceIndex !== null) {
|
||||||
return diff % interval === 0
|
// Calculate the end date of this occurrence
|
||||||
|
const virtualEndDate = getVirtualOccurrenceEndDate(base, candidateStartStr)
|
||||||
|
|
||||||
|
// Check if this occurrence spans through the current date
|
||||||
|
if (dateStr >= candidateStartStr && dateStr <= virtualEndDate) {
|
||||||
|
// Create virtual occurrence (if not already created)
|
||||||
|
const virtualId = base.id + '_v_' + candidateStartStr
|
||||||
|
const alreadyExists = dayEvents.some((ev) => ev.id === virtualId)
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
dayEvents.push({
|
||||||
|
...base,
|
||||||
|
id: virtualId,
|
||||||
|
startDate: candidateStartStr,
|
||||||
|
endDate: virtualEndDate,
|
||||||
|
_recurrenceIndex: occurrenceIndex,
|
||||||
|
_baseId: base.id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// Count valid occurrences after base end and before target
|
occurrenceFound = true
|
||||||
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 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() - baseStart.getFullYear()) * 12 +
|
|
||||||
(target.getMonth() - baseStart.getMonth())
|
|
||||||
// diffMonths should be multiple of interval; sequential index = diffMonths/interval
|
|
||||||
recurrenceIndex = diffMonths / interval
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
recurrenceIndex = 0
|
|
||||||
}
|
}
|
||||||
dayEvents.push({
|
|
||||||
...base,
|
|
||||||
id: base.id + '_v_' + dateStr,
|
|
||||||
startDate: dateStr,
|
|
||||||
endDate: dateStr,
|
|
||||||
_recurrenceIndex: recurrenceIndex,
|
|
||||||
_baseId: base.id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dow = cur.getDay()
|
const dow = cur.getDay()
|
||||||
|
@ -3,7 +3,7 @@ import { useCalendarStore } from '@/stores/CalendarStore'
|
|||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import WeekdaySelector from './WeekdaySelector.vue'
|
import WeekdaySelector from './WeekdaySelector.vue'
|
||||||
import Numeric from './Numeric.vue'
|
import Numeric from './Numeric.vue'
|
||||||
import { addDaysStr } from '@/utils/date'
|
import { addDaysStr, getMondayOfISOWeek } from '@/utils/date'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) },
|
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) },
|
||||||
@ -173,11 +173,9 @@ function openEditDialog(payload) {
|
|||||||
// Count valid repeat occurrences (pattern + interval alignment) AFTER the base span
|
// Count valid repeat occurrences (pattern + interval alignment) AFTER the base span
|
||||||
const interval = event.repeatInterval || 1
|
const interval = event.repeatInterval || 1
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
const baseBlockStart = new Date(baseStart)
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
|
||||||
function isAligned(d) {
|
function isAligned(d) {
|
||||||
const blk = new Date(d)
|
const blk = getMondayOfISOWeek(d)
|
||||||
blk.setDate(d.getDate() - d.getDay())
|
|
||||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
||||||
return diff % interval === 0
|
return diff % interval === 0
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { toLocalString, fromLocalString, getLocaleWeekendDays } from '@/utils/date'
|
import {
|
||||||
|
toLocalString,
|
||||||
|
fromLocalString,
|
||||||
|
getLocaleWeekendDays,
|
||||||
|
getMondayOfISOWeek,
|
||||||
|
getOccurrenceIndex,
|
||||||
|
getWeeklyOccurrenceIndex,
|
||||||
|
getMonthlyOccurrenceIndex,
|
||||||
|
getVirtualOccurrenceEndDate,
|
||||||
|
occursOnOrSpansDate,
|
||||||
|
} from '@/utils/date'
|
||||||
|
|
||||||
const MIN_YEAR = 1900
|
const MIN_YEAR = 1900
|
||||||
const MAX_YEAR = 2100
|
const MAX_YEAR = 2100
|
||||||
@ -9,7 +19,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
today: toLocalString(new Date()),
|
today: toLocalString(new Date()),
|
||||||
now: new Date().toISOString(), // store as ISO string
|
now: new Date().toISOString(), // store as ISO string
|
||||||
events: new Map(), // id -> event object (primary)
|
events: new Map(), // id -> event object (primary)
|
||||||
dates: new Map(), // dateStr -> Set of event ids
|
|
||||||
weekend: getLocaleWeekendDays(),
|
weekend: getLocaleWeekendDays(),
|
||||||
config: {
|
config: {
|
||||||
select_days: 1000,
|
select_days: 1000,
|
||||||
@ -28,74 +37,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
actions: {
|
actions: {
|
||||||
// Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences.
|
// Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences.
|
||||||
occursOnDate(event, dateStr) {
|
occursOnDate(event, dateStr) {
|
||||||
if (!event || !event.isRepeating || event.repeat === 'none') return false
|
return getOccurrenceIndex(event, dateStr) !== null
|
||||||
// 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()
|
||||||
@ -136,7 +78,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
||||||
this._indexEventDates(event.id)
|
|
||||||
return event.id
|
return event.id
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -170,14 +111,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteEvent(eventId) {
|
deleteEvent(eventId) {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.events.delete(eventId)
|
this.events.delete(eventId)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -213,11 +146,9 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
let found = 0
|
let found = 0
|
||||||
let safety = 0
|
let safety = 0
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
const baseBlockStart = new Date(baseStart)
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
|
||||||
function isAligned(d) {
|
function isAligned(d) {
|
||||||
const blk = new Date(d)
|
const blk = getMondayOfISOWeek(d)
|
||||||
blk.setDate(d.getDate() - d.getDay())
|
|
||||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
||||||
return diff % interval === 0
|
return diff % interval === 0
|
||||||
}
|
}
|
||||||
@ -236,12 +167,10 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
|
|
||||||
// Count occurrences BEFORE target (always include the base occurrence as first)
|
// Count occurrences BEFORE target (always include the base occurrence as first)
|
||||||
const baseStart = new Date(base.startDate + 'T00:00:00')
|
const baseStart = new Date(base.startDate + 'T00:00:00')
|
||||||
const baseBlockStart = new Date(baseStart)
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
function isAligned(d) {
|
function isAligned(d) {
|
||||||
const block = new Date(d)
|
const block = getMondayOfISOWeek(d)
|
||||||
block.setDate(d.getDate() - d.getDay())
|
|
||||||
const diff = Math.floor((block - baseBlockStart) / WEEK_MS)
|
const diff = Math.floor((block - baseBlockStart) / WEEK_MS)
|
||||||
return diff % interval === 0
|
return diff % interval === 0
|
||||||
}
|
}
|
||||||
@ -473,43 +402,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
return ev ? { ...ev } : null
|
return ev ? { ...ev } : null
|
||||||
},
|
},
|
||||||
|
|
||||||
_removeEventFromAllDatesById(eventId) {
|
|
||||||
for (const [dateStr, set] of this.dates) {
|
|
||||||
if (set.has(eventId)) {
|
|
||||||
set.delete(eventId)
|
|
||||||
if (set.size === 0) this.dates.delete(dateStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_addEventToDateRangeWithId(eventId, baseData, startDate, endDate) {
|
|
||||||
// Update base data
|
|
||||||
this.events.set(eventId, {
|
|
||||||
...baseData,
|
|
||||||
id: eventId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
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.dates.has(dateStr)) this.dates.set(dateStr, new Set())
|
|
||||||
this.dates.get(dateStr).add(ev.id)
|
|
||||||
cur.setDate(cur.getDate() + 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// expandRepeats removed: no physical occurrence expansion
|
// expandRepeats removed: no physical occurrence expansion
|
||||||
|
|
||||||
// Adjust start/end range of a base event (non-generated) and reindex occurrences
|
// Adjust start/end range of a base event (non-generated) and reindex occurrences
|
||||||
@ -566,10 +458,13 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
snapshot.repeatWeekdays = rotated
|
snapshot.repeatWeekdays = rotated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reindex
|
// Update the event directly
|
||||||
this._removeEventFromAllDatesById(eventId)
|
this.events.set(eventId, {
|
||||||
this._addEventToDateRangeWithId(eventId, snapshot, snapshot.startDate, snapshot.endDate)
|
...snapshot,
|
||||||
// no expansion
|
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
|
// Split a repeating series at a specific occurrence date and move that occurrence (and future) to new range
|
||||||
@ -596,11 +491,9 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
const pattern = base.repeatWeekdays || []
|
const pattern = base.repeatWeekdays || []
|
||||||
if (!pattern.some(Boolean)) return
|
if (!pattern.some(Boolean)) return
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
const blockStartBase = new Date(baseStart)
|
const blockStartBase = getMondayOfISOWeek(baseStart)
|
||||||
blockStartBase.setDate(blockStartBase.getDate() - blockStartBase.getDay())
|
|
||||||
function isAligned(d) {
|
function isAligned(d) {
|
||||||
const blk = new Date(d)
|
const blk = getMondayOfISOWeek(d)
|
||||||
blk.setDate(d.getDate() - d.getDay())
|
|
||||||
const diff = Math.floor((blk - blockStartBase) / WEEK_MS)
|
const diff = Math.floor((blk - blockStartBase) / WEEK_MS)
|
||||||
return diff % interval === 0
|
return diff % interval === 0
|
||||||
}
|
}
|
||||||
@ -694,12 +587,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
return newId
|
return newId
|
||||||
},
|
},
|
||||||
|
|
||||||
_reindexBaseEvent(eventId, snapshot, startDate, endDate) {
|
|
||||||
if (!snapshot) return
|
|
||||||
this._removeEventFromAllDatesById(eventId)
|
|
||||||
this._addEventToDateRangeWithId(eventId, snapshot, startDate, endDate)
|
|
||||||
},
|
|
||||||
|
|
||||||
_terminateRepeatSeriesAtIndex(baseId, index) {
|
_terminateRepeatSeriesAtIndex(baseId, index) {
|
||||||
const ev = this.events.get(baseId)
|
const ev = this.events.get(baseId)
|
||||||
if (!ev || !ev.isRepeating) return
|
if (!ev || !ev.isRepeating) return
|
||||||
@ -713,18 +600,13 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
|
|
||||||
// _findEventInAnyList removed (direct map access)
|
// _findEventInAnyList removed (direct map access)
|
||||||
|
|
||||||
_addEventToDateRange(event) {
|
|
||||||
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
|
||||||
this._indexEventDates(event.id)
|
|
||||||
},
|
|
||||||
|
|
||||||
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
|
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
key: 'calendar-store',
|
key: 'calendar-store',
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
// Persist new structures; keep legacy 'events' only for transitional restore if needed
|
// Persist only events map, no dates indexing
|
||||||
paths: ['today', 'config', 'events', 'dates'],
|
paths: ['today', 'config', 'events'],
|
||||||
serializer: {
|
serializer: {
|
||||||
serialize(value) {
|
serialize(value) {
|
||||||
return JSON.stringify(value, (_k, v) => {
|
return JSON.stringify(value, (_k, v) => {
|
||||||
|
@ -51,6 +51,18 @@ function fromLocalString(dateString) {
|
|||||||
return new Date(year, month - 1, day)
|
return new Date(year, month - 1, day)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Monday of the ISO week for a given date
|
||||||
|
* @param {Date} date - The date to get the Monday for
|
||||||
|
* @returns {Date} Date object representing the Monday of the ISO week
|
||||||
|
*/
|
||||||
|
function getMondayOfISOWeek(date) {
|
||||||
|
const d = new Date(date)
|
||||||
|
const dayOfWeek = (d.getDay() + 6) % 7 // Convert to Monday=0, Sunday=6
|
||||||
|
d.setDate(d.getDate() - dayOfWeek)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of Monday for a given date (0-6, where Monday = 0)
|
* Get the index of Monday for a given date (0-6, where Monday = 0)
|
||||||
* @param {Date} d - The date
|
* @param {Date} d - The date
|
||||||
@ -59,6 +71,259 @@ function fromLocalString(dateString) {
|
|||||||
const mondayIndex = (d) => (d.getDay() + 6) % 7
|
const mondayIndex = (d) => (d.getDay() + 6) % 7
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Calculate the occurrence index for a repeating weekly event on a specific date
|
||||||
|
* @param {Object} event - The event object with repeat info
|
||||||
|
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||||
|
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not valid
|
||||||
|
*/
|
||||||
|
function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||||
|
if (!event.isRepeating || event.repeat !== 'weeks') return null
|
||||||
|
|
||||||
|
const pattern = event.repeatWeekdays || []
|
||||||
|
if (!pattern.some(Boolean)) return null
|
||||||
|
|
||||||
|
const d = fromLocalString(dateStr)
|
||||||
|
const dow = d.getDay()
|
||||||
|
if (!pattern[dow]) return null
|
||||||
|
|
||||||
|
const baseStart = fromLocalString(event.startDate)
|
||||||
|
const interval = event.repeatInterval || 1
|
||||||
|
|
||||||
|
// Check if date resides in a week block that aligns with interval
|
||||||
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
|
const currentBlockStart = getMondayOfISOWeek(d)
|
||||||
|
const WEEK_MS = 7 * 86400000
|
||||||
|
const blocksDiff = Math.floor((currentBlockStart - baseBlockStart) / WEEK_MS)
|
||||||
|
|
||||||
|
if (blocksDiff < 0 || blocksDiff % interval !== 0) return null
|
||||||
|
|
||||||
|
// For same week as base start, count from base start to target
|
||||||
|
if (currentBlockStart.getTime() === baseBlockStart.getTime()) {
|
||||||
|
let occurrenceIndex = 0
|
||||||
|
const cursor = new Date(baseStart)
|
||||||
|
while (cursor < d) {
|
||||||
|
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||||
|
cursor.setDate(cursor.getDate() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against repeat count limit
|
||||||
|
if (event.repeatCount !== 'unlimited') {
|
||||||
|
const limit = parseInt(event.repeatCount, 10)
|
||||||
|
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return occurrenceIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// For different weeks, calculate based on complete intervals
|
||||||
|
const weekdaysPerInterval = pattern.filter(Boolean).length
|
||||||
|
const completeIntervals = blocksDiff / interval
|
||||||
|
let occurrenceIndex = completeIntervals * weekdaysPerInterval
|
||||||
|
|
||||||
|
// Add occurrences from the current week up to the target date
|
||||||
|
const cursor = new Date(currentBlockStart)
|
||||||
|
while (cursor < d) {
|
||||||
|
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||||
|
cursor.setDate(cursor.getDate() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against repeat count limit
|
||||||
|
if (event.repeatCount !== 'unlimited') {
|
||||||
|
const limit = parseInt(event.repeatCount, 10)
|
||||||
|
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return occurrenceIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the occurrence index for a repeating monthly event on a specific date
|
||||||
|
* @param {Object} event - The event object with repeat info
|
||||||
|
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||||
|
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not valid
|
||||||
|
*/
|
||||||
|
function getMonthlyOccurrenceIndex(event, dateStr) {
|
||||||
|
if (!event.isRepeating || event.repeat !== 'months') return null
|
||||||
|
|
||||||
|
const baseStart = fromLocalString(event.startDate)
|
||||||
|
const d = fromLocalString(dateStr)
|
||||||
|
const diffMonths =
|
||||||
|
(d.getFullYear() - baseStart.getFullYear()) * 12 + (d.getMonth() - baseStart.getMonth())
|
||||||
|
|
||||||
|
if (diffMonths < 0) return null
|
||||||
|
|
||||||
|
const interval = event.repeatInterval || 1
|
||||||
|
if (diffMonths % interval !== 0) return null
|
||||||
|
|
||||||
|
// Check day match (clamped for shorter months)
|
||||||
|
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 null
|
||||||
|
|
||||||
|
const occurrenceIndex = diffMonths / interval
|
||||||
|
|
||||||
|
// Check against repeat count limit
|
||||||
|
if (event.repeatCount !== 'unlimited') {
|
||||||
|
const limit = parseInt(event.repeatCount, 10)
|
||||||
|
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return occurrenceIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a repeating event occurs on a specific date and return occurrence index
|
||||||
|
* @param {Object} event - The event object with repeat info
|
||||||
|
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||||
|
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not occurring
|
||||||
|
*/
|
||||||
|
function getOccurrenceIndex(event, dateStr) {
|
||||||
|
if (!event || !event.isRepeating || event.repeat === 'none') return null
|
||||||
|
if (dateStr < event.startDate) return null
|
||||||
|
|
||||||
|
if (event.repeat === 'weeks') {
|
||||||
|
return getWeeklyOccurrenceIndex(event, dateStr)
|
||||||
|
} else if (event.repeat === 'months') {
|
||||||
|
return getMonthlyOccurrenceIndex(event, dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the end date for a virtual occurrence of a repeating event
|
||||||
|
* @param {Object} event - The base event object
|
||||||
|
* @param {string} occurrenceStartDate - The start date of the occurrence (YYYY-MM-DD)
|
||||||
|
* @returns {string} The end date of the occurrence (YYYY-MM-DD)
|
||||||
|
*/
|
||||||
|
function getVirtualOccurrenceEndDate(event, occurrenceStartDate) {
|
||||||
|
const baseStart = fromLocalString(event.startDate)
|
||||||
|
const baseEnd = fromLocalString(event.endDate)
|
||||||
|
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||||
|
|
||||||
|
const occurrenceStart = fromLocalString(occurrenceStartDate)
|
||||||
|
const occurrenceEnd = new Date(occurrenceStart)
|
||||||
|
occurrenceEnd.setDate(occurrenceEnd.getDate() + spanDays)
|
||||||
|
|
||||||
|
return toLocalString(occurrenceEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a repeating event occurs on or spans through a specific date
|
||||||
|
* @param {Object} event - The event object with repeat info
|
||||||
|
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||||
|
* @returns {boolean} True if the event occurs on or spans through the date
|
||||||
|
*/
|
||||||
|
function occursOnOrSpansDate(event, dateStr) {
|
||||||
|
if (!event || !event.isRepeating || event.repeat === 'none') return false
|
||||||
|
|
||||||
|
// Check if this is the base event spanning naturally
|
||||||
|
if (dateStr >= event.startDate && dateStr <= event.endDate) return true
|
||||||
|
|
||||||
|
// For virtual occurrences, we need to check if any occurrence spans through this date
|
||||||
|
const baseStart = fromLocalString(event.startDate)
|
||||||
|
const baseEnd = fromLocalString(event.endDate)
|
||||||
|
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||||
|
|
||||||
|
if (spanDays === 0) {
|
||||||
|
// Single day event - just check if it occurs on this date
|
||||||
|
return getOccurrenceIndex(event, dateStr) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-day event - check if any occurrence's span includes this date
|
||||||
|
const targetDate = fromLocalString(dateStr)
|
||||||
|
|
||||||
|
if (event.repeat === 'weeks') {
|
||||||
|
const pattern = event.repeatWeekdays || []
|
||||||
|
if (!pattern.some(Boolean)) return false
|
||||||
|
|
||||||
|
const interval = event.repeatInterval || 1
|
||||||
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
|
const WEEK_MS = 7 * 86400000
|
||||||
|
|
||||||
|
// Check a reasonable range of weeks around the target date
|
||||||
|
for (
|
||||||
|
let weekOffset = -Math.ceil(spanDays / 7) - 1;
|
||||||
|
weekOffset <= Math.ceil(spanDays / 7) + 1;
|
||||||
|
weekOffset++
|
||||||
|
) {
|
||||||
|
const weekStart = new Date(baseBlockStart)
|
||||||
|
weekStart.setDate(weekStart.getDate() + weekOffset * 7)
|
||||||
|
|
||||||
|
// Check if this week aligns with the interval
|
||||||
|
const blocksDiff = Math.floor((weekStart - baseBlockStart) / WEEK_MS)
|
||||||
|
if (blocksDiff < 0 || blocksDiff % interval !== 0) continue
|
||||||
|
|
||||||
|
// Check each day in this week
|
||||||
|
for (let day = 0; day < 7; day++) {
|
||||||
|
const candidateStart = new Date(weekStart)
|
||||||
|
candidateStart.setDate(candidateStart.getDate() + day)
|
||||||
|
|
||||||
|
// Skip if before base start
|
||||||
|
if (candidateStart < baseStart) continue
|
||||||
|
|
||||||
|
// Check if this day matches the pattern
|
||||||
|
if (!pattern[candidateStart.getDay()]) continue
|
||||||
|
|
||||||
|
// Check repeat count limit
|
||||||
|
const occIndex = getWeeklyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||||
|
if (occIndex === null) continue
|
||||||
|
|
||||||
|
// Calculate end date for this occurrence
|
||||||
|
const candidateEnd = new Date(candidateStart)
|
||||||
|
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||||
|
|
||||||
|
// Check if target date falls within this occurrence's span
|
||||||
|
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.repeat === 'months') {
|
||||||
|
const interval = event.repeatInterval || 1
|
||||||
|
const baseDay = baseStart.getDate()
|
||||||
|
|
||||||
|
// Check a reasonable range of months around the target date
|
||||||
|
const targetYear = targetDate.getFullYear()
|
||||||
|
const targetMonth = targetDate.getMonth()
|
||||||
|
const baseYear = baseStart.getFullYear()
|
||||||
|
const baseMonth = baseStart.getMonth()
|
||||||
|
|
||||||
|
for (let monthOffset = -spanDays * 2; monthOffset <= spanDays * 2; monthOffset++) {
|
||||||
|
const candidateYear = baseYear + Math.floor((baseMonth + monthOffset) / 12)
|
||||||
|
const candidateMonth = (baseMonth + monthOffset + 12) % 12
|
||||||
|
|
||||||
|
// Check if this month aligns with the interval
|
||||||
|
const diffMonths = (candidateYear - baseYear) * 12 + (candidateMonth - baseMonth)
|
||||||
|
if (diffMonths < 0 || diffMonths % interval !== 0) continue
|
||||||
|
|
||||||
|
// Calculate the actual day (clamped for shorter months)
|
||||||
|
const daysInMonth = new Date(candidateYear, candidateMonth + 1, 0).getDate()
|
||||||
|
const effectiveDay = Math.min(baseDay, daysInMonth)
|
||||||
|
|
||||||
|
const candidateStart = new Date(candidateYear, candidateMonth, effectiveDay)
|
||||||
|
|
||||||
|
// Skip if before base start
|
||||||
|
if (candidateStart < baseStart) continue
|
||||||
|
|
||||||
|
// Check repeat count limit
|
||||||
|
const occIndex = getMonthlyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||||
|
if (occIndex === null) continue
|
||||||
|
|
||||||
|
// Calculate end date for this occurrence
|
||||||
|
const candidateEnd = new Date(candidateStart)
|
||||||
|
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||||
|
|
||||||
|
// Check if target date falls within this occurrence's span
|
||||||
|
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} /**
|
||||||
* Pad a number with leading zeros to make it 2 digits
|
* Pad a number with leading zeros to make it 2 digits
|
||||||
* @param {number} n - Number to pad
|
* @param {number} n - Number to pad
|
||||||
* @returns {string} Padded string
|
* @returns {string} Padded string
|
||||||
@ -207,6 +472,12 @@ export {
|
|||||||
isoWeekInfo,
|
isoWeekInfo,
|
||||||
toLocalString,
|
toLocalString,
|
||||||
fromLocalString,
|
fromLocalString,
|
||||||
|
getMondayOfISOWeek,
|
||||||
|
getWeeklyOccurrenceIndex,
|
||||||
|
getMonthlyOccurrenceIndex,
|
||||||
|
getOccurrenceIndex,
|
||||||
|
getVirtualOccurrenceEndDate,
|
||||||
|
occursOnOrSpansDate,
|
||||||
mondayIndex,
|
mondayIndex,
|
||||||
pad,
|
pad,
|
||||||
daysInclusive,
|
daysInclusive,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user