ultisuite-client/components/agenda/agenda-sidebar.tsx
R3D347HR4Y 9ea2d3325d
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance authentication flows with embedded support and UI improvements
- Updated login and signup components to utilize AuthCard for better user experience during redirection.
- Introduced AuthentikEmbedDialog for seamless integration of Authentik's identity portal within the application.
- Enhanced password recovery and signup flows with dynamic theme handling and improved loading states.
- Refactored existing components to streamline authentication processes and improve maintainability.
2026-06-21 00:12:45 +02:00

348 lines
13 KiB
TypeScript

"use client"
import { useMemo, useState } from "react"
import { toast } from "sonner"
import { MoreVertical, Pencil, Plus, Trash2 } from "lucide-react"
import { AgendaCalendarDialog } from "@/components/agenda/agenda-calendar-dialog"
import {
CalendarViewDialog,
ExternalCalendarDialog,
} from "@/components/agenda/agenda-calendars-settings"
import { AgendaMiniMonth } from "@/components/agenda/agenda-mini-month"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Skeleton } from "@/components/ui/skeleton"
import { useDeleteAgendaCalendar } from "@/lib/api/hooks/use-calendar-mutations"
import { useLabels } from "@/lib/api/hooks/use-folder-label-queries"
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
import { ALL_AGENDAS_VIEW_LABEL, isExternalCalendarId, isReservedAgendaViewName } from "@/lib/agenda/agenda-calendar-visibility"
import { calendarColor } from "@/lib/agenda/agenda-events"
import { useAgendaSettingsStore, useAgendaUIStore } from "@/lib/agenda/agenda-store"
import type { AgendaCalendar } from "@/lib/agenda/agenda-types"
import { useMergedAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars"
import { usePersistHydrated } from "@/hooks/use-persist-hydrated"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
export function AgendaSidebar({
selectedDate,
onSelectDate,
onCreateEvent,
}: {
selectedDate: Date
onSelectDate: (date: Date) => void
onCreateEvent: () => void
}) {
const isMobile = useIsMobile()
const settingsHydrated = usePersistHydrated(useAgendaSettingsStore)
const sidebarCollapsed = useAgendaUIStore((s) => s.sidebarCollapsed)
const setSidebarCollapsed = useAgendaUIStore((s) => s.setSidebarCollapsed)
const storedHiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds)
const hiddenIds = settingsHydrated ? storedHiddenIds : []
const weekStart = useAgendaSettingsStore((s) => s.weekStart)
const calendarViews = useAgendaSettingsStore((s) => s.calendarViews)
const activeCalendarViewId = useAgendaSettingsStore((s) => s.activeCalendarViewId)
const setActiveCalendarViewId = useAgendaSettingsStore((s) => s.setActiveCalendarViewId)
const toggleCalendar = useAgendaSettingsStore((s) => s.toggleCalendarVisible)
const { data: accounts = [] } = useMailAccounts()
const { data: labels = [] } = useLabels()
const { calendars, isLoading } = useMergedAgendaCalendars()
const deleteMutation = useDeleteAgendaCalendar()
const [calendarDialogOpen, setCalendarDialogOpen] = useState(false)
const [editingCalendar, setEditingCalendar] = useState<AgendaCalendar | null>(null)
const [deletingCalendar, setDeletingCalendar] = useState<AgendaCalendar | null>(null)
const [externalDialogOpen, setExternalDialogOpen] = useState(false)
const [viewDialogOpen, setViewDialogOpen] = useState(false)
const calendarOptions = useMemo(
() =>
(calendars ?? []).map((calendar) => ({
id: calendar.id,
label: calendar.display_name,
color: calendarColor(calendar),
})),
[calendars],
)
const labelOptions = useMemo(
() => labels.map((label) => ({ id: label.id, label: label.name })),
[labels],
)
const open = !sidebarCollapsed
const confirmDelete = async () => {
if (!deletingCalendar) return
try {
await deleteMutation.mutateAsync({ id: deletingCalendar.id })
toast.success(`Agenda « ${deletingCalendar.display_name} » supprimé`)
} catch {
toast.error("Impossible de supprimer cet agenda")
} finally {
setDeletingCalendar(null)
}
}
return (
<>
<aside
className={cn(
"flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto bg-app-canvas px-3 pt-3 pb-4",
isMobile
? cn(
"fixed inset-y-0 left-0 z-50 shadow-xl transition-transform duration-200 ease-linear",
open ? "translate-x-0" : "-translate-x-full pointer-events-none",
)
: cn(!open && "hidden"),
)}
aria-hidden={isMobile && !open}
>
<Button
className="h-13 w-fit gap-3 rounded-2xl border border-border/60 bg-card px-5 text-[0.95rem] font-medium text-foreground shadow-md hover:bg-mail-nav-hover hover:shadow-lg"
variant="ghost"
onClick={() => {
onCreateEvent()
if (isMobile) setSidebarCollapsed(true)
}}
>
<Plus className="size-6 text-primary" />
Nouvel événement
</Button>
<AgendaMiniMonth
selected={selectedDate}
weekStart={weekStart}
onSelect={(d) => {
onSelectDate(d)
if (isMobile) setSidebarCollapsed(true)
}}
/>
<div className="flex min-h-0 flex-col gap-0.5">
<div className="flex items-center justify-between pr-1 pl-2">
<span className="py-1 text-sm font-medium text-foreground/90">Vues</span>
<Button
variant="ghost"
size="icon"
className="size-7 rounded-full text-muted-foreground"
aria-label="Créer une vue"
onClick={() => setViewDialogOpen(true)}
>
<Plus className="size-4" />
</Button>
</div>
{settingsHydrated && calendarViews.length > 0 ? (
<>
<button
type="button"
onClick={() => setActiveCalendarViewId(null)}
className={cn(
"rounded-lg px-2 py-1 text-left text-sm transition-colors hover:bg-mail-nav-hover",
activeCalendarViewId === null
? "bg-mail-nav-selected font-medium text-foreground"
: "text-foreground/85",
)}
>
{ALL_AGENDAS_VIEW_LABEL}
</button>
{calendarViews
.filter((view) => !isReservedAgendaViewName(view.name))
.map((view) => (
<button
key={view.id}
type="button"
onClick={() => setActiveCalendarViewId(view.id)}
className={cn(
"rounded-lg px-2 py-1 text-left text-sm transition-colors hover:bg-mail-nav-hover",
activeCalendarViewId === view.id
? "bg-mail-nav-selected font-medium text-foreground"
: "text-foreground/85",
)}
>
{view.name}
</button>
))}
</>
) : null}
<div className="mt-2 flex items-center justify-between border-t border-border/60 pr-1 pl-2 pt-2">
<span className="py-0.5 text-sm font-medium text-foreground/90">
Mes agendas
</span>
<Button
variant="ghost"
size="icon"
className="size-7 rounded-full text-muted-foreground"
aria-label="Créer un agenda"
onClick={() => {
setEditingCalendar(null)
setCalendarDialogOpen(true)
}}
>
<Plus className="size-4" />
</Button>
</div>
{isLoading && (
<div className="flex flex-col gap-1.5 px-2 py-0.5">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-5 w-32" />
</div>
)}
{(calendars ?? []).map((cal) => {
const color = calendarColor(cal)
const visible = !hiddenIds.includes(cal.id)
const isExternal = isExternalCalendarId(cal.id)
return (
<div
key={cal.id}
className="group flex items-center gap-2 rounded-lg px-2 py-0.5 hover:bg-mail-nav-hover"
>
<label className="flex min-w-0 flex-1 cursor-pointer items-center gap-2.5">
<input
type="checkbox"
checked={visible}
onChange={() => toggleCalendar(cal.id)}
className="peer sr-only"
/>
<span
aria-hidden
className={cn(
"flex size-4.5 shrink-0 items-center justify-center rounded-[5px] border-2 transition-colors",
)}
style={{
borderColor: color,
backgroundColor: visible ? color : "transparent",
}}
>
{visible && (
<svg viewBox="0 0 24 24" className="size-3.5 text-white" fill="none">
<path
d="M5 13l4 4L19 7"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</span>
<span className="truncate text-sm text-foreground/85">
{cal.display_name}
{isExternal ? (
<span className="ml-1 text-[10px] text-muted-foreground">(iCal)</span>
) : null}
</span>
</label>
{!isExternal ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0 rounded-full text-muted-foreground opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100"
aria-label={`Options de ${cal.display_name}`}
>
<MoreVertical className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-44">
<DropdownMenuItem
onSelect={() => {
setEditingCalendar(cal)
setCalendarDialogOpen(true)
}}
>
<Pencil className="size-4" /> Modifier
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => setDeletingCalendar(cal)}
>
<Trash2 className="size-4" /> Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
)
})}
</div>
</aside>
<AgendaCalendarDialog
open={calendarDialogOpen}
onOpenChange={setCalendarDialogOpen}
calendar={editingCalendar}
onAddExternalICal={
editingCalendar
? undefined
: () => {
setCalendarDialogOpen(false)
setExternalDialogOpen(true)
}
}
/>
<ExternalCalendarDialog
open={externalDialogOpen}
onOpenChange={setExternalDialogOpen}
calendar={null}
accounts={accounts}
/>
<CalendarViewDialog
open={viewDialogOpen}
onOpenChange={setViewDialogOpen}
view={null}
calendarOptions={calendarOptions}
labelOptions={labelOptions}
/>
<AlertDialog
open={deletingCalendar !== null}
onOpenChange={(o) => {
if (!o) setDeletingCalendar(null)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Supprimer « {deletingCalendar?.display_name} » ?
</AlertDialogTitle>
<AlertDialogDescription>
Tous les événements de cet agenda seront définitivement supprimés.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-white hover:bg-destructive/90"
onClick={() => void confirmDelete()}
>
Supprimer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}