Corrections on store and repeats.
This commit is contained in:
parent
9ab5ec8602
commit
07b22fa885
@ -19,7 +19,7 @@ const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occur
|
||||
const title = ref('')
|
||||
const recurrenceEnabled = ref(false)
|
||||
const recurrenceInterval = ref(1) // N in "Every N weeks/months"
|
||||
const recurrenceFrequency = ref('weeks') // 'weeks' | 'months' | 'years'
|
||||
const recurrenceFrequency = ref('weeks') // 'weeks' | 'months'
|
||||
const recurrenceWeekdays = ref([false, false, false, false, false, false, false])
|
||||
const recurrenceOccurrences = ref(0) // 0 = unlimited
|
||||
const colorId = ref(0)
|
||||
@ -42,31 +42,11 @@ const fallbackWeekdays = computed(() => {
|
||||
return fallback
|
||||
})
|
||||
|
||||
function preventFocusOnMouseDown(event) {
|
||||
// Prevent focus when clicking with mouse, but allow keyboard navigation
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Bridge legacy repeat API (store still expects repeat & repeatWeekdays)
|
||||
// Repeat mapping uses 'weeks' | 'months' | 'none' directly (legacy 'weekly'/'monthly' accepted on load)
|
||||
const repeat = computed({
|
||||
get() {
|
||||
if (!recurrenceEnabled.value) return 'none'
|
||||
if (recurrenceFrequency.value === 'weeks') {
|
||||
if (recurrenceInterval.value === 1) return 'weekly'
|
||||
if (recurrenceInterval.value === 2) return 'biweekly'
|
||||
// Fallback map >2 to weekly (future: custom)
|
||||
return 'weekly'
|
||||
} else if (recurrenceFrequency.value === 'months') {
|
||||
if (recurrenceInterval.value === 1) return 'monthly'
|
||||
if (recurrenceInterval.value === 12) return 'yearly'
|
||||
// Fallback map >1 to monthly
|
||||
return 'monthly'
|
||||
} else {
|
||||
// years (map to yearly via 12 * interval months)
|
||||
if (recurrenceInterval.value === 1) return 'yearly'
|
||||
// Multi-year -> treat as yearly (future: custom)
|
||||
return 'yearly'
|
||||
}
|
||||
return recurrenceFrequency.value // 'weeks' | 'months'
|
||||
},
|
||||
set(val) {
|
||||
if (val === 'none') {
|
||||
@ -74,27 +54,8 @@ const repeat = computed({
|
||||
return
|
||||
}
|
||||
recurrenceEnabled.value = true
|
||||
switch (val) {
|
||||
case 'weekly':
|
||||
recurrenceFrequency.value = 'weeks'
|
||||
recurrenceInterval.value = 1
|
||||
break
|
||||
case 'biweekly':
|
||||
recurrenceFrequency.value = 'weeks'
|
||||
recurrenceInterval.value = 2
|
||||
break
|
||||
case 'monthly':
|
||||
recurrenceFrequency.value = 'months'
|
||||
recurrenceInterval.value = 1
|
||||
break
|
||||
case 'yearly':
|
||||
recurrenceFrequency.value = 'years'
|
||||
recurrenceInterval.value = 1
|
||||
break
|
||||
default:
|
||||
recurrenceFrequency.value = 'weeks'
|
||||
recurrenceInterval.value = 1
|
||||
}
|
||||
if (val === 'weeks' || val === 'weekly') recurrenceFrequency.value = 'weeks'
|
||||
else if (val === 'months' || val === 'monthly') recurrenceFrequency.value = 'months'
|
||||
},
|
||||
})
|
||||
|
||||
@ -153,6 +114,7 @@ function openCreateDialog() {
|
||||
endDate: props.selection.end,
|
||||
colorId: colorId.value,
|
||||
repeat: repeat.value,
|
||||
repeatInterval: recurrenceInterval.value,
|
||||
repeatCount:
|
||||
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
||||
repeatWeekdays: buildStoreWeekdayPattern(),
|
||||
@ -204,6 +166,7 @@ function openEditDialog(eventInstanceId) {
|
||||
title.value = event.title
|
||||
loadWeekdayPatternFromStore(event.repeatWeekdays)
|
||||
repeat.value = event.repeat // triggers setter mapping into recurrence state
|
||||
if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval
|
||||
// Map repeatCount
|
||||
const rc = event.repeatCount ?? 'unlimited'
|
||||
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
||||
@ -244,6 +207,7 @@ function updateEventInStore() {
|
||||
event.title = title.value
|
||||
event.colorId = colorId.value
|
||||
event.repeat = repeat.value
|
||||
event.repeatInterval = recurrenceInterval.value
|
||||
event.repeatWeekdays = buildStoreWeekdayPattern()
|
||||
event.repeatCount =
|
||||
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
|
||||
@ -304,7 +268,7 @@ watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
||||
watch(
|
||||
recurrenceWeekdays,
|
||||
() => {
|
||||
if (editingEventId.value && showDialog.value && repeat.value === 'weekly') updateEventInStore()
|
||||
if (editingEventId.value && showDialog.value && repeat.value === 'weeks') updateEventInStore()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@ -396,12 +360,6 @@ const finalOccurrenceDate = computed(() => {
|
||||
const d = new Date(start)
|
||||
d.setMonth(d.getMonth() + monthsToAdd)
|
||||
return d
|
||||
} else {
|
||||
// years
|
||||
const yearsToAdd = recurrenceInterval.value * (count - 1)
|
||||
const d = new Date(start)
|
||||
d.setFullYear(d.getFullYear() + yearsToAdd)
|
||||
return d
|
||||
}
|
||||
})
|
||||
|
||||
@ -427,21 +385,15 @@ const formattedFinalOccurrence = computed(() => {
|
||||
|
||||
const recurrenceSummary = computed(() => {
|
||||
if (!recurrenceEnabled.value) return 'Does not recur'
|
||||
const unit = recurrenceFrequency.value // weeks | months | years (plural)
|
||||
const singular = unit.slice(0, -1)
|
||||
const unitary = { weeks: 'Weekly', months: 'Monthly', years: 'Annually' }
|
||||
let base =
|
||||
recurrenceInterval.value > 1 ? `Every ${recurrenceInterval.value} ${unit}` : unitary[unit]
|
||||
if (recurrenceFrequency.value === 'weeks') {
|
||||
const sel = weekdays.filter((_, i) => recurrenceWeekdays.value[i])
|
||||
if (sel.length) base += ' on ' + sel.join(', ')
|
||||
return recurrenceInterval.value === 1 ? 'Weekly' : `Every ${recurrenceInterval.value} weeks`
|
||||
}
|
||||
base +=
|
||||
' · ' +
|
||||
(recurrenceOccurrences.value === 0
|
||||
? 'no end'
|
||||
: `${recurrenceOccurrences.value} ${recurrenceOccurrences.value === 1 ? 'time' : 'times'}`)
|
||||
return base
|
||||
// months frequency
|
||||
if (recurrenceInterval.value % 12 === 0) {
|
||||
const years = recurrenceInterval.value / 12
|
||||
return years === 1 ? 'Annually' : `Every ${years} years`
|
||||
}
|
||||
return recurrenceInterval.value === 1 ? 'Monthly' : `Every ${recurrenceInterval.value} months`
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -476,15 +428,7 @@ const recurrenceSummary = computed(() => {
|
||||
<span>Repeat</span>
|
||||
</label>
|
||||
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
||||
{{
|
||||
recurrenceInterval === 1
|
||||
? recurrenceFrequency === 'months'
|
||||
? 'Monthly'
|
||||
: recurrenceFrequency === 'years'
|
||||
? 'Annually'
|
||||
: 'Every week'
|
||||
: `Every ${recurrenceInterval} ${recurrenceFrequency}`
|
||||
}}
|
||||
{{ recurrenceSummary }}
|
||||
<template v-if="recurrenceOccurrences > 0">
|
||||
until {{ formattedFinalOccurrence }}</template
|
||||
>
|
||||
@ -504,7 +448,6 @@ const recurrenceSummary = computed(() => {
|
||||
<select v-model="recurrenceFrequency" class="freq-select">
|
||||
<option value="weeks">weeks</option>
|
||||
<option value="months">months</option>
|
||||
<option value="years">years</option>
|
||||
</select>
|
||||
<Numeric
|
||||
class="occ-stepper"
|
||||
@ -797,6 +740,7 @@ const recurrenceSummary = computed(() => {
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--panel-alt);
|
||||
color: var(--ink);
|
||||
border-radius: 0.45rem;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
@ -806,6 +750,7 @@ const recurrenceSummary = computed(() => {
|
||||
outline: none;
|
||||
border-color: var(--input-focus);
|
||||
background: var(--panel-accent);
|
||||
color: var(--ink);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--input-focus),
|
||||
0 0 0 4px rgba(37, 99, 235, 0.15);
|
||||
|
@ -3,22 +3,22 @@
|
||||
<div
|
||||
v-for="span in eventSpans"
|
||||
:key="span.id"
|
||||
class="event-span"
|
||||
:class="[`event-color-${span.colorId}`]"
|
||||
class="event-span"
|
||||
:class="[`event-color-${span.colorId}`]"
|
||||
:style="{
|
||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
||||
gridRow: `${span.row}`
|
||||
gridRow: `${span.row}`,
|
||||
}"
|
||||
@click="handleEventClick(span)"
|
||||
@pointerdown="handleEventPointerDown(span, $event)"
|
||||
@click="handleEventClick(span)"
|
||||
@pointerdown="handleEventPointerDown(span, $event)"
|
||||
>
|
||||
<span class="event-title">{{ span.title }}</span>
|
||||
<div
|
||||
class="resize-handle left"
|
||||
<span class="event-title">{{ span.title }}</span>
|
||||
<div
|
||||
class="resize-handle left"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
||||
></div>
|
||||
<div
|
||||
class="resize-handle right"
|
||||
<div
|
||||
class="resize-handle right"
|
||||
@pointerdown="handleResizePointerDown(span, 'resize-right', $event)"
|
||||
></div>
|
||||
</div>
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
@ -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.
|
||||
},
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user