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

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

View File

@@ -0,0 +1,48 @@
/**
* German date/time formatting helpers.
* Used across client and server.
*/
/**
* Format date as DD.MM.YYYY
*/
export function formatDate(date: Date): string {
const d = new Date(date);
return d.toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
/**
* Format time as HH:MM
*/
export function formatTime(date: Date): string {
const d = new Date(date);
return d.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
});
}
/**
* Format date and time as DD.MM.YYYY HH:MM:SS
*/
export function formatDateTime(date: Date): string {
const d = new Date(date);
return `${formatDate(d)} ${d.toLocaleTimeString("de-DE")}`;
}
/**
* Format date with weekday as "Mo., DD.MM.YYYY"
*/
export function formatDateWithWeekday(date: Date): string {
const d = new Date(date);
return d.toLocaleDateString("de-DE", {
weekday: "short",
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}

View File

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

View File

@@ -0,0 +1,49 @@
import { rrulestr, Frequency } from "rrule";
export interface ParsedRRule {
freq: string; // "YEARLY", "MONTHLY", "WEEKLY", "DAILY", etc.
until?: Date;
count?: number;
interval?: number;
byDay?: string[]; // ["MO", "WE", "FR"]
}
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",
};
/**
* Parses an RRULE string and extracts the relevant fields.
* Handles both with and without "RRULE:" prefix.
*/
export function parseRRule(ruleString: string): ParsedRRule | null {
if (!ruleString) {
return null;
}
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;
}
}