Files
calchat/apps/client/src/components/ProposedEventCard.tsx
T
Gilmour109 0a2aef2098 fix: recurring event display and AI query improvements
- Use occurrenceStart instead of startTime in getEventsInRange so
  recurring events show their actual occurrence date to the AI
- Add lazy CalDAV sync in ChatService (syncOnce before first DB access)
- Add CaldavService.sync() with internal config check (silent no-op)
- Show German recurrence description (e.g. "Jede Woche") instead of
  generic "Wiederkehrend" in EventCardBase via formatRecurrenceRule()
- Move RepeatType and REPEAT_TYPE_LABELS from editEvent to shared
- Separate calendar overlay useFocusEffect from event loading
2026-02-09 18:17:39 +01:00

181 lines
5.6 KiB
TypeScript

import { View, Text, Pressable } from "react-native";
import { Feather, Ionicons } from "@expo/vector-icons";
import { ProposedEventChange, formatDate, formatTime } from "@calchat/shared";
import { rrulestr } from "rrule";
import { useThemeStore } from "../stores/ThemeStore";
import { EventCardBase } from "./EventCardBase";
type ProposedEventCardProps = {
proposedChange: ProposedEventChange;
onConfirm: (proposal: ProposedEventChange) => void;
onReject: () => void;
onEdit?: (proposal: ProposedEventChange) => void;
};
const ActionButtons = ({
isDisabled,
respondedAction,
showEdit,
onConfirm,
onReject,
onEdit,
}: {
isDisabled: boolean;
respondedAction?: "confirm" | "reject";
showEdit: boolean;
onConfirm: () => void;
onReject: () => void;
onEdit?: () => void;
}) => {
const { theme } = useThemeStore();
return (
<View className="flex-row mt-3 gap-2">
<Pressable
onPress={onConfirm}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"
style={{
backgroundColor: isDisabled
? theme.disabledButton
: theme.confirmButton,
borderWidth: respondedAction === "confirm" ? 2 : 0,
borderColor: theme.confirmButton,
}}
>
<Text style={{ color: theme.buttonText }} className="font-medium">
Annehmen
</Text>
</Pressable>
<Pressable
onPress={onReject}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"
style={{
backgroundColor: isDisabled
? theme.disabledButton
: theme.rejectButton,
borderWidth: respondedAction === "reject" ? 2 : 0,
borderColor: theme.rejectButton,
}}
>
<Text style={{ color: theme.buttonText }} className="font-medium">
Ablehnen
</Text>
</Pressable>
{showEdit && onEdit && (
<Pressable
onPress={onEdit}
className="py-2 px-3 rounded-lg items-center"
style={{
backgroundColor: theme.secondaryBg,
borderWidth: 1,
borderColor: theme.borderPrimary,
}}
>
<Feather name="edit-2" size={18} color={theme.textPrimary} />
</Pressable>
)}
</View>
);
};
export const ProposedEventCard = ({
proposedChange,
onConfirm,
onReject,
onEdit,
}: ProposedEventCardProps) => {
const { theme } = useThemeStore();
const event = proposedChange.event;
const isDisabled = !!proposedChange.respondedAction;
// For delete/single action, the occurrenceDate becomes a new exception
const newExceptionDate =
proposedChange.action === "delete" &&
proposedChange.deleteMode === "single" &&
proposedChange.occurrenceDate;
// For update actions, check if a new UNTIL date is being set
const newUntilDate =
proposedChange.action === "update" &&
event?.recurrenceRule &&
rrulestr(event.recurrenceRule).options.until;
if (!event) {
return null;
}
return (
<View className="mt-2">
<EventCardBase
className="m-2"
title={event.title}
startTime={event.startTime}
endTime={event.endTime}
description={event.description}
recurrenceRule={event.recurrenceRule}
>
{/* 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>
)}
{/* Show conflicting events warning */}
{proposedChange.conflictingEvents &&
proposedChange.conflictingEvents.length > 0 && (
<View className="mb-2">
{proposedChange.conflictingEvents.map((conflict, index) => (
<View key={index} className="flex-row items-center mt-1">
<Ionicons
name="alert-circle"
size={16}
color={theme.rejectButton}
style={{ marginRight: 8 }}
/>
<Text
style={{ color: theme.rejectButton }}
className="text-sm flex-1"
>
Konflikt: {conflict.title} ({formatTime(conflict.startTime)}{" "}
- {formatTime(conflict.endTime)})
</Text>
</View>
))}
</View>
)}
<ActionButtons
isDisabled={isDisabled}
respondedAction={proposedChange.respondedAction}
showEdit={proposedChange.action !== "delete" && !isDisabled}
onConfirm={() => onConfirm(proposedChange)}
onReject={onReject}
onEdit={onEdit ? () => onEdit(proposedChange) : undefined}
/>
</EventCardBase>
</View>
);
};