-
{{ span.title }}
-
@@ -33,8 +33,8 @@ import { toLocalString, fromLocalString, daysInclusive, addDaysStr } from '@/uti
const props = defineProps({
week: {
type: Object,
- required: true
- }
+ required: true,
+ },
})
const emit = defineEmits(['event-click'])
@@ -47,44 +47,51 @@ const justDragged = ref(false)
// Generate repeat occurrences for a specific date
function generateRepeatOccurrencesForDate(targetDateStr) {
const occurrences = []
-
+
// Get all events from the store and check for repeating ones
for (const [, eventList] of store.events) {
for (const baseEvent of eventList) {
if (!baseEvent.isRepeating || baseEvent.repeat === 'none') {
continue
}
-
+
const targetDate = new Date(fromLocalString(targetDateStr))
const baseStartDate = new Date(fromLocalString(baseEvent.startDate))
const baseEndDate = new Date(fromLocalString(baseEvent.endDate))
const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000))
-
- if (baseEvent.repeat === 'weekly') {
+
+ if (baseEvent.repeat === 'weeks') {
const repeatWeekdays = baseEvent.repeatWeekdays
const targetWeekday = targetDate.getDay()
if (!repeatWeekdays[targetWeekday]) continue
if (targetDate < baseStartDate) continue
- const maxOccurrences = baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
+ const maxOccurrences =
+ baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
+ const interval = baseEvent.repeatInterval || 1
if (maxOccurrences === 0) continue
// Count occurrences from start up to (and including) target
let occIdx = 0
+ // Determine the week distance from baseStartDate to targetDate
+ const msPerDay = 24 * 60 * 60 * 1000
+ const daysDiff = Math.floor((targetDate - baseStartDate) / msPerDay)
+ const weeksDiff = Math.floor(daysDiff / 7)
+ if (weeksDiff % interval !== 0) continue
+ // Count occurrences only among valid weeks and selected weekdays
const cursor = new Date(baseStartDate)
while (cursor < targetDate && occIdx < maxOccurrences) {
- if (repeatWeekdays[cursor.getDay()]) occIdx++
+ const cDaysDiff = Math.floor((cursor - baseStartDate) / msPerDay)
+ const cWeeksDiff = Math.floor(cDaysDiff / 7)
+ if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++
cursor.setDate(cursor.getDate() + 1)
}
- // If target itself is the base start and it's selected, occIdx == 0 => base event (skip)
- if (cursor.getTime() === targetDate.getTime()) {
- // We haven't advanced past target, so if its weekday is selected and this is the first occurrence, skip
- if (occIdx === 0) continue
- } else {
- // We advanced past target; if target weekday is selected this is the next occurrence index already counted, so decrement for proper index
- // Ensure occIdx corresponds to this occurrence (already counted earlier occurrences only)
+ if (targetDate.getTime() === baseStartDate.getTime()) {
+ // skip base occurrence
+ continue
}
if (occIdx >= maxOccurrences) continue
const occStart = new Date(targetDate)
- const occEnd = new Date(occStart); occEnd.setDate(occStart.getDate() + spanDays)
+ const occEnd = new Date(occStart)
+ occEnd.setDate(occStart.getDate() + spanDays)
const occStartStr = toLocalString(occStart)
const occEndStr = toLocalString(occEnd)
occurrences.push({
@@ -93,71 +100,51 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
startDate: occStartStr,
endDate: occEndStr,
isRepeatOccurrence: true,
- repeatIndex: occIdx
+ repeatIndex: occIdx,
})
continue
} else {
- // Handle other repeat types (biweekly, monthly, yearly)
+ // Handle other repeat types (months)
let intervalsPassed = 0
const timeDiff = targetDate - baseStartDate
-
- switch (baseEvent.repeat) {
- case 'biweekly':
- intervalsPassed = Math.floor(timeDiff / (14 * 24 * 60 * 60 * 1000))
- break
- case 'monthly':
- intervalsPassed = Math.floor((targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 +
- (targetDate.getMonth() - baseStartDate.getMonth()))
- break
- case 'yearly':
- intervalsPassed = targetDate.getFullYear() - baseStartDate.getFullYear()
- break
+ if (baseEvent.repeat === 'months') {
+ intervalsPassed =
+ (targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 +
+ (targetDate.getMonth() - baseStartDate.getMonth())
+ } else {
+ continue
}
-
+ const interval = baseEvent.repeatInterval || 1
+ if (intervalsPassed < 0 || intervalsPassed % interval !== 0) continue
+
// Check a few occurrences around the target date
- for (let i = Math.max(0, intervalsPassed - 2); i <= intervalsPassed + 2; i++) {
- const maxOccurrences = baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
- if (i >= maxOccurrences) break
-
- const currentStart = new Date(baseStartDate)
-
- switch (baseEvent.repeat) {
- case 'biweekly':
- currentStart.setDate(baseStartDate.getDate() + i * 14)
- break
- case 'monthly':
- currentStart.setMonth(baseStartDate.getMonth() + i)
- break
- case 'yearly':
- currentStart.setFullYear(baseStartDate.getFullYear() + i)
- break
- }
-
- const currentEnd = new Date(currentStart)
- currentEnd.setDate(currentStart.getDate() + spanDays)
-
- // Check if this occurrence intersects with the target date
- const currentStartStr = toLocalString(currentStart)
- const currentEndStr = toLocalString(currentEnd)
-
- if (currentStartStr <= targetDateStr && targetDateStr <= currentEndStr) {
- // Skip the original occurrence (i === 0) since it's already in the base events
- if (i === 0) continue
-
- occurrences.push({
- ...baseEvent,
- id: `${baseEvent.id}_repeat_${i}`,
- startDate: currentStartStr,
- endDate: currentEndStr,
- isRepeatOccurrence: true,
- repeatIndex: i
- })
- }
+ const maxOccurrences =
+ baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
+ if (maxOccurrences === 0) continue
+ const i = intervalsPassed
+ if (i >= maxOccurrences) continue
+ // Skip base occurrence
+ if (i === 0) continue
+ const currentStart = new Date(baseStartDate)
+ currentStart.setMonth(baseStartDate.getMonth() + i)
+ const currentEnd = new Date(currentStart)
+ currentEnd.setDate(currentStart.getDate() + spanDays)
+ const currentStartStr = toLocalString(currentStart)
+ const currentEndStr = toLocalString(currentEnd)
+ if (currentStartStr <= targetDateStr && targetDateStr <= currentEndStr) {
+ occurrences.push({
+ ...baseEvent,
+ id: `${baseEvent.id}_repeat_${i}`,
+ startDate: currentStartStr,
+ endDate: currentEndStr,
+ isRepeatOccurrence: true,
+ repeatIndex: i,
+ })
}
}
}
}
-
+
return occurrences
}
@@ -180,45 +167,51 @@ function handleEventClick(span) {
function handleEventPointerDown(span, event) {
// Don't start drag if clicking on resize handle
if (event.target.classList.contains('resize-handle')) return
-
+
event.stopPropagation()
// Do not preventDefault here to allow click unless drag threshold is passed
-
+
// Get the date under the pointer
const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget)
const anchorDate = hit ? hit.date : span.startDate
-
- startLocalDrag({
- id: span.id,
- mode: 'move',
- pointerStartX: event.clientX,
- pointerStartY: event.clientY,
- anchorDate,
- startDate: span.startDate,
- endDate: span.endDate
- }, event)
+
+ startLocalDrag(
+ {
+ id: span.id,
+ mode: 'move',
+ pointerStartX: event.clientX,
+ pointerStartY: event.clientY,
+ anchorDate,
+ startDate: span.startDate,
+ endDate: span.endDate,
+ },
+ event,
+ )
}
// Handle resize handle pointer down
function handleResizePointerDown(span, mode, event) {
event.stopPropagation()
// Start drag from the current edge; anchorDate not needed for resize
- startLocalDrag({
- id: span.id,
- mode,
- pointerStartX: event.clientX,
- pointerStartY: event.clientY,
- anchorDate: null,
- startDate: span.startDate,
- endDate: span.endDate
- }, event)
+ startLocalDrag(
+ {
+ id: span.id,
+ mode,
+ pointerStartX: event.clientX,
+ pointerStartY: event.clientY,
+ anchorDate: null,
+ startDate: span.startDate,
+ endDate: span.endDate,
+ },
+ event,
+ )
}
// Get date under pointer coordinates
function getDateUnderPointer(clientX, clientY, targetEl) {
// First try to find a day cell directly under the pointer
let element = document.elementFromPoint(clientX, clientY)
-
+
// If we hit an event element, temporarily hide it and try again
const hiddenElements = []
while (element && element.classList.contains('event-span')) {
@@ -226,17 +219,17 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
hiddenElements.push(element)
element = document.elementFromPoint(clientX, clientY)
}
-
+
// Restore pointer events for hidden elements
- hiddenElements.forEach(el => el.style.pointerEvents = 'auto')
-
+ hiddenElements.forEach((el) => (el.style.pointerEvents = 'auto'))
+
if (element) {
// Look for a day cell with data-date attribute
const dayElement = element.closest('[data-date]')
if (dayElement && dayElement.dataset.date) {
return { date: dayElement.dataset.date }
}
-
+
// Also check if we're over a week element and can calculate position
const weekElement = element.closest('.week-row')
if (weekElement) {
@@ -244,7 +237,7 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
const relativeX = clientX - rect.left
const dayWidth = rect.width / 7
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
-
+
const daysGrid = weekElement.querySelector('.days-grid')
if (daysGrid && daysGrid.children[dayIndex]) {
const dayEl = daysGrid.children[dayIndex]
@@ -262,7 +255,7 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
const allWeekElements = document.querySelectorAll('.week-row')
let bestWeek = null
let bestDistance = Infinity
-
+
for (const week of allWeekElements) {
const rect = week.getBoundingClientRect()
if (clientY >= rect.top && clientY <= rect.bottom) {
@@ -273,13 +266,13 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
}
}
}
-
+
if (bestWeek) {
const rect = bestWeek.getBoundingClientRect()
const relativeX = clientX - rect.left
const dayWidth = rect.width / 7
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
-
+
const daysGrid = bestWeek.querySelector('.days-grid')
if (daysGrid && daysGrid.children[dayIndex]) {
const dayEl = daysGrid.children[dayIndex]
@@ -289,16 +282,16 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
}
return null
}
-
+
const rect = weekElement.getBoundingClientRect()
const relativeX = clientX - rect.left
const dayWidth = rect.width / 7
const dayIndex = Math.floor(Math.max(0, Math.min(6, relativeX / dayWidth)))
-
+
if (props.week.days[dayIndex]) {
return { date: props.week.days[dayIndex].date }
}
-
+
return null
}
@@ -316,21 +309,21 @@ function startLocalDrag(init, evt) {
...init,
anchorOffset,
originSpanDays: spanDays,
- eventMoved: false
+ eventMoved: false,
}
// Capture pointer events globally
if (evt.currentTarget && evt.pointerId !== undefined) {
- try {
+ try {
evt.currentTarget.setPointerCapture(evt.pointerId)
} catch (e) {
console.warn('Could not set pointer capture:', e)
}
}
-
+
// Prevent default to avoid text selection and other interference
evt.preventDefault()
-
+
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
window.addEventListener('pointercancel', onDragPointerUp, { passive: false })
@@ -347,10 +340,10 @@ function onDragPointerMove(e) {
const hitEl = document.elementFromPoint(e.clientX, e.clientY)
const hit = getDateUnderPointer(e.clientX, e.clientY, hitEl)
-
+
// If we can't find a date, don't update the range but keep the drag active
if (!hit || !hit.date) return
-
+
const [ns, ne] = computeTentativeRangeFromPointer(st, hit.date)
if (!ns || !ne) return
applyRangeDuringDrag(st, ns, ne)
@@ -359,26 +352,28 @@ function onDragPointerMove(e) {
function onDragPointerUp(e) {
const st = dragState.value
if (!st) return
-
+
// Release pointer capture if it was set
if (e.target && e.pointerId !== undefined) {
- try {
- e.target.releasePointerCapture(e.pointerId)
+ try {
+ e.target.releasePointerCapture(e.pointerId)
} catch (err) {
// Ignore errors - capture might not have been set
}
}
-
+
const moved = !!st.eventMoved
dragState.value = null
-
+
window.removeEventListener('pointermove', onDragPointerMove)
window.removeEventListener('pointerup', onDragPointerUp)
window.removeEventListener('pointercancel', onDragPointerUp)
-
+
if (moved) {
justDragged.value = true
- setTimeout(() => { justDragged.value = false }, 120)
+ setTimeout(() => {
+ justDragged.value = false
+ }, 120)
}
}
@@ -407,20 +402,40 @@ function normalizeDateOrder(aStr, bStr) {
}
function applyRangeDuringDrag(st, startDate, endDate) {
- const ev = store.getEventById(st.id)
+ let ev = store.getEventById(st.id)
+ let isRepeatOccurrence = false
+ let baseId = st.id
+ let repeatIndex = 0
+ let grabbedWeekday = null
+
+ // If not found (repeat occurrences aren't stored) parse synthetic id
+ if (!ev && typeof st.id === 'string' && st.id.includes('_repeat_')) {
+ const [bid, suffix] = st.id.split('_repeat_')
+ baseId = bid
+ ev = store.getEventById(baseId)
+ if (ev) {
+ const parts = suffix.split('_')
+ repeatIndex = parseInt(parts[0], 10) || 0
+ grabbedWeekday = parts.length > 1 ? parseInt(parts[1], 10) : null
+ isRepeatOccurrence = repeatIndex >= 0
+ }
+ }
+
if (!ev) return
- if (ev.isRepeatOccurrence) {
- const idParts = String(st.id).split('_repeat_')
- const baseId = idParts[0]
- const repeatParts = idParts[1].split('_')
- const repeatIndex = parseInt(repeatParts[0], 10) || 0
- const grabbedWeekday = repeatParts.length > 1 ? parseInt(repeatParts[1], 10) : null
-
+
+ const mode = st.mode === 'resize-left' || st.mode === 'resize-right' ? st.mode : 'move'
+ if (isRepeatOccurrence) {
if (repeatIndex === 0) {
- store.setEventRange(baseId, startDate, endDate)
+ store.setEventRange(baseId, startDate, endDate, { mode })
} else {
if (!st.splitNewBaseId) {
- const newId = store.splitRepeatSeries(baseId, repeatIndex, startDate, endDate, grabbedWeekday)
+ const newId = store.splitRepeatSeries(
+ baseId,
+ repeatIndex,
+ startDate,
+ endDate,
+ grabbedWeekday,
+ )
if (newId) {
st.splitNewBaseId = newId
st.id = newId
@@ -428,11 +443,11 @@ function applyRangeDuringDrag(st, startDate, endDate) {
st.endDate = endDate
}
} else {
- store.setEventRange(st.splitNewBaseId, startDate, endDate)
+ store.setEventRange(st.splitNewBaseId, startDate, endDate, { mode })
}
}
} else {
- store.setEventRange(st.id, startDate, endDate)
+ store.setEventRange(st.id, startDate, endDate, { mode })
}
}
@@ -440,31 +455,31 @@ function applyRangeDuringDrag(st, startDate, endDate) {
const eventSpans = computed(() => {
const spans = []
const weekEvents = new Map()
-
+
// Collect events from all days in this week, including repeat occurrences
props.week.days.forEach((day, dayIndex) => {
// Get base events for this day
- day.events.forEach(event => {
+ day.events.forEach((event) => {
if (!weekEvents.has(event.id)) {
weekEvents.set(event.id, {
...event,
startIdx: dayIndex,
- endIdx: dayIndex
+ endIdx: dayIndex,
})
} else {
const existing = weekEvents.get(event.id)
existing.endIdx = dayIndex
}
})
-
+
// Generate repeat occurrences for this day
const repeatOccurrences = generateRepeatOccurrencesForDate(day.date)
- repeatOccurrences.forEach(event => {
+ repeatOccurrences.forEach((event) => {
if (!weekEvents.has(event.id)) {
weekEvents.set(event.id, {
...event,
startIdx: dayIndex,
- endIdx: dayIndex
+ endIdx: dayIndex,
})
} else {
const existing = weekEvents.get(event.id)
@@ -472,7 +487,7 @@ const eventSpans = computed(() => {
}
})
})
-
+
// Convert to array and sort
const eventArray = Array.from(weekEvents.values())
eventArray.sort((a, b) => {
@@ -480,22 +495,22 @@ const eventSpans = computed(() => {
const spanA = a.endIdx - a.startIdx
const spanB = b.endIdx - b.startIdx
if (spanA !== spanB) return spanB - spanA
-
+
// Then by start position
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
-
+
// Then by start time if available
const timeA = a.startTime ? timeToMinutes(a.startTime) : 0
const timeB = b.startTime ? timeToMinutes(b.startTime) : 0
if (timeA !== timeB) return timeA - timeB
-
+
// Fallback to ID
return String(a.id).localeCompare(String(b.id))
})
-
+
// Assign rows to avoid overlaps
const rowsLastEnd = []
- eventArray.forEach(event => {
+ eventArray.forEach((event) => {
let placedRow = 0
while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) {
placedRow++
@@ -506,7 +521,7 @@ const eventSpans = computed(() => {
rowsLastEnd[placedRow] = event.endIdx
event.row = placedRow + 1
})
-
+
return eventArray
})
diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js
index fb6a0e5..c92e893 100644
--- a/src/stores/CalendarStore.js
+++ b/src/stores/CalendarStore.js
@@ -13,14 +13,14 @@ export const useCalendarStore = defineStore('calendar', {
config: {
select_days: 1000,
min_year: MIN_YEAR,
- max_year: MAX_YEAR
- }
+ max_year: MAX_YEAR,
+ },
}),
getters: {
// Basic configuration getters
minYear: () => MIN_YEAR,
- maxYear: () => MAX_YEAR
+ maxYear: () => MAX_YEAR,
},
actions: {
@@ -49,13 +49,20 @@ export const useCalendarStore = defineStore('calendar', {
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,
- repeat: eventData.repeat || 'none',
+ colorId:
+ eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
+ startTime: singleDay ? eventData.startTime || '09:00' : null,
+ durationMinutes: singleDay ? eventData.durationMinutes || 60 : null,
+ repeat:
+ (eventData.repeat === 'weekly'
+ ? 'weeks'
+ : eventData.repeat === 'monthly'
+ ? 'months'
+ : eventData.repeat) || 'none',
+ repeatInterval: eventData.repeatInterval || 1,
repeatCount: eventData.repeatCount || 'unlimited',
repeatWeekdays: eventData.repeatWeekdays,
- isRepeating: (eventData.repeat && eventData.repeat !== 'none')
+ isRepeating: eventData.repeat && eventData.repeat !== 'none',
}
const startDate = new Date(fromLocalString(event.startDate))
@@ -68,12 +75,13 @@ export const useCalendarStore = defineStore('calendar', {
}
this.events.get(dateStr).push({ ...event, isSpanning: startDate < endDate })
}
+ // No physical expansion; repeats are virtual
return event.id
},
getEventById(id) {
for (const [, list] of this.events) {
- const found = list.find(e => e.id === id)
+ const found = list.find((e) => e.id === id)
if (found) return found
}
return null
@@ -110,7 +118,7 @@ export const useCalendarStore = defineStore('calendar', {
deleteEvent(eventId) {
const datesToCleanup = []
for (const [dateStr, eventList] of this.events) {
- const eventIndex = eventList.findIndex(event => event.id === eventId)
+ const eventIndex = eventList.findIndex((event) => event.id === eventId)
if (eventIndex !== -1) {
eventList.splice(eventIndex, 1)
if (eventList.length === 0) {
@@ -118,17 +126,21 @@ export const useCalendarStore = defineStore('calendar', {
}
}
}
- datesToCleanup.forEach(dateStr => this.events.delete(dateStr))
+ datesToCleanup.forEach((dateStr) => this.events.delete(dateStr))
},
deleteSingleOccurrence(ctx) {
- const { baseId, occurrenceIndex, weekday } = ctx
+ const { baseId, occurrenceIndex } = ctx
const base = this.getEventById(baseId)
if (!base || base.repeat !== 'weekly') return
+ if (!base || base.repeat !== 'weeks') return
// Strategy: clone pattern into two: reduce base repeatCount to exclude this occurrence; create new series for remainder excluding this one
// Simpler: convert base to explicit exception by creating a one-off non-repeating event? For now: create exception by inserting a blocking dummy? Instead just split by shifting repeatCount logic: decrement repeatCount and create a new series starting after this single occurrence.
// Implementation (approx): terminate at occurrenceIndex; create a new series starting next occurrence with remaining occurrences.
- const remaining = base.repeatCount === 'unlimited' ? 'unlimited' : String(Math.max(0, parseInt(base.repeatCount,10) - (occurrenceIndex+1)))
+ const remaining =
+ base.repeatCount === 'unlimited'
+ ? 'unlimited'
+ : String(Math.max(0, parseInt(base.repeatCount, 10) - (occurrenceIndex + 1)))
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
if (remaining === '0') return
// Find date of next occurrence
@@ -145,9 +157,9 @@ export const useCalendarStore = defineStore('calendar', {
startDate: nextStartStr,
endDate: nextStartStr,
colorId: base.colorId,
- repeat: 'weekly',
+ repeat: 'weeks',
repeatCount: remaining,
- repeatWeekdays: base.repeatWeekdays
+ repeatWeekdays: base.repeatWeekdays,
})
},
@@ -164,21 +176,19 @@ export const useCalendarStore = defineStore('calendar', {
const spanDays = Math.round((oldEnd - oldStart) / (24 * 60 * 60 * 1000))
let newStart = null
- if (base.repeat === 'weekly' && base.repeatWeekdays) {
+ if (base.repeat === 'weeks' && base.repeatWeekdays) {
const probe = new Date(oldStart)
- for (let i = 0; i < 14; i++) { // search ahead up to 2 weeks
+ for (let i = 0; i < 14; i++) {
+ // search ahead up to 2 weeks
probe.setDate(probe.getDate() + 1)
- if (base.repeatWeekdays[probe.getDay()]) { newStart = new Date(probe); break }
+ if (base.repeatWeekdays[probe.getDay()]) {
+ newStart = new Date(probe)
+ break
+ }
}
- } else if (base.repeat === 'biweekly') {
- newStart = new Date(oldStart)
- newStart.setDate(newStart.getDate() + 14)
- } else if (base.repeat === 'monthly') {
+ } else if (base.repeat === 'months') {
newStart = new Date(oldStart)
newStart.setMonth(newStart.getMonth() + 1)
- } else if (base.repeat === 'yearly') {
- newStart = new Date(oldStart)
- newStart.setFullYear(newStart.getFullYear() + 1)
} else {
// Unknown pattern: delete entire series
this.deleteEvent(baseId)
@@ -207,63 +217,7 @@ export const useCalendarStore = defineStore('calendar', {
newEnd.setDate(newEnd.getDate() + spanDays)
base.startDate = toLocalString(newStart)
base.endDate = toLocalString(newEnd)
- // Reindex across map
- this._removeEventFromAllDatesById(baseId)
- this._addEventToDateRangeWithId(baseId, base, base.startDate, base.endDate)
- },
-
- updateEvent(eventId, updates) {
- // Remove event from current dates
- for (const [dateStr, eventList] of this.events) {
- const index = eventList.findIndex(e => e.id === eventId)
- if (index !== -1) {
- const event = eventList[index]
- eventList.splice(index, 1)
- if (eventList.length === 0) {
- this.events.delete(dateStr)
- }
-
- // Create updated event and add to new date range
- const updatedEvent = { ...event, ...updates }
- this._addEventToDateRange(updatedEvent)
- return
- }
- }
- },
-
- // Minimal public API for component-driven drag
- setEventRange(eventId, startDate, endDate) {
- const snapshot = this._snapshotBaseEvent(eventId)
- if (!snapshot) return
-
- // Calculate rotated weekdays for weekly repeats
- if (snapshot.repeat === 'weekly' && snapshot.repeatWeekdays) {
- const originalStartDate = new Date(fromLocalString(snapshot.startDate))
- const newStartDate = new Date(fromLocalString(startDate))
- const dayShift = newStartDate.getDay() - originalStartDate.getDay()
-
- if (dayShift !== 0) {
- const rotatedWeekdays = [false, false, false, false, false, false, false]
-
- for (let i = 0; i < 7; i++) {
- if (snapshot.repeatWeekdays[i]) {
- let newDay = (i + dayShift) % 7
- if (newDay < 0) newDay += 7
- rotatedWeekdays[newDay] = true
- }
- }
- snapshot.repeatWeekdays = rotatedWeekdays
- }
- }
-
- this._removeEventFromAllDatesById(eventId)
- this._addEventToDateRangeWithId(eventId, snapshot, startDate, endDate)
- },
-
- splitRepeatSeries(baseId, index, startDate, endDate, grabbedWeekday = null) {
- const base = this.getEventById(baseId)
- if (!base) return null
-
+ // old occurrence expansion removed (series handled differently now)
const originalRepeatCount = base.repeatCount
// Always cap original series at the split occurrence index (occurrences 0..index-1)
// Keep its weekday pattern unchanged.
@@ -284,12 +238,12 @@ export const useCalendarStore = defineStore('calendar', {
// Handle weekdays for weekly repeats
let newRepeatWeekdays = base.repeatWeekdays
- if (base.repeat === 'weekly' && base.repeatWeekdays) {
+ if (base.repeat === 'weeks' && base.repeatWeekdays) {
const newStartDate = new Date(fromLocalString(startDate))
let dayShift = 0
if (grabbedWeekday != null) {
// Rotate so that the grabbed weekday maps to the new start weekday
- dayShift = newStartDate.getDay() - grabbedWeekday
+ dayShift = newStartDate.getDay() - grabbedWeekday
} else {
// Fallback: rotate by difference between new and original start weekday
const originalStartDate = new Date(fromLocalString(base.startDate))
@@ -315,16 +269,15 @@ export const useCalendarStore = defineStore('calendar', {
colorId: base.colorId,
repeat: base.repeat,
repeatCount: newRepeatCount,
- repeatWeekdays: newRepeatWeekdays
+ repeatWeekdays: newRepeatWeekdays,
})
return newId
},
-
_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)
+ const e = eventList.find((x) => x.id === eventId)
if (e) return { ...e }
}
return null
@@ -350,7 +303,7 @@ export const useCalendarStore = defineStore('calendar', {
id: eventId,
startDate,
endDate,
- isSpanning: multi
+ isSpanning: multi,
}
// Normalize single-day time fields
if (!multi) {
@@ -369,6 +322,98 @@ export const useCalendarStore = defineStore('calendar', {
}
},
+ // expandRepeats removed: no physical occurrence expansion
+
+ // Adjust start/end range of a base event (non-generated) and reindex occurrences
+ setEventRange(eventId, newStartStr, newEndStr, { mode = 'auto' } = {}) {
+ const snapshot = this._findEventInAnyList(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
+ }
+ }
+ // Reindex
+ this._removeEventFromAllDatesById(eventId)
+ this._addEventToDateRangeWithId(eventId, snapshot, snapshot.startDate, snapshot.endDate)
+ // no expansion
+ },
+
+ // Split a repeating series at a given occurrence index; returns new series id
+ splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) {
+ const base = this._findEventInAnyList(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
+ },
+
_reindexBaseEvent(eventId, snapshot, startDate, endDate) {
if (!snapshot) return
this._removeEventFromAllDatesById(eventId)
@@ -393,7 +438,7 @@ export const useCalendarStore = defineStore('calendar', {
_findEventInAnyList(eventId) {
for (const [, eventList] of this.events) {
- const found = eventList.find(e => e.id === eventId)
+ const found = eventList.find((e) => e.id === eventId)
if (found) return found
}
return null
@@ -403,7 +448,7 @@ export const useCalendarStore = defineStore('calendar', {
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)) {
@@ -414,62 +459,6 @@ export const useCalendarStore = defineStore('calendar', {
}
},
- getEventById(id) {
- // Check for base events first
- for (const [, list] of this.events) {
- const found = list.find(e => e.id === id)
- if (found) return found
- }
-
- // Check if it's a repeat occurrence ID
- if (typeof id === 'string' && id.includes('_repeat_')) {
- const parts = id.split('_repeat_')
- const baseId = parts[0]
- const repeatIndex = parseInt(parts[1], 10)
-
- if (isNaN(repeatIndex)) return null
-
- const baseEvent = this.getEventById(baseId)
- if (baseEvent && baseEvent.isRepeating) {
- // Generate the specific occurrence
- const baseStartDate = new Date(fromLocalString(baseEvent.startDate))
- const baseEndDate = new Date(fromLocalString(baseEvent.endDate))
- const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000))
-
- const currentStart = new Date(baseStartDate)
- switch (baseEvent.repeat) {
- case 'daily':
- currentStart.setDate(baseStartDate.getDate() + repeatIndex)
- break
- case 'weekly':
- currentStart.setDate(baseStartDate.getDate() + repeatIndex * 7)
- break
- case 'biweekly':
- currentStart.setDate(baseStartDate.getDate() + repeatIndex * 14)
- break
- case 'monthly':
- currentStart.setMonth(baseStartDate.getMonth() + repeatIndex)
- break
- case 'yearly':
- currentStart.setFullYear(baseStartDate.getFullYear() + repeatIndex)
- break
- }
-
- const currentEnd = new Date(currentStart)
- currentEnd.setDate(currentStart.getDate() + spanDays)
-
- return {
- ...baseEvent,
- id: id,
- startDate: toLocalString(currentStart),
- endDate: toLocalString(currentEnd),
- isRepeatOccurrence: true,
- repeatIndex: repeatIndex
- }
- }
- }
-
- return null
- }
- }
+ // NOTE: legacy dynamic getEventById for synthetic occurrences removed.
+ },
})