Major new version #2
@ -1,16 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useCalendarStore } from '@/stores/CalendarStore'
|
import { useCalendarStore } from '@/stores/CalendarStore'
|
||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||||
import BaseDialog from './BaseDialog.vue'
|
import BaseDialog from './BaseDialog.vue'
|
||||||
import WeekdaySelector from './WeekdaySelector.vue'
|
import WeekdaySelector from './WeekdaySelector.vue'
|
||||||
import Numeric from './Numeric.vue'
|
import Numeric from './Numeric.vue'
|
||||||
import {
|
import { addDaysStr, getMondayOfISOWeek, fromLocalString, DEFAULT_TZ } from '@/utils/date'
|
||||||
addDaysStr,
|
|
||||||
getMondayOfISOWeek,
|
|
||||||
fromLocalString,
|
|
||||||
toLocalString,
|
|
||||||
DEFAULT_TZ,
|
|
||||||
} from '@/utils/date'
|
|
||||||
import { addDays, addMonths } from 'date-fns'
|
import { addDays, addMonths } from 'date-fns'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -23,11 +17,22 @@ const calendarStore = useCalendarStore()
|
|||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const dialogMode = ref('create') // 'create' or 'edit'
|
const dialogMode = ref('create') // 'create' or 'edit'
|
||||||
const editingEventId = ref(null) // base event id if repeating occurrence clicked
|
const editingEventId = ref(null)
|
||||||
// Track the id of an unsaved event created in the current create dialog session
|
|
||||||
const unsavedCreateId = ref(null)
|
const unsavedCreateId = ref(null)
|
||||||
const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate }
|
const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||||
const title = ref('')
|
const title = computed({
|
||||||
|
get() {
|
||||||
|
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||||
|
return calendarStore.events.get(editingEventId.value).title || ''
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||||
|
calendarStore.events.get(editingEventId.value).title = v
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
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'
|
const recurrenceFrequency = ref('weeks') // 'weeks' | 'months'
|
||||||
@ -36,80 +41,17 @@ const recurrenceOccurrences = ref(0) // 0 = unlimited
|
|||||||
const colorId = ref(0)
|
const colorId = ref(0)
|
||||||
const eventSaved = ref(false)
|
const eventSaved = ref(false)
|
||||||
const titleInput = ref(null)
|
const titleInput = ref(null)
|
||||||
const modalRef = ref(null)
|
|
||||||
|
|
||||||
// UI display mode - stays fixed unless user manually changes it
|
|
||||||
const uiDisplayFrequency = ref('weeks') // 'weeks' | 'months' | 'years'
|
const uiDisplayFrequency = ref('weeks') // 'weeks' | 'months' | 'years'
|
||||||
|
|
||||||
// Drag functionality
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const dragOffset = ref({ x: 0, y: 0 })
|
|
||||||
const modalPosition = ref({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
function startDrag(event) {
|
|
||||||
if (!modalRef.value) return
|
|
||||||
|
|
||||||
isDragging.value = true
|
|
||||||
const rect = modalRef.value.getBoundingClientRect()
|
|
||||||
dragOffset.value = {
|
|
||||||
x: event.clientX - rect.left,
|
|
||||||
y: event.clientY - rect.top,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set pointer capture for better touch handling
|
|
||||||
if (event.pointerId !== undefined) {
|
|
||||||
try {
|
|
||||||
event.target.setPointerCapture(event.pointerId)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Could not set pointer capture:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('pointermove', handleDrag, { passive: false })
|
|
||||||
document.addEventListener('pointerup', stopDrag)
|
|
||||||
document.addEventListener('pointercancel', stopDrag)
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDrag(event) {
|
|
||||||
if (!isDragging.value || !modalRef.value) return
|
|
||||||
|
|
||||||
modalPosition.value = {
|
|
||||||
x: event.clientX - dragOffset.value.x,
|
|
||||||
y: event.clientY - dragOffset.value.y,
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopDrag() {
|
|
||||||
isDragging.value = false
|
|
||||||
document.removeEventListener('pointermove', handleDrag)
|
|
||||||
document.removeEventListener('pointerup', stopDrag)
|
|
||||||
document.removeEventListener('pointercancel', stopDrag)
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalStyle = computed(() => {
|
|
||||||
if (modalPosition.value.x !== 0 || modalPosition.value.y !== 0) {
|
|
||||||
return {
|
|
||||||
transform: 'none',
|
|
||||||
left: `${modalPosition.value.x}px`,
|
|
||||||
top: `${modalPosition.value.y}px`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Helper to get starting weekday (Sunday-first index)
|
// Helper to get starting weekday (Sunday-first index)
|
||||||
function getStartingWeekday(selectionData = null) {
|
function getStartingWeekday(selectionData = null) {
|
||||||
const currentSelection = selectionData || props.selection
|
const currentSelection = selectionData || props.selection
|
||||||
if (!currentSelection.start) return 0 // Default to Sunday
|
if (!currentSelection.start) return 0
|
||||||
const date = fromLocalString(currentSelection.start, DEFAULT_TZ)
|
const date = fromLocalString(currentSelection.start, DEFAULT_TZ)
|
||||||
const dayOfWeek = date.getDay() // 0=Sunday, 1=Monday, ...
|
return date.getDay() // 0=Sunday, 6=Saturday
|
||||||
return dayOfWeek // Keep Sunday-first (0=Sunday, 6=Saturday)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed property for fallback weekdays - true for the initial day of the event, false for others
|
|
||||||
const fallbackWeekdays = computed(() => {
|
const fallbackWeekdays = computed(() => {
|
||||||
const startingDay = getStartingWeekday()
|
const startingDay = getStartingWeekday()
|
||||||
const fallback = [false, false, false, false, false, false, false]
|
const fallback = [false, false, false, false, false, false, false]
|
||||||
@ -117,40 +59,35 @@ const fallbackWeekdays = computed(() => {
|
|||||||
return fallback
|
return fallback
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed property to handle UI frequency (weeks/months/years) vs store frequency (weeks/months)
|
// Maps UI frequency display (including years) to store frequency (weeks/months only)
|
||||||
const displayFrequency = computed({
|
const displayFrequency = computed({
|
||||||
get() {
|
get() {
|
||||||
return uiDisplayFrequency.value
|
return uiDisplayFrequency.value
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
const oldFreq = uiDisplayFrequency.value
|
const oldFreq = uiDisplayFrequency.value
|
||||||
const currentDisplayValue = displayInterval.value // Get the current display value before changing
|
const currentDisplayValue = displayInterval.value
|
||||||
uiDisplayFrequency.value = val
|
uiDisplayFrequency.value = val
|
||||||
|
|
||||||
if (oldFreq === val) return // No change
|
if (oldFreq === val) return
|
||||||
|
|
||||||
if (oldFreq === 'years' && val === 'months') {
|
if (oldFreq === 'years' && val === 'months') {
|
||||||
// Converting from years to months: keep the display value the same, but now it represents months
|
|
||||||
recurrenceFrequency.value = 'months'
|
recurrenceFrequency.value = 'months'
|
||||||
recurrenceInterval.value = currentDisplayValue // Keep the same number, now as months
|
recurrenceInterval.value = currentDisplayValue
|
||||||
} else if (oldFreq === 'months' && val === 'years') {
|
} else if (oldFreq === 'months' && val === 'years') {
|
||||||
// Converting from months to years: keep the display value the same, but now it represents years
|
|
||||||
recurrenceFrequency.value = 'months'
|
recurrenceFrequency.value = 'months'
|
||||||
recurrenceInterval.value = currentDisplayValue * 12 // Convert to months for storage
|
recurrenceInterval.value = currentDisplayValue * 12
|
||||||
} else if (val === 'years') {
|
} else if (val === 'years') {
|
||||||
// Converting from weeks to years
|
|
||||||
recurrenceFrequency.value = 'months'
|
recurrenceFrequency.value = 'months'
|
||||||
recurrenceInterval.value = 12 // Default to 1 year
|
recurrenceInterval.value = 12
|
||||||
} else if (val === 'months') {
|
} else if (val === 'months') {
|
||||||
// Converting to months from weeks
|
|
||||||
recurrenceFrequency.value = 'months'
|
recurrenceFrequency.value = 'months'
|
||||||
if (oldFreq === 'weeks') {
|
if (oldFreq === 'weeks') {
|
||||||
recurrenceInterval.value = 1 // Default to 1 month
|
recurrenceInterval.value = 1
|
||||||
}
|
}
|
||||||
} else if (val === 'weeks') {
|
} else if (val === 'weeks') {
|
||||||
// Converting to weeks
|
|
||||||
recurrenceFrequency.value = 'weeks'
|
recurrenceFrequency.value = 'weeks'
|
||||||
recurrenceInterval.value = 1 // Default to 1 week
|
recurrenceInterval.value = 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -172,11 +109,43 @@ const displayInterval = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Repeat mapping uses 'weeks' | 'months' | 'none' directly (store only supports weeks/months)
|
const selectedColor = computed({
|
||||||
|
get() {
|
||||||
|
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||||
|
return calendarStore.events.get(editingEventId.value).colorId
|
||||||
|
}
|
||||||
|
return colorId.value
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
const n = parseInt(v)
|
||||||
|
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||||
|
calendarStore.events.get(editingEventId.value).colorId = n
|
||||||
|
}
|
||||||
|
colorId.value = n
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Maps 0 (unlimited) to 'unlimited' string in store
|
||||||
|
const repeatCountBinding = computed({
|
||||||
|
get() {
|
||||||
|
if (editingEventId.value && calendarStore.events?.has(editingEventId.value)) {
|
||||||
|
const rc = calendarStore.events.get(editingEventId.value).repeatCount
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
recurrenceOccurrences.value = v
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const repeat = computed({
|
const repeat = computed({
|
||||||
get() {
|
get() {
|
||||||
if (!recurrenceEnabled.value) return 'none'
|
if (!recurrenceEnabled.value) return 'none'
|
||||||
return recurrenceFrequency.value // Always 'weeks' | 'months' for store
|
return recurrenceFrequency.value
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
if (val === 'none') {
|
if (val === 'none') {
|
||||||
@ -192,18 +161,13 @@ const repeat = computed({
|
|||||||
}
|
}
|
||||||
} else if (val === 'months') {
|
} else if (val === 'months') {
|
||||||
recurrenceFrequency.value = 'months'
|
recurrenceFrequency.value = 'months'
|
||||||
// Don't change UI display frequency here - let it be determined by context
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert Sunday-first recurrenceWeekdays to Sunday-first pattern for store
|
|
||||||
function buildStoreWeekdayPattern() {
|
function buildStoreWeekdayPattern() {
|
||||||
// store expects Sun..Sat; we have Sun..Sat
|
|
||||||
// Direct mapping: recurrenceWeekdays indices 0..6 (Sun..Sat) -> store array [Sun,Mon,Tue,Wed,Thu,Fri,Sat]
|
|
||||||
let sunFirst = [...recurrenceWeekdays.value]
|
let sunFirst = [...recurrenceWeekdays.value]
|
||||||
|
|
||||||
// Ensure at least one day is selected - fallback to starting day
|
|
||||||
if (!sunFirst.some(Boolean)) {
|
if (!sunFirst.some(Boolean)) {
|
||||||
const startingDay = getStartingWeekday()
|
const startingDay = getStartingWeekday()
|
||||||
sunFirst[startingDay] = true
|
sunFirst[startingDay] = true
|
||||||
@ -214,39 +178,23 @@ function buildStoreWeekdayPattern() {
|
|||||||
|
|
||||||
function loadWeekdayPatternFromStore(storePattern) {
|
function loadWeekdayPatternFromStore(storePattern) {
|
||||||
if (!Array.isArray(storePattern) || storePattern.length !== 7) return
|
if (!Array.isArray(storePattern) || storePattern.length !== 7) return
|
||||||
// store: Sun..Sat -> keep as Sun..Sat
|
|
||||||
recurrenceWeekdays.value = [...storePattern]
|
recurrenceWeekdays.value = [...storePattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedColor = computed({
|
|
||||||
get: () => colorId.value,
|
|
||||||
set: (val) => {
|
|
||||||
colorId.value = parseInt(val)
|
|
||||||
// Update the event immediately when color changes
|
|
||||||
if (editingEventId.value) {
|
|
||||||
updateEventInStore()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function openCreateDialog(selectionData = null) {
|
function openCreateDialog(selectionData = null) {
|
||||||
// Start a fresh create dialog; delete any prior unsaved create
|
|
||||||
if (unsavedCreateId.value && !eventSaved.value) {
|
if (unsavedCreateId.value && !eventSaved.value) {
|
||||||
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
||||||
calendarStore.deleteEvent(unsavedCreateId.value)
|
calendarStore.deleteEvent(unsavedCreateId.value)
|
||||||
}
|
}
|
||||||
unsavedCreateId.value = null
|
unsavedCreateId.value = null
|
||||||
}
|
}
|
||||||
// Reset saved flag for new create
|
|
||||||
const currentSelection = selectionData || props.selection
|
const currentSelection = selectionData || props.selection
|
||||||
|
|
||||||
// Convert new format to start/end for compatibility with existing logic
|
|
||||||
let start, end
|
let start, end
|
||||||
if (currentSelection.startDate && currentSelection.dayCount) {
|
if (currentSelection.startDate && currentSelection.dayCount) {
|
||||||
start = currentSelection.startDate
|
start = currentSelection.startDate
|
||||||
end = addDaysStr(currentSelection.startDate, currentSelection.dayCount - 1)
|
end = addDaysStr(currentSelection.startDate, currentSelection.dayCount - 1)
|
||||||
} else if (currentSelection.start && currentSelection.end) {
|
} else if (currentSelection.start && currentSelection.end) {
|
||||||
// Fallback for old format
|
|
||||||
start = currentSelection.start
|
start = currentSelection.start
|
||||||
end = currentSelection.end
|
end = currentSelection.end
|
||||||
} else {
|
} else {
|
||||||
@ -256,11 +204,10 @@ function openCreateDialog(selectionData = null) {
|
|||||||
|
|
||||||
occurrenceContext.value = null
|
occurrenceContext.value = null
|
||||||
dialogMode.value = 'create'
|
dialogMode.value = 'create'
|
||||||
title.value = ''
|
|
||||||
recurrenceEnabled.value = false
|
recurrenceEnabled.value = false
|
||||||
recurrenceInterval.value = 1
|
recurrenceInterval.value = 1
|
||||||
recurrenceFrequency.value = 'weeks'
|
recurrenceFrequency.value = 'weeks'
|
||||||
uiDisplayFrequency.value = 'weeks' // Set initial UI display mode
|
uiDisplayFrequency.value = 'weeks'
|
||||||
recurrenceWeekdays.value = [false, false, false, false, false, false, false]
|
recurrenceWeekdays.value = [false, false, false, false, false, false, false]
|
||||||
recurrenceOccurrences.value = 0
|
recurrenceOccurrences.value = 0
|
||||||
colorId.value = calendarStore.selectEventColorId(start, end)
|
colorId.value = calendarStore.selectEventColorId(start, end)
|
||||||
@ -280,12 +227,10 @@ function openCreateDialog(selectionData = null) {
|
|||||||
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
|
||||||
repeatWeekdays: buildStoreWeekdayPattern(),
|
repeatWeekdays: buildStoreWeekdayPattern(),
|
||||||
})
|
})
|
||||||
// Track unsaved create id until saved or discarded
|
|
||||||
unsavedCreateId.value = editingEventId.value
|
unsavedCreateId.value = editingEventId.value
|
||||||
|
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
|
|
||||||
// Focus and select text after dialog is shown
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (titleInput.value) {
|
if (titleInput.value) {
|
||||||
titleInput.value.focus()
|
titleInput.value.focus()
|
||||||
@ -297,8 +242,6 @@ function openCreateDialog(selectionData = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditDialog(payload) {
|
function openEditDialog(payload) {
|
||||||
// If we are switching away from an unsaved create to a DIFFERENT event, remove the temporary one.
|
|
||||||
// If user clicked the same newly created event again, keep it (no deletion, no dialog flicker).
|
|
||||||
if (
|
if (
|
||||||
dialogMode.value === 'create' &&
|
dialogMode.value === 'create' &&
|
||||||
unsavedCreateId.value &&
|
unsavedCreateId.value &&
|
||||||
@ -313,7 +256,7 @@ function openEditDialog(payload) {
|
|||||||
}
|
}
|
||||||
occurrenceContext.value = null
|
occurrenceContext.value = null
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
// Payload expected: { id: baseId, instanceId, occurrenceIndex }
|
|
||||||
const baseId = payload.id
|
const baseId = payload.id
|
||||||
let occurrenceIndex = payload.occurrenceIndex || 0
|
let occurrenceIndex = payload.occurrenceIndex || 0
|
||||||
let weekday = null
|
let weekday = null
|
||||||
@ -321,7 +264,7 @@ function openEditDialog(payload) {
|
|||||||
|
|
||||||
const event = calendarStore.getEventById(baseId)
|
const event = calendarStore.getEventById(baseId)
|
||||||
if (!event) return
|
if (!event) return
|
||||||
// Compute occurrenceDate based on occurrenceIndex rather than parsing instanceId
|
|
||||||
if (event.isRepeating) {
|
if (event.isRepeating) {
|
||||||
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
||||||
const pattern = event.repeatWeekdays || []
|
const pattern = event.repeatWeekdays || []
|
||||||
@ -331,7 +274,6 @@ function openEditDialog(payload) {
|
|||||||
occurrenceDate = baseStart
|
occurrenceDate = baseStart
|
||||||
weekday = baseStart.getDay()
|
weekday = baseStart.getDay()
|
||||||
} else {
|
} else {
|
||||||
// Count valid repeat occurrences (pattern + interval alignment) AFTER the base span
|
|
||||||
const interval = event.repeatInterval || 1
|
const interval = event.repeatInterval || 1
|
||||||
const WEEK_MS = 7 * 86400000
|
const WEEK_MS = 7 * 86400000
|
||||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||||
@ -341,7 +283,7 @@ function openEditDialog(payload) {
|
|||||||
return diff % interval === 0
|
return diff % interval === 0
|
||||||
}
|
}
|
||||||
let cur = addDays(baseEnd, 1)
|
let cur = addDays(baseEnd, 1)
|
||||||
let found = 0 // number of repeat occurrences found so far
|
let found = 0
|
||||||
let safety = 0
|
let safety = 0
|
||||||
while (found < occurrenceIndex && safety < 20000) {
|
while (found < occurrenceIndex && safety < 20000) {
|
||||||
if (pattern[cur.getDay()] && isAligned(cur)) {
|
if (pattern[cur.getDay()] && isAligned(cur)) {
|
||||||
@ -361,7 +303,6 @@ function openEditDialog(payload) {
|
|||||||
}
|
}
|
||||||
dialogMode.value = 'edit'
|
dialogMode.value = 'edit'
|
||||||
editingEventId.value = baseId
|
editingEventId.value = baseId
|
||||||
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
|
if (event.repeatInterval) recurrenceInterval.value = event.repeatInterval
|
||||||
@ -370,22 +311,20 @@ function openEditDialog(payload) {
|
|||||||
if (event.repeat === 'weeks') {
|
if (event.repeat === 'weeks') {
|
||||||
uiDisplayFrequency.value = 'weeks'
|
uiDisplayFrequency.value = 'weeks'
|
||||||
} else if (event.repeat === 'months') {
|
} else if (event.repeat === 'months') {
|
||||||
// If it's a yearly interval (multiple of 12), show as years
|
|
||||||
if (event.repeatInterval && event.repeatInterval % 12 === 0 && event.repeatInterval >= 12) {
|
if (event.repeatInterval && event.repeatInterval % 12 === 0 && event.repeatInterval >= 12) {
|
||||||
uiDisplayFrequency.value = 'years'
|
uiDisplayFrequency.value = 'years'
|
||||||
} else {
|
} else {
|
||||||
uiDisplayFrequency.value = 'months'
|
uiDisplayFrequency.value = 'months'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uiDisplayFrequency.value = 'weeks' // fallback
|
uiDisplayFrequency.value = 'weeks'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
colorId.value = event.colorId
|
colorId.value = event.colorId
|
||||||
eventSaved.value = false
|
eventSaved.value = false
|
||||||
// Build occurrence context: treat any occurrenceIndex > 0 as a specific occurrence (weekday only relevant for weekly)
|
|
||||||
if (event.isRepeating) {
|
if (event.isRepeating) {
|
||||||
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
if (event.repeat === 'weeks' && occurrenceIndex >= 0) {
|
||||||
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
|
occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
|
||||||
@ -395,7 +334,6 @@ function openEditDialog(payload) {
|
|||||||
}
|
}
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
|
|
||||||
// Focus and select text after dialog is shown
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (titleInput.value) {
|
if (titleInput.value) {
|
||||||
titleInput.value.focus()
|
titleInput.value.focus()
|
||||||
@ -408,7 +346,6 @@ function openEditDialog(payload) {
|
|||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
// If we were creating a new event and user cancels (didn't save), delete it
|
|
||||||
if (unsavedCreateId.value && !eventSaved.value) {
|
if (unsavedCreateId.value && !eventSaved.value) {
|
||||||
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
||||||
calendarStore.deleteEvent(unsavedCreateId.value)
|
calendarStore.deleteEvent(unsavedCreateId.value)
|
||||||
@ -421,11 +358,8 @@ function closeDialog() {
|
|||||||
function updateEventInStore() {
|
function updateEventInStore() {
|
||||||
if (!editingEventId.value) return
|
if (!editingEventId.value) return
|
||||||
|
|
||||||
// For simple property updates (title, color, repeat), update all instances directly
|
|
||||||
// This avoids the expensive remove/re-add cycle
|
|
||||||
if (calendarStore.events?.has(editingEventId.value)) {
|
if (calendarStore.events?.has(editingEventId.value)) {
|
||||||
const event = calendarStore.events.get(editingEventId.value)
|
const event = calendarStore.events.get(editingEventId.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.repeatInterval = recurrenceInterval.value
|
||||||
@ -440,7 +374,6 @@ function saveEvent() {
|
|||||||
if (editingEventId.value) updateEventInStore()
|
if (editingEventId.value) updateEventInStore()
|
||||||
eventSaved.value = true
|
eventSaved.value = true
|
||||||
if (dialogMode.value === 'create') {
|
if (dialogMode.value === 'create') {
|
||||||
// This create is now saved; clear tracking so it won't be auto-deleted
|
|
||||||
unsavedCreateId.value = null
|
unsavedCreateId.value = null
|
||||||
}
|
}
|
||||||
if (dialogMode.value === 'create') emit('clear-selection')
|
if (dialogMode.value === 'create') emit('clear-selection')
|
||||||
@ -467,32 +400,11 @@ function deleteEventFrom() {
|
|||||||
closeDialog()
|
closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWeekday(index) {
|
|
||||||
recurrenceWeekdays.value[index] = !recurrenceWeekdays.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for title changes and update the event immediately
|
|
||||||
watch(title, (newTitle) => {
|
|
||||||
if (editingEventId.value && showDialog.value) {
|
|
||||||
updateEventInStore()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Watch for dialog being closed unexpectedly and cleanup unsaved events
|
|
||||||
// Removed placeholder watcher
|
|
||||||
|
|
||||||
// Watch for editingEventId changes (switching to a different event while create in progress)
|
|
||||||
// Removed editingEventId placeholder cleanup watcher
|
|
||||||
|
|
||||||
// Cleanup drag listeners on unmount
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (unsavedCreateId.value && !eventSaved.value) {
|
if (unsavedCreateId.value && !eventSaved.value) {
|
||||||
if (calendarStore.events?.has(unsavedCreateId.value))
|
if (calendarStore.events?.has(unsavedCreateId.value))
|
||||||
calendarStore.deleteEvent(unsavedCreateId.value)
|
calendarStore.deleteEvent(unsavedCreateId.value)
|
||||||
}
|
}
|
||||||
document.removeEventListener('pointermove', handleDrag)
|
|
||||||
document.removeEventListener('pointerup', stopDrag)
|
|
||||||
document.removeEventListener('pointercancel', stopDrag)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
||||||
@ -505,31 +417,12 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
watch(recurrenceOccurrences, () => {
|
|
||||||
if (editingEventId.value && showDialog.value) updateEventInStore()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle Esc key to close dialog
|
|
||||||
function handleKeydown(event) {
|
|
||||||
if (event.key === 'Escape' && showDialog.value) {
|
|
||||||
closeDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openCreateDialog,
|
openCreateDialog,
|
||||||
openEditDialog,
|
openEditDialog,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed helpers for delete UI
|
|
||||||
const isRepeatingEdit = computed(
|
const isRepeatingEdit = computed(
|
||||||
() => dialogMode.value === 'edit' && recurrenceEnabled.value && repeat.value !== 'none',
|
() => dialogMode.value === 'edit' && recurrenceEnabled.value && repeat.value !== 'none',
|
||||||
)
|
)
|
||||||
@ -540,11 +433,8 @@ const isLastOccurrence = computed(() => {
|
|||||||
const event = calendarStore.getEventById(editingEventId.value)
|
const event = calendarStore.getEventById(editingEventId.value)
|
||||||
if (!event || !event.isRepeating) return false
|
if (!event || !event.isRepeating) return false
|
||||||
|
|
||||||
// For unlimited events, there is no "last" occurrence
|
|
||||||
if (event.repeatCount === 'unlimited' || recurrenceOccurrences.value === 0) return false
|
if (event.repeatCount === 'unlimited' || recurrenceOccurrences.value === 0) return false
|
||||||
|
|
||||||
// For limited events, check if current occurrence index is the last one
|
|
||||||
// occurrenceIndex is 0-based, so the last occurrence has index (totalCount - 1)
|
|
||||||
const totalCount = parseInt(event.repeatCount, 10) || 0
|
const totalCount = parseInt(event.repeatCount, 10) || 0
|
||||||
return occurrenceContext.value.occurrenceIndex === totalCount - 1
|
return occurrenceContext.value.occurrenceIndex === totalCount - 1
|
||||||
})
|
})
|
||||||
@ -573,9 +463,7 @@ const formattedOccurrenceShort = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Short date shown in header (works for create, edit base, and occurrence)
|
|
||||||
const headerDateShort = computed(() => {
|
const headerDateShort = computed(() => {
|
||||||
// If editing a specific occurrence use that date
|
|
||||||
if (occurrenceContext.value?.occurrenceDate) {
|
if (occurrenceContext.value?.occurrenceDate) {
|
||||||
try {
|
try {
|
||||||
return occurrenceContext.value.occurrenceDate
|
return occurrenceContext.value.occurrenceDate
|
||||||
@ -585,7 +473,6 @@ const headerDateShort = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise use the base event start date (covers create + edit base)
|
|
||||||
if (editingEventId.value) {
|
if (editingEventId.value) {
|
||||||
const ev = calendarStore.getEventById(editingEventId.value)
|
const ev = calendarStore.getEventById(editingEventId.value)
|
||||||
if (ev?.startDate) {
|
if (ev?.startDate) {
|
||||||
@ -604,8 +491,7 @@ const headerDateShort = computed(() => {
|
|||||||
const finalOccurrenceDate = computed(() => {
|
const finalOccurrenceDate = computed(() => {
|
||||||
if (!recurrenceEnabled.value) return null
|
if (!recurrenceEnabled.value) return null
|
||||||
const count = recurrenceOccurrences.value
|
const count = recurrenceOccurrences.value
|
||||||
if (!count || count < 1) return null // unlimited or invalid
|
if (!count || count < 1) return null
|
||||||
// Need start date
|
|
||||||
const base = editingEventId.value ? calendarStore.getEventById(editingEventId.value) : null
|
const base = editingEventId.value ? calendarStore.getEventById(editingEventId.value) : null
|
||||||
if (!base) return null
|
if (!base) return null
|
||||||
const start = fromLocalString(base.startDate, DEFAULT_TZ)
|
const start = fromLocalString(base.startDate, DEFAULT_TZ)
|
||||||
@ -667,12 +553,10 @@ const recurrenceSummary = computed(() => {
|
|||||||
} else if (uiDisplayFrequency.value === 'years') {
|
} else if (uiDisplayFrequency.value === 'years') {
|
||||||
return displayInterval.value === 1 ? 'Annually' : `Every ${displayInterval.value} years`
|
return displayInterval.value === 1 ? 'Annually' : `Every ${displayInterval.value} years`
|
||||||
}
|
}
|
||||||
// For months frequency - always check the underlying recurrenceInterval for yearly patterns
|
|
||||||
if (recurrenceFrequency.value === 'months' && recurrenceInterval.value % 12 === 0) {
|
if (recurrenceFrequency.value === 'months' && recurrenceInterval.value % 12 === 0) {
|
||||||
const years = recurrenceInterval.value / 12
|
const years = recurrenceInterval.value / 12
|
||||||
return years === 1 ? 'Annually' : `Every ${years} years`
|
return years === 1 ? 'Annually' : `Every ${years} years`
|
||||||
}
|
}
|
||||||
// Regular monthly display
|
|
||||||
return displayInterval.value === 1 ? 'Monthly' : `Every ${displayInterval.value} months`
|
return displayInterval.value === 1 ? 'Monthly' : `Every ${displayInterval.value} months`
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -683,7 +567,7 @@ const recurrenceSummary = computed(() => {
|
|||||||
{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event'
|
{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event'
|
||||||
}}<template v-if="headerDateShort"> · {{ headerDateShort }}</template>
|
}}<template v-if="headerDateShort"> · {{ headerDateShort }}</template>
|
||||||
</template>
|
</template>
|
||||||
<label class="ec-field" ref="modalRef">
|
<label class="ec-field">
|
||||||
<input type="text" v-model="title" autocomplete="off" ref="titleInput" />
|
<input type="text" v-model="title" autocomplete="off" ref="titleInput" />
|
||||||
</label>
|
</label>
|
||||||
<div class="ec-color-swatches">
|
<div class="ec-color-swatches">
|
||||||
@ -728,7 +612,7 @@ const recurrenceSummary = computed(() => {
|
|||||||
</select>
|
</select>
|
||||||
<Numeric
|
<Numeric
|
||||||
class="occ-stepper"
|
class="occ-stepper"
|
||||||
v-model="recurrenceOccurrences"
|
v-model="repeatCountBinding"
|
||||||
:min="2"
|
:min="2"
|
||||||
:prefix-values="[{ value: 0, display: '∞' }]"
|
:prefix-values="[{ value: 0, display: '∞' }]"
|
||||||
number-postfix=" times"
|
number-postfix=" times"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user