112 lines
3.4 KiB
TypeScript
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
|
|
}
|