"use client" import { useEffect, useMemo, useRef, useState, type MouseEvent, type PointerEvent as ReactPointerEvent, } from "react" import { format, isSameDay } from "date-fns" import { fr } from "date-fns/locale" import { AgendaEventChip } from "@/components/agenda/agenda-event-chip" import type { AnchorRect } from "@/components/agenda/agenda-floating-card" import { formatEventTime, roundToStep } from "@/lib/agenda/agenda-date" import { readableTextColor } from "@/lib/agenda/agenda-colors" import { layoutDayEvents } from "@/lib/agenda/agenda-event-layout" import { eventsOnDay, isMultiDay } from "@/lib/agenda/agenda-events" import type { AgendaEvent } from "@/lib/agenda/agenda-types" import { cn } from "@/lib/utils" const HOUR_PX = 48 const GUTTER_PX = 56 const MIN_EVENT_PX = 22 function anchorFromEvent(e: MouseEvent): AnchorRect { const rect = e.currentTarget.getBoundingClientRect() return { left: rect.left, top: rect.top, width: rect.width, height: rect.height } } interface DragState { dayIndex: number anchorMin: number startMin: number endMin: number moved: boolean } export function AgendaViewWeek({ days, events, onCreateRange, onEventClick, onOpenDay, }: { days: Date[] events: AgendaEvent[] onCreateRange: (start: Date, end: Date, allDay: boolean, anchor: AnchorRect) => void onEventClick: (event: AgendaEvent, anchor: AnchorRect) => void onOpenDay: (day: Date) => void }) { const scrollRef = useRef(null) const [drag, setDrag] = useState(null) const [now, setNow] = useState(() => new Date()) useEffect(() => { const id = window.setInterval(() => setNow(new Date()), 60_000) return () => window.clearInterval(id) }, []) useEffect(() => { const el = scrollRef.current if (!el) return const target = Math.max(0, (Math.min(now.getHours(), 18) - 1.5) * HOUR_PX) el.scrollTop = target // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const perDay = useMemo( () => days.map((day) => { const dayEvents = eventsOnDay(events, day) const banners = dayEvents .filter((e) => e.allDay || isMultiDay(e)) .sort((a, b) => a.start.getTime() - b.start.getTime()) const timed = dayEvents.filter((e) => !e.allDay && !isMultiDay(e)) return { day, banners, positioned: layoutDayEvents(timed, day) } }), [days, events], ) const hasBanners = perDay.some((d) => d.banners.length > 0) const colTemplate = `${GUTTER_PX}px repeat(${days.length}, minmax(0, 1fr))` const minuteFromPointer = (e: ReactPointerEvent): number => { const rect = e.currentTarget.getBoundingClientRect() const minutes = ((e.clientY - rect.top) / HOUR_PX) * 60 return Math.min(24 * 60, Math.max(0, roundToStep(minutes, 15))) } const handlePointerDown = (dayIndex: number) => (e: ReactPointerEvent) => { if (e.button !== 0 || (e.target as HTMLElement).closest("[data-agenda-event]")) return const min = minuteFromPointer(e) e.currentTarget.setPointerCapture(e.pointerId) setDrag({ dayIndex, anchorMin: min, startMin: min, endMin: min + 15, moved: false }) } const handlePointerMove = (dayIndex: number) => (e: ReactPointerEvent) => { setDrag((d) => { if (!d || d.dayIndex !== dayIndex) return d const min = minuteFromPointer(e) if (min === d.anchorMin && !d.moved) return d return { ...d, moved: true, startMin: Math.min(d.anchorMin, min), endMin: Math.max(d.anchorMin, min, Math.min(d.anchorMin, min) + 15), } }) } const handlePointerUp = (dayIndex: number) => (e: ReactPointerEvent) => { if (!drag || drag.dayIndex !== dayIndex) return const day = days[dayIndex] const startMin = drag.startMin const endMin = drag.moved ? drag.endMin : drag.startMin + 60 const start = new Date(day) start.setHours(0, startMin, 0, 0) const end = new Date(day) end.setHours(0, Math.max(endMin, startMin + 15), 0, 0) const colRect = e.currentTarget.getBoundingClientRect() const anchor: AnchorRect = { left: colRect.left, top: colRect.top + (startMin / 60) * HOUR_PX, width: colRect.width, height: Math.max(((endMin - startMin) / 60) * HOUR_PX, MIN_EVENT_PX), } setDrag(null) onCreateRange(start, end, false, anchor) } return (
{/* En-tête : jours + rangée journée entière */}
{days.map((day) => { const isToday = isSameDay(day, now) return (
{format(day, "EEE", { locale: fr }).replace(".", "")}
) })}
{hasBanners && (
{perDay.map(({ day, banners }) => (
{banners.map((event) => ( { e.stopPropagation() onEventClick(event, anchorFromEvent(e)) }} /> ))}
))}
)}
{/* Grille horaire */}
{/* Gouttière heures */}
{Array.from({ length: 23 }, (_, i) => i + 1).map((h) => ( {String(h).padStart(2, "0")}:00 ))}
{perDay.map(({ day, positioned }, dayIndex) => { const isToday = isSameDay(day, now) const nowTop = (now.getHours() * 60 + now.getMinutes()) * (HOUR_PX / 60) return (
{/* Lignes d'heures */} {Array.from({ length: 23 }, (_, i) => i + 1).map((h) => (
))} {/* Sélection en cours */} {drag && drag.dayIndex === dayIndex && drag.moved && (
{formatMinutes(drag.startMin)} – {formatMinutes(drag.endMin)}
)} {/* Événements positionnés */} {positioned.map(({ event, top, duration, leftPct, widthPct }) => { const compact = (duration / 60) * HOUR_PX < 40 return ( ) })} {/* Indicateur maintenant */} {isToday && (
)}
) })}
) } function formatMinutes(min: number): string { const h = Math.floor(min / 60) const m = min % 60 return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}` }