fix: implement recurrance configuration properly

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-29 15:29:10 -04:00
parent ee9495487e
commit 460c31e224
3 changed files with 710 additions and 139 deletions
+139 -40
View File
@@ -30,7 +30,10 @@ const timeZoneValue = computed({
// Format helpers
const formatDate = (date: Date): string => {
return date.toISOString().split('T')[0]
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const formatTime = (date: Date): string => {
@@ -45,16 +48,57 @@ const formatDateTime = (isoString: string | null | undefined): string => {
const parseDateTime = (date: string, time: string): Date => {
if (timelessValue.value) {
return new Date(date)
return new Date(`${date}T00:00:00`)
}
return new Date(`${date}T${time}`)
}
const formatDisplayDate = (value: string): string => {
if (!value) return ''
return new Date(`${value}T00:00:00`).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
const formatDisplayTime = (value: string): string => {
if (!value) return ''
const [hour, minute] = value.split(':').map(Number)
const date = new Date()
date.setHours(hour, minute, 0, 0)
return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
}
const pickerDate = (value: string): Date | null => value ? new Date(`${value}T00:00:00`) : null
const normalizePickerDate = (value: unknown): string => {
const selected = Array.isArray(value) ? value[0] : value
const date = selected instanceof Date ? selected : new Date(String(selected))
return Number.isNaN(date.getTime()) ? '' : formatDate(date)
}
// Date/time fields
const startDate = ref('')
const startTime = ref('')
const endDate = ref('')
const endTime = ref('')
const startDateMenu = ref(false)
const startTimeMenu = ref(false)
const endDateMenu = ref(false)
const endTimeMenu = ref(false)
const selectStartDate = (value: unknown) => {
const date = normalizePickerDate(value)
if (date) startDate.value = date
startDateMenu.value = false
}
const selectEndDate = (value: unknown) => {
const date = normalizePickerDate(value)
if (date) endDate.value = date
endDateMenu.value = false
}
// Initialize date/time from props
watch(() => props.startsOn, (newValue) => {
@@ -81,17 +125,23 @@ watch(() => props.endsOn, (newValue) => {
}
}, { immediate: true })
// Update parent when date/time changes
watch([startDate, startTime], () => {
if (props.mode === 'edit') {
emit('update:startsOn', parseDateTime(startDate.value, startTime.value).toISOString())
}
})
const ensureValidRange = (): boolean => {
if (!startDate.value || !endDate.value || !startTime.value || !endTime.value) return false
const start = parseDateTime(startDate.value, startTime.value)
const end = parseDateTime(endDate.value, endTime.value)
if (end.getTime() >= start.getTime()) return false
watch([endDate, endTime], () => {
if (props.mode === 'edit') {
emit('update:endsOn', parseDateTime(endDate.value, endTime.value).toISOString())
}
const correctedEnd = new Date(start)
if (!timelessValue.value) correctedEnd.setHours(correctedEnd.getHours() + 1)
endDate.value = formatDate(correctedEnd)
endTime.value = formatTime(correctedEnd)
return true
}
watch([startDate, startTime, endDate, endTime, timelessValue], () => {
if (props.mode !== 'edit' || ensureValidRange()) return
emit('update:startsOn', parseDateTime(startDate.value, startTime.value).toISOString())
emit('update:endsOn', parseDateTime(endDate.value, endTime.value).toISOString())
})
</script>
@@ -127,22 +177,46 @@ watch([endDate, endTime], () => {
<div class="text-subtitle-2 mb-2">Start</div>
<v-row dense>
<v-col cols="12" md="6">
<v-text-field
v-model="startDate"
label="Start Date"
type="date"
variant="outlined"
density="compact"
/>
<v-menu v-model="startDateMenu" :close-on-content-click="false" location="bottom start">
<template #activator="{ props: activatorProps }">
<v-text-field
v-bind="activatorProps"
:model-value="formatDisplayDate(startDate)"
label="Start date"
prepend-inner-icon="mdi-calendar"
variant="outlined"
density="compact"
readonly
/>
</template>
<v-date-picker
:model-value="pickerDate(startDate)"
color="primary"
show-adjacent-months
@update:model-value="selectStartDate"
/>
</v-menu>
</v-col>
<v-col v-if="!timelessValue" cols="12" md="6">
<v-text-field
v-model="startTime"
label="Start Time"
type="time"
variant="outlined"
density="compact"
/>
<v-menu v-model="startTimeMenu" :close-on-content-click="false" location="bottom start">
<template #activator="{ props: activatorProps }">
<v-text-field
v-bind="activatorProps"
:model-value="formatDisplayTime(startTime)"
label="Start time"
prepend-inner-icon="mdi-clock-outline"
variant="outlined"
density="compact"
readonly
/>
</template>
<v-card>
<v-time-picker v-model="startTime" color="primary" format="ampm" />
<v-card-actions class="justify-end">
<v-btn variant="text" @click="startTimeMenu = false">Done</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-col>
</v-row>
</div>
@@ -151,22 +225,47 @@ watch([endDate, endTime], () => {
<div class="text-subtitle-2 mb-2">End</div>
<v-row dense>
<v-col cols="12" md="6">
<v-text-field
v-model="endDate"
label="End Date"
type="date"
variant="outlined"
density="compact"
/>
<v-menu v-model="endDateMenu" :close-on-content-click="false" location="bottom start">
<template #activator="{ props: activatorProps }">
<v-text-field
v-bind="activatorProps"
:model-value="formatDisplayDate(endDate)"
label="End date"
prepend-inner-icon="mdi-calendar"
variant="outlined"
density="compact"
readonly
/>
</template>
<v-date-picker
:model-value="pickerDate(endDate)"
:min="pickerDate(startDate)"
color="primary"
show-adjacent-months
@update:model-value="selectEndDate"
/>
</v-menu>
</v-col>
<v-col v-if="!timelessValue" cols="12" md="6">
<v-text-field
v-model="endTime"
label="End Time"
type="time"
variant="outlined"
density="compact"
/>
<v-menu v-model="endTimeMenu" :close-on-content-click="false" location="bottom start">
<template #activator="{ props: activatorProps }">
<v-text-field
v-bind="activatorProps"
:model-value="formatDisplayTime(endTime)"
label="End time"
prepend-inner-icon="mdi-clock-outline"
variant="outlined"
density="compact"
readonly
/>
</template>
<v-card>
<v-time-picker v-model="endTime" color="primary" format="ampm" />
<v-card-actions class="justify-end">
<v-btn variant="text" @click="endTimeMenu = false">Done</v-btn>
</v-card-actions>
</v-card>
</v-menu>
</v-col>
</v-row>
</div>