ultisuite-client/lib/agenda/agenda-events.ts
R3D347HR4Y 3bbf3691b0
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
bordel c'est beau
2026-06-11 10:10:39 +02:00

112 lines
3.4 KiB
TypeScript

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<AgendaEvent, "key" | "start" | "end"> = {
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<number>(
(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
}