import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import LoadingCard from '@/components/LoadingCard'; import MainCard from '@/components/MainCard'; import HoverInfo from '@/components/HoverInfo'; import { DatePicker, TimePicker } from '@/components/ResponsivePickers'; import { Card, CardContent, } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { can } from '@/lib/permissions'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, } from '@/components/ui/select'; import { ChevronLeft, ChevronRight, Calendar, Plus, Pencil, Trash2, ArrowLeftRight, } from 'lucide-react'; import { eventApi } from '../api/event'; import { useAuth } from '../context/useAuth'; import { Alert, AlertDescription } from '@/components/ui/alert'; const MONTHS = [ 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre', ]; const DAYS_IN_MONTH = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ]; const WEEKDAY_INITIALS = ['D', 'L', 'M', 'M', 'J', 'V', 'S']; const HEADER_HEIGHT = 35; const DAY_COLUMN_WIDTH = 45; const MIN_MONTH_WIDTH = 90; const MIN_ROW_HEIGHT = 17.5; const DEFAULT_EVENT_COLOR = '#2563eb'; const EVENT_BAR_WIDTH = 8; const EVENT_LANE_GAP = 11; const EVENT_LEFT_PADDING = 3; const CalendarPage = () => { const { user, isAdmin } = useAuth(); const canCreate = can(user, 'create_personal_event'); const canManageEvent = (event) => { const creatorId = event?.created_by?.id ?? event?.created_by; return user?.id != null && creatorId != null && String(creatorId) === String(user.id); }; const [requestError, setRequestError] = useState(''); const [year, setYear] = useState( new Date().getFullYear(), ); const [schoolView, setSchoolView] = useState(false); const [selectedMonth, setSelectedMonth] = useState( new Date().getMonth(), ); const [isMobile, setIsMobile] = useState(false); const [calendarViewportHeight, setCalendarViewportHeight] = useState(null); const [events, setEvents] = useState([]); const [loadingEvents, setLoadingEvents] = useState(true); const [eventProfiles, setEventProfiles] = useState([]); const [loadingProfiles, setLoadingProfiles] = useState(false); const [selectedProfileId, setSelectedProfileId] = useState('manual'); const [savingProfile, setSavingProfile] = useState(false); const [hoveredEvent, setHoveredEvent] = useState(null); const [hoverPosition, setHoverPosition] = useState(null); /* * ============================================================ * MENU CONTEXTUEL * ============================================================ */ const [contextMenu, setContextMenu] = useState(null); /* * contextMenu : * * { * x: number, * y: number, * type: 'day' | 'event', * date?: 'YYYY-MM-DD', * event?: object * } */ /* * ============================================================ * DIALOG ÉVÉNEMENT * ============================================================ */ const [eventDialogOpen, setEventDialogOpen] = useState(false); const [editingEvent, setEditingEvent] = useState(null); const [savingEvent, setSavingEvent] = useState(false); const [eventForm, setEventForm] = useState({ name: '', start_date: '', end_date: '', start_time: '', end_time: '', color: DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin), }); const calendarViewportRef = useRef(null); /* SÉLECTION D'UNE PÉRIODE */ const [selectionStart, setSelectionStart] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null); const [isSelecting, setIsSelecting] = useState(false); const selectionEndRef = useRef(null); const isSelectingRef = useRef(false); /* * ============================================================ * UTILITAIRES * ============================================================ */ const isLeapYear = (value) => { return ( (value % 4 === 0 && value % 100 !== 0) || value % 400 === 0 ); }; const getMonthYear = (monthIndex) => !isMobile && schoolView && monthIndex < 8 ? year + 1 : year; const getDaysInMonth = (monthIndex) => { if ( monthIndex === 1 && isLeapYear(getMonthYear(monthIndex)) ) { return 29; } return DAYS_IN_MONTH[monthIndex]; }; const formatDateForInput = ( monthIndex, day, ) => { return `${getMonthYear(monthIndex)}-${String( monthIndex + 1, ).padStart(2, '0')}-${String(day).padStart( 2, '0', )}`; }; const parseDate = (value) => { if (!value) return null; const [ yearValue, monthValue, dayValue, ] = String(value) .split('-') .map(Number); if ( !yearValue || !monthValue || !dayValue ) { return null; } return new Date( yearValue, monthValue - 1, dayValue, ); }; const getEventStartDate = ( event, ) => { return event?.start_date ?? ''; }; const getEventEndDate = ( event, ) => { return ( event?.end_date ?? event?.start_date ?? '' ); }; const isDateInEvent = ( event, monthIndex, day, ) => { const eventStart = parseDate( getEventStartDate(event), ); const eventEnd = parseDate( getEventEndDate(event), ); if ( !eventStart || !eventEnd ) { return false; } const cellDate = new Date( getMonthYear(monthIndex), monthIndex, day, ); return ( cellDate >= eventStart && cellDate <= eventEnd ); }; const isSingleDayEvent = ( event, ) => { return ( getEventStartDate(event) === getEventEndDate(event) ); }; const getPeriodEventPosition = ( event, monthIndex, day, ) => { const startDate = parseDate( getEventStartDate(event), ); const endDate = parseDate( getEventEndDate(event), ); if ( !startDate || !endDate ) { return null; } const currentDate = new Date( getMonthYear(monthIndex), monthIndex, day, ); return { isStart: currentDate.getTime() === startDate.getTime(), isEnd: currentDate.getTime() === endDate.getTime(), }; }; const getOrderedSelection = () => { if (isMobile) return null; if (!selectionStart || !selectionEnd) return null; return selectionStart <= selectionEnd ? { start: selectionStart, end: selectionEnd } : { start: selectionEnd, end: selectionStart }; }; const isDateSelected = (date) => { const selection = getOrderedSelection(); return Boolean( selection && date >= selection.start && date <= selection.end, ); }; const clearSelection = () => { setSelectionStart(null); setSelectionEnd(null); setIsSelecting(false); selectionEndRef.current = null; isSelectingRef.current = false; }; const beginSelection = (pointerEvent, date) => { if (!canCreate || isMobile || pointerEvent.pointerType === 'touch' || pointerEvent.button !== 0) return; pointerEvent.preventDefault(); closeContextMenu(); setHoveredEvent(null); setHoverPosition(null); selectionEndRef.current = date; isSelectingRef.current = true; setSelectionStart(date); setSelectionEnd(date); setIsSelecting(true); }; const updateSelectionFromPointer = (pointerEvent) => { if (isMobile || !isSelectingRef.current) return; const element = document.elementFromPoint( pointerEvent.clientX, pointerEvent.clientY, ); const cell = element?.closest?.('[data-calendar-date]'); const date = cell?.dataset?.calendarDate; if (!date || date === selectionEndRef.current) return; selectionEndRef.current = date; setSelectionEnd(date); }; const finishSelection = () => { if (!isSelectingRef.current) return; isSelectingRef.current = false; setIsSelecting(false); }; /* * ============================================================ * CHARGEMENT DES ÉVÉNEMENTS * ============================================================ */ const loadEvents = async () => { try { setRequestError(''); setLoadingEvents(true); const data = await eventApi.getAllEvents(); const loadedEvents = Array.isArray(data) ? data : data?.results ?? []; setEvents(loadedEvents); } catch (error) { setRequestError(error.message); console.error( 'Erreur lors du chargement des événements :', error, ); setEvents([]); } finally { setLoadingEvents(false); } }; const loadEventProfiles = async () => { if (!canCreate) { setEventProfiles([]); return; } try { setLoadingProfiles(true); const data = await eventApi.getProfiles(); setEventProfiles(Array.isArray(data) ? data : data?.results ?? []); } catch (error) { console.error('Erreur lors du chargement des profils d’événement :', error); setEventProfiles([]); } finally { setLoadingProfiles(false); } }; useEffect(() => { loadEvents(); loadEventProfiles(); }, []); /* * ============================================================ * RESPONSIVE * ============================================================ */ useEffect(() => { const updateMobile = () => { setIsMobile( window.innerWidth < 768, ); }; updateMobile(); window.addEventListener( 'resize', updateMobile, ); return () => { window.removeEventListener( 'resize', updateMobile, ); }; }, []); useEffect(() => { const handlePointerMove = (pointerEvent) => { updateSelectionFromPointer(pointerEvent); }; const handlePointerUp = () => { finishSelection(); }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointercancel', handlePointerUp); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointercancel', handlePointerUp); }; }, []); useEffect(() => { if (isMobile) clearSelection(); }, [isMobile]); useLayoutEffect(() => { if (loadingEvents || !calendarViewportRef.current) return; const viewport = calendarViewportRef.current; const updateHeight = () => { const height = viewport.getBoundingClientRect().height; setCalendarViewportHeight((previous) => previous === height ? previous : height, ); }; updateHeight(); const observer = new ResizeObserver(updateHeight); observer.observe(viewport); return () => observer.disconnect(); }, [loadingEvents]); /* * ============================================================ * ANNÉE * ============================================================ */ const previousYear = () => { setYear((current) => current - 1); }; const nextYear = () => { setYear((current) => current + 1); }; const currentYear = () => { setYear(new Date().getFullYear()); }; const previousPeriod = () => { if (!isMobile) { previousYear(); return; } if (selectedMonth === 0) { setSelectedMonth(11); setYear((current) => current - 1); return; } setSelectedMonth((current) => current - 1); }; const nextPeriod = () => { if (!isMobile) { nextYear(); return; } if (selectedMonth === 11) { setSelectedMonth(0); setYear((current) => current + 1); return; } setSelectedMonth((current) => current + 1); }; const currentPeriod = () => { const now = new Date(); setYear( !isMobile && schoolView && now.getMonth() < 8 ? now.getFullYear() - 1 : now.getFullYear(), ); if (isMobile) { setSelectedMonth(now.getMonth()); } }; /* * ============================================================ * SURVOL * ============================================================ */ const handleEventMouseEnter = ( event, mouseEvent, ) => { setHoveredEvent(event); setHoverPosition({ x: mouseEvent.clientX, y: mouseEvent.clientY, }); }; const handleEventMouseMove = ( mouseEvent, ) => { setHoverPosition({ x: mouseEvent.clientX, y: mouseEvent.clientY, }); }; const handleEventMouseLeave = () => { setHoveredEvent(null); setHoverPosition(null); }; /* * ============================================================ * MENU CONTEXTUEL * ============================================================ */ const closeContextMenu = () => { setContextMenu(null); }; const handleDayContextMenu = ( mouseEvent, date, ) => { mouseEvent.preventDefault(); mouseEvent.stopPropagation(); if (!canCreate) return; setHoveredEvent(null); setHoverPosition(null); const selection = getOrderedSelection(); const useSelection = Boolean( selection && date >= selection.start && date <= selection.end, ); setContextMenu({ x: mouseEvent.clientX, y: mouseEvent.clientY, type: useSelection ? 'selection' : 'day', date, startDate: useSelection ? selection.start : date, endDate: useSelection ? selection.end : date, }); }; const handleEventContextMenu = ( mouseEvent, event, ) => { mouseEvent.preventDefault(); mouseEvent.stopPropagation(); setHoveredEvent(null); setHoverPosition(null); setContextMenu({ x: mouseEvent.clientX, y: mouseEvent.clientY, type: 'event', event, }); }; useEffect(() => { if (!contextMenu) return; const handleOutsideClick = () => { closeContextMenu(); }; const handleEscape = ( keyboardEvent, ) => { if ( keyboardEvent.key === 'Escape' ) { closeContextMenu(); } }; document.addEventListener( 'mousedown', handleOutsideClick, ); document.addEventListener( 'keydown', handleEscape, ); return () => { document.removeEventListener( 'mousedown', handleOutsideClick, ); document.removeEventListener( 'keydown', handleEscape, ); }; }, [contextMenu]); /* * ============================================================ * FORMULAIRE ÉVÉNEMENT * ============================================================ */ const resetEventForm = () => { setEventForm({ name: '', start_date: '', end_date: '', start_time: '', end_time: '', color: DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin), }); setSelectedProfileId('manual'); }; const openCreateDialog = ( startDate = '', endDate = startDate, ) => { if (!canCreate) return; setRequestError(''); setEditingEvent(null); setSelectedProfileId('manual'); setEventForm({ name: '', start_date: startDate, end_date: endDate || startDate, start_time: '', end_time: '', color: DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin), }); clearSelection(); closeContextMenu(); setEventDialogOpen(true); }; const openEditDialog = ( event, ) => { if (!canManageEvent(event)) return; setRequestError(''); setEditingEvent(event); setSelectedProfileId('manual'); setEventForm({ name: event?.name ?? '', start_date: event?.start_date ?? '', end_date: event?.end_date ?? event?.start_date ?? '', start_time: event?.start_time ? String( event.start_time, ).slice(0, 5) : '', end_time: event?.end_time ? String( event.end_time, ).slice(0, 5) : '', color: event?.color || DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin) && event?.is_public === true, }); closeContextMenu(); setEventDialogOpen(true); }; const handleFormChange = ( field, value, ) => { setEventForm( (current) => ({ ...current, [field]: value, }), ); }; const handleStartDateChange = ( value, ) => { setEventForm( (current) => { const next = { ...current, start_date: value, }; /* * Si la date de fin est vide ou * antérieure à la nouvelle date * de début, on la recale sur * la date de début. */ if ( !current.end_date || current.end_date < value ) { next.end_date = value; } return next; }, ); }; const formatProfileTime = (value) => { if (!value) return ''; return String(value).slice(0, 5); }; const handleProfileChange = (value) => { setSelectedProfileId(value); }; const handleLoadProfile = () => { if (selectedProfileId === 'manual') return; const profile = eventProfiles.find( (item) => String(item.id) === String(selectedProfileId), ); if (!profile) return; setEventForm((current) => ({ ...current, name: profile.name ?? '', start_time: formatProfileTime(profile.start_time), end_time: formatProfileTime(profile.end_time), color: profile.color || DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin) && profile.is_public === true, })); }; const getProfilePayload = () => ({ name: eventForm.name.trim(), start_time: eventForm.start_time || null, end_time: eventForm.end_time || null, color: eventForm.color || DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin) && eventForm.is_public, }); const handleSaveProfile = async () => { if (savingProfile || !eventForm.name.trim()) return; try { setRequestError(''); setSavingProfile(true); const payload = getProfilePayload(); let savedProfile; if (selectedProfileId !== 'manual') { savedProfile = await eventApi.updateProfile(selectedProfileId, payload); } else { savedProfile = await eventApi.createProfile(payload); } await loadEventProfiles(); setSelectedProfileId(String(savedProfile.id)); } catch (error) { setRequestError(error.message); console.error('Erreur lors de l’enregistrement du profil d’événement :', error); } finally { setSavingProfile(false); } }; const handleDeleteProfile = async () => { if (savingProfile || selectedProfileId === 'manual') return; const profile = eventProfiles.find((item) => String(item.id) === String(selectedProfileId)); if (!profile) return; const confirmed = window.confirm(`Supprimer le profil « ${profile.name} » ?`); if (!confirmed) return; try { setRequestError(''); setSavingProfile(true); await eventApi.deleteProfile(profile.id); setSelectedProfileId('manual'); await loadEventProfiles(); } catch (error) { setRequestError(error.message); console.error('Erreur lors de la suppression du profil d’événement :', error); } finally { setSavingProfile(false); } }; const handleSaveEvent = async ( submitEvent, ) => { setRequestError(''); if (!editingEvent && !canCreate) { setRequestError('L’ajout d’événements n’est pas activé pour ton compte.'); return; } if (savingEvent || (editingEvent && !canManageEvent(editingEvent))) return; if (!eventForm.name.trim()) { return; } if (!eventForm.start_date) { return; } const startDate = parseDate( eventForm.start_date, ); const endDate = parseDate( eventForm.end_date || eventForm.start_date, ); if ( !startDate || !endDate || endDate < startDate ) { return; } try { setSavingEvent(true); const payload = { name: eventForm.name.trim(), start_date: eventForm.start_date, end_date: eventForm.end_date || eventForm.start_date, start_time: eventForm.start_time || null, end_time: eventForm.end_time || null, color: eventForm.color || DEFAULT_EVENT_COLOR, is_public: Boolean(isAdmin) && eventForm.is_public, }; if (submitEvent) { await eventApi.updateEvent( editingEvent.id, payload, ); } else { await eventApi.createEvent( payload, ); } setEventDialogOpen(false); setEditingEvent(null); resetEventForm(); await loadEvents(); } catch (error) { setRequestError(error.message); console.error( submitEvent ? 'Erreur lors de la modification de l’événement :' : 'Erreur lors de la création de l’événement :', error, ); } finally { setSavingEvent(false); } }; const handleDeleteEvent = async ( event, ) => { closeContextMenu(); if (!canManageEvent(event)) return; const confirmed = window.confirm( `Supprimer l’événement « ${ event?.name || 'Sans nom' } » ?`, ); if (!confirmed) { return; } try { await eventApi.deleteEvent( event.id, ); await loadEvents(); } catch (error) { setRequestError(error.message); console.error( 'Erreur lors de la suppression de l’événement :', error, ); } }; /* * ============================================================ * AUJOURD'HUI * ============================================================ */ const today = new Date(); const isToday = ( monthIndex, day, ) => { return ( today.getFullYear() === getMonthYear(monthIndex) && today.getMonth() === monthIndex && today.getDate() === day ); }; /* * ============================================================ * ÉVÉNEMENTS DE L'ANNÉE * ============================================================ */ const yearEvents = events.filter( (event) => { const start = parseDate( getEventStartDate(event), ); const end = parseDate( getEventEndDate(event), ); if (!start || !end) { return false; } const yearStart = new Date(year, !isMobile && schoolView ? 8 : 0, 1); const yearEnd = !isMobile && schoolView ? new Date(year + 1, 7, 31) : new Date(year, 11, 31); return ( end >= yearStart && start <= yearEnd ); }, ); /* * ============================================================ * LANES DES ÉVÉNEMENTS CONTINUS * ============================================================ */ const eventLanes = useMemo(() => { const periodEvents = yearEvents .filter((event) => !isSingleDayEvent(event)) .map((event) => ({ event, start: parseDate(getEventStartDate(event)), end: parseDate(getEventEndDate(event)), })) .filter(({ start, end }) => start && end) .sort((a, b) => { const startDifference = a.start - b.start; if (startDifference !== 0) return startDifference; const endDifference = b.end - a.end; if (endDifference !== 0) return endDifference; return String(a.event.id).localeCompare(String(b.event.id)); }); const laneEndDates = []; const lanes = new Map(); periodEvents.forEach(({ event, start, end }) => { let lane = laneEndDates.findIndex( (laneEndDate) => laneEndDate < start, ); if (lane === -1) { lane = laneEndDates.length; } laneEndDates[lane] = end; lanes.set(String(event.id), lane); }); return lanes; }, [yearEvents]); /* * ============================================================ * DIMENSIONS * ============================================================ */ const displayedMonthIndices = isMobile ? [selectedMonth] : schoolView ? [8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7] : MONTHS.map((_, index) => index); const rowCount = isMobile ? getDaysInMonth(selectedMonth) : 31; const minimumGridWidth = isMobile ? 0 : DAY_COLUMN_WIDTH + MONTHS.length * MIN_MONTH_WIDTH; const gridTemplateColumns = isMobile ? `${DAY_COLUMN_WIDTH}px minmax(0, 1fr)` : ` ${DAY_COLUMN_WIDTH}px repeat( 12, minmax( ${MIN_MONTH_WIDTH}px, 1fr ) ) `; /* * ============================================================ * RENDER * ============================================================ */ if (loadingEvents) { return ( ); } return ( <> {/* ====================================================== HOVER EVENT ====================================================== */} {hoveredEvent && ( )} {/* ====================================================== MENU CONTEXTUEL ====================================================== */} {contextMenu && (
{ mouseEvent.stopPropagation(); }} onContextMenu={( mouseEvent, ) => { mouseEvent.preventDefault(); mouseEvent.stopPropagation(); }} > {canCreate && (contextMenu.type === 'day' || contextMenu.type === 'selection') && ( )} {contextMenu.type === 'event' && !canManageEvent(contextMenu.event) && (

Événement public · Lecture seule

)} {contextMenu.type === 'event' && canManageEvent(contextMenu.event) && ( <>
)}
)} {/* ====================================================== DIALOG CRÉATION / MODIFICATION ====================================================== */} { if ( savingEvent && !open ) { return; } setEventDialogOpen(open); if (!open) { setEditingEvent(null); resetEventForm(); } }} >
{editingEvent ? 'Modifier l’événement' : 'Créer un événement'} {editingEvent ? 'Modifie les informations de cet événement.' : 'Ajoute un nouvel événement au calendrier.'}
{!editingEvent && canCreate && (
)}
{requestError && ( {requestError} )}
{/* NOM */}
handleFormChange( 'name', event.target.value, ) } placeholder="Nom de l'événement" autoFocus />
{/* DATES */}
handleStartDateChange( value, ) } />
handleFormChange( 'end_date', value, ) } />
{/* HEURES */}
handleFormChange( 'start_time', value, ) } />
handleFormChange( 'end_time', value, ) } />
{/* COULEUR ET VISIBILITÉ */}
handleFormChange('color', event.target.value)} className="h-10 w-14 shrink-0 cursor-pointer rounded-md border border-input bg-background p-1" /> handleFormChange('color', event.target.value)} placeholder="#2563eb" className="min-w-0 flex-1" />
{isAdmin && (
setEventForm((current) => ({ ...current, is_public: checked }))} />
)}

{isAdmin && eventForm.is_public ? 'Visible par tous les utilisateurs connectés. Seul toi peux le modifier ou le supprimer.' : 'Visible uniquement par toi dans ton calendrier.'}

{!editingEvent && canCreate && (
{selectedProfileId !== 'manual' && ( )}
)}
{/* ====================================================== CARTE PRINCIPALE ====================================================== */} {!isMobile && ( )} } >
{!isAdmin && (

{canCreate ? 'Tes événements personnels sont visibles uniquement par toi.' : 'Pour ajouter un événement personnel, demande l’accès à un responsable.'}

)} {canCreate && }
{requestError && ( {requestError} )}
{/* ================================================== HEADER JOUR ================================================== */}
Jour
{/* ================================================== HEADER MOIS ================================================== */} {displayedMonthIndices.map( (monthIndex) => (
{MONTHS[monthIndex]}
), )} {/* ================================================== JOURS ================================================== */} {Array.from( { length: rowCount }, (_, dayIndex) => { const day = dayIndex + 1; return ( {/* NUMÉRO DU JOUR */}
{day}
{/* ================================================== MOIS ================================================== */} {displayedMonthIndices.map( (monthIndex) => { const valid = day <= getDaysInMonth( monthIndex, ); const todayCell = valid && isToday( monthIndex, day, ); if (!valid) { return (
); } const cellDate = formatDateForInput( monthIndex, day, ); const cellDayNumber = Math.floor( Date.UTC(getMonthYear(monthIndex), monthIndex, day) / 86400000, ); const alternateWeek = Math.floor((cellDayNumber + 3) / 7) % 2 !== 0; const weekdayInitial = WEEKDAY_INITIALS[ new Date(Date.UTC(getMonthYear(monthIndex), monthIndex, day)).getUTCDay() ]; const selectedCell = isDateSelected(cellDate); const dayEvents = yearEvents.filter( ( event, ) => isDateInEvent( event, monthIndex, day, ), ); const periodEvents = dayEvents.filter( ( event, ) => !isSingleDayEvent( event, ), ); const singleDayEvents = dayEvents.filter( ( event, ) => isSingleDayEvent( event, ), ); return (
beginSelection( pointerEvent, cellDate, ) } onContextMenu={( mouseEvent, ) => handleDayContextMenu( mouseEvent, cellDate, ) } > {/* AUJOURD'HUI */} {todayCell && (
{weekdayInitial}
)} {!todayCell && ( )} {/* ================================================== ÉVÉNEMENTS SUR UNE PÉRIODE ================================================== */} {periodEvents.map( ( event, ) => { const position = getPeriodEventPosition( event, monthIndex, day, ); if ( !position ) { return null; } const lane = eventLanes.get( String(event.id), ) ?? 0; const laneOffset = lane * EVENT_LANE_GAP; const isStart = position.isStart; const isEnd = position.isEnd; const eventColor = event.color || DEFAULT_EVENT_COLOR; return (
{ pointerEvent.stopPropagation(); }} onMouseEnter={( mouseEvent, ) => handleEventMouseEnter( event, mouseEvent, ) } onMouseMove={ handleEventMouseMove } onMouseLeave={ handleEventMouseLeave } onContextMenu={( mouseEvent, ) => handleEventContextMenu( mouseEvent, event, ) } /> ); }, )} {/* ================================================== ÉVÉNEMENTS SUR UNE SEULE DATE ================================================== */} {singleDayEvents.length > 0 && (
{singleDayEvents.map( ( event, ) => { const eventColor = event.color || DEFAULT_EVENT_COLOR; return (
{ pointerEvent.stopPropagation(); }} onMouseEnter={( mouseEvent, ) => handleEventMouseEnter( event, mouseEvent, ) } onMouseMove={ handleEventMouseMove } onMouseLeave={ handleEventMouseLeave } onContextMenu={( mouseEvent, ) => handleEventContextMenu( mouseEvent, event, ) } /> ); }, )}
)}
); }, )} ); }, )}
); }; export default CalendarPage;