feat: implement comprehensive recurring event single occurrence editing

- Add detach functionality for individual occurrences of recurring events
- Create POST /api/events/<id>/occurrences/<date>/detach endpoint
- Implement EventException-based EXDATE generation for master events
- Add user confirmation dialog for single vs series editing choice
- Implement manual recurrence expansion with DST timezone tolerance
- Support FREQ=DAILY and FREQ=WEEKLY with BYDAY patterns and UNTIL dates
- Create standalone events from detached occurrences without affecting master series
- Add GET /api/events/<id> endpoint for fetching master event data
- Allow editing recurring series even when master event date is in the past
- Replace browser confirm dialogs with Syncfusion dialog components
- Remove debug logging while preserving error handling
- Update documentation for recurring event functionality

BREAKING: Frontend now manually expands recurring events instead of relying on Syncfusion's EXDATE handling

This enables users to edit individual occurrences of recurring events (creating standalone events)
or edit the entire series (updating all future occurrences) through an intuitive UI workflow.
The system properly handles timezone transitions, holiday exclusions, and complex recurrence patterns.
This commit is contained in:
RobbStarkAustria
2025-10-12 20:04:23 +00:00
parent 773628c324
commit e53cc619ec
6 changed files with 739 additions and 120 deletions

View File

@@ -25,6 +25,13 @@ export async function fetchEvents(
return data;
}
export async function fetchEventById(eventId: string) {
const res = await fetch(`/api/events/${encodeURIComponent(eventId)}`);
const data = await res.json();
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Laden des Termins');
return data;
}
export async function deleteEvent(eventId: string) {
const res = await fetch(`/api/events/${encodeURIComponent(eventId)}`, {
method: 'DELETE',
@@ -34,6 +41,26 @@ export async function deleteEvent(eventId: string) {
return data;
}
export async function deleteEventOccurrence(eventId: string, occurrenceDate: string) {
const res = await fetch(`/api/events/${encodeURIComponent(eventId)}/occurrences/${encodeURIComponent(occurrenceDate)}`, {
method: 'DELETE',
});
const data = await res.json();
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Löschen des Einzeltermins');
return data;
}
export async function updateEventOccurrence(eventId: string, occurrenceDate: string, payload: UpdateEventPayload) {
const res = await fetch(`/api/events/${encodeURIComponent(eventId)}/occurrences/${encodeURIComponent(occurrenceDate)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Aktualisieren des Einzeltermins');
return data;
}
export interface UpdateEventPayload {
[key: string]: unknown;
}
@@ -48,3 +75,23 @@ export async function updateEvent(eventId: string, payload: UpdateEventPayload)
if (!res.ok || data.error) throw new Error(data.error || 'Fehler beim Aktualisieren des Termins');
return data;
}
export const detachEventOccurrence = async (masterId: number, occurrenceDate: string, eventData: object) => {
const url = `/api/events/${masterId}/occurrences/${occurrenceDate}/detach`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(eventData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
return data;
};