Major new version #2
@ -142,9 +142,59 @@ function createWeek(virtualWeek) {
|
|||||||
let monthToLabel = null
|
let monthToLabel = null
|
||||||
let labelYear = null
|
let labelYear = null
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const dateStr = toLocalString(cur)
|
const dateStr = toLocalString(cur)
|
||||||
const eventsForDay = calendarStore.events.get(dateStr) || []
|
const storedEvents = calendarStore.events.get(dateStr) || []
|
||||||
|
// 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
|
||||||
|
let recurrenceIndex = 0
|
||||||
|
try {
|
||||||
|
if (base.repeat === 'weeks') {
|
||||||
|
const pattern = base.repeatWeekdays || []
|
||||||
|
const baseDate = new Date(base.startDate + '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++
|
||||||
|
}
|
||||||
|
if (cur.toDateString() === target.toDateString()) recurrenceIndex = matched
|
||||||
|
} else if (base.repeat === 'months') {
|
||||||
|
const baseDate = new Date(base.startDate + 'T00:00:00')
|
||||||
|
const target = new Date(dateStr + 'T00:00:00')
|
||||||
|
const diffMonths =
|
||||||
|
(target.getFullYear() - baseDate.getFullYear()) * 12 +
|
||||||
|
(target.getMonth() - baseDate.getMonth())
|
||||||
|
recurrenceIndex = diffMonths // matches existing monthly logic semantics
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
dayEvents.push({
|
||||||
|
...base,
|
||||||
|
id: base.id + '_v_' + dateStr,
|
||||||
|
startDate: dateStr,
|
||||||
|
endDate: dateStr,
|
||||||
|
_recurrenceIndex: recurrenceIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
const dow = cur.getDay()
|
const dow = cur.getDay()
|
||||||
const isFirst = cur.getDate() === 1
|
const isFirst = cur.getDate() === 1
|
||||||
|
|
||||||
@ -177,7 +227,7 @@ function createWeek(virtualWeek) {
|
|||||||
selection.value.dayCount > 0 &&
|
selection.value.dayCount > 0 &&
|
||||||
dateStr >= selection.value.startDate &&
|
dateStr >= selection.value.startDate &&
|
||||||
dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1),
|
dateStr <= addDaysStr(selection.value.startDate, selection.value.dayCount - 1),
|
||||||
events: eventsForDay,
|
events: dayEvents,
|
||||||
})
|
})
|
||||||
cur.setDate(cur.getDate() + 1)
|
cur.setDate(cur.getDate() + 1)
|
||||||
}
|
}
|
||||||
|
@ -155,6 +155,8 @@ function openEditDialog(eventInstanceId) {
|
|||||||
let occurrenceIndex = 0
|
let occurrenceIndex = 0
|
||||||
let weekday = null
|
let weekday = null
|
||||||
let occurrenceDate = null
|
let occurrenceDate = null
|
||||||
|
|
||||||
|
// Support legacy synthetic id pattern: baseId_repeat_<index>[_<weekday>]
|
||||||
if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) {
|
if (typeof eventInstanceId === 'string' && eventInstanceId.includes('_repeat_')) {
|
||||||
const [bid, suffix] = eventInstanceId.split('_repeat_')
|
const [bid, suffix] = eventInstanceId.split('_repeat_')
|
||||||
baseId = bid
|
baseId = bid
|
||||||
@ -162,10 +164,57 @@ function openEditDialog(eventInstanceId) {
|
|||||||
occurrenceIndex = parseInt(parts[0], 10) || 0
|
occurrenceIndex = parseInt(parts[0], 10) || 0
|
||||||
if (parts.length > 1) weekday = parseInt(parts[1], 10)
|
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)
|
const event = calendarStore.getEventById(baseId)
|
||||||
if (!event) return
|
if (!event) return
|
||||||
// Derive occurrence date for repeat occurrences (occurrenceIndex > 0 means not the base)
|
// Derive occurrence date for repeat occurrences if not already determined above
|
||||||
if (
|
if (
|
||||||
|
!occurrenceDate &&
|
||||||
event.isRepeating &&
|
event.isRepeating &&
|
||||||
((event.repeat === 'weeks' && occurrenceIndex >= 0) ||
|
((event.repeat === 'weeks' && occurrenceIndex >= 0) ||
|
||||||
(event.repeat === 'months' && occurrenceIndex > 0))
|
(event.repeat === 'months' && occurrenceIndex > 0))
|
||||||
@ -173,8 +222,6 @@ function openEditDialog(eventInstanceId) {
|
|||||||
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
||||||
const repeatWeekdaysLocal = event.repeatWeekdays || []
|
const repeatWeekdaysLocal = event.repeatWeekdays || []
|
||||||
const baseDate = new Date(event.startDate + 'T00:00:00')
|
const baseDate = new Date(event.startDate + 'T00:00:00')
|
||||||
// occurrenceIndex counts prior occurrences AFTER base;
|
|
||||||
// For occurrenceIndex = 0 we want first matching day after base.
|
|
||||||
let cur = new Date(baseDate)
|
let cur = new Date(baseDate)
|
||||||
let matched = -1
|
let matched = -1
|
||||||
let safety = 0
|
let safety = 0
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
:key="span.id"
|
:key="span.id"
|
||||||
class="event-span"
|
class="event-span"
|
||||||
:class="[`event-color-${span.colorId}`]"
|
:class="[`event-color-${span.colorId}`]"
|
||||||
|
:data-recurrence="span._recurrenceIndex != null ? span._recurrenceIndex : undefined"
|
||||||
:style="{
|
:style="{
|
||||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
||||||
gridRow: `${span.row}`,
|
gridRow: `${span.row}`,
|
||||||
@ -24,54 +25,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useCalendarStore } from '@/stores/CalendarStore'
|
import { useCalendarStore } from '@/stores/CalendarStore'
|
||||||
import { toLocalString, fromLocalString, daysInclusive, addDaysStr } from '@/utils/date'
|
import { daysInclusive, addDaysStr } from '@/utils/date'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
week: {
|
week: { type: Object, required: true },
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['event-click'])
|
const emit = defineEmits(['event-click'])
|
||||||
const store = useCalendarStore()
|
const store = useCalendarStore()
|
||||||
|
|
||||||
// Local drag state
|
// Drag state
|
||||||
const dragState = ref(null)
|
const dragState = ref(null)
|
||||||
const justDragged = ref(false)
|
const justDragged = ref(false)
|
||||||
|
|
||||||
// (legacy helpers removed)
|
// Consolidate already-provided day.events into contiguous spans (no recurrence generation)
|
||||||
|
const eventSpans = computed(() => {
|
||||||
|
const weekEvents = new Map()
|
||||||
|
props.week.days.forEach((day, dayIndex) => {
|
||||||
|
day.events.forEach((ev) => {
|
||||||
|
const key = ev.id
|
||||||
|
if (!weekEvents.has(key)) {
|
||||||
|
weekEvents.set(key, { ...ev, startIdx: dayIndex, endIdx: dayIndex })
|
||||||
|
} else {
|
||||||
|
const ref = weekEvents.get(key)
|
||||||
|
ref.endIdx = Math.max(ref.endIdx, dayIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const arr = Array.from(weekEvents.values())
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const spanA = a.endIdx - a.startIdx
|
||||||
|
const spanB = b.endIdx - b.startIdx
|
||||||
|
if (spanA !== spanB) return spanB - spanA
|
||||||
|
if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx
|
||||||
|
return String(a.id).localeCompare(String(b.id))
|
||||||
|
})
|
||||||
|
// Assign non-overlapping rows
|
||||||
|
const rowsLastEnd = []
|
||||||
|
arr.forEach((ev) => {
|
||||||
|
let row = 0
|
||||||
|
while (row < rowsLastEnd.length && !(ev.startIdx > rowsLastEnd[row])) row++
|
||||||
|
if (row === rowsLastEnd.length) rowsLastEnd.push(-1)
|
||||||
|
rowsLastEnd[row] = ev.endIdx
|
||||||
|
ev.row = row + 1
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Handle event click
|
|
||||||
function handleEventClick(span) {
|
function handleEventClick(span) {
|
||||||
if (justDragged.value) return
|
if (justDragged.value) return
|
||||||
// Emit the actual span id (may include repeat suffix) so edit dialog knows occurrence context
|
|
||||||
emit('event-click', span.id)
|
emit('event-click', span.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle event pointer down for dragging
|
|
||||||
function handleEventPointerDown(span, event) {
|
function handleEventPointerDown(span, event) {
|
||||||
// Don't start drag if clicking on resize handle
|
|
||||||
if (event.target.classList.contains('resize-handle')) return
|
if (event.target.classList.contains('resize-handle')) return
|
||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Do not preventDefault here to allow click unless drag threshold is passed
|
const baseId = extractBaseId(span.id)
|
||||||
|
const isVirtual = baseId !== span.id
|
||||||
// Get the date under the pointer
|
|
||||||
const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget)
|
|
||||||
const anchorDate = hit ? hit.date : span.startDate
|
|
||||||
|
|
||||||
startLocalDrag(
|
startLocalDrag(
|
||||||
{
|
{
|
||||||
id: span.id,
|
id: baseId,
|
||||||
|
originalId: span.id,
|
||||||
|
isVirtual,
|
||||||
mode: 'move',
|
mode: 'move',
|
||||||
pointerStartX: event.clientX,
|
pointerStartX: event.clientX,
|
||||||
pointerStartY: event.clientY,
|
pointerStartY: event.clientY,
|
||||||
anchorDate,
|
anchorDate: span.startDate,
|
||||||
startDate: span.startDate,
|
startDate: span.startDate,
|
||||||
endDate: span.endDate,
|
endDate: span.endDate,
|
||||||
},
|
},
|
||||||
@ -79,13 +107,15 @@ function handleEventPointerDown(span, event) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle resize handle pointer down
|
|
||||||
function handleResizePointerDown(span, mode, event) {
|
function handleResizePointerDown(span, mode, event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Start drag from the current edge; anchorDate not needed for resize
|
const baseId = extractBaseId(span.id)
|
||||||
|
const isVirtual = baseId !== span.id
|
||||||
startLocalDrag(
|
startLocalDrag(
|
||||||
{
|
{
|
||||||
id: span.id,
|
id: baseId,
|
||||||
|
originalId: span.id,
|
||||||
|
isVirtual,
|
||||||
mode,
|
mode,
|
||||||
pointerStartX: event.clientX,
|
pointerStartX: event.clientX,
|
||||||
pointerStartY: event.clientY,
|
pointerStartY: event.clientY,
|
||||||
@ -97,94 +127,6 @@ function handleResizePointerDown(span, mode, 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')) {
|
|
||||||
element.style.pointerEvents = 'none'
|
|
||||||
hiddenElements.push(element)
|
|
||||||
element = document.elementFromPoint(clientX, clientY)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore pointer events for hidden elements
|
|
||||||
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) {
|
|
||||||
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)))
|
|
||||||
|
|
||||||
const daysGrid = weekElement.querySelector('.days-grid')
|
|
||||||
if (daysGrid && daysGrid.children[dayIndex]) {
|
|
||||||
const dayEl = daysGrid.children[dayIndex]
|
|
||||||
const date = dayEl?.dataset?.date
|
|
||||||
if (date) return { date }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try to find the week overlay and calculate position
|
|
||||||
const overlayEl = targetEl?.closest('.week-overlay')
|
|
||||||
const weekElement = overlayEl ? overlayEl.parentElement : null
|
|
||||||
if (!weekElement) {
|
|
||||||
// If we're outside this week, try to find any week element under the pointer
|
|
||||||
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) {
|
|
||||||
const distance = Math.abs(clientY - (rect.top + rect.height / 2))
|
|
||||||
if (distance < bestDistance) {
|
|
||||||
bestDistance = distance
|
|
||||||
bestWeek = week
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
const date = dayEl?.dataset?.date
|
|
||||||
if (date) return { date }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local drag handling
|
// Local drag handling
|
||||||
function startLocalDrag(init, evt) {
|
function startLocalDrag(init, evt) {
|
||||||
const spanDays = daysInclusive(init.startDate, init.endDate)
|
const spanDays = daysInclusive(init.startDate, init.endDate)
|
||||||
@ -292,249 +234,11 @@ function normalizeDateOrder(aStr, bStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyRangeDuringDrag(st, startDate, endDate) {
|
function applyRangeDuringDrag(st, startDate, endDate) {
|
||||||
let ev = store.getEventById(st.id)
|
// If dragging a virtual occurrence, map to base move without changing recurrence pattern mid-series.
|
||||||
let isRepeatOccurrence = false
|
// We disallow resizing individual virtual occurrences; treat as move of whole series anchor.
|
||||||
let baseId = st.id
|
if (st.isVirtual && st.mode !== 'move') return
|
||||||
let repeatIndex = 0
|
store.setEventRange(st.id, startDate, endDate, { mode: st.mode })
|
||||||
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
|
|
||||||
|
|
||||||
const mode = st.mode === 'resize-left' || st.mode === 'resize-right' ? st.mode : 'move'
|
|
||||||
if (isRepeatOccurrence) {
|
|
||||||
if (repeatIndex === 0) {
|
|
||||||
store.setEventRange(baseId, startDate, endDate, { mode })
|
|
||||||
} else {
|
|
||||||
if (!st.splitNewBaseId) {
|
|
||||||
const newId = store.splitRepeatSeries(
|
|
||||||
baseId,
|
|
||||||
repeatIndex,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
grabbedWeekday,
|
|
||||||
)
|
|
||||||
if (newId) {
|
|
||||||
st.splitNewBaseId = newId
|
|
||||||
st.id = newId
|
|
||||||
st.startDate = startDate
|
|
||||||
st.endDate = endDate
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
store.setEventRange(st.splitNewBaseId, startDate, endDate, { mode })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
store.setEventRange(st.id, startDate, endDate, { mode })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate event spans for this week
|
|
||||||
const eventSpans = computed(() => {
|
|
||||||
const spans = []
|
|
||||||
const weekEvents = new Map()
|
|
||||||
|
|
||||||
// Collect stored base events
|
|
||||||
props.week.days.forEach((day, dayIndex) => {
|
|
||||||
day.events.forEach((ev) => {
|
|
||||||
if (!weekEvents.has(ev.id)) {
|
|
||||||
weekEvents.set(ev.id, { ...ev, startIdx: dayIndex, endIdx: dayIndex })
|
|
||||||
} else weekEvents.get(ev.id).endIdx = dayIndex
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate virtual repeats numerically
|
|
||||||
const weekStart = fromLocalString(props.week.days[0].date)
|
|
||||||
const weekEnd = fromLocalString(props.week.days[6].date)
|
|
||||||
const weekStartTime = weekStart.getTime()
|
|
||||||
const weekEndTime = weekEnd.getTime()
|
|
||||||
const DAY_MS = 86400000
|
|
||||||
|
|
||||||
// All repeating base events
|
|
||||||
const baseEvents = []
|
|
||||||
const seen = new Set()
|
|
||||||
for (const [, list] of store.events) {
|
|
||||||
for (const ev of list) {
|
|
||||||
if (ev.isRepeating && !seen.has(ev.id)) {
|
|
||||||
seen.add(ev.id)
|
|
||||||
baseEvents.push(ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const base of baseEvents) {
|
|
||||||
if (!base.isRepeating || base.repeat === 'none') continue
|
|
||||||
const baseStart = fromLocalString(base.startDate)
|
|
||||||
const baseEnd = fromLocalString(base.endDate)
|
|
||||||
const spanDays = Math.floor((baseEnd - baseStart) / DAY_MS)
|
|
||||||
const maxOccurrences =
|
|
||||||
base.repeatCount === 'unlimited' ? Infinity : parseInt(base.repeatCount, 10)
|
|
||||||
if (maxOccurrences === 0) continue
|
|
||||||
|
|
||||||
if (base.repeat === 'weeks') {
|
|
||||||
const pattern = base.repeatWeekdays || []
|
|
||||||
const interval = base.repeatInterval || 1
|
|
||||||
if (!pattern.some(Boolean)) continue
|
|
||||||
// Align base block start to week (Sunday=0)
|
|
||||||
const baseBlockStart = new Date(baseStart)
|
|
||||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
|
||||||
// Search window
|
|
||||||
const searchStart = new Date(weekStart)
|
|
||||||
searchStart.setDate(searchStart.getDate() - 7) // one block back for early-week carries
|
|
||||||
const searchEnd = new Date(weekEnd)
|
|
||||||
searchEnd.setDate(searchEnd.getDate() + 7) // one block forward for late-week upcoming
|
|
||||||
const startBlocks = Math.floor((searchStart - baseBlockStart) / (7 * DAY_MS))
|
|
||||||
const endBlocks = Math.floor((searchEnd - baseBlockStart) / (7 * DAY_MS))
|
|
||||||
for (let b = Math.max(0, startBlocks); b <= endBlocks; b++) {
|
|
||||||
if (b % interval !== 0) continue
|
|
||||||
const blockStart = new Date(baseBlockStart)
|
|
||||||
blockStart.setDate(baseBlockStart.getDate() + b * 7)
|
|
||||||
for (let dow = 0; dow < 7; dow++) {
|
|
||||||
if (!pattern[dow]) continue
|
|
||||||
const cand = new Date(blockStart)
|
|
||||||
cand.setDate(blockStart.getDate() + dow)
|
|
||||||
if (cand < baseStart) continue
|
|
||||||
const isBase = cand.getTime() === baseStart.getTime()
|
|
||||||
const candStartTime = cand.getTime()
|
|
||||||
const candEndTime = candStartTime + spanDays * DAY_MS
|
|
||||||
const overlaps = candStartTime <= weekEndTime && candEndTime >= weekStartTime
|
|
||||||
if (!isBase && overlaps) {
|
|
||||||
let occIdx = 0
|
|
||||||
const cursor = new Date(baseStart)
|
|
||||||
while (cursor < cand && occIdx < maxOccurrences) {
|
|
||||||
const weeksFromBase = Math.floor((cursor - baseBlockStart) / (7 * DAY_MS))
|
|
||||||
if (
|
|
||||||
weeksFromBase % interval === 0 &&
|
|
||||||
pattern[cursor.getDay()] &&
|
|
||||||
cursor.getTime() !== baseStart.getTime()
|
|
||||||
) {
|
|
||||||
occIdx++
|
|
||||||
}
|
|
||||||
cursor.setDate(cursor.getDate() + 1)
|
|
||||||
}
|
|
||||||
if (occIdx >= maxOccurrences && isFinite(maxOccurrences)) break
|
|
||||||
const occStartStr = toLocalString(cand)
|
|
||||||
const occEnd = new Date(cand)
|
|
||||||
occEnd.setDate(occEnd.getDate() + spanDays)
|
|
||||||
const occEndStr = toLocalString(occEnd)
|
|
||||||
let startIdx = -1
|
|
||||||
let endIdx = -1
|
|
||||||
props.week.days.forEach((d, idx) => {
|
|
||||||
if (startIdx === -1 && d.date >= occStartStr && d.date <= occEndStr) startIdx = idx
|
|
||||||
if (d.date >= occStartStr && d.date <= occEndStr) endIdx = idx
|
|
||||||
})
|
|
||||||
const id = `${base.id}_repeat_${occIdx}_${cand.getDay()}`
|
|
||||||
if ((startIdx !== -1 || endIdx !== -1) && !weekEvents.has(id)) {
|
|
||||||
weekEvents.set(id, {
|
|
||||||
...base,
|
|
||||||
id,
|
|
||||||
startDate: occStartStr,
|
|
||||||
endDate: occEndStr,
|
|
||||||
isRepeatOccurrence: true,
|
|
||||||
repeatIndex: occIdx,
|
|
||||||
startIdx: startIdx === -1 ? 0 : startIdx,
|
|
||||||
endIdx: endIdx === -1 ? 6 : endIdx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (base.repeat === 'months') {
|
|
||||||
const interval = base.repeatInterval || 1
|
|
||||||
const baseDay = baseStart.getDate()
|
|
||||||
const startMonthIndex = baseStart.getFullYear() * 12 + baseStart.getMonth()
|
|
||||||
const endMonthIndex = weekEnd.getFullYear() * 12 + weekEnd.getMonth()
|
|
||||||
for (let mi = startMonthIndex; mi <= endMonthIndex + 1; mi++) {
|
|
||||||
// +1 to catch overlap spilling in
|
|
||||||
const diff = mi - startMonthIndex
|
|
||||||
if (diff === 0) continue // base occurrence already stored
|
|
||||||
if (diff % interval !== 0) continue
|
|
||||||
if (diff > maxOccurrences && isFinite(maxOccurrences)) break
|
|
||||||
const y = Math.floor(mi / 12)
|
|
||||||
const m = mi % 12
|
|
||||||
const daysInMonth = new Date(y, m + 1, 0).getDate()
|
|
||||||
const dom = Math.min(baseDay, daysInMonth)
|
|
||||||
const cand = new Date(y, m, dom)
|
|
||||||
if (cand < baseStart) continue
|
|
||||||
const candEnd = new Date(cand)
|
|
||||||
const spanDays = Math.floor((baseEnd - baseStart) / DAY_MS)
|
|
||||||
candEnd.setDate(candEnd.getDate() + spanDays)
|
|
||||||
const candStartStr = toLocalString(cand)
|
|
||||||
const candEndStr = toLocalString(candEnd)
|
|
||||||
const overlaps = cand.getTime() <= weekEndTime && candEnd.getTime() >= weekStartTime
|
|
||||||
if (!overlaps) continue
|
|
||||||
let startIdx = -1
|
|
||||||
let endIdx = -1
|
|
||||||
props.week.days.forEach((d, idx) => {
|
|
||||||
if (startIdx === -1 && d.date >= candStartStr && d.date <= candEndStr) startIdx = idx
|
|
||||||
if (d.date >= candStartStr && d.date <= candEndStr) endIdx = idx
|
|
||||||
})
|
|
||||||
if (startIdx === -1 && endIdx === -1) continue
|
|
||||||
const id = `${base.id}_repeat_${diff}`
|
|
||||||
if (!weekEvents.has(id)) {
|
|
||||||
weekEvents.set(id, {
|
|
||||||
...base,
|
|
||||||
id,
|
|
||||||
startDate: candStartStr,
|
|
||||||
endDate: candEndStr,
|
|
||||||
isRepeatOccurrence: true,
|
|
||||||
repeatIndex: diff,
|
|
||||||
startIdx: startIdx === -1 ? 0 : startIdx,
|
|
||||||
endIdx: endIdx === -1 ? 6 : endIdx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to array and sort
|
|
||||||
const eventArray = Array.from(weekEvents.values())
|
|
||||||
eventArray.sort((a, b) => {
|
|
||||||
// Sort by span length (longer first)
|
|
||||||
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) => {
|
|
||||||
let placedRow = 0
|
|
||||||
while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) {
|
|
||||||
placedRow++
|
|
||||||
}
|
|
||||||
if (placedRow === rowsLastEnd.length) {
|
|
||||||
rowsLastEnd.push(-1)
|
|
||||||
}
|
|
||||||
rowsLastEnd[placedRow] = event.endIdx
|
|
||||||
event.row = placedRow + 1
|
|
||||||
})
|
|
||||||
|
|
||||||
return eventArray
|
|
||||||
})
|
|
||||||
|
|
||||||
function timeToMinutes(timeStr) {
|
function timeToMinutes(timeStr) {
|
||||||
if (!timeStr) return 0
|
if (!timeStr) return 0
|
||||||
|
@ -25,6 +25,77 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
// Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences.
|
||||||
|
occursOnDate(event, dateStr) {
|
||||||
|
if (!event || !event.isRepeating || event.repeat === 'none') return false
|
||||||
|
// 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()
|
||||||
this.now = d.toISOString()
|
this.now = d.toISOString()
|
||||||
@ -133,28 +204,96 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
if (!base || !base.isRepeating) return
|
if (!base || !base.isRepeating) return
|
||||||
// WEEKLY SERIES ------------------------------------------------------
|
// WEEKLY SERIES ------------------------------------------------------
|
||||||
if (base.repeat === 'weeks') {
|
if (base.repeat === 'weeks') {
|
||||||
// Strategy: split series around the target occurrence, omitting it.
|
const interval = base.repeatInterval || 1
|
||||||
const remaining =
|
const pattern = base.repeatWeekdays || []
|
||||||
base.repeatCount === 'unlimited'
|
if (!pattern.some(Boolean)) return
|
||||||
? 'unlimited'
|
// Preserve original count before any truncation
|
||||||
: String(Math.max(0, parseInt(base.repeatCount, 10) - (occurrenceIndex + 1)))
|
const originalCountRaw = base.repeatCount
|
||||||
// Keep occurrences before the deleted one
|
|
||||||
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
|
// Determine target occurrence date
|
||||||
if (remaining === '0') return
|
let targetDate = null
|
||||||
// Find date of next occurrence (first after deleted)
|
if (ctx.occurrenceDate instanceof Date) {
|
||||||
const startDate = new Date(base.startDate + 'T00:00:00')
|
targetDate = new Date(
|
||||||
let idx = 0
|
ctx.occurrenceDate.getFullYear(),
|
||||||
let cur = new Date(startDate)
|
ctx.occurrenceDate.getMonth(),
|
||||||
while (idx <= occurrenceIndex && idx < 10000) {
|
ctx.occurrenceDate.getDate(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Fallback: derive from occurrenceIndex (legacy path)
|
||||||
|
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) {
|
||||||
cur.setDate(cur.getDate() + 1)
|
cur.setDate(cur.getDate() + 1)
|
||||||
if (base.repeatWeekdays && base.repeatWeekdays[cur.getDay()]) idx++
|
const blockStart = new Date(cur)
|
||||||
|
blockStart.setDate(cur.getDate() - cur.getDay())
|
||||||
|
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++
|
||||||
|
safety++
|
||||||
}
|
}
|
||||||
const nextStartStr = toLocalString(cur)
|
targetDate = cur
|
||||||
// Preserve multi‑day span if any
|
}
|
||||||
|
if (!targetDate) return
|
||||||
|
|
||||||
|
// Count occurrences BEFORE target (always include the base occurrence as first)
|
||||||
|
const baseStart = new Date(base.startDate + 'T00:00:00')
|
||||||
|
const baseBlockStart = new Date(baseStart)
|
||||||
|
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||||
|
const WEEK_MS = 7 * 86400000
|
||||||
|
function isAligned(d) {
|
||||||
|
const block = new Date(d)
|
||||||
|
block.setDate(d.getDate() - d.getDay())
|
||||||
|
const diff = Math.floor((block - baseBlockStart) / WEEK_MS)
|
||||||
|
return diff % interval === 0
|
||||||
|
}
|
||||||
|
// Start with 1 (base occurrence) if target is after base; if target IS base (should not happen here) leave 0
|
||||||
|
let countBefore = targetDate > baseStart ? 1 : 0
|
||||||
|
let probe = new Date(baseStart)
|
||||||
|
probe.setDate(probe.getDate() + 1) // start counting AFTER base
|
||||||
|
let safety2 = 0
|
||||||
|
while (probe < targetDate && safety2 < 50000) {
|
||||||
|
if (pattern[probe.getDay()] && isAligned(probe)) countBefore++
|
||||||
|
probe.setDate(probe.getDate() + 1)
|
||||||
|
safety2++
|
||||||
|
}
|
||||||
|
// Terminate original series to keep only occurrences before target
|
||||||
|
this._terminateRepeatSeriesAtIndex(baseId, countBefore)
|
||||||
|
|
||||||
|
// Calculate remaining occurrences for new series using ORIGINAL total
|
||||||
|
let remainingCount = 'unlimited'
|
||||||
|
if (originalCountRaw !== 'unlimited') {
|
||||||
|
const originalTotal = parseInt(originalCountRaw, 10)
|
||||||
|
if (!isNaN(originalTotal)) {
|
||||||
|
const rem = originalTotal - countBefore - 1 // kept + deleted
|
||||||
|
if (rem <= 0) return // nothing left to continue
|
||||||
|
remainingCount = String(rem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continuation starts at NEXT valid occurrence (matching weekday & aligned block)
|
||||||
|
let continuationStart = new Date(targetDate)
|
||||||
|
let searchSafety = 0
|
||||||
|
let foundNext = false
|
||||||
|
while (searchSafety < 50000) {
|
||||||
|
continuationStart.setDate(continuationStart.getDate() + 1)
|
||||||
|
if (pattern[continuationStart.getDay()] && isAligned(continuationStart)) {
|
||||||
|
foundNext = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
searchSafety++
|
||||||
|
}
|
||||||
|
if (!foundNext) return // no remaining occurrences
|
||||||
|
|
||||||
const spanDays = Math.round(
|
const spanDays = Math.round(
|
||||||
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
|
(fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000),
|
||||||
)
|
)
|
||||||
const nextEnd = new Date(fromLocalString(nextStartStr))
|
const nextStartStr = toLocalString(continuationStart)
|
||||||
|
const nextEnd = new Date(continuationStart)
|
||||||
nextEnd.setDate(nextEnd.getDate() + spanDays)
|
nextEnd.setDate(nextEnd.getDate() + spanDays)
|
||||||
const nextEndStr = toLocalString(nextEnd)
|
const nextEndStr = toLocalString(nextEnd)
|
||||||
this.createEvent({
|
this.createEvent({
|
||||||
@ -163,7 +302,8 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
endDate: nextEndStr,
|
endDate: nextEndStr,
|
||||||
colorId: base.colorId,
|
colorId: base.colorId,
|
||||||
repeat: 'weeks',
|
repeat: 'weeks',
|
||||||
repeatCount: remaining,
|
repeatInterval: interval,
|
||||||
|
repeatCount: remainingCount,
|
||||||
repeatWeekdays: base.repeatWeekdays,
|
repeatWeekdays: base.repeatWeekdays,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -175,6 +315,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
if (occurrenceIndex <= 0) return // base itself handled elsewhere
|
if (occurrenceIndex <= 0) return // base itself handled elsewhere
|
||||||
if (occurrenceIndex % interval !== 0) return // should not happen (synthetic id only for valid occurrences)
|
if (occurrenceIndex % interval !== 0) return // should not happen (synthetic id only for valid occurrences)
|
||||||
// Count prior occurrences (including base) before the deleted one
|
// Count prior occurrences (including base) before the deleted one
|
||||||
|
const originalCountRaw = base.repeatCount
|
||||||
const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1
|
const priorOccurrences = Math.floor((occurrenceIndex - 1) / interval) + 1
|
||||||
// Truncate base series to keep only priorOccurrences
|
// Truncate base series to keep only priorOccurrences
|
||||||
this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences)
|
this._terminateRepeatSeriesAtIndex(baseId, priorOccurrences)
|
||||||
@ -184,8 +325,8 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
)
|
)
|
||||||
// Remaining occurrences after deletion
|
// Remaining occurrences after deletion
|
||||||
let remainingCount = 'unlimited'
|
let remainingCount = 'unlimited'
|
||||||
if (base.repeatCount !== 'unlimited') {
|
if (originalCountRaw !== 'unlimited') {
|
||||||
const total = parseInt(base.repeatCount, 10)
|
const total = parseInt(originalCountRaw, 10)
|
||||||
if (!isNaN(total)) {
|
if (!isNaN(total)) {
|
||||||
const rem = total - priorOccurrences - 1 // subtract kept + deleted
|
const rem = total - priorOccurrences - 1 // subtract kept + deleted
|
||||||
if (rem <= 0) return // nothing left
|
if (rem <= 0) return // nothing left
|
||||||
@ -214,7 +355,13 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
|
|
||||||
deleteFromOccurrence(ctx) {
|
deleteFromOccurrence(ctx) {
|
||||||
const { baseId, occurrenceIndex } = ctx
|
const { baseId, occurrenceIndex } = ctx
|
||||||
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
|
const base = this.getEventById(baseId)
|
||||||
|
if (!base || !base.isRepeating) return
|
||||||
|
// We want to keep occurrences up to and including the selected one; that becomes new repeatCount.
|
||||||
|
// occurrenceIndex here represents the number of repeats AFTER the base (weekly: 0 = first repeat; monthly: diffMonths)
|
||||||
|
// Total kept occurrences = base (1) + occurrenceIndex
|
||||||
|
const keptTotal = 1 + Math.max(0, occurrenceIndex)
|
||||||
|
this._terminateRepeatSeriesAtIndex(baseId, keptTotal)
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteFirstOccurrence(baseId) {
|
deleteFirstOccurrence(baseId) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user