feat: add RRULE parsing to shared package and improve ProposedEventCard UI

- Add rrule library to shared package for RRULE string parsing
- Add rruleHelpers.ts with parseRRule() returning freq, until, count, interval, byDay
- Add formatters.ts with German date/time formatters for client and server
- Extend CreateEventDTO with exceptionDates field for proposals
- Extend ChatModel schema with exceptionDates, deleteMode, occurrenceDate
- Update proposeUpdateEvent tool to support isRecurring and recurrenceRule params
- ProposedEventCard now shows green "Neue Ausnahme" and "Neues Ende" text
- Add Sport test scenario with dynamic exception and UNTIL responses
- Update CLAUDE.md documentation
This commit is contained in:
2026-01-27 21:15:19 +01:00
parent 4575483940
commit 617543a603
15 changed files with 359 additions and 51 deletions

View File

@@ -1,34 +1,15 @@
import { View, Text, Pressable } from "react-native";
import { ProposedEventChange, RecurringDeleteMode } from "@calchat/shared";
import { Feather } from "@expo/vector-icons";
import { ProposedEventChange, parseRRule, formatDate } from "@calchat/shared";
import { useThemeStore } from "../stores/ThemeStore";
import { EventCardBase } from "./EventCardBase";
const DELETE_MODE_LABELS: Record<RecurringDeleteMode, string> = {
single: "Nur dieses Vorkommen",
future: "Dieses & zukuenftige",
all: "Alle Vorkommen",
};
type ProposedEventCardProps = {
proposedChange: ProposedEventChange;
onConfirm: () => void;
onReject: () => void;
};
const DeleteModeBadge = ({ mode }: { mode: RecurringDeleteMode }) => {
const { theme } = useThemeStore();
return (
<View
className="self-start px-2 py-1 rounded-md mb-2"
style={{ backgroundColor: theme.rejectButton }}
>
<Text style={{ color: theme.buttonText }} className="text-xs font-medium">
{DELETE_MODE_LABELS[mode]}
</Text>
</View>
);
};
const ConfirmRejectButtons = ({
isDisabled,
respondedAction,
@@ -84,15 +65,21 @@ export const ProposedEventCard = ({
onConfirm,
onReject,
}: ProposedEventCardProps) => {
const { theme } = useThemeStore();
const event = proposedChange.event;
// respondedAction is now part of the proposedChange
const isDisabled = !!proposedChange.respondedAction;
// Show delete mode badge for delete actions on recurring events
const showDeleteModeBadge =
// For delete/single action, the occurrenceDate becomes a new exception
const newExceptionDate =
proposedChange.action === "delete" &&
event?.isRecurring &&
proposedChange.deleteMode;
proposedChange.deleteMode === "single" &&
proposedChange.occurrenceDate;
// For update actions, check if a new UNTIL date is being set
const newUntilDate =
proposedChange.action === "update" &&
event?.recurrenceRule &&
parseRRule(event.recurrenceRule)?.until;
if (!event) {
return null;
@@ -108,8 +95,33 @@ export const ProposedEventCard = ({
description={event.description}
isRecurring={event.isRecurring}
>
{showDeleteModeBadge && (
<DeleteModeBadge mode={proposedChange.deleteMode!} />
{/* Show new exception date for delete/single actions */}
{newExceptionDate && (
<View className="flex-row items-center mb-2">
<Feather
name="plus-circle"
size={16}
color={theme.confirmButton}
style={{ marginRight: 8 }}
/>
<Text style={{ color: theme.confirmButton }} className="font-medium">
Neue Ausnahme: {formatDate(new Date(proposedChange.occurrenceDate!))}
</Text>
</View>
)}
{/* Show new UNTIL date for update actions */}
{newUntilDate && (
<View className="flex-row items-center mb-2">
<Feather
name="plus-circle"
size={16}
color={theme.confirmButton}
style={{ marginRight: 8 }}
/>
<Text style={{ color: theme.confirmButton }} className="font-medium">
Neues Ende: {formatDate(newUntilDate)}
</Text>
</View>
)}
<ConfirmRejectButtons
isDisabled={isDisabled}