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

@@ -10,11 +10,16 @@ import {
UpdateEventDTO,
EventAction,
CreateMessageDTO,
RecurringDeleteMode,
} from "@calchat/shared";
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
import { EventService } from "./EventService";
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
type TestResponse = { content: string; proposedChanges?: ProposedEventChange[] };
type TestResponse = {
content: string;
proposedChanges?: ProposedEventChange[];
};
// Test response index (cycles through responses)
let responseIndex = 0;
@@ -25,8 +30,7 @@ const staticResponses: TestResponse[] = [
// === MULTI-EVENT TEST RESPONSES ===
// Response 0: 3 Meetings an verschiedenen Tagen
{
content:
"Alles klar! Ich erstelle dir 3 Team-Meetings für diese Woche:",
content: "Alles klar! Ich erstelle dir 3 Team-Meetings für diese Woche:",
proposedChanges: [
{
id: "multi-1-a",
@@ -62,8 +66,7 @@ const staticResponses: TestResponse[] = [
},
// Response 1: 5 Termine für einen Projekttag
{
content:
"Ich habe deinen kompletten Projekttag am Dienstag geplant:",
content: "Ich habe deinen kompletten Projekttag am Dienstag geplant:",
proposedChanges: [
{
id: "multi-2-a",
@@ -119,8 +122,7 @@ const staticResponses: TestResponse[] = [
},
// Response 2: 2 wiederkehrende Termine
{
content:
"Ich erstelle dir zwei wiederkehrende Fitness-Termine:",
content: "Ich erstelle dir zwei wiederkehrende Fitness-Termine:",
proposedChanges: [
{
id: "multi-3-a",
@@ -209,7 +211,8 @@ const staticResponses: TestResponse[] = [
title: "Arzttermin Dr. Müller",
startTime: getDay("Wednesday", 1, 9, 30),
endTime: getDay("Wednesday", 1, 10, 30),
description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
description:
"Routineuntersuchung - Versichertenkarte nicht vergessen",
},
},
],
@@ -403,6 +406,7 @@ export class ChatService {
constructor(
private chatRepo: ChatRepository,
private eventRepo: EventRepository,
private eventService: EventService,
private aiProvider: AIProvider,
) {}
@@ -462,9 +466,15 @@ export class ChatService {
event?: CreateEventDTO,
eventId?: string,
updates?: UpdateEventDTO,
deleteMode?: RecurringDeleteMode,
occurrenceDate?: string,
): Promise<ChatResponse> {
// Update specific proposal with respondedAction
await this.chatRepo.updateProposalResponse(messageId, proposalId, "confirm");
await this.chatRepo.updateProposalResponse(
messageId,
proposalId,
"confirm",
);
// Perform the actual event operation
let content: string;
@@ -478,10 +488,25 @@ export class ChatService {
? `Der Termin "${updatedEvent.title}" wurde aktualisiert.`
: "Termin nicht gefunden.";
} else if (action === "delete" && eventId) {
await this.eventRepo.delete(eventId);
// Use deleteRecurring for proper handling of recurring events
const mode = deleteMode || "all";
await this.eventService.deleteRecurring(
eventId,
userId,
mode,
occurrenceDate,
);
// Build appropriate response message
let deleteDescription = "";
if (deleteMode === "single") {
deleteDescription = " (dieses Vorkommen)";
} else if (deleteMode === "future") {
deleteDescription = " (dieses und zukünftige Vorkommen)";
}
content = event?.title
? `Der Termin "${event.title}" wurde gelöscht.`
: "Der Termin wurde gelöscht.";
? `Der Termin "${event.title}"${deleteDescription} wurde gelöscht.`
: `Der Termin${deleteDescription} wurde gelöscht.`;
} else {
content = "Ungültige Aktion.";
}