feat: Serientermin-Bearbeitung: Nur diesen Termin oder Serie
Klick auf einen wiederkehrenden Termin oeffnet zuerst einen Dialog: "Nur diesen Termin" oder "Ganze Serie". * Serie: bearbeitet den Master wie bisher * Nur dieser: fuegt EXDATE fuer das geklickte Datum zum Master hinzu und legt einen eigenstaendigen Ersatz-Termin mit den bearbeiteten Daten an Backend: * CalendarEvent.exdates speichert Ausnahmedaten kommasepariert * POST /events/<id>/exception fuegt EXDATE hinzu, erstellt optional das Replacement-Event mit frischer UID * _build_vevent schreibt jetzt EXDATE-Zeilen in die ical_data, sodass CalDAV-Clients die Ausnahmen auch sehen werden Frontend: * FullCalendar rrule-Plugin bekommt die exdate-Liste und blendet die uebersprungenen Tage aus * Drag & Drop verschiebt weiterhin die ganze Serie (Shortcut - fuer Einzelverschiebung Termin anklicken und bearbeiten) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,16 @@
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Scope picker for recurring events -->
|
||||
<Dialog v-model:visible="showScopeDialog" header="Serientermin bearbeiten" modal :style="{ width: '440px' }">
|
||||
<p>Dieser Termin gehoert zu einer Serie.<br>Was moechtest du bearbeiten?</p>
|
||||
<template #footer>
|
||||
<Button label="Abbrechen" text @click="showScopeDialog = false" />
|
||||
<Button label="Nur diesen Termin" outlined @click="openEditScope('occurrence')" />
|
||||
<Button label="Ganze Serie" @click="openEditScope('series')" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="confirmDeleteCal" header="Kalender loeschen" modal :style="{ width: '400px' }">
|
||||
<p>Moechtest du den Kalender <strong>{{ selectedCal?.name }}</strong> mit allen Terminen loeschen?</p>
|
||||
<template #footer>
|
||||
@@ -271,6 +281,11 @@ const sharePermission = ref('read')
|
||||
const icalPassword = ref('')
|
||||
const confirmDeleteEvent = ref(false)
|
||||
const confirmDeleteCal = ref(false)
|
||||
const showScopeDialog = ref(false)
|
||||
const pendingScopeEvent = ref(null)
|
||||
const pendingScopeOccurrence = ref(null)
|
||||
// 'series' (edit master), 'occurrence' (split off single date), or null (non-recurring)
|
||||
const currentEditScope = ref(null)
|
||||
const permOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Lesen+Schreiben', value: 'readwrite' }]
|
||||
|
||||
const ownCalendars = computed(() => calendars.value.filter(c => c.permission === 'owner'))
|
||||
@@ -354,12 +369,14 @@ function toFcEvent(e, cal) {
|
||||
extendedProps: { ...e, _cal: cal },
|
||||
}
|
||||
if (e.recurrence_rule) {
|
||||
// FullCalendar rrule plugin consumes the RRULE together with dtstart
|
||||
fc.rrule = {
|
||||
freq: extractFreq(e.recurrence_rule),
|
||||
dtstart: e.dtstart,
|
||||
...parseRRule(e.recurrence_rule),
|
||||
}
|
||||
if (e.exdates && e.exdates.length) {
|
||||
fc.exdate = e.exdates
|
||||
}
|
||||
if (e.dtend && e.dtstart) {
|
||||
const ms = new Date(e.dtend).getTime() - new Date(e.dtstart).getTime()
|
||||
if (ms > 0) fc.duration = msToDuration(ms)
|
||||
@@ -443,9 +460,47 @@ function loadRRuleIntoForm(rrule) {
|
||||
|
||||
function onEventClick(info) {
|
||||
const e = info.event.extendedProps
|
||||
if (e.recurrence_rule) {
|
||||
pendingScopeEvent.value = e
|
||||
pendingScopeOccurrence.value = info.event.start
|
||||
showScopeDialog.value = true
|
||||
return
|
||||
}
|
||||
currentEditScope.value = null
|
||||
openEditEvent(e)
|
||||
}
|
||||
|
||||
function openEditScope(scope) {
|
||||
showScopeDialog.value = false
|
||||
const e = pendingScopeEvent.value
|
||||
const occ = pendingScopeOccurrence.value
|
||||
if (scope === 'series') {
|
||||
currentEditScope.value = 'series'
|
||||
openEditEvent(e)
|
||||
} else {
|
||||
// "Nur diesen Termin" - form starts from the clicked occurrence's
|
||||
// start/end, non-recurring. Save will add an EXDATE on the master
|
||||
// plus create a standalone event.
|
||||
currentEditScope.value = 'occurrence'
|
||||
editingEvent.value = e
|
||||
const durationMs = e.dtend ? new Date(e.dtend) - new Date(e.dtstart) : 3600000
|
||||
const occEnd = new Date(occ.getTime() + durationMs)
|
||||
eventForm.value = {
|
||||
summary: e.summary,
|
||||
description: e.description || '',
|
||||
location: e.location || '',
|
||||
calendar_id: e.calendar_id,
|
||||
dtstart: toLocalISO(occ, e.all_day),
|
||||
dtend: toLocalISO(occEnd, e.all_day),
|
||||
all_day: e.all_day,
|
||||
}
|
||||
loadRRuleIntoForm('')
|
||||
// Freeze recurrence editor - this is a single instance
|
||||
recurFreq.value = 'none'
|
||||
showEventDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onDateSelect(info) {
|
||||
openNewEvent(info.start, info.end, info.allDay)
|
||||
}
|
||||
@@ -534,12 +589,28 @@ async function saveEvent() {
|
||||
calendar_id: eventForm.value.calendar_id,
|
||||
}
|
||||
try {
|
||||
if (editingEvent.value) {
|
||||
if (currentEditScope.value === 'occurrence' && editingEvent.value) {
|
||||
// Split single occurrence off the series: add EXDATE + create replacement
|
||||
await apiClient.post(`/events/${editingEvent.value.id}/exception`, {
|
||||
occurrence_date: toLocalISO(pendingScopeOccurrence.value, editingEvent.value.all_day),
|
||||
replacement: {
|
||||
summary: payload.summary,
|
||||
description: payload.description,
|
||||
location: payload.location,
|
||||
dtstart: payload.dtstart,
|
||||
dtend: payload.dtend,
|
||||
all_day: payload.all_day,
|
||||
},
|
||||
})
|
||||
} else if (editingEvent.value) {
|
||||
await apiClient.put(`/events/${editingEvent.value.id}`, payload)
|
||||
} else {
|
||||
await apiClient.post(`/calendars/${payload.calendar_id}/events`, payload)
|
||||
}
|
||||
showEventDialog.value = false
|
||||
currentEditScope.value = null
|
||||
pendingScopeEvent.value = null
|
||||
pendingScopeOccurrence.value = null
|
||||
refreshEvents()
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||
|
||||
Reference in New Issue
Block a user