"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useParams, useRouter, useSearchParams } from "next/navigation" import { addDays, addHours, addMinutes, addMonths, startOfHour } from "date-fns" import { AgendaEventDialog, type AgendaEventDialogState } from "@/components/agenda/agenda-event-dialog" import { AgendaEventPopover, type AgendaEventPopoverState } from "@/components/agenda/agenda-event-popover" import type { AnchorRect } from "@/components/agenda/agenda-floating-card" import { AgendaHeader } from "@/components/agenda/agenda-header" import { AgendaQuickCreate, type AgendaQuickCreateState } from "@/components/agenda/agenda-quick-create" import { AgendaSidebar } from "@/components/agenda/agenda-sidebar" import { AgendaViewMonth } from "@/components/agenda/agenda-view-month" import { AgendaViewWeek } from "@/components/agenda/agenda-view-week" import { Skeleton } from "@/components/ui/skeleton" import { useAgendaEvents } from "@/lib/api/hooks/use-calendar-queries" import { isExternalCalendarId } from "@/lib/agenda/agenda-calendar-visibility" import { parseICSDate, viewDays, viewRange } from "@/lib/agenda/agenda-date" import { useExternalAgendaEvents } from "@/lib/agenda/use-external-agenda-events" import { useResolvedWeekStartsOn } from "@/lib/agenda/use-resolved-week-start" import { draftToPendingEvent } from "@/lib/agenda/agenda-pending-event" import { useAgendaSettingsStore } from "@/lib/agenda/agenda-store" import { useAgendaEventMove } from "@/lib/agenda/use-agenda-event-move" import { useVisibleAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars" import type { AgendaEvent, AgendaEventDraft } from "@/lib/agenda/agenda-types" import { useAgendaRouteRoot } from "@/lib/agenda/agenda-route-context" import { buildAgendaPath, parseAgendaSegments, type AgendaView, } from "@/lib/agenda/agenda-url" import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity" import { useIsMobile } from "@/hooks/use-mobile" import { AGENDA_CALENDAR_CARD_CLASS, AGENDA_MAIN_INSET_X } from "@/lib/agenda/agenda-chrome-classes" import { cn } from "@/lib/utils" export function AgendaPage() { const router = useRouter() const params = useParams() const searchParams = useSearchParams() const agendaRouteRoot = useAgendaRouteRoot() const isMobile = useIsMobile() const identity = useChromeIdentity() const route = useMemo( () => parseAgendaSegments(params.segments as string[] | undefined), [params.segments], ) const lastView = useAgendaSettingsStore((s) => s.lastView) const setLastView = useAgendaSettingsStore((s) => s.setLastView) const externalCalendars = useAgendaSettingsStore((s) => s.externalCalendars) const weekStart = useAgendaSettingsStore((s) => s.weekStart) const weekStartsOn = useResolvedWeekStartsOn(weekStart) const defaultQuickDurationMinutes = useAgendaSettingsStore( (s) => s.defaultQuickDurationMinutes, ) const view: AgendaView = route.view ?? (isMobile ? "day" : lastView) const date = route.date useEffect(() => { if (!route.view) { router.replace( buildAgendaPath(view, date, agendaRouteRoot) + window.location.search ) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [route.view]) useEffect(() => { if (route.view) setLastView(route.view) }, [route.view, setLastView]) const navigate = useCallback( (nextView: AgendaView, nextDate: Date) => { router.push(buildAgendaPath(nextView, nextDate, agendaRouteRoot)) }, [router, agendaRouteRoot], ) const step = useCallback( (delta: 1 | -1) => { const next = view === "month" ? addMonths(date, delta) : addDays(date, view === "week" ? delta * 7 : delta) navigate(view, next) }, [view, date, navigate], ) const { calendars, visibleCalendars, isLoading: calendarsLoading, } = useVisibleAgendaCalendars() const visibleApiCalendars = useMemo( () => visibleCalendars.filter((calendar) => !isExternalCalendarId(calendar.id)), [visibleCalendars], ) const visibleExternalCalendars = useMemo( () => externalCalendars.filter((calendar) => visibleCalendars.some((visible) => visible.id === calendar.id), ), [externalCalendars, visibleCalendars], ) const writableCalendars = useMemo( () => calendars.filter((calendar) => !isExternalCalendarId(calendar.id)), [calendars], ) const writableVisibleCalendars = useMemo( () => visibleCalendars.filter((calendar) => !isExternalCalendarId(calendar.id)), [visibleCalendars], ) const fetchRange = useMemo( () => viewRange("month", date, weekStart, weekStartsOn), [date, weekStart, weekStartsOn], ) const { events: apiEvents } = useAgendaEvents( visibleApiCalendars, fetchRange.start, fetchRange.end, ) const { events: externalEvents } = useExternalAgendaEvents( visibleExternalCalendars, fetchRange.start, fetchRange.end, ) const events = useMemo( () => [...apiEvents, ...externalEvents].sort( (a, b) => a.start.getTime() - b.start.getTime(), ), [apiEvents, externalEvents], ) const [quickCreate, setQuickCreate] = useState(null) const [dialogState, setDialogState] = useState(null) const [popover, setPopover] = useState(null) /** Brouillon affiché sur la grille tant que la modale est ouverte. */ const [pendingDraft, setPendingDraft] = useState(null) const defaultCalendarId = writableVisibleCalendars[0]?.id ?? writableCalendars[0]?.id ?? "" const userEmail = identity?.email const clearPending = useCallback(() => setPendingDraft(null), []) const closeOverlays = useCallback(() => { setQuickCreate(null) setPopover(null) clearPending() }, [clearPending]) const showPendingDraft = useCallback((draft: AgendaEventDraft) => { setPendingDraft(draft) }, []) const openCreateDialog = useCallback( (base?: Partial, opts?: { keepPending?: boolean }) => { setPopover(null) setQuickCreate(null) if (!opts?.keepPending) clearPending() const start = base?.start ?? addHours(startOfHour(new Date()), 1) const end = base?.end ?? addMinutes(start, defaultQuickDurationMinutes) const draft: AgendaEventDraft = { title: "", start, end, allDay: false, calendarId: defaultCalendarId, ...base, } showPendingDraft(draft) setDialogState({ mode: "create", draft }) }, [clearPending, defaultCalendarId, defaultQuickDurationMinutes, showPendingDraft], ) const openEditDialog = useCallback( (event: AgendaEvent) => { closeOverlays() const masterStart = parseICSDate(event.master.start) ?? event.start const masterEnd = parseICSDate(event.master.end) ?? event.end setDialogState({ mode: "edit", event, draft: { title: event.title === "(Sans titre)" ? "" : event.title, start: masterStart, end: masterEnd, allDay: event.allDay, calendarId: event.calendarId, description: event.description, location: event.location, attendees: event.attendees, rrule: event.rrule, color: event.master.color, }, }) }, [closeOverlays], ) const handledNewParam = useRef(false) useEffect(() => { if (handledNewParam.current || !route.view) return if (searchParams.get("new") !== "1") return handledNewParam.current = true const guest = searchParams.get("guest")?.trim() const guestName = searchParams.get("guest_name")?.trim() openCreateDialog({ title: searchParams.get("title")?.trim() ?? "", attendees: guest ? [{ email: guest, name: guestName || guest, status: "NEEDS-ACTION" }] : [], }) router.replace(buildAgendaPath(view, date)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams, route.view]) useEffect(() => { const onKey = (e: KeyboardEvent) => { const target = e.target as HTMLElement if ( e.metaKey || e.ctrlKey || e.altKey || target.closest("input, textarea, select, [contenteditable], [role=dialog]") ) return switch (e.key.toLowerCase()) { case "j": case "d": navigate("day", date) break case "s": case "w": navigate("week", date) break case "m": navigate("month", date) break case "t": navigate(view, new Date()) break case "c": openCreateDialog() break default: return } e.preventDefault() } window.addEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey) }, [navigate, view, date, openCreateDialog]) const handleEventClick = useCallback((event: AgendaEvent, anchor: AnchorRect) => { setQuickCreate(null) clearPending() setPopover({ event, anchor }) }, [clearPending]) const handleCreateRange = useCallback( (start: Date, end: Date, allDay: boolean, anchor: AnchorRect, viaDrag: boolean) => { setPopover(null) const draft: AgendaEventDraft = { title: "", start, end, allDay, calendarId: defaultCalendarId, } showPendingDraft(draft) if (viaDrag) { openCreateDialog(draft, { keepPending: true }) } else { setDialogState(null) setQuickCreate({ start, end, allDay, anchor }) } }, [defaultCalendarId, openCreateDialog, showPendingDraft], ) const { moveEvent } = useAgendaEventMove() const handleEventMove = useCallback( (event: AgendaEvent, targetStart: Date) => { void moveEvent(event, targetStart) }, [moveEvent], ) const pendingEvent = useMemo(() => { if (!pendingDraft) return null return draftToPendingEvent(pendingDraft, visibleCalendars.length > 0 ? visibleCalendars : calendars) }, [pendingDraft, visibleCalendars, calendars]) const days = useMemo( () => viewDays(view, date, weekStart, weekStartsOn), [view, date, weekStart, weekStartsOn], ) return ( <> navigate(view, new Date())} onStep={step} onViewChange={(v) => navigate(v, date)} />
navigate(view, d)} onCreateEvent={() => openCreateDialog()} />
{calendarsLoading ? (
) : view === "month" ? ( navigate("day", day)} /> ) : ( navigate("day", day)} /> )}
0 ? writableVisibleCalendars : writableCalendars} defaultCalendarId={defaultCalendarId} userEmail={userEmail} onClose={() => { setQuickCreate(null) clearPending() }} onMoreOptions={(draft) => { setQuickCreate(null) showPendingDraft(draft) setDialogState({ mode: "create", draft }) }} /> setPopover(null)} onEdit={openEditDialog} /> { setDialogState(null) clearPending() }} calendars={writableCalendars} userEmail={userEmail} onDraftChange={showPendingDraft} /> ) }