Major new version #2
@ -29,6 +29,7 @@ const dialogMode = ref('create') // 'create' or 'edit'
|
||||
const editingEventId = ref(null)
|
||||
const unsavedCreateId = ref(null)
|
||||
const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||
const initialWeekday = ref(null)
|
||||
const title = computed({
|
||||
get() {
|
||||
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||
@ -63,10 +64,13 @@ function getStartingWeekday(selectionData = null) {
|
||||
}
|
||||
|
||||
const fallbackWeekdays = computed(() => {
|
||||
const startingDay = getStartingWeekday()
|
||||
const fallback = [false, false, false, false, false, false, false]
|
||||
fallback[startingDay] = true
|
||||
return fallback
|
||||
let weekday = initialWeekday.value
|
||||
if (weekday == null) {
|
||||
weekday = getStartingWeekday()
|
||||
}
|
||||
const fb = [false, false, false, false, false, false, false]
|
||||
fb[weekday] = true
|
||||
return fb
|
||||
})
|
||||
|
||||
// Maps UI frequency display (including years) to store frequency (weeks/months only)
|
||||
@ -140,14 +144,24 @@ const selectedColor = computed({
|
||||
const repeatCountBinding = computed({
|
||||
get() {
|
||||
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||
const rc = calendarStore.events.get(editingEventId.value).repeatCount
|
||||
const ev = calendarStore.events.get(editingEventId.value)
|
||||
const rc = ev.recur?.count ?? 'unlimited'
|
||||
return rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
||||
}
|
||||
return recurrenceOccurrences.value
|
||||
},
|
||||
set(v) {
|
||||
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||
calendarStore.events.get(editingEventId.value).repeatCount = v === 0 ? 'unlimited' : String(v)
|
||||
const ev = calendarStore.events.get(editingEventId.value)
|
||||
if (!ev.recur && v !== 0) {
|
||||
ev.recur = {
|
||||
freq: recurrenceFrequency.value,
|
||||
interval: recurrenceInterval.value,
|
||||
count: 'unlimited',
|
||||
weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null,
|
||||
}
|
||||
}
|
||||
if (ev.recur) ev.recur.count = v === 0 ? 'unlimited' : String(v)
|
||||
calendarStore.touchEvents()
|
||||
}
|
||||
recurrenceOccurrences.value = v
|
||||
@ -178,14 +192,7 @@ const repeat = computed({
|
||||
})
|
||||
|
||||
function buildStoreWeekdayPattern() {
|
||||
let sunFirst = [...recurrenceWeekdays.value]
|
||||
|
||||
if (!sunFirst.some(Boolean)) {
|
||||
const startingDay = getStartingWeekday()
|
||||
sunFirst[startingDay] = true
|
||||
}
|
||||
|
||||
return sunFirst
|
||||
return [...recurrenceWeekdays.value]
|
||||
}
|
||||
|
||||
function loadWeekdayPatternFromStore(storePattern) {
|
||||
@ -222,6 +229,7 @@ function openCreateDialog(selectionData = null) {
|
||||
}
|
||||
|
||||
occurrenceContext.value = null
|
||||
initialWeekday.value = null
|
||||
dialogMode.value = 'create'
|
||||
recurrenceEnabled.value = false
|
||||
recurrenceInterval.value = 1
|
||||
@ -234,17 +242,23 @@ function openCreateDialog(selectionData = null) {
|
||||
|
||||
const startingDay = getStartingWeekday({ start, end })
|
||||
recurrenceWeekdays.value[startingDay] = true
|
||||
initialWeekday.value = startingDay
|
||||
|
||||
editingEventId.value = calendarStore.createEvent({
|
||||
title: '',
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
colorId: colorId.value,
|
||||
repeat: repeat.value,
|
||||
repeatInterval: recurrenceInterval.value,
|
||||
repeatCount:
|
||||
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
||||
repeatWeekdays: buildStoreWeekdayPattern(),
|
||||
recur:
|
||||
recurrenceEnabled.value && repeat.value !== 'none'
|
||||
? {
|
||||
freq: recurrenceFrequency.value,
|
||||
interval: recurrenceInterval.value,
|
||||
count:
|
||||
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value),
|
||||
weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
unsavedCreateId.value = editingEventId.value
|
||||
|
||||
@ -277,6 +291,7 @@ function openEditDialog(payload) {
|
||||
unsavedCreateId.value = null
|
||||
}
|
||||
occurrenceContext.value = null
|
||||
initialWeekday.value = null
|
||||
if (!payload) return
|
||||
|
||||
const baseId = payload.id
|
||||
@ -287,16 +302,16 @@ function openEditDialog(payload) {
|
||||
const event = calendarStore.getEventById(baseId)
|
||||
if (!event) return
|
||||
|
||||
if (event.isRepeating) {
|
||||
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
||||
const pattern = event.repeatWeekdays || []
|
||||
if (event.recur) {
|
||||
if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) {
|
||||
const pattern = event.recur.weekdays || []
|
||||
const baseStart = fromLocalString(event.startDate, DEFAULT_TZ)
|
||||
const baseEnd = fromLocalString(event.endDate, DEFAULT_TZ)
|
||||
if (occurrenceIndex === 0) {
|
||||
occurrenceDate = baseStart
|
||||
weekday = baseStart.getDay()
|
||||
} else {
|
||||
const interval = event.repeatInterval || 1
|
||||
const interval = event.recur.interval || 1
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
function isAligned(d) {
|
||||
@ -318,22 +333,24 @@ function openEditDialog(payload) {
|
||||
occurrenceDate = cur
|
||||
weekday = cur.getDay()
|
||||
}
|
||||
} else if (event.repeat === 'months' && occurrenceIndex >= 0) {
|
||||
} else if (event.recur.freq === 'months' && occurrenceIndex >= 0) {
|
||||
const baseDate = fromLocalString(event.startDate, DEFAULT_TZ)
|
||||
occurrenceDate = addMonths(baseDate, occurrenceIndex)
|
||||
}
|
||||
}
|
||||
dialogMode.value = 'edit'
|
||||
editingEventId.value = baseId
|
||||
loadWeekdayPatternFromStore(event.repeatWeekdays)
|
||||
repeat.value = event.repeat // triggers setter mapping into recurrence state
|
||||
if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval
|
||||
loadWeekdayPatternFromStore(event.recur?.weekdays)
|
||||
initialWeekday.value =
|
||||
weekday != null ? weekday : fromLocalString(event.startDate, DEFAULT_TZ).getDay()
|
||||
repeat.value = event.recur ? event.recur.freq : 'none'
|
||||
if (event.recur?.interval) recurrenceInterval.value = event.recur.interval
|
||||
|
||||
// Set UI display frequency based on loaded data
|
||||
if (event.repeat === 'weeks') {
|
||||
if (event.recur?.freq === 'weeks') {
|
||||
uiDisplayFrequency.value = 'weeks'
|
||||
} else if (event.repeat === 'months') {
|
||||
if (event.repeatInterval && event.repeatInterval % 12 === 0 && event.repeatInterval >= 12) {
|
||||
} else if (event.recur?.freq === 'months') {
|
||||
if (event.recur.interval && event.recur.interval % 12 === 0 && event.recur.interval >= 12) {
|
||||
uiDisplayFrequency.value = 'years'
|
||||
} else {
|
||||
uiDisplayFrequency.value = 'months'
|
||||
@ -342,15 +359,15 @@ function openEditDialog(payload) {
|
||||
uiDisplayFrequency.value = 'weeks'
|
||||
}
|
||||
|
||||
const rc = event.repeatCount ?? 'unlimited'
|
||||
const rc = event.recur?.count ?? 'unlimited'
|
||||
recurrenceOccurrences.value = rc === 'unlimited' ? 0 : parseInt(rc, 10) || 0
|
||||
colorId.value = event.colorId
|
||||
eventSaved.value = false
|
||||
|
||||
if (event.isRepeating) {
|
||||
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
||||
if (event.recur) {
|
||||
if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) {
|
||||
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||
} else if (event.repeat === 'months' && occurrenceIndex > 0) {
|
||||
} else if (event.recur.freq === 'months' && occurrenceIndex > 0) {
|
||||
occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate }
|
||||
}
|
||||
}
|
||||
@ -379,12 +396,17 @@ function updateEventInStore() {
|
||||
if (calendarStore.events?.has(editingEventId.value)) {
|
||||
const event = calendarStore.events.get(editingEventId.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)
|
||||
event.isRepeating = recurrenceEnabled.value && repeat.value !== 'none'
|
||||
if (recurrenceEnabled.value && repeat.value !== 'none') {
|
||||
event.recur = {
|
||||
freq: recurrenceFrequency.value,
|
||||
interval: recurrenceInterval.value,
|
||||
count:
|
||||
recurrenceOccurrences.value === 0 ? 'unlimited' : String(recurrenceOccurrences.value),
|
||||
weekdays: recurrenceFrequency.value === 'weeks' ? buildStoreWeekdayPattern() : null,
|
||||
}
|
||||
} else {
|
||||
event.recur = null
|
||||
}
|
||||
calendarStore.touchEvents()
|
||||
}
|
||||
}
|
||||
@ -468,11 +490,9 @@ const isRepeatingBaseEdit = computed(() => isRepeatingEdit.value && !occurrenceC
|
||||
const isLastOccurrence = computed(() => {
|
||||
if (!occurrenceContext.value || !editingEventId.value) return false
|
||||
const event = calendarStore.getEventById(editingEventId.value)
|
||||
if (!event || !event.isRepeating) return false
|
||||
|
||||
if (event.repeatCount === 'unlimited' || recurrenceOccurrences.value === 0) return false
|
||||
|
||||
const totalCount = parseInt(event.repeatCount, 10) || 0
|
||||
if (!event || !event.recur) return false
|
||||
if (event.recur.count === 'unlimited' || recurrenceOccurrences.value === 0) return false
|
||||
const totalCount = parseInt(event.recur.count, 10) || 0
|
||||
return occurrenceContext.value.occurrenceIndex === totalCount - 1
|
||||
})
|
||||
const formattedOccurrenceShort = computed(() => {
|
||||
|
@ -176,11 +176,11 @@ function startLocalDrag(init, evt) {
|
||||
const baseEv = store.getEventById(init.id)
|
||||
if (
|
||||
baseEv &&
|
||||
baseEv.isRepeating &&
|
||||
baseEv.repeat === 'weeks' &&
|
||||
Array.isArray(baseEv.repeatWeekdays)
|
||||
baseEv.recur &&
|
||||
baseEv.recur.freq === 'weeks' &&
|
||||
Array.isArray(baseEv.recur.weekdays)
|
||||
) {
|
||||
originalPattern = [...baseEv.repeatWeekdays]
|
||||
originalPattern = [...baseEv.recur.weekdays]
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@ -286,8 +286,8 @@ function onDragPointerMove(e) {
|
||||
const shift = currentWeekday - st.originalWeekday
|
||||
const rotated = store._rotateWeekdayPattern([...st.originalPattern], shift)
|
||||
const ev = store.getEventById(st.id)
|
||||
if (ev && ev.repeat === 'weeks') {
|
||||
ev.repeatWeekdays = rotated
|
||||
if (ev && ev.recur && ev.recur.freq === 'weeks') {
|
||||
ev.recur.weekdays = rotated
|
||||
store.touchEvents()
|
||||
}
|
||||
} catch {}
|
||||
|
@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
getLocalizedWeekdayNames,
|
||||
getLocaleFirstDay,
|
||||
@ -44,7 +44,10 @@ import {
|
||||
const model = defineModel({
|
||||
type: Array,
|
||||
default: () => [false, false, false, false, false, false, false],
|
||||
})
|
||||
}) // external value consumers see
|
||||
|
||||
// Internal state preserves the user's explicit picks even if all false
|
||||
const internal = ref([false, false, false, false, false, false, false])
|
||||
|
||||
const props = defineProps({
|
||||
weekend: { type: Array, default: undefined },
|
||||
@ -55,12 +58,11 @@ const props = defineProps({
|
||||
firstDay: { type: Number, default: null },
|
||||
})
|
||||
|
||||
// If external model provided is entirely false, keep as-is (user will see fallback styling),
|
||||
// only overwrite if null/undefined.
|
||||
if (!model.value) model.value = [...props.fallback]
|
||||
// Initialize internal from external if it has any true; else keep empty (fallback handled on emit)
|
||||
if (model.value?.some?.(Boolean)) internal.value = [...model.value]
|
||||
const labelsMondayFirst = getLocalizedWeekdayNames()
|
||||
const labels = [labelsMondayFirst[6], ...labelsMondayFirst.slice(0, 6)]
|
||||
const anySelected = computed(() => model.value.some(Boolean))
|
||||
const anySelected = computed(() => internal.value.some(Boolean))
|
||||
const localeFirst = getLocaleFirstDay()
|
||||
const localeWeekend = getLocaleWeekendDays()
|
||||
const firstDay = computed(() => (props.firstDay ?? localeFirst) % 7)
|
||||
@ -71,10 +73,38 @@ const weekendDays = computed(() => {
|
||||
})
|
||||
|
||||
const displayLabels = computed(() => reorderByFirstDay(labels, firstDay.value))
|
||||
const displayValuesCommitted = computed(() => reorderByFirstDay(model.value, firstDay.value))
|
||||
const displayValuesCommitted = computed(() => reorderByFirstDay(internal.value, firstDay.value))
|
||||
const displayWorking = computed(() => reorderByFirstDay(weekendDays.value, firstDay.value))
|
||||
const displayDefault = computed(() => reorderByFirstDay(props.fallback, firstDay.value))
|
||||
|
||||
// Expose a normalized pattern (Sunday-first) that substitutes the fallback day if none selected.
|
||||
// This keeps UI visually showing fallback (muted) but downstream logic can opt-in by reading this.
|
||||
function computeFallbackPattern() {
|
||||
const fb = props.fallback && props.fallback.length === 7 ? props.fallback : null
|
||||
if (fb && fb.some(Boolean)) return [...fb]
|
||||
const arr = [false, false, false, false, false, false, false]
|
||||
const idx = fb ? fb.findIndex(Boolean) : -1
|
||||
if (idx >= 0) arr[idx] = true
|
||||
else arr[0] = true
|
||||
return arr
|
||||
}
|
||||
function emitExternal() {
|
||||
model.value = internal.value.some(Boolean) ? [...internal.value] : computeFallbackPattern()
|
||||
}
|
||||
emitExternal()
|
||||
watch(
|
||||
() => model.value,
|
||||
(nv) => {
|
||||
if (!nv) return
|
||||
if (!nv.some(Boolean)) return
|
||||
const fb = computeFallbackPattern()
|
||||
const isFallback = fb.every((v, i) => v === nv[i])
|
||||
// If internal is empty and model only reflects fallback, do not sync into internal
|
||||
if (isFallback && !internal.value.some(Boolean)) return
|
||||
internal.value = [...nv]
|
||||
},
|
||||
)
|
||||
|
||||
// Mapping from display index to original model index
|
||||
const orderIndices = computed(() => Array.from({ length: 7 }, (_, i) => (i + firstDay.value) % 7))
|
||||
|
||||
@ -135,8 +165,8 @@ function isPressing(di) {
|
||||
}
|
||||
|
||||
function onPointerDown(di) {
|
||||
originalValues = [...model.value]
|
||||
dragVal.value = !model.value[(di + firstDay.value) % 7]
|
||||
originalValues = [...internal.value]
|
||||
dragVal.value = !internal.value[(di + firstDay.value) % 7]
|
||||
dragStart.value = di
|
||||
previewEnd.value = di
|
||||
dragging.value = true
|
||||
@ -155,7 +185,8 @@ function onPointerUp() {
|
||||
// simple click: toggle single
|
||||
const next = [...originalValues]
|
||||
next[(dragStart.value + firstDay.value) % 7] = dragVal.value
|
||||
model.value = next
|
||||
internal.value = next
|
||||
emitExternal()
|
||||
cleanupDrag()
|
||||
} else {
|
||||
commitDrag()
|
||||
@ -169,7 +200,8 @@ function commitDrag() {
|
||||
: [previewEnd.value, dragStart.value]
|
||||
const next = [...originalValues]
|
||||
for (let di = s; di <= e; di++) next[orderIndices.value[di]] = dragVal.value
|
||||
model.value = next
|
||||
internal.value = next
|
||||
emitExternal()
|
||||
cleanupDrag()
|
||||
}
|
||||
function cancelDrag() {
|
||||
@ -185,14 +217,15 @@ function cleanupDrag() {
|
||||
function toggleWeekend(work) {
|
||||
const base = weekendDays.value
|
||||
const target = work ? base : base.map((v) => !v)
|
||||
const current = model.value
|
||||
const current = internal.value
|
||||
const allOn = current.every(Boolean)
|
||||
const isTargetActive = current.every((v, i) => v === target[i])
|
||||
if (allOn || isTargetActive) {
|
||||
model.value = [false, false, false, false, false, false, false]
|
||||
internal.value = [false, false, false, false, false, false, false]
|
||||
} else {
|
||||
model.value = [...target]
|
||||
internal.value = [...target]
|
||||
}
|
||||
emitExternal()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -65,14 +65,14 @@ export function createVirtualWeekManager({
|
||||
const repeatingBases = []
|
||||
if (calendarStore.events) {
|
||||
for (const ev of calendarStore.events.values()) {
|
||||
if (ev.isRepeating) repeatingBases.push(ev)
|
||||
if (ev.recur) repeatingBases.push(ev)
|
||||
}
|
||||
}
|
||||
|
||||
const collectEventsForDate = (dateStr, curDateObj) => {
|
||||
const storedEvents = []
|
||||
for (const ev of calendarStore.events.values()) {
|
||||
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
if (!ev.recur && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
storedEvents.push(ev)
|
||||
}
|
||||
}
|
||||
@ -289,7 +289,7 @@ export function createVirtualWeekManager({
|
||||
if (!visibleWeeks.value.length) return
|
||||
const repeatingBases = []
|
||||
if (calendarStore.events) {
|
||||
for (const ev of calendarStore.events.values()) if (ev.isRepeating) repeatingBases.push(ev)
|
||||
for (const ev of calendarStore.events.values()) if (ev.recur) repeatingBases.push(ev)
|
||||
}
|
||||
const selStart = selection.value.startDate
|
||||
const selCount = selection.value.dayCount
|
||||
@ -303,7 +303,7 @@ export function createVirtualWeekManager({
|
||||
// Rebuild events list for this day
|
||||
const storedEvents = []
|
||||
for (const ev of calendarStore.events.values()) {
|
||||
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
if (!ev.recur && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
storedEvents.push(ev)
|
||||
}
|
||||
}
|
||||
|
@ -137,11 +137,17 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
eventData.colorId ?? this.selectEventColorId(eventData.startDate, eventData.endDate),
|
||||
startTime: singleDay ? eventData.startTime || '09:00' : null,
|
||||
durationMinutes: singleDay ? eventData.durationMinutes || 60 : null,
|
||||
repeat: ['weeks', 'months'].includes(eventData.repeat) ? eventData.repeat : 'none',
|
||||
repeatInterval: eventData.repeatInterval || 1,
|
||||
repeatCount: eventData.repeatCount || 'unlimited',
|
||||
repeatWeekdays: eventData.repeatWeekdays,
|
||||
isRepeating: eventData.repeat && eventData.repeat !== 'none',
|
||||
recur:
|
||||
eventData.recur && ['weeks', 'months'].includes(eventData.recur.freq)
|
||||
? {
|
||||
freq: eventData.recur.freq,
|
||||
interval: eventData.recur.interval || 1,
|
||||
count: eventData.recur.count ?? 'unlimited',
|
||||
weekdays: Array.isArray(eventData.recur.weekdays)
|
||||
? [...eventData.recur.weekdays]
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
||||
this.notifyEventsChanged()
|
||||
@ -181,12 +187,12 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
deleteFirstOccurrence(baseId) {
|
||||
const base = this.getEventById(baseId)
|
||||
if (!base) return
|
||||
if (!base.isRepeating) {
|
||||
if (!base.recur) {
|
||||
this.deleteEvent(baseId)
|
||||
return
|
||||
}
|
||||
const numericCount =
|
||||
base.repeatCount === 'unlimited' ? Infinity : parseInt(base.repeatCount, 10)
|
||||
base.recur.count === 'unlimited' ? Infinity : parseInt(base.recur.count, 10)
|
||||
if (numericCount <= 1) {
|
||||
this.deleteEvent(baseId)
|
||||
return
|
||||
@ -205,7 +211,7 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
)
|
||||
base.startDate = nextStartStr
|
||||
base.endDate = newEndStr
|
||||
if (numericCount !== Infinity) base.repeatCount = String(Math.max(1, numericCount - 1))
|
||||
if (numericCount !== Infinity) base.recur.count = String(Math.max(1, numericCount - 1))
|
||||
this.events.set(baseId, { ...base, isSpanning: base.startDate < base.endDate })
|
||||
this.notifyEventsChanged()
|
||||
},
|
||||
@ -215,7 +221,7 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
if (occurrenceIndex == null) return
|
||||
const base = this.getEventById(baseId)
|
||||
if (!base) return
|
||||
if (!base.isRepeating) {
|
||||
if (!base.recur) {
|
||||
if (occurrenceIndex === 0) this.deleteEvent(baseId)
|
||||
return
|
||||
}
|
||||
@ -224,7 +230,8 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
return
|
||||
}
|
||||
const snapshot = { ...base }
|
||||
base.repeatCount = occurrenceIndex
|
||||
snapshot.recur = snapshot.recur ? { ...snapshot.recur } : null
|
||||
base.recur.count = occurrenceIndex
|
||||
const nextStartStr = getOccurrenceDate(snapshot, occurrenceIndex + 1, DEFAULT_TZ)
|
||||
if (!nextStartStr) return
|
||||
const durationDays = Math.max(
|
||||
@ -236,7 +243,7 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
)
|
||||
const newEndStr = toLocalString(addDays(fromLocalString(nextStartStr), durationDays))
|
||||
const originalNumeric =
|
||||
snapshot.repeatCount === 'unlimited' ? Infinity : parseInt(snapshot.repeatCount, 10)
|
||||
snapshot.recur.count === 'unlimited' ? Infinity : parseInt(snapshot.recur.count, 10)
|
||||
let remainingCount = 'unlimited'
|
||||
if (originalNumeric !== Infinity) {
|
||||
const rem = originalNumeric - (occurrenceIndex + 1)
|
||||
@ -248,10 +255,14 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
startDate: nextStartStr,
|
||||
endDate: newEndStr,
|
||||
colorId: snapshot.colorId,
|
||||
repeat: snapshot.repeat,
|
||||
repeatInterval: snapshot.repeatInterval,
|
||||
repeatCount: remainingCount,
|
||||
repeatWeekdays: snapshot.repeatWeekdays,
|
||||
recur: snapshot.recur
|
||||
? {
|
||||
freq: snapshot.recur.freq,
|
||||
interval: snapshot.recur.interval,
|
||||
count: remainingCount,
|
||||
weekdays: snapshot.recur.weekdays ? [...snapshot.recur.weekdays] : null,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
this.notifyEventsChanged()
|
||||
},
|
||||
@ -259,7 +270,7 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
deleteFromOccurrence(ctx) {
|
||||
const { baseId, occurrenceIndex } = ctx
|
||||
const base = this.getEventById(baseId)
|
||||
if (!base || !base.isRepeating) return
|
||||
if (!base || !base.recur) return
|
||||
if (occurrenceIndex === 0) {
|
||||
this.deleteEvent(baseId)
|
||||
return
|
||||
@ -288,15 +299,15 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
if (
|
||||
rotatePattern &&
|
||||
(mode === 'move' || mode === 'resize-left') &&
|
||||
snapshot.isRepeating &&
|
||||
snapshot.repeat === 'weeks' &&
|
||||
Array.isArray(snapshot.repeatWeekdays)
|
||||
snapshot.recur &&
|
||||
snapshot.recur.freq === 'weeks' &&
|
||||
Array.isArray(snapshot.recur.weekdays)
|
||||
) {
|
||||
const oldDow = prevStart.getDay()
|
||||
const newDow = newStart.getDay()
|
||||
const shift = newDow - oldDow
|
||||
if (shift !== 0) {
|
||||
snapshot.repeatWeekdays = this._rotateWeekdayPattern(snapshot.repeatWeekdays, shift)
|
||||
snapshot.recur.weekdays = this._rotateWeekdayPattern(snapshot.recur.weekdays, shift)
|
||||
}
|
||||
}
|
||||
this.events.set(eventId, { ...snapshot, isSpanning: snapshot.startDate < snapshot.endDate })
|
||||
@ -305,8 +316,8 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
|
||||
splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) {
|
||||
const base = this.events.get(baseId)
|
||||
if (!base || !base.isRepeating) return
|
||||
const originalCountRaw = base.repeatCount
|
||||
if (!base || !base.recur) return
|
||||
const originalCountRaw = base.recur.count
|
||||
const occurrenceDate = fromLocalString(occurrenceDateStr, DEFAULT_TZ)
|
||||
const baseStart = fromLocalString(base.startDate, DEFAULT_TZ)
|
||||
// If series effectively has <=1 occurrence, treat as simple move (no split) and flatten
|
||||
@ -317,9 +328,8 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
}
|
||||
if (totalOccurrences <= 1) {
|
||||
// Flatten to non-repeating if not already
|
||||
if (base.isRepeating) {
|
||||
base.repeat = 'none'
|
||||
base.isRepeating = false
|
||||
if (base.recur) {
|
||||
base.recur = null
|
||||
this.events.set(baseId, { ...base })
|
||||
}
|
||||
this.setEventRange(baseId, newStartStr, newEndStr, { mode: 'move', rotatePattern: true })
|
||||
@ -330,9 +340,9 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
return baseId
|
||||
}
|
||||
let keptOccurrences = 0
|
||||
if (base.repeat === 'weeks') {
|
||||
const interval = base.repeatInterval || 1
|
||||
const pattern = base.repeatWeekdays || []
|
||||
if (base.recur.freq === 'weeks') {
|
||||
const interval = base.recur.interval || 1
|
||||
const pattern = base.recur.weekdays || []
|
||||
if (!pattern.some(Boolean)) return
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const blockStartBase = getMondayOfISOWeek(baseStart)
|
||||
@ -346,11 +356,11 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
if (pattern[cursor.getDay()] && isAligned(cursor)) keptOccurrences++
|
||||
cursor = addDays(cursor, 1)
|
||||
}
|
||||
} else if (base.repeat === 'months') {
|
||||
} else if (base.recur.freq === 'months') {
|
||||
const diffMonths =
|
||||
(occurrenceDate.getFullYear() - baseStart.getFullYear()) * 12 +
|
||||
(occurrenceDate.getMonth() - baseStart.getMonth())
|
||||
const interval = base.repeatInterval || 1
|
||||
const interval = base.recur.interval || 1
|
||||
if (diffMonths <= 0 || diffMonths % interval !== 0) return
|
||||
keptOccurrences = diffMonths
|
||||
} else {
|
||||
@ -359,7 +369,12 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
this._terminateRepeatSeriesAtIndex(baseId, keptOccurrences)
|
||||
// After truncation compute base kept count
|
||||
const truncated = this.events.get(baseId)
|
||||
if (truncated && truncated.repeatCount && truncated.repeatCount !== 'unlimited') {
|
||||
if (
|
||||
truncated &&
|
||||
truncated.recur &&
|
||||
truncated.recur.count &&
|
||||
truncated.recur.count !== 'unlimited'
|
||||
) {
|
||||
// keptOccurrences already reflects number before split; adjust not needed further
|
||||
}
|
||||
let remainingCount = 'unlimited'
|
||||
@ -371,13 +386,13 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
remainingCount = String(rem)
|
||||
}
|
||||
}
|
||||
let repeatWeekdays = base.repeatWeekdays
|
||||
if (base.repeat === 'weeks' && Array.isArray(base.repeatWeekdays)) {
|
||||
let weekdays = base.recur.weekdays
|
||||
if (base.recur.freq === 'weeks' && Array.isArray(base.recur.weekdays)) {
|
||||
const origWeekday = occurrenceDate.getDay()
|
||||
const newWeekday = fromLocalString(newStartStr, DEFAULT_TZ).getDay()
|
||||
const shift = newWeekday - origWeekday
|
||||
if (shift !== 0) {
|
||||
repeatWeekdays = this._rotateWeekdayPattern(base.repeatWeekdays, shift)
|
||||
weekdays = this._rotateWeekdayPattern(base.recur.weekdays, shift)
|
||||
}
|
||||
}
|
||||
const newId = this.createEvent({
|
||||
@ -385,29 +400,29 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
startDate: newStartStr,
|
||||
endDate: newEndStr,
|
||||
colorId: base.colorId,
|
||||
repeat: base.repeat,
|
||||
repeatInterval: base.repeatInterval,
|
||||
repeatCount: remainingCount,
|
||||
repeatWeekdays,
|
||||
recur: {
|
||||
freq: base.recur.freq,
|
||||
interval: base.recur.interval,
|
||||
count: remainingCount,
|
||||
weekdays,
|
||||
},
|
||||
})
|
||||
// Flatten base if single occurrence now
|
||||
if (truncated && truncated.isRepeating) {
|
||||
if (truncated && truncated.recur) {
|
||||
const baseCountNum =
|
||||
truncated.repeatCount === 'unlimited' ? Infinity : parseInt(truncated.repeatCount, 10)
|
||||
truncated.recur.count === 'unlimited' ? Infinity : parseInt(truncated.recur.count, 10)
|
||||
if (baseCountNum <= 1) {
|
||||
truncated.repeat = 'none'
|
||||
truncated.isRepeating = false
|
||||
truncated.recur = null
|
||||
this.events.set(baseId, { ...truncated })
|
||||
}
|
||||
}
|
||||
// Flatten new if single occurrence only
|
||||
const newly = this.events.get(newId)
|
||||
if (newly && newly.isRepeating) {
|
||||
if (newly && newly.recur) {
|
||||
const newCountNum =
|
||||
newly.repeatCount === 'unlimited' ? Infinity : parseInt(newly.repeatCount, 10)
|
||||
newly.recur.count === 'unlimited' ? Infinity : parseInt(newly.recur.count, 10)
|
||||
if (newCountNum <= 1) {
|
||||
newly.repeat = 'none'
|
||||
newly.isRepeating = false
|
||||
newly.recur = null
|
||||
this.events.set(newId, { ...newly })
|
||||
}
|
||||
}
|
||||
@ -417,8 +432,8 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
|
||||
splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr) {
|
||||
const base = this.events.get(baseId)
|
||||
if (!base || !base.isRepeating) return null
|
||||
const originalCountRaw = base.repeatCount
|
||||
if (!base || !base.recur) return null
|
||||
const originalCountRaw = base.recur.count
|
||||
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
|
||||
let newSeriesCount = 'unlimited'
|
||||
if (originalCountRaw !== 'unlimited') {
|
||||
@ -433,21 +448,25 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
startDate: newStartStr,
|
||||
endDate: newEndStr,
|
||||
colorId: base.colorId,
|
||||
repeat: base.repeat,
|
||||
repeatInterval: base.repeatInterval,
|
||||
repeatCount: newSeriesCount,
|
||||
repeatWeekdays: base.repeatWeekdays,
|
||||
recur: base.recur
|
||||
? {
|
||||
freq: base.recur.freq,
|
||||
interval: base.recur.interval,
|
||||
count: newSeriesCount,
|
||||
weekdays: base.recur.weekdays ? [...base.recur.weekdays] : null,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
},
|
||||
|
||||
_terminateRepeatSeriesAtIndex(baseId, index) {
|
||||
const ev = this.events.get(baseId)
|
||||
if (!ev || !ev.isRepeating) return
|
||||
if (ev.repeatCount === 'unlimited') {
|
||||
ev.repeatCount = String(index)
|
||||
if (!ev || !ev.recur) return
|
||||
if (ev.recur.count === 'unlimited') {
|
||||
ev.recur.count = String(index)
|
||||
} else {
|
||||
const rc = parseInt(ev.repeatCount, 10)
|
||||
if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index))
|
||||
const rc = parseInt(ev.recur.count, 10)
|
||||
if (!isNaN(rc)) ev.recur.count = String(Math.min(rc, index))
|
||||
}
|
||||
this.notifyEventsChanged()
|
||||
},
|
||||
|
@ -81,9 +81,14 @@ function countPatternDaysInInterval(startDate, endDate, patternArr) {
|
||||
}
|
||||
|
||||
// Recurrence: Weekly ------------------------------------------------------
|
||||
function _getRecur(event) {
|
||||
return event && event.recur ? event.recur : null
|
||||
}
|
||||
|
||||
function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat !== 'weeks') return null
|
||||
const pattern = event.repeatWeekdays || []
|
||||
const recur = _getRecur(event)
|
||||
if (!recur || recur.freq !== 'weeks') return null
|
||||
const pattern = recur.weekdays || []
|
||||
if (!pattern.some(Boolean)) return null
|
||||
|
||||
const target = fromLocalString(dateStr, timeZone)
|
||||
@ -93,7 +98,7 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
const dow = dateFns.getDay(target)
|
||||
if (!pattern[dow]) return null // target not active
|
||||
|
||||
const interval = event.repeatInterval || 1
|
||||
const interval = recur.interval || 1
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart, timeZone)
|
||||
const currentBlockStart = getMondayOfISOWeek(target, timeZone)
|
||||
// Number of weeks between block starts (each block start is a Monday)
|
||||
@ -106,8 +111,9 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
// Same ISO week as base: count pattern days from baseStart up to target (inclusive)
|
||||
if (weekDiff === 0) {
|
||||
let n = countPatternDaysInInterval(baseStart, target, pattern) - 1
|
||||
if (!baseCountsAsPattern) n += 1 // Shift indices so base occurrence stays 0
|
||||
return n < 0 || n >= event.repeatCount ? null : n
|
||||
if (!baseCountsAsPattern) n += 1
|
||||
const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10)
|
||||
return n < 0 || n >= maxCount ? null : n
|
||||
}
|
||||
|
||||
const baseWeekEnd = dateFns.addDays(baseBlockStart, 6)
|
||||
@ -120,42 +126,48 @@ function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
const currentWeekCount = countPatternDaysInInterval(currentBlockStart, target, pattern)
|
||||
let n = firstWeekCount + middleWeeksCount + currentWeekCount - 1
|
||||
if (!baseCountsAsPattern) n += 1
|
||||
return n >= event.repeatCount ? null : n
|
||||
const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10)
|
||||
return n >= maxCount ? null : n
|
||||
}
|
||||
|
||||
// Recurrence: Monthly -----------------------------------------------------
|
||||
function getMonthlyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat !== 'months') return null
|
||||
const recur = _getRecur(event)
|
||||
if (!recur || recur.freq !== 'months') return null
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
const d = fromLocalString(dateStr, timeZone)
|
||||
const diffMonths = dateFns.differenceInCalendarMonths(d, baseStart)
|
||||
if (diffMonths < 0) return null
|
||||
const interval = event.repeatInterval || 1
|
||||
const interval = recur.interval || 1
|
||||
if (diffMonths % interval !== 0) return null
|
||||
const baseDay = dateFns.getDate(baseStart)
|
||||
const effectiveDay = Math.min(baseDay, dateFns.getDaysInMonth(d))
|
||||
if (dateFns.getDate(d) !== effectiveDay) return null
|
||||
const n = diffMonths / interval
|
||||
return n >= event.repeatCount ? null : n
|
||||
const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10)
|
||||
return n >= maxCount ? null : n
|
||||
}
|
||||
|
||||
function getOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat === 'none') return null
|
||||
const recur = _getRecur(event)
|
||||
if (!recur) return null
|
||||
if (dateStr < event.startDate) return null
|
||||
if (event.repeat === 'weeks') return getWeeklyOccurrenceIndex(event, dateStr, timeZone)
|
||||
if (event.repeat === 'months') return getMonthlyOccurrenceIndex(event, dateStr, timeZone)
|
||||
if (recur.freq === 'weeks') return getWeeklyOccurrenceIndex(event, dateStr, timeZone)
|
||||
if (recur.freq === 'months') return getMonthlyOccurrenceIndex(event, dateStr, timeZone)
|
||||
return null
|
||||
}
|
||||
|
||||
// Reverse lookup: given a recurrence index (0-based) return the occurrence start date string.
|
||||
// Returns null if the index is out of range or the event is not repeating.
|
||||
function getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat !== 'weeks') return null
|
||||
const recur = _getRecur(event)
|
||||
if (!recur || recur.freq !== 'weeks') return null
|
||||
if (occurrenceIndex < 0 || !Number.isInteger(occurrenceIndex)) return null
|
||||
if (event.repeatCount !== 'unlimited' && occurrenceIndex >= event.repeatCount) return null
|
||||
const pattern = event.repeatWeekdays || []
|
||||
const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10)
|
||||
if (occurrenceIndex >= maxCount) return null
|
||||
const pattern = recur.weekdays || []
|
||||
if (!pattern.some(Boolean)) return null
|
||||
const interval = event.repeatInterval || 1
|
||||
const interval = recur.interval || 1
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
if (occurrenceIndex === 0) return toLocalString(baseStart, timeZone)
|
||||
const baseWeekMonday = getMondayOfISOWeek(baseStart, timeZone)
|
||||
@ -192,10 +204,12 @@ function getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ)
|
||||
}
|
||||
|
||||
function getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat !== 'months') return null
|
||||
const recur = _getRecur(event)
|
||||
if (!recur || recur.freq !== 'months') return null
|
||||
if (occurrenceIndex < 0 || !Number.isInteger(occurrenceIndex)) return null
|
||||
if (event.repeatCount !== 'unlimited' && occurrenceIndex >= event.repeatCount) return null
|
||||
const interval = event.repeatInterval || 1
|
||||
const maxCount = recur.count === 'unlimited' ? Infinity : parseInt(recur.count, 10)
|
||||
if (occurrenceIndex >= maxCount) return null
|
||||
const interval = recur.interval || 1
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
const targetMonthOffset = occurrenceIndex * interval
|
||||
const monthDate = dateFns.addMonths(baseStart, targetMonthOffset)
|
||||
@ -208,9 +222,10 @@ function getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ)
|
||||
}
|
||||
|
||||
function getOccurrenceDate(event, occurrenceIndex, timeZone = DEFAULT_TZ) {
|
||||
if (!event?.isRepeating || event.repeat === 'none') return null
|
||||
if (event.repeat === 'weeks') return getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone)
|
||||
if (event.repeat === 'months') return getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone)
|
||||
const recur = _getRecur(event)
|
||||
if (!recur) return null
|
||||
if (recur.freq === 'weeks') return getWeeklyOccurrenceDate(event, occurrenceIndex, timeZone)
|
||||
if (recur.freq === 'months') return getMonthlyOccurrenceDate(event, occurrenceIndex, timeZone)
|
||||
return null
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user