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(); 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 { 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 { 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); }