90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
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<number, WorkItem[]>()
|
|
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)
|
|
}
|