ultisuite-client/components/agenda/agenda-page.tsx
R3D347HR4Y 3bbf3691b0
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
bordel c'est beau
2026-06-11 10:10:39 +02:00

285 lines
9.0 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useParams, useRouter, useSearchParams } from "next/navigation"
import { addDays, addHours, 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 { useAgendaCalendars, useAgendaEvents } from "@/lib/api/hooks/use-calendar-queries"
import { parseICSDate, viewDays, viewRange } from "@/lib/agenda/agenda-date"
import { useAgendaSettingsStore } from "@/lib/agenda/agenda-store"
import type { AgendaEvent, AgendaEventDraft } from "@/lib/agenda/agenda-types"
import {
buildAgendaPath,
parseAgendaSegments,
type AgendaView,
} from "@/lib/agenda/agenda-url"
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
import { useIsMobile } from "@/hooks/use-mobile"
export function AgendaPage() {
const router = useRouter()
const params = useParams()
const searchParams = useSearchParams()
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 hiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds)
const view: AgendaView = route.view ?? (isMobile ? "day" : lastView)
const date = route.date
// Normalise l'URL quand la vue est implicite.
useEffect(() => {
if (!route.view) {
router.replace(buildAgendaPath(view, date) + 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))
},
[router],
)
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],
)
// Données
const { data: calendars = [], isLoading: calendarsLoading } = useAgendaCalendars()
const visibleCalendars = useMemo(
() => calendars.filter((c) => !hiddenIds.includes(c.id)),
[calendars, hiddenIds],
)
const fetchRange = useMemo(() => viewRange("month", date), [date])
const { events } = useAgendaEvents(visibleCalendars, fetchRange.start, fetchRange.end)
// UI : création rapide / dialog / détails
const [quickCreate, setQuickCreate] = useState<AgendaQuickCreateState | null>(null)
const [dialogState, setDialogState] = useState<AgendaEventDialogState | null>(null)
const [popover, setPopover] = useState<AgendaEventPopoverState | null>(null)
const defaultCalendarId = visibleCalendars[0]?.id ?? calendars[0]?.id ?? ""
const userEmail = identity?.email
const closeOverlays = useCallback(() => {
setQuickCreate(null)
setPopover(null)
}, [])
const openCreateDialog = useCallback(
(base?: Partial<AgendaEventDraft>) => {
closeOverlays()
const start = base?.start ?? addHours(startOfHour(new Date()), 1)
const end = base?.end ?? addHours(start, 1)
setDialogState({
mode: "create",
draft: {
title: "",
start,
end,
allDay: false,
calendarId: defaultCalendarId,
...base,
},
})
},
[closeOverlays, defaultCalendarId],
)
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],
)
// Interop : /agenda?new=1&guest=…&title=…
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])
// Raccourcis clavier façon Google Calendar.
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)
setPopover({ event, anchor })
}, [])
const handleCreateRange = useCallback(
(start: Date, end: Date, allDay: boolean, anchor: AnchorRect) => {
setPopover(null)
setQuickCreate({ start, end, allDay, anchor })
},
[],
)
const days = useMemo(() => viewDays(view, date), [view, date])
return (
<>
<AgendaHeader
view={view}
date={date}
onToday={() => navigate(view, new Date())}
onStep={step}
onViewChange={(v) => navigate(v, date)}
/>
<div className="flex min-h-0 flex-1">
<AgendaSidebar
selectedDate={date}
onSelectDate={(d) => navigate(view, d)}
onCreateEvent={() => openCreateDialog()}
/>
<main className="flex min-w-0 flex-1 flex-col">
{calendarsLoading ? (
<div className="flex h-full flex-col gap-3 p-6">
<Skeleton className="h-8 w-64" />
<Skeleton className="min-h-0 flex-1" />
</div>
) : view === "month" ? (
<AgendaViewMonth
date={date}
events={events}
onCreateAt={(day, anchor) =>
handleCreateRange(day, addDays(day, 1), true, anchor)
}
onEventClick={handleEventClick}
onOpenDay={(day) => navigate("day", day)}
/>
) : (
<AgendaViewWeek
days={days}
events={events}
onCreateRange={handleCreateRange}
onEventClick={handleEventClick}
onOpenDay={(day) => navigate("day", day)}
/>
)}
</main>
</div>
<AgendaQuickCreate
state={quickCreate}
calendars={visibleCalendars.length > 0 ? visibleCalendars : calendars}
defaultCalendarId={defaultCalendarId}
userEmail={userEmail}
onClose={() => setQuickCreate(null)}
onMoreOptions={(draft) => {
setQuickCreate(null)
setDialogState({ mode: "create", draft })
}}
/>
<AgendaEventPopover
state={popover}
calendars={calendars}
userEmail={userEmail}
onClose={() => setPopover(null)}
onEdit={openEditDialog}
/>
<AgendaEventDialog
state={dialogState}
onClose={() => setDialogState(null)}
calendars={calendars}
userEmail={userEmail}
/>
</>
)
}