add event CRUD actions and recurring event expansion

- Implement full CRUD in MongoEventRepository (findById, findByUserId, findByDateRange, update, delete)
- Extend ChatService to handle create/update/delete actions with dynamic test responses
- Add recurrenceExpander utility using rrule library for RRULE parsing
- Add eventFormatters utility for German-localized week/month overviews
- Add German translations for days and months in shared Constants
- Update client ChatService to support all event actions (action, eventId, updates params)
This commit is contained in:
2026-01-04 16:15:30 +01:00
parent 9fecf94c7d
commit 77f15b6dd1
11 changed files with 577 additions and 174 deletions

View File

@@ -0,0 +1,125 @@
import {
MONTHS,
DAY_INDEX_TO_DAY,
DAY_TO_GERMAN,
DAY_TO_GERMAN_SHORT,
MONTH_TO_GERMAN,
} from '@caldav/shared';
import { EventRepository } from '../services/interfaces';
import { expandRecurringEvents, ExpandedEvent } from './recurrenceExpander';
// Private formatting helpers
function formatTime(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
function formatDateShort(date: Date): string {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
return `${day}.${month}.`;
}
function getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
function formatWeeksText(events: ExpandedEvent[], weeks: number): string {
const weeksText = weeks === 1 ? 'die nächste Woche' : `die nächsten ${weeks} Wochen`;
if (events.length === 0) {
return `Du hast für ${weeksText} keine Termine.`;
}
const lines: string[] = [`Hier sind deine Termine für ${weeksText}:\n`];
for (const event of events) {
const day = DAY_INDEX_TO_DAY[event.occurrenceStart.getDay()];
const weekday = DAY_TO_GERMAN[day];
const dateStr = formatDateShort(event.occurrenceStart);
const timeStr = formatTime(event.occurrenceStart);
lines.push(`${weekday}, ${dateStr} - ${timeStr} Uhr: ${event.title}`);
}
lines.push(`\nInsgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'}.`);
return lines.join('\n');
}
function formatMonthText(events: ExpandedEvent[], monthName: string): string {
if (events.length === 0) {
return `Du hast im ${monthName} keine Termine.`;
}
// Group events by calendar week
const weekGroups = new Map<number, ExpandedEvent[]>();
for (const event of events) {
const weekNum = getWeekNumber(event.occurrenceStart);
if (!weekGroups.has(weekNum)) {
weekGroups.set(weekNum, []);
}
weekGroups.get(weekNum)!.push(event);
}
const lines: string[] = [`Hier ist deine Monatsübersicht für ${monthName}:\n`];
// Sort weeks and format
const sortedWeeks = Array.from(weekGroups.keys()).sort((a, b) => a - b);
for (const weekNum of sortedWeeks) {
const weekEvents = weekGroups.get(weekNum)!;
lines.push(`KW ${weekNum}: ${weekEvents.length} Termin${weekEvents.length === 1 ? '' : 'e'}`);
for (const event of weekEvents) {
const day = DAY_INDEX_TO_DAY[event.occurrenceStart.getDay()];
const weekdayShort = DAY_TO_GERMAN_SHORT[day];
const dateStr = formatDateShort(event.occurrenceStart);
const timeStr = formatTime(event.occurrenceStart);
lines.push(`${weekdayShort} ${dateStr}, ${timeStr}: ${event.title}`);
}
lines.push('');
}
lines.push(`Insgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'} im ${monthName}.`);
return lines.join('\n');
}
// Public API
/**
* Get a formatted overview of events for the next x weeks.
* Recurring events are expanded to show all occurrences within the range.
*/
export async function getWeeksOverview(
eventRepo: EventRepository,
userId: string,
weeks: number
): Promise<string> {
const now = new Date();
const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000);
const events = await eventRepo.findByUserId(userId);
const expanded = expandRecurringEvents(events, now, endDate);
return formatWeeksText(expanded, weeks);
}
/**
* Get a formatted overview of events for a specific month.
* Recurring events are expanded to show all occurrences within the month.
*/
export async function getMonthOverview(
eventRepo: EventRepository,
userId: string,
year: number,
month: number
): Promise<string> {
const startOfMonth = new Date(year, month, 1);
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
const events = await eventRepo.findByUserId(userId);
const expanded = expandRecurringEvents(events, startOfMonth, endOfMonth);
const monthName = MONTH_TO_GERMAN[MONTHS[month]];
return formatMonthText(expanded, monthName);
}