Major new version #2
@ -15,9 +15,9 @@ const handleCreateEvent = (eventData) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditEvent = (eventInstanceId) => {
|
||||
const handleEditEvent = (eventClickPayload) => {
|
||||
if (eventDialog.value) {
|
||||
eventDialog.value.openEditDialog(eventInstanceId)
|
||||
eventDialog.value.openEditDialog(eventClickPayload)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 {
|
||||
recurrenceIndex = 0
|
||||
}
|
||||
} catch {}
|
||||
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
|
||||
|
@ -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,16 +40,13 @@ 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
|
||||
@ -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>
|
||||
|
@ -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)
|
||||
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)
|
||||
if (repeatWeekdaysLocal[cur.getDay()]) matched++
|
||||
safety++
|
||||
}
|
||||
occurrenceDate = cur
|
||||
} else if (event.repeat === 'months') {
|
||||
const cur = new Date(event.startDate + 'T00:00:00')
|
||||
weekday = cur.getDay()
|
||||
}
|
||||
} 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,9 +248,8 @@ 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) {
|
||||
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
|
||||
@ -295,8 +260,6 @@ function updateEventInStore() {
|
||||
event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveEvent() {
|
||||
if (editingEventId.value) {
|
||||
|
@ -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,
|
||||
|
@ -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,25 +202,36 @@ 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++
|
||||
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
|
||||
}
|
||||
}
|
||||
if (!targetDate) return
|
||||
|
||||
// Count occurrences BEFORE target (always include the base occurrence as first)
|
||||
@ -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 multi‑day 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(
|
||||
@ -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) {
|
||||
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
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user