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

@@ -0,0 +1,162 @@
import { Modal, Pressable, Text, View } from "react-native";
import { RecurringDeleteMode } from "@calchat/shared";
import { useThemeStore } from "../stores/ThemeStore";
type DeleteEventModalProps = {
visible: boolean;
eventTitle: string;
isRecurring: boolean;
onConfirm: (mode: RecurringDeleteMode) => void;
onCancel: () => void;
};
type DeleteOption = {
mode: RecurringDeleteMode;
label: string;
description: string;
};
const RECURRING_DELETE_OPTIONS: DeleteOption[] = [
{
mode: "single",
label: "Nur dieses Vorkommen",
description: "Nur der ausgewaehlte Termin wird entfernt",
},
{
mode: "future",
label: "Dieses und zukuenftige",
description: "Dieser und alle folgenden Termine werden entfernt",
},
{
mode: "all",
label: "Alle Vorkommen",
description: "Die gesamte Terminserie wird geloescht",
},
];
export const DeleteEventModal = ({
visible,
eventTitle,
isRecurring,
onConfirm,
onCancel,
}: DeleteEventModalProps) => {
const { theme } = useThemeStore();
return (
<Modal
visible={visible}
transparent={true}
animationType="fade"
onRequestClose={onCancel}
>
<Pressable
className="flex-1 justify-center items-center"
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
onPress={onCancel}
>
<Pressable
className="w-11/12 rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.primeBg,
borderWidth: 4,
borderColor: theme.borderPrimary,
}}
onPress={(e) => e.stopPropagation()}
>
{/* Header */}
<View
className="px-4 py-3"
style={{
backgroundColor: theme.chatBot,
borderBottomWidth: 3,
borderBottomColor: theme.borderPrimary,
}}
>
<Text
className="font-bold text-lg"
style={{ color: theme.textPrimary }}
>
{isRecurring
? "Wiederkehrenden Termin loeschen"
: "Termin loeschen"}
</Text>
<Text style={{ color: theme.textSecondary }} numberOfLines={1}>
{eventTitle}
</Text>
</View>
{/* Content */}
<View className="p-4">
{isRecurring ? (
// Recurring event: show three options
RECURRING_DELETE_OPTIONS.map((option) => (
<Pressable
key={option.mode}
onPress={() => onConfirm(option.mode)}
className="py-3 px-4 rounded-lg mb-2"
style={{
backgroundColor: theme.secondaryBg,
borderWidth: 1,
borderColor: theme.borderPrimary,
}}
>
<Text
className="font-medium text-base"
style={{ color: theme.textPrimary }}
>
{option.label}
</Text>
<Text
className="text-sm mt-1"
style={{ color: theme.textMuted }}
>
{option.description}
</Text>
</Pressable>
))
) : (
// Non-recurring event: simple confirmation
<View>
<Text
className="text-base mb-4"
style={{ color: theme.textPrimary }}
>
Möchtest du diesen Termin wirklich löschen?
</Text>
<Pressable
onPress={() => onConfirm("all")}
className="py-3 px-4 rounded-lg"
style={{
backgroundColor: theme.rejectButton,
}}
>
<Text
className="font-medium text-base text-center"
style={{ color: theme.buttonText }}
>
Loeschen
</Text>
</Pressable>
</View>
)}
</View>
{/* Cancel button */}
<Pressable
onPress={onCancel}
className="py-3 items-center"
style={{
borderTopWidth: 1,
borderTopColor: theme.placeholderBg,
}}
>
<Text style={{ color: theme.primeFg }} className="font-bold">
Abbrechen
</Text>
</Pressable>
</Pressable>
</Pressable>
</Modal>
);
};