feat: add recurring event deletion with three modes

Implement three deletion modes for recurring events:
- single: exclude specific occurrence via EXDATE mechanism
- future: set RRULE UNTIL to stop future occurrences
- all: delete entire event series

Changes include:
- Add exceptionDates field to CalendarEvent model
- Add RecurringDeleteMode type and DeleteRecurringEventDTO
- EventService.deleteRecurring() with mode-based logic using rrule library
- EventController DELETE endpoint accepts mode/occurrenceDate query params
- recurrenceExpander filters out exception dates during expansion
- AI tools support deleteMode and occurrenceDate for proposed deletions
- ChatService.confirmEvent() handles recurring delete modes
- New DeleteEventModal component for unified delete confirmation UI
- Calendar screen integrates modal for both recurring and non-recurring events
This commit is contained in:
2026-01-25 15:19:31 +01:00
parent a42e2a7c1c
commit 2b999d9b0f
35 changed files with 787 additions and 200 deletions

View File

@@ -140,7 +140,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
{
name: "proposeDeleteEvent",
description:
"Propose deleting an event. The user must confirm. Use this when the user wants to remove an appointment.",
"Propose deleting an event. The user must confirm. Use this when the user wants to remove an appointment. For recurring events, specify deleteMode to control which occurrences to delete.",
parameters: {
type: "object",
properties: {
@@ -148,6 +148,17 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
type: "string",
description: "ID of the event to delete",
},
deleteMode: {
type: "string",
enum: ["single", "future", "all"],
description:
"For recurring events: 'single' = only this occurrence, 'future' = this and all future, 'all' = entire recurring event. Defaults to 'all' for non-recurring events.",
},
occurrenceDate: {
type: "string",
description:
"ISO date string (YYYY-MM-DD) of the specific occurrence to delete. Required for 'single' and 'future' modes.",
},
},
required: ["eventId"],
},

View File

@@ -3,6 +3,7 @@ import {
getDay,
Day,
DAY_TO_GERMAN,
RecurringDeleteMode,
} from "@calchat/shared";
import { AIContext } from "../../services/interfaces";
import { formatDate, formatTime, formatDateTime } from "./eventFormatter";
@@ -111,6 +112,8 @@ export function executeToolCall(
case "proposeDeleteEvent": {
const eventId = args.eventId as string;
const deleteMode = (args.deleteMode as RecurringDeleteMode) || "all";
const occurrenceDate = args.occurrenceDate as string | undefined;
const existingEvent = context.existingEvents.find(
(e) => e.id === eventId,
);
@@ -119,8 +122,24 @@ export function executeToolCall(
return { content: `Event mit ID ${eventId} nicht gefunden.` };
}
// Build descriptive content based on delete mode
let modeDescription = "";
if (existingEvent.isRecurring) {
switch (deleteMode) {
case "single":
modeDescription = " (nur dieses Vorkommen)";
break;
case "future":
modeDescription = " (dieses und alle zukünftigen Vorkommen)";
break;
case "all":
modeDescription = " (alle Vorkommen)";
break;
}
}
return {
content: `Lösch-Vorschlag für "${existingEvent.title}" erstellt.`,
content: `Lösch-Vorschlag für "${existingEvent.title}"${modeDescription} erstellt.`,
proposedChange: {
action: "delete",
eventId,
@@ -131,6 +150,10 @@ export function executeToolCall(
description: existingEvent.description,
isRecurring: existingEvent.isRecurring,
},
deleteMode: existingEvent.isRecurring ? deleteMode : undefined,
occurrenceDate: existingEvent.isRecurring
? occurrenceDate
: undefined,
},
};
}