feat: add EditEventScreen with calendar and chat mode support

Add a unified event editor that works in two modes:
- Calendar mode: Create/edit events directly via EventService API
- Chat mode: Edit AI-proposed events before confirming them

The chat mode allows users to modify proposed events (title, time,
recurrence) and persists changes both locally and to the server.

New components: DateTimePicker, ScrollableDropdown, useDropdownPosition
New API: PUT /api/chat/messages/:messageId/proposal
This commit is contained in:
2026-01-31 18:46:31 +01:00
parent 617543a603
commit 6f0d172bf2
33 changed files with 1394 additions and 289 deletions

View File

@@ -26,7 +26,6 @@ export interface CreateEventDTO {
startTime: Date;
endTime: Date;
note?: string;
isRecurring?: boolean;
recurrenceRule?: string;
exceptionDates?: string[]; // For display in proposals
}
@@ -37,7 +36,6 @@ export interface UpdateEventDTO {
startTime?: Date;
endTime?: Date;
note?: string;
isRecurring?: boolean;
recurrenceRule?: string;
exceptionDates?: string[];
}

View File

@@ -34,3 +34,15 @@ export function getDay(
result.setHours(hour, minute, 0, 0);
return result;
}
/**
* Check if an event spans multiple days.
* Compares dates at midnight to determine if start and end are on different calendar days.
*/
export function isMultiDayEvent(start: Date, end: Date): boolean {
const startDate = new Date(start);
const endDate = new Date(end);
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
return startDate.getTime() !== endDate.getTime();
}

View File

@@ -46,3 +46,26 @@ export function formatDateWithWeekday(date: Date): string {
year: "numeric",
});
}
/**
* Format date as DD.MM. (short, without year)
*/
export function formatDateShort(date: Date): string {
const d = new Date(date);
return d.toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
});
}
/**
* Format date with weekday short as "Mo., DD.MM."
*/
export function formatDateWithWeekdayShort(date: Date): string {
const d = new Date(date);
return d.toLocaleDateString("de-DE", {
weekday: "short",
day: "2-digit",
month: "2-digit",
});
}

View File

@@ -1,3 +1,3 @@
export * from "./dateHelpers";
export * from "./rruleHelpers";
export * from "./formatters";
export * from "./rruleHelpers";

View File

@@ -1,49 +1,29 @@
import { rrulestr, Frequency } from "rrule";
/**
* RRULE building and parsing helpers.
*/
export interface ParsedRRule {
freq: string; // "YEARLY", "MONTHLY", "WEEKLY", "DAILY", etc.
until?: Date;
count?: number;
interval?: number;
byDay?: string[]; // ["MO", "WE", "FR"]
}
export type RepeatType = "Tag" | "Woche" | "Monat" | "Jahr";
const FREQ_NAMES: Record<Frequency, string> = {
[Frequency.YEARLY]: "YEARLY",
[Frequency.MONTHLY]: "MONTHLY",
[Frequency.WEEKLY]: "WEEKLY",
[Frequency.DAILY]: "DAILY",
[Frequency.HOURLY]: "HOURLY",
[Frequency.MINUTELY]: "MINUTELY",
[Frequency.SECONDLY]: "SECONDLY",
const REPEAT_TYPE_TO_FREQ: Record<RepeatType, string> = {
Tag: "DAILY",
Woche: "WEEKLY",
Monat: "MONTHLY",
Jahr: "YEARLY",
};
/**
* Parses an RRULE string and extracts the relevant fields.
* Handles both with and without "RRULE:" prefix.
* Build an RRULE string from repeat count and type.
*
* @param repeatType - The type of repetition (Tag, Woche, Monat, Jahr)
* @param interval - The interval between repetitions (default: 1)
* @returns RRULE string like "FREQ=WEEKLY;INTERVAL=2"
*/
export function parseRRule(ruleString: string): ParsedRRule | null {
if (!ruleString) {
return null;
export function buildRRule(repeatType: RepeatType, interval: number = 1): string {
const freq = REPEAT_TYPE_TO_FREQ[repeatType];
if (interval <= 1) {
return `FREQ=${freq}`;
}
try {
// Ensure RRULE: prefix is present
const normalized = ruleString.startsWith("RRULE:")
? ruleString
: `RRULE:${ruleString}`;
const rule = rrulestr(normalized);
const options = rule.options;
return {
freq: FREQ_NAMES[options.freq] || "UNKNOWN",
until: options.until || undefined,
count: options.count || undefined,
interval: options.interval > 1 ? options.interval : undefined,
byDay: options.byweekday?.map((d) => d.toString()) || undefined,
};
} catch {
return null;
}
return `FREQ=${freq};INTERVAL=${interval}`;
}