import { addDays } from "date-fns" import { parseICSDate } from "./agenda-date.ts" import { fallbackCalendarColor, normalizeAgendaColor, } from "./agenda-colors.ts" import { expandOccurrences, parseRRule } from "./agenda-recurrence.ts" import type { AgendaApiEvent, AgendaCalendar, AgendaEvent, } from "./agenda-types.ts" export function calendarColor(calendar: AgendaCalendar): string { return normalizeAgendaColor(calendar.color) || fallbackCalendarColor(calendar.id) } /** * Transforme les événements API d'un agenda en occurrences affichables, * en développant les récurrences dans la fenêtre demandée. */ export function expandApiEvents( calendar: AgendaCalendar, events: AgendaApiEvent[], rangeStart: Date, rangeEnd: Date, ): AgendaEvent[] { const baseColor = calendarColor(calendar) const out: AgendaEvent[] = [] for (const api of events) { const start = parseICSDate(api.start) if (!start) continue let end = api.end ? parseICSDate(api.end) : null if (!end || end <= start) { end = api.all_day ? addDays(start, 1) : new Date(start.getTime() + 30 * 60000) } const durationMs = end.getTime() - start.getTime() const color = normalizeAgendaColor(api.color) || baseColor const base: Omit = { calendarId: calendar.id, path: api.path ?? "", etag: api.etag ?? "", uid: api.uid, title: api.summary || "(Sans titre)", description: api.description ?? "", location: api.location ?? "", meetUrl: api.meet_url ?? "", organizer: api.organizer ?? "", attendees: api.attendees ?? [], allDay: api.all_day, color, rrule: api.rrule ?? "", recurring: Boolean(api.rrule), master: api, } const rule = api.rrule ? parseRRule(api.rrule) : null if (!rule) { if (end >= rangeStart && start <= rangeEnd) { out.push({ ...base, key: `${base.path}@${start.getTime()}`, start, end }) } continue } const exdates = new Set( (api.exdates ?? []) .map((ex) => parseICSDate(ex)?.getTime()) .filter((t): t is number => typeof t === "number"), ) // Étend la fenêtre vers l'amont pour capter les occurrences qui débordent. const occurrenceWindowStart = new Date(rangeStart.getTime() - durationMs) for (const occStart of expandOccurrences( start, rule, exdates, occurrenceWindowStart, rangeEnd, )) { const occEnd = new Date(occStart.getTime() + durationMs) out.push({ ...base, key: `${base.path}@${occStart.getTime()}`, start: occStart, end: occEnd, }) } } return out.sort((a, b) => a.start.getTime() - b.start.getTime()) } /** Événements (déjà développés) qui couvrent un jour donné. */ export function eventsOnDay(events: AgendaEvent[], day: Date): AgendaEvent[] { const dayStart = new Date(day) dayStart.setHours(0, 0, 0, 0) const dayEnd = addDays(dayStart, 1) return events.filter((e) => e.start < dayEnd && e.end > dayStart) } export function isMultiDay(event: AgendaEvent): boolean { if (event.allDay) { return event.end.getTime() - event.start.getTime() > 24 * 3600 * 1000 } const sameDay = event.start.getFullYear() === event.end.getFullYear() && event.start.getMonth() === event.end.getMonth() && event.start.getDate() === event.end.getDate() return !sameDay }