0a2aef2098
- 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
181 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
};
|