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 title = ref('')
|
||||||
const recurrenceEnabled = ref(false)
|
const recurrenceEnabled = ref(false)
|
||||||
const recurrenceInterval = ref(1) // N in "Every N weeks/months"
|
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 recurrenceWeekdays = ref([false, false, false, false, false, false, false])
|
||||||
const recurrenceOccurrences = ref(0) // 0 = unlimited
|
const recurrenceOccurrences = ref(0) // 0 = unlimited
|
||||||
const colorId = ref(0)
|
const colorId = ref(0)
|
||||||
@ -42,31 +42,11 @@ const fallbackWeekdays = computed(() => {
|
|||||||
return fallback
|
return fallback
|
||||||
})
|
})
|
||||||
|
|
||||||
function preventFocusOnMouseDown(event) {
|
// Repeat mapping uses 'weeks' | 'months' | 'none' directly (legacy 'weekly'/'monthly' accepted on load)
|
||||||
// Prevent focus when clicking with mouse, but allow keyboard navigation
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bridge legacy repeat API (store still expects repeat & repeatWeekdays)
|
|
||||||
const repeat = computed({
|
const repeat = computed({
|
||||||
get() {
|
get() {
|
||||||
if (!recurrenceEnabled.value) return 'none'
|
if (!recurrenceEnabled.value) return 'none'
|
||||||
if (recurrenceFrequency.value === 'weeks') {
|
return recurrenceFrequency.value // 'weeks' | 'months'
|
||||||
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'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
if (val === 'none') {
|
if (val === 'none') {
|
||||||
@ -74,27 +54,8 @@ const repeat = computed({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
recurrenceEnabled.value = true
|
recurrenceEnabled.value = true
|
||||||
switch (val) {
|
if (val === 'weeks' || val === 'weekly') recurrenceFrequency.value = 'weeks'
|
||||||
case 'weekly':
|
else if (val === 'months' || val === 'monthly') recurrenceFrequency.value = 'months'
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -153,6 +114,7 @@ function openCreateDialog() {
|
|||||||
endDate: props.selection.end,
|
endDate: props.selection.end,
|
||||||
colorId: colorId.value,
|
colorId: colorId.value,
|
||||||
repeat: repeat.value,
|
repeat: repeat.value,
|
||||||
|
repeatInterval: recurrenceInterval.value,
|
||||||
repeatCount:
|
repeatCount:
|
||||||
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
||||||
repeatWeekdays: buildStoreWeekdayPattern(),
|
repeatWeekdays: buildStoreWeekdayPattern(),
|
||||||
@ -204,6 +166,7 @@ function openEditDialog(eventInstanceId) {
|
|||||||
title.value = event.title
|
title.value = event.title
|
||||||
loadWeekdayPatternFromStore(event.repeatWeekdays)
|
loadWeekdayPatternFromStore(event.repeatWeekdays)
|
||||||
repeat.value = event.repeat // triggers setter mapping into recurrence state
|
repeat.value = event.repeat // triggers setter mapping into recurrence state
|
||||||
|
if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval
|
||||||
// Map repeatCount
|
// Map repeatCount
|
||||||
const rc = event.repeatCount ?? 'unlimited'
|
const rc = event.repeatCount ?? 'unlimited'
|
||||||
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
||||||
@ -244,6 +207,7 @@ function updateEventInStore() {
|
|||||||
event.title = title.value
|
event.title = title.value
|
||||||
event.colorId = colorId.value
|
event.colorId = colorId.value
|
||||||
event.repeat = repeat.value
|
event.repeat = repeat.value
|
||||||
|
event.repeatInterval = recurrenceInterval.value
|
||||||
event.repeatWeekdays = buildStoreWeekdayPattern()
|
event.repeatWeekdays = buildStoreWeekdayPattern()
|
||||||
event.repeatCount =
|
event.repeatCount =
|
||||||
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
|
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value)
|
||||||
@ -304,7 +268,7 @@ watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
|||||||
watch(
|
watch(
|
||||||
recurrenceWeekdays,
|
recurrenceWeekdays,
|
||||||
() => {
|
() => {
|
||||||
if (editingEventId.value && showDialog.value && repeat.value === 'weekly') updateEventInStore()
|
if (editingEventId.value && showDialog.value && repeat.value === 'weeks') updateEventInStore()
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@ -396,12 +360,6 @@ const finalOccurrenceDate = computed(() => {
|
|||||||
const d = new Date(start)
|
const d = new Date(start)
|
||||||
d.setMonth(d.getMonth() + monthsToAdd)
|
d.setMonth(d.getMonth() + monthsToAdd)
|
||||||
return d
|
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(() => {
|
const recurrenceSummary = computed(() => {
|
||||||
if (!recurrenceEnabled.value) return 'Does not recur'
|
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') {
|
if (recurrenceFrequency.value === 'weeks') {
|
||||||
const sel = weekdays.filter((_, i) => recurrenceWeekdays.value[i])
|
return recurrenceInterval.value === 1 ? 'Weekly' : `Every ${recurrenceInterval.value} weeks`
|
||||||
if (sel.length) base += ' on ' + sel.join(', ')
|
|
||||||
}
|
}
|
||||||
base +=
|
// months frequency
|
||||||
' · ' +
|
if (recurrenceInterval.value % 12 === 0) {
|
||||||
(recurrenceOccurrences.value === 0
|
const years = recurrenceInterval.value / 12
|
||||||
? 'no end'
|
return years === 1 ? 'Annually' : `Every ${years} years`
|
||||||
: `${recurrenceOccurrences.value} ${recurrenceOccurrences.value === 1 ? 'time' : 'times'}`)
|
}
|
||||||
return base
|
return recurrenceInterval.value === 1 ? 'Monthly' : `Every ${recurrenceInterval.value} months`
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -476,15 +428,7 @@ const recurrenceSummary = computed(() => {
|
|||||||
<span>Repeat</span>
|
<span>Repeat</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
||||||
{{
|
{{ recurrenceSummary }}
|
||||||
recurrenceInterval === 1
|
|
||||||
? recurrenceFrequency === 'months'
|
|
||||||
? 'Monthly'
|
|
||||||
: recurrenceFrequency === 'years'
|
|
||||||
? 'Annually'
|
|
||||||
: 'Every week'
|
|
||||||
: `Every ${recurrenceInterval} ${recurrenceFrequency}`
|
|
||||||
}}
|
|
||||||
<template v-if="recurrenceOccurrences > 0">
|
<template v-if="recurrenceOccurrences > 0">
|
||||||
until {{ formattedFinalOccurrence }}</template
|
until {{ formattedFinalOccurrence }}</template
|
||||||
>
|
>
|
||||||
@ -504,7 +448,6 @@ const recurrenceSummary = computed(() => {
|
|||||||
<select v-model="recurrenceFrequency" class="freq-select">
|
<select v-model="recurrenceFrequency" class="freq-select">
|
||||||
<option value="weeks">weeks</option>
|
<option value="weeks">weeks</option>
|
||||||
<option value="months">months</option>
|
<option value="months">months</option>
|
||||||
<option value="years">years</option>
|
|
||||||
</select>
|
</select>
|
||||||
<Numeric
|
<Numeric
|
||||||
class="occ-stepper"
|
class="occ-stepper"
|
||||||
@ -797,6 +740,7 @@ const recurrenceSummary = computed(() => {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border: 1px solid var(--input-border);
|
border: 1px solid var(--input-border);
|
||||||
background: var(--panel-alt);
|
background: var(--panel-alt);
|
||||||
|
color: var(--ink);
|
||||||
border-radius: 0.45rem;
|
border-radius: 0.45rem;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
@ -806,6 +750,7 @@ const recurrenceSummary = computed(() => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--input-focus);
|
border-color: var(--input-focus);
|
||||||
background: var(--panel-accent);
|
background: var(--panel-accent);
|
||||||
|
color: var(--ink);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px var(--input-focus),
|
0 0 0 1px var(--input-focus),
|
||||||
0 0 0 4px rgba(37, 99, 235, 0.15);
|
0 0 0 4px rgba(37, 99, 235, 0.15);
|
||||||
|
@ -3,16 +3,16 @@
|
|||||||
<div
|
<div
|
||||||
v-for="span in eventSpans"
|
v-for="span in eventSpans"
|
||||||
:key="span.id"
|
:key="span.id"
|
||||||
class="event-span"
|
class="event-span"
|
||||||
:class="[`event-color-${span.colorId}`]"
|
:class="[`event-color-${span.colorId}`]"
|
||||||
:style="{
|
:style="{
|
||||||
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
gridColumn: `${span.startIdx + 1} / ${span.endIdx + 2}`,
|
||||||
gridRow: `${span.row}`
|
gridRow: `${span.row}`,
|
||||||
}"
|
}"
|
||||||
@click="handleEventClick(span)"
|
@click="handleEventClick(span)"
|
||||||
@pointerdown="handleEventPointerDown(span, $event)"
|
@pointerdown="handleEventPointerDown(span, $event)"
|
||||||
>
|
>
|
||||||
<span class="event-title">{{ span.title }}</span>
|
<span class="event-title">{{ span.title }}</span>
|
||||||
<div
|
<div
|
||||||
class="resize-handle left"
|
class="resize-handle left"
|
||||||
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
@pointerdown="handleResizePointerDown(span, 'resize-left', $event)"
|
||||||
@ -33,8 +33,8 @@ import { toLocalString, fromLocalString, daysInclusive, addDaysStr } from '@/uti
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
week: {
|
week: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['event-click'])
|
const emit = defineEmits(['event-click'])
|
||||||
@ -60,31 +60,38 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
|
|||||||
const baseEndDate = new Date(fromLocalString(baseEvent.endDate))
|
const baseEndDate = new Date(fromLocalString(baseEvent.endDate))
|
||||||
const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000))
|
const spanDays = Math.floor((baseEndDate - baseStartDate) / (24 * 60 * 60 * 1000))
|
||||||
|
|
||||||
if (baseEvent.repeat === 'weekly') {
|
if (baseEvent.repeat === 'weeks') {
|
||||||
const repeatWeekdays = baseEvent.repeatWeekdays
|
const repeatWeekdays = baseEvent.repeatWeekdays
|
||||||
const targetWeekday = targetDate.getDay()
|
const targetWeekday = targetDate.getDay()
|
||||||
if (!repeatWeekdays[targetWeekday]) continue
|
if (!repeatWeekdays[targetWeekday]) continue
|
||||||
if (targetDate < baseStartDate) 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
|
if (maxOccurrences === 0) continue
|
||||||
// Count occurrences from start up to (and including) target
|
// Count occurrences from start up to (and including) target
|
||||||
let occIdx = 0
|
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)
|
const cursor = new Date(baseStartDate)
|
||||||
while (cursor < targetDate && occIdx < maxOccurrences) {
|
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)
|
cursor.setDate(cursor.getDate() + 1)
|
||||||
}
|
}
|
||||||
// If target itself is the base start and it's selected, occIdx == 0 => base event (skip)
|
if (targetDate.getTime() === baseStartDate.getTime()) {
|
||||||
if (cursor.getTime() === targetDate.getTime()) {
|
// skip base occurrence
|
||||||
// We haven't advanced past target, so if its weekday is selected and this is the first occurrence, skip
|
continue
|
||||||
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 (occIdx >= maxOccurrences) continue
|
if (occIdx >= maxOccurrences) continue
|
||||||
const occStart = new Date(targetDate)
|
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 occStartStr = toLocalString(occStart)
|
||||||
const occEndStr = toLocalString(occEnd)
|
const occEndStr = toLocalString(occEnd)
|
||||||
occurrences.push({
|
occurrences.push({
|
||||||
@ -93,66 +100,46 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
|
|||||||
startDate: occStartStr,
|
startDate: occStartStr,
|
||||||
endDate: occEndStr,
|
endDate: occEndStr,
|
||||||
isRepeatOccurrence: true,
|
isRepeatOccurrence: true,
|
||||||
repeatIndex: occIdx
|
repeatIndex: occIdx,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// Handle other repeat types (biweekly, monthly, yearly)
|
// Handle other repeat types (months)
|
||||||
let intervalsPassed = 0
|
let intervalsPassed = 0
|
||||||
const timeDiff = targetDate - baseStartDate
|
const timeDiff = targetDate - baseStartDate
|
||||||
|
if (baseEvent.repeat === 'months') {
|
||||||
switch (baseEvent.repeat) {
|
intervalsPassed =
|
||||||
case 'biweekly':
|
(targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 +
|
||||||
intervalsPassed = Math.floor(timeDiff / (14 * 24 * 60 * 60 * 1000))
|
(targetDate.getMonth() - baseStartDate.getMonth())
|
||||||
break
|
} else {
|
||||||
case 'monthly':
|
continue
|
||||||
intervalsPassed = Math.floor((targetDate.getFullYear() - baseStartDate.getFullYear()) * 12 +
|
|
||||||
(targetDate.getMonth() - baseStartDate.getMonth()))
|
|
||||||
break
|
|
||||||
case 'yearly':
|
|
||||||
intervalsPassed = targetDate.getFullYear() - baseStartDate.getFullYear()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
const interval = baseEvent.repeatInterval || 1
|
||||||
|
if (intervalsPassed < 0 || intervalsPassed % interval !== 0) continue
|
||||||
|
|
||||||
// Check a few occurrences around the target date
|
// Check a few occurrences around the target date
|
||||||
for (let i = Math.max(0, intervalsPassed - 2); i <= intervalsPassed + 2; i++) {
|
const maxOccurrences =
|
||||||
const maxOccurrences = baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
|
baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
|
||||||
if (i >= maxOccurrences) break
|
if (maxOccurrences === 0) continue
|
||||||
|
const i = intervalsPassed
|
||||||
const currentStart = new Date(baseStartDate)
|
if (i >= maxOccurrences) continue
|
||||||
|
// Skip base occurrence
|
||||||
switch (baseEvent.repeat) {
|
if (i === 0) continue
|
||||||
case 'biweekly':
|
const currentStart = new Date(baseStartDate)
|
||||||
currentStart.setDate(baseStartDate.getDate() + i * 14)
|
currentStart.setMonth(baseStartDate.getMonth() + i)
|
||||||
break
|
const currentEnd = new Date(currentStart)
|
||||||
case 'monthly':
|
currentEnd.setDate(currentStart.getDate() + spanDays)
|
||||||
currentStart.setMonth(baseStartDate.getMonth() + i)
|
const currentStartStr = toLocalString(currentStart)
|
||||||
break
|
const currentEndStr = toLocalString(currentEnd)
|
||||||
case 'yearly':
|
if (currentStartStr <= targetDateStr && targetDateStr <= currentEndStr) {
|
||||||
currentStart.setFullYear(baseStartDate.getFullYear() + i)
|
occurrences.push({
|
||||||
break
|
...baseEvent,
|
||||||
}
|
id: `${baseEvent.id}_repeat_${i}`,
|
||||||
|
startDate: currentStartStr,
|
||||||
const currentEnd = new Date(currentStart)
|
endDate: currentEndStr,
|
||||||
currentEnd.setDate(currentStart.getDate() + spanDays)
|
isRepeatOccurrence: true,
|
||||||
|
repeatIndex: i,
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,30 +175,36 @@ function handleEventPointerDown(span, event) {
|
|||||||
const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget)
|
const hit = getDateUnderPointer(event.clientX, event.clientY, event.currentTarget)
|
||||||
const anchorDate = hit ? hit.date : span.startDate
|
const anchorDate = hit ? hit.date : span.startDate
|
||||||
|
|
||||||
startLocalDrag({
|
startLocalDrag(
|
||||||
id: span.id,
|
{
|
||||||
mode: 'move',
|
id: span.id,
|
||||||
pointerStartX: event.clientX,
|
mode: 'move',
|
||||||
pointerStartY: event.clientY,
|
pointerStartX: event.clientX,
|
||||||
anchorDate,
|
pointerStartY: event.clientY,
|
||||||
startDate: span.startDate,
|
anchorDate,
|
||||||
endDate: span.endDate
|
startDate: span.startDate,
|
||||||
}, event)
|
endDate: span.endDate,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle resize handle pointer down
|
// 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
|
// Start drag from the current edge; anchorDate not needed for resize
|
||||||
startLocalDrag({
|
startLocalDrag(
|
||||||
id: span.id,
|
{
|
||||||
mode,
|
id: span.id,
|
||||||
pointerStartX: event.clientX,
|
mode,
|
||||||
pointerStartY: event.clientY,
|
pointerStartX: event.clientX,
|
||||||
anchorDate: null,
|
pointerStartY: event.clientY,
|
||||||
startDate: span.startDate,
|
anchorDate: null,
|
||||||
endDate: span.endDate
|
startDate: span.startDate,
|
||||||
}, event)
|
endDate: span.endDate,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get date under pointer coordinates
|
// Get date under pointer coordinates
|
||||||
@ -228,7 +221,7 @@ function getDateUnderPointer(clientX, clientY, targetEl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore pointer events for hidden elements
|
// Restore pointer events for hidden elements
|
||||||
hiddenElements.forEach(el => el.style.pointerEvents = 'auto')
|
hiddenElements.forEach((el) => (el.style.pointerEvents = 'auto'))
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
// Look for a day cell with data-date attribute
|
// Look for a day cell with data-date attribute
|
||||||
@ -316,7 +309,7 @@ function startLocalDrag(init, evt) {
|
|||||||
...init,
|
...init,
|
||||||
anchorOffset,
|
anchorOffset,
|
||||||
originSpanDays: spanDays,
|
originSpanDays: spanDays,
|
||||||
eventMoved: false
|
eventMoved: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture pointer events globally
|
// Capture pointer events globally
|
||||||
@ -378,7 +371,9 @@ function onDragPointerUp(e) {
|
|||||||
|
|
||||||
if (moved) {
|
if (moved) {
|
||||||
justDragged.value = true
|
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) {
|
function applyRangeDuringDrag(st, startDate, endDate) {
|
||||||
const ev = store.getEventById(st.id)
|
let ev = store.getEventById(st.id)
|
||||||
if (!ev) return
|
let isRepeatOccurrence = false
|
||||||
if (ev.isRepeatOccurrence) {
|
let baseId = st.id
|
||||||
const idParts = String(st.id).split('_repeat_')
|
let repeatIndex = 0
|
||||||
const baseId = idParts[0]
|
let grabbedWeekday = null
|
||||||
const repeatParts = idParts[1].split('_')
|
|
||||||
const repeatIndex = parseInt(repeatParts[0], 10) || 0
|
|
||||||
const grabbedWeekday = repeatParts.length > 1 ? parseInt(repeatParts[1], 10) : 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) {
|
if (repeatIndex === 0) {
|
||||||
store.setEventRange(baseId, startDate, endDate)
|
store.setEventRange(baseId, startDate, endDate, { mode })
|
||||||
} else {
|
} else {
|
||||||
if (!st.splitNewBaseId) {
|
if (!st.splitNewBaseId) {
|
||||||
const newId = store.splitRepeatSeries(baseId, repeatIndex, startDate, endDate, grabbedWeekday)
|
const newId = store.splitRepeatSeries(
|
||||||
|
baseId,
|
||||||
|
repeatIndex,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
grabbedWeekday,
|
||||||
|
)
|
||||||
if (newId) {
|
if (newId) {
|
||||||
st.splitNewBaseId = newId
|
st.splitNewBaseId = newId
|
||||||
st.id = newId
|
st.id = newId
|
||||||
@ -428,11 +443,11 @@ function applyRangeDuringDrag(st, startDate, endDate) {
|
|||||||
st.endDate = endDate
|
st.endDate = endDate
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
store.setEventRange(st.splitNewBaseId, startDate, endDate)
|
store.setEventRange(st.splitNewBaseId, startDate, endDate, { mode })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
store.setEventRange(st.id, startDate, endDate)
|
store.setEventRange(st.id, startDate, endDate, { mode })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,12 +459,12 @@ const eventSpans = computed(() => {
|
|||||||
// Collect events from all days in this week, including repeat occurrences
|
// Collect events from all days in this week, including repeat occurrences
|
||||||
props.week.days.forEach((day, dayIndex) => {
|
props.week.days.forEach((day, dayIndex) => {
|
||||||
// Get base events for this day
|
// Get base events for this day
|
||||||
day.events.forEach(event => {
|
day.events.forEach((event) => {
|
||||||
if (!weekEvents.has(event.id)) {
|
if (!weekEvents.has(event.id)) {
|
||||||
weekEvents.set(event.id, {
|
weekEvents.set(event.id, {
|
||||||
...event,
|
...event,
|
||||||
startIdx: dayIndex,
|
startIdx: dayIndex,
|
||||||
endIdx: dayIndex
|
endIdx: dayIndex,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const existing = weekEvents.get(event.id)
|
const existing = weekEvents.get(event.id)
|
||||||
@ -459,12 +474,12 @@ const eventSpans = computed(() => {
|
|||||||
|
|
||||||
// Generate repeat occurrences for this day
|
// Generate repeat occurrences for this day
|
||||||
const repeatOccurrences = generateRepeatOccurrencesForDate(day.date)
|
const repeatOccurrences = generateRepeatOccurrencesForDate(day.date)
|
||||||
repeatOccurrences.forEach(event => {
|
repeatOccurrences.forEach((event) => {
|
||||||
if (!weekEvents.has(event.id)) {
|
if (!weekEvents.has(event.id)) {
|
||||||
weekEvents.set(event.id, {
|
weekEvents.set(event.id, {
|
||||||
...event,
|
...event,
|
||||||
startIdx: dayIndex,
|
startIdx: dayIndex,
|
||||||
endIdx: dayIndex
|
endIdx: dayIndex,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const existing = weekEvents.get(event.id)
|
const existing = weekEvents.get(event.id)
|
||||||
@ -495,7 +510,7 @@ const eventSpans = computed(() => {
|
|||||||
|
|
||||||
// Assign rows to avoid overlaps
|
// Assign rows to avoid overlaps
|
||||||
const rowsLastEnd = []
|
const rowsLastEnd = []
|
||||||
eventArray.forEach(event => {
|
eventArray.forEach((event) => {
|
||||||
let placedRow = 0
|
let placedRow = 0
|
||||||
while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) {
|
while (placedRow < rowsLastEnd.length && !(event.startIdx > rowsLastEnd[placedRow])) {
|
||||||
placedRow++
|
placedRow++
|
||||||
|
@ -13,14 +13,14 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
config: {
|
config: {
|
||||||
select_days: 1000,
|
select_days: 1000,
|
||||||
min_year: MIN_YEAR,
|
min_year: MIN_YEAR,
|
||||||
max_year: MAX_YEAR
|
max_year: MAX_YEAR,
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
// Basic configuration getters
|
// Basic configuration getters
|
||||||
minYear: () => MIN_YEAR,
|
minYear: () => MIN_YEAR,
|
||||||
maxYear: () => MAX_YEAR
|
maxYear: () => MAX_YEAR,
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@ -49,13 +49,20 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
title: eventData.title,
|
title: eventData.title,
|
||||||
startDate: eventData.startDate,
|
startDate: eventData.startDate,
|
||||||
endDate: eventData.endDate,
|
endDate: eventData.endDate,
|
||||||
colorId: eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
|
colorId:
|
||||||
startTime: singleDay ? (eventData.startTime || '09:00') : null,
|
eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
|
||||||
durationMinutes: singleDay ? (eventData.durationMinutes || 60) : null,
|
startTime: singleDay ? eventData.startTime || '09:00' : null,
|
||||||
repeat: eventData.repeat || 'none',
|
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',
|
repeatCount: eventData.repeatCount || 'unlimited',
|
||||||
repeatWeekdays: eventData.repeatWeekdays,
|
repeatWeekdays: eventData.repeatWeekdays,
|
||||||
isRepeating: (eventData.repeat && eventData.repeat !== 'none')
|
isRepeating: eventData.repeat && eventData.repeat !== 'none',
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = new Date(fromLocalString(event.startDate))
|
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 })
|
this.events.get(dateStr).push({ ...event, isSpanning: startDate < endDate })
|
||||||
}
|
}
|
||||||
|
// No physical expansion; repeats are virtual
|
||||||
return event.id
|
return event.id
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventById(id) {
|
getEventById(id) {
|
||||||
for (const [, list] of this.events) {
|
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
|
if (found) return found
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -110,7 +118,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
deleteEvent(eventId) {
|
deleteEvent(eventId) {
|
||||||
const datesToCleanup = []
|
const datesToCleanup = []
|
||||||
for (const [dateStr, eventList] of this.events) {
|
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) {
|
if (eventIndex !== -1) {
|
||||||
eventList.splice(eventIndex, 1)
|
eventList.splice(eventIndex, 1)
|
||||||
if (eventList.length === 0) {
|
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) {
|
deleteSingleOccurrence(ctx) {
|
||||||
const { baseId, occurrenceIndex, weekday } = ctx
|
const { baseId, occurrenceIndex } = ctx
|
||||||
const base = this.getEventById(baseId)
|
const base = this.getEventById(baseId)
|
||||||
if (!base || base.repeat !== 'weekly') return
|
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
|
// 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.
|
// 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.
|
// 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)
|
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
|
||||||
if (remaining === '0') return
|
if (remaining === '0') return
|
||||||
// Find date of next occurrence
|
// Find date of next occurrence
|
||||||
@ -145,9 +157,9 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
startDate: nextStartStr,
|
startDate: nextStartStr,
|
||||||
endDate: nextStartStr,
|
endDate: nextStartStr,
|
||||||
colorId: base.colorId,
|
colorId: base.colorId,
|
||||||
repeat: 'weekly',
|
repeat: 'weeks',
|
||||||
repeatCount: remaining,
|
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))
|
const spanDays = Math.round((oldEnd - oldStart) / (24 * 60 * 60 * 1000))
|
||||||
let newStart = null
|
let newStart = null
|
||||||
|
|
||||||
if (base.repeat === 'weekly' && base.repeatWeekdays) {
|
if (base.repeat === 'weeks' && base.repeatWeekdays) {
|
||||||
const probe = new Date(oldStart)
|
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)
|
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') {
|
} else if (base.repeat === 'months') {
|
||||||
newStart = new Date(oldStart)
|
|
||||||
newStart.setDate(newStart.getDate() + 14)
|
|
||||||
} else if (base.repeat === 'monthly') {
|
|
||||||
newStart = new Date(oldStart)
|
newStart = new Date(oldStart)
|
||||||
newStart.setMonth(newStart.getMonth() + 1)
|
newStart.setMonth(newStart.getMonth() + 1)
|
||||||
} else if (base.repeat === 'yearly') {
|
|
||||||
newStart = new Date(oldStart)
|
|
||||||
newStart.setFullYear(newStart.getFullYear() + 1)
|
|
||||||
} else {
|
} else {
|
||||||
// Unknown pattern: delete entire series
|
// Unknown pattern: delete entire series
|
||||||
this.deleteEvent(baseId)
|
this.deleteEvent(baseId)
|
||||||
@ -207,63 +217,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
newEnd.setDate(newEnd.getDate() + spanDays)
|
newEnd.setDate(newEnd.getDate() + spanDays)
|
||||||
base.startDate = toLocalString(newStart)
|
base.startDate = toLocalString(newStart)
|
||||||
base.endDate = toLocalString(newEnd)
|
base.endDate = toLocalString(newEnd)
|
||||||
// Reindex across map
|
// old occurrence expansion removed (series handled differently now)
|
||||||
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
|
|
||||||
|
|
||||||
const originalRepeatCount = base.repeatCount
|
const originalRepeatCount = base.repeatCount
|
||||||
// Always cap original series at the split occurrence index (occurrences 0..index-1)
|
// Always cap original series at the split occurrence index (occurrences 0..index-1)
|
||||||
// Keep its weekday pattern unchanged.
|
// Keep its weekday pattern unchanged.
|
||||||
@ -284,12 +238,12 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
|
|
||||||
// Handle weekdays for weekly repeats
|
// Handle weekdays for weekly repeats
|
||||||
let newRepeatWeekdays = base.repeatWeekdays
|
let newRepeatWeekdays = base.repeatWeekdays
|
||||||
if (base.repeat === 'weekly' && base.repeatWeekdays) {
|
if (base.repeat === 'weeks' && base.repeatWeekdays) {
|
||||||
const newStartDate = new Date(fromLocalString(startDate))
|
const newStartDate = new Date(fromLocalString(startDate))
|
||||||
let dayShift = 0
|
let dayShift = 0
|
||||||
if (grabbedWeekday != null) {
|
if (grabbedWeekday != null) {
|
||||||
// Rotate so that the grabbed weekday maps to the new start weekday
|
// Rotate so that the grabbed weekday maps to the new start weekday
|
||||||
dayShift = newStartDate.getDay() - grabbedWeekday
|
dayShift = newStartDate.getDay() - grabbedWeekday
|
||||||
} else {
|
} else {
|
||||||
// Fallback: rotate by difference between new and original start weekday
|
// Fallback: rotate by difference between new and original start weekday
|
||||||
const originalStartDate = new Date(fromLocalString(base.startDate))
|
const originalStartDate = new Date(fromLocalString(base.startDate))
|
||||||
@ -315,16 +269,15 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
colorId: base.colorId,
|
colorId: base.colorId,
|
||||||
repeat: base.repeat,
|
repeat: base.repeat,
|
||||||
repeatCount: newRepeatCount,
|
repeatCount: newRepeatCount,
|
||||||
repeatWeekdays: newRepeatWeekdays
|
repeatWeekdays: newRepeatWeekdays,
|
||||||
})
|
})
|
||||||
return newId
|
return newId
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
_snapshotBaseEvent(eventId) {
|
_snapshotBaseEvent(eventId) {
|
||||||
// Return a shallow snapshot of any instance for metadata
|
// Return a shallow snapshot of any instance for metadata
|
||||||
for (const [, eventList] of this.events) {
|
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 }
|
if (e) return { ...e }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -350,7 +303,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
id: eventId,
|
id: eventId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
isSpanning: multi
|
isSpanning: multi,
|
||||||
}
|
}
|
||||||
// Normalize single-day time fields
|
// Normalize single-day time fields
|
||||||
if (!multi) {
|
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) {
|
_reindexBaseEvent(eventId, snapshot, startDate, endDate) {
|
||||||
if (!snapshot) return
|
if (!snapshot) return
|
||||||
this._removeEventFromAllDatesById(eventId)
|
this._removeEventFromAllDatesById(eventId)
|
||||||
@ -393,7 +438,7 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
|
|
||||||
_findEventInAnyList(eventId) {
|
_findEventInAnyList(eventId) {
|
||||||
for (const [, eventList] of this.events) {
|
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
|
if (found) return found
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -414,62 +459,6 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventById(id) {
|
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user