import type { AgendaEvent } from "./agenda-types.ts" export interface PositionedEvent { event: AgendaEvent /** Minutes depuis minuit. */ top: number /** Durée en minutes (minimum appliqué côté rendu). */ duration: number leftPct: number widthPct: number } interface WorkItem { event: AgendaEvent startMin: number endMin: number col: number cluster: number } /** * Positionne les événements horaires d'une colonne jour façon Google Calendar : * les événements qui se chevauchent se partagent la largeur par colonnes. */ export function layoutDayEvents(events: AgendaEvent[], day: Date): PositionedEvent[] { const dayStart = new Date(day) dayStart.setHours(0, 0, 0, 0) const dayStartMs = dayStart.getTime() const minutesInDay = 24 * 60 const items: WorkItem[] = events .map((event) => { const startMin = Math.max(0, (event.start.getTime() - dayStartMs) / 60000) const endMin = Math.min(minutesInDay, (event.end.getTime() - dayStartMs) / 60000) return { event, startMin, endMin: Math.max(endMin, startMin + 15), col: 0, cluster: 0 } }) .filter((it) => it.startMin < minutesInDay && it.endMin > 0) .sort((a, b) => a.startMin - b.startMin || b.endMin - a.endMin) // Regroupe en clusters d'événements transitivement chevauchants. let clusterId = 0 let clusterEnd = -1 for (const it of items) { if (it.startMin >= clusterEnd) { clusterId++ clusterEnd = it.endMin } else { clusterEnd = Math.max(clusterEnd, it.endMin) } it.cluster = clusterId } const positioned: PositionedEvent[] = [] const clusters = new Map() for (const it of items) { const list = clusters.get(it.cluster) ?? [] list.push(it) clusters.set(it.cluster, list) } for (const list of clusters.values()) { // Attribution gloutonne de colonnes. const colEnds: number[] = [] for (const it of list) { let col = colEnds.findIndex((end) => end <= it.startMin) if (col === -1) { col = colEnds.length colEnds.push(it.endMin) } else { colEnds[col] = it.endMin } it.col = col } const colCount = colEnds.length const width = 100 / colCount for (const it of list) { positioned.push({ event: it.event, top: it.startMin, duration: it.endMin - it.startMin, leftPct: it.col * width, // Léger débord à la Google quand il reste de la place à droite. widthPct: it.col === colCount - 1 ? width : Math.min(width * 1.7, 100 - it.col * width), }) } } return positioned.sort((a, b) => a.top - b.top) }