diff --git a/CLAUDE.md b/CLAUDE.md
index d7dd982..7f8b520 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -77,9 +77,10 @@ src/
├── components/
│ ├── BaseBackground.tsx # Common screen wrapper
│ ├── Header.tsx # Header component
-│ ├── EventCard.tsx # Event card for calendar display
+│ ├── EventCardBase.tsx # Shared event card layout with icons (used by EventCard & ProposedEventCard)
+│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal
-│ └── ProposedEventCard.tsx # Inline event proposal with confirm/reject buttons
+│ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons)
├── Themes.tsx # Centralized color/theme definitions
├── services/
│ ├── index.ts # Re-exports all services
@@ -305,8 +306,9 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
- `ApiClient`: get(), post(), put(), delete() implemented
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented
- `ChatService`: sendMessage(), confirmEvent(convId, msgId, action, event?, eventId?, updates?), rejectEvent() - supports create/update/delete actions
-- `EventCard`: Displays event details (title, date, time, duration, recurring indicator) with Feather icons and edit/delete buttons
-- `ProposedEventCard`: Displays proposed events (title, date, description, recurring indicator) with confirm/reject buttons
+- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
+- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display
+- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[]
- `ChatStore`: Zustand store with addMessage(), updateMessage(), clearMessages() - persists messages across tab switches
diff --git a/apps/client/src/components/EventCard.tsx b/apps/client/src/components/EventCard.tsx
index b6b3683..b71ccd5 100644
--- a/apps/client/src/components/EventCard.tsx
+++ b/apps/client/src/components/EventCard.tsx
@@ -1,7 +1,8 @@
-import { View, Text, Pressable } from "react-native";
+import { View, Pressable } from "react-native";
import { ExpandedEvent } from "@caldav/shared";
import { Feather } from "@expo/vector-icons";
import currentTheme from "../Themes";
+import { EventCardBase } from "./EventCardBase";
type EventCardProps = {
event: ExpandedEvent;
@@ -9,117 +10,16 @@ type EventCardProps = {
onDelete: () => void;
};
-function formatDate(date: Date): string {
- const d = new Date(date);
- return d.toLocaleDateString("de-DE", {
- weekday: "short",
- day: "2-digit",
- month: "2-digit",
- year: "numeric",
- });
-}
-
-function formatTime(date: Date): string {
- const d = new Date(date);
- return d.toLocaleTimeString("de-DE", {
- hour: "2-digit",
- minute: "2-digit",
- });
-}
-
-function formatDuration(start: Date, end: Date): string {
- const startDate = new Date(start);
- const endDate = new Date(end);
- const diffMs = endDate.getTime() - startDate.getTime();
- const diffMins = Math.round(diffMs / 60000);
-
- if (diffMins < 60) {
- return `${diffMins} min`;
- }
-
- const hours = Math.floor(diffMins / 60);
- const mins = diffMins % 60;
-
- if (mins === 0) {
- return hours === 1 ? "1 Std" : `${hours} Std`;
- }
-
- return `${hours} Std ${mins} min`;
-}
-
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
return (
-
- {/* Header with title */}
-
+
- {event.title}
-
-
- {/* Content */}
-
- {/* Date */}
-
-
-
- {formatDate(event.occurrenceStart)}
-
-
-
- {/* Time with duration */}
-
-
-
- {formatTime(event.occurrenceStart)} -{" "}
- {formatTime(event.occurrenceEnd)} (
- {formatDuration(event.occurrenceStart, event.occurrenceEnd)})
-
-
-
- {/* Recurring indicator */}
- {event.isRecurring && (
-
-
-
- Wiederkehrend
-
-
- )}
-
- {/* Description */}
- {event.description && (
-
- {event.description}
-
- )}
-
{/* Action buttons */}
{
/>
-
+
);
};
diff --git a/apps/client/src/components/EventCardBase.tsx b/apps/client/src/components/EventCardBase.tsx
new file mode 100644
index 0000000..02ec251
--- /dev/null
+++ b/apps/client/src/components/EventCardBase.tsx
@@ -0,0 +1,141 @@
+import { View, Text } from "react-native";
+import { Feather } from "@expo/vector-icons";
+import { ReactNode } from "react";
+import currentTheme from "../Themes";
+
+type EventCardBaseProps = {
+ className?: string;
+ title: string;
+ startTime: Date;
+ endTime: Date;
+ description?: string;
+ isRecurring?: boolean;
+ children?: ReactNode;
+};
+
+function formatDate(date: Date): string {
+ const d = new Date(date);
+ return d.toLocaleDateString("de-DE", {
+ weekday: "short",
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ });
+}
+
+function formatTime(date: Date): string {
+ const d = new Date(date);
+ return d.toLocaleTimeString("de-DE", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+function formatDuration(start: Date, end: Date): string {
+ const startDate = new Date(start);
+ const endDate = new Date(end);
+ const diffMs = endDate.getTime() - startDate.getTime();
+ const diffMins = Math.round(diffMs / 60000);
+
+ if (diffMins < 60) {
+ return `${diffMins} min`;
+ }
+
+ const hours = Math.floor(diffMins / 60);
+ const mins = diffMins % 60;
+
+ if (mins === 0) {
+ return hours === 1 ? "1 Std" : `${hours} Std`;
+ }
+
+ return `${hours} Std ${mins} min`;
+}
+
+export const EventCardBase = ({
+ className,
+ title,
+ startTime,
+ endTime,
+ description,
+ isRecurring,
+ children,
+}: EventCardBaseProps) => {
+ return (
+
+ {/* Header with title */}
+
+ {title}
+
+
+ {/* Content */}
+
+ {/* Date */}
+
+
+
+ {formatDate(startTime)}
+
+
+
+ {/* Time with duration */}
+
+
+
+ {formatTime(startTime)} - {formatTime(endTime)} (
+ {formatDuration(startTime, endTime)})
+
+
+
+ {/* Recurring indicator */}
+ {isRecurring && (
+
+
+
+ Wiederkehrend
+
+
+ )}
+
+ {/* Description */}
+ {description && (
+
+ {description}
+
+ )}
+
+ {/* Action buttons slot */}
+ {children}
+
+
+ );
+};
+
+export default EventCardBase;
diff --git a/apps/client/src/components/ProposedEventCard.tsx b/apps/client/src/components/ProposedEventCard.tsx
index 34d3348..973f11c 100644
--- a/apps/client/src/components/ProposedEventCard.tsx
+++ b/apps/client/src/components/ProposedEventCard.tsx
@@ -1,6 +1,7 @@
import { View, Text, Pressable } from "react-native";
import { ProposedEventChange } from "@caldav/shared";
import currentTheme from "../Themes";
+import { EventCardBase } from "./EventCardBase";
type ProposedEventCardProps = {
proposedChange: ProposedEventChange;
@@ -9,17 +10,52 @@ type ProposedEventCardProps = {
onReject: () => void;
};
-function formatDateTime(date?: Date): string {
- if (!date) return "";
- const d = new Date(date);
- return d.toLocaleDateString("de-DE", {
- weekday: "long",
- day: "2-digit",
- month: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- });
-}
+const ConfirmRejectButtons = ({
+ isDisabled,
+ respondedAction,
+ onConfirm,
+ onReject,
+}: {
+ isDisabled: boolean;
+ respondedAction?: "confirm" | "reject";
+ onConfirm: () => void;
+ onReject: () => void;
+}) => (
+
+
+
+ Annehmen
+
+
+
+
+ Ablehnen
+
+
+
+);
export const ProposedEventCard = ({
proposedChange,
@@ -30,71 +66,27 @@ export const ProposedEventCard = ({
const event = proposedChange.event;
const isDisabled = !!respondedAction;
- return (
-
- {/* Event Details */}
- {event?.title}
-
- {formatDateTime(event?.startTime)}
-
- {event?.description && (
-
- {event.description}
-
- )}
- {event?.isRecurring && (
-
- Wiederkehrend
-
- )}
+ if (!event) {
+ return null;
+ }
- {/* Buttons */}
-
-
-
- Annehmen
-
-
-
-
- Ablehnen
-
-
-
+ return (
+
+
+
+
);
};
diff --git a/apps/server/src/services/ChatService.ts b/apps/server/src/services/ChatService.ts
index 4061842..2d69897 100644
--- a/apps/server/src/services/ChatService.ts
+++ b/apps/server/src/services/ChatService.ts
@@ -175,11 +175,17 @@ async function getTestResponse(
const jensEvent = events.find((e) => e.title === "Meeting mit Jens");
if (jensEvent) {
return {
- content:
- "Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:",
+ content: "Soll ich diesen Termin wirklich löschen?",
proposedChange: {
action: "delete",
eventId: jensEvent.id,
+ event: {
+ title: jensEvent.title,
+ startTime: jensEvent.startTime,
+ endTime: jensEvent.endTime,
+ description: jensEvent.description,
+ isRecurring: jensEvent.isRecurring,
+ },
},
};
}
@@ -285,7 +291,9 @@ export class ChatService {
: "Termin nicht gefunden.";
} else if (action === "delete" && eventId) {
await this.eventRepo.delete(eventId);
- content = "Der Termin wurde gelöscht.";
+ content = event?.title
+ ? `Der Termin "${event.title}" wurde gelöscht.`
+ : "Der Termin wurde gelöscht.";
} else {
content = "Ungültige Aktion.";
}