Files
n3wt-school/Front-End/src/components/Calendar/DayView.js
2026-04-05 12:08:04 +02:00

272 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import {
format,
startOfWeek,
addDays,
subDays,
isSameDay,
isToday,
} from 'date-fns';
import { fr } from 'date-fns/locale';
import { getWeekEvents } from '@/utils/events';
import { CalendarDays, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
const { currentDate, setCurrentDate, parentView, schedules } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date());
const scrollRef = useRef(null);
const timeSlots = Array.from({ length: 24 }, (_, i) => i);
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
const isCurrentDay = isSameDay(currentDate, new Date());
const dayEvents = getWeekEvents(currentDate, events) || [];
useEffect(() => {
const interval = setInterval(() => setCurrentTime(new Date()), 60000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (scrollRef.current && isCurrentDay) {
const currentHour = new Date().getHours();
setTimeout(() => {
scrollRef.current.scrollTop = currentHour * 80 - 200;
}, 0);
}
}, [currentDate, isCurrentDay]);
const getCurrentTimePosition = () => {
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
return `${(hours + minutes / 60) * 5}rem`;
};
const getScheduleColor = (event) => {
const schedule = schedules?.find(
(item) => Number(item.id) === Number(event.planning)
);
return schedule?.color || event.color || '#6B7280';
};
const getScheduleClassLevelLabel = (event) => {
const schedule = schedules?.find(
(item) => Number(item.id) === Number(event.planning)
);
const scheduleName = schedule?.name || '';
if (!scheduleName) return '';
return scheduleName;
};
const calculateEventStyle = (event, allDayEvents) => {
const start = new Date(event.start);
const end = new Date(event.end);
const startMinutes = (start.getMinutes() / 60) * 5;
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
const scheduleColor = getScheduleColor(event);
const overlapping = allDayEvents.filter((other) => {
if (other.id === event.id) return false;
const oStart = new Date(other.start);
const oEnd = new Date(other.end);
return !(oEnd <= start || oStart >= end);
});
const eventIndex = overlapping.findIndex((e) => e.id > event.id) + 1;
const total = overlapping.length + 1;
return {
height: `${Math.max(duration, 1.5)}rem`,
position: 'absolute',
width: `calc((100% / ${total}) - 4px)`,
left: `calc((100% / ${total}) * ${eventIndex})`,
backgroundColor: `${event.color}15`,
borderLeft: `3px solid ${event.color}`,
borderRadius: '0.25rem',
zIndex: 1,
transform: `translateY(${startMinutes}rem)`,
};
};
return (
<div className="flex flex-col h-full">
{/* Barre de navigation (remplace le header Calendar sur mobile) */}
<div className="flex items-center justify-between px-3 py-2 bg-white border-b shrink-0">
<button
onClick={onOpenDrawer}
className="p-2 hover:bg-gray-100 rounded-full"
aria-label="Ouvrir les plannings"
>
<CalendarDays className="w-5 h-5 text-gray-600" />
</button>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentDate(subDays(currentDate, 1))}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronLeft className="w-5 h-5" />
</button>
<label className="relative cursor-pointer">
<span className="px-2 py-1 text-sm font-semibold text-gray-800 hover:bg-gray-100 rounded-md capitalize">
{format(currentDate, 'EEE d MMM', { locale: fr })}
</span>
<input
type="date"
className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
value={format(currentDate, 'yyyy-MM-dd')}
onChange={(e) => {
if (e.target.value) setCurrentDate(new Date(e.target.value + 'T12:00:00'));
}}
/>
</label>
<button
onClick={() => setCurrentDate(addDays(currentDate, 1))}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
<button
onClick={() => onDateClick?.(currentDate)}
className="w-9 h-9 flex items-center justify-center bg-primary text-white rounded-full hover:bg-secondary shadow-md transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
{/* Bandeau jours de la semaine */}
<div className="flex gap-1 px-2 py-2 bg-white border-b overflow-x-auto shrink-0">
{weekDays.map((day) => (
<button
key={day.toISOString()}
onClick={() => setCurrentDate(day)}
className={`flex flex-col items-center min-w-[2.75rem] px-1 py-1.5 rounded-xl transition-colors ${
isSameDay(day, currentDate)
? 'bg-primary text-white'
: isToday(day)
? 'border border-tertiary text-primary'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<span className="text-xs font-medium uppercase">
{format(day, 'EEE', { locale: fr })}
</span>
<span className="text-sm font-bold">{format(day, 'd')}</span>
</button>
))}
</div>
{/* Grille horaire */}
<div ref={scrollRef} className="flex-1 overflow-y-auto relative">
{isCurrentDay && (
<div
className="absolute left-0 right-0 z-10 border-primary border pointer-events-none"
style={{ top: getCurrentTimePosition() }}
>
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-primary" />
</div>
)}
<div
className="grid w-full bg-gray-100 gap-[1px]"
style={{ gridTemplateColumns: '2.5rem 1fr' }}
>
{timeSlots.map((hour) => (
<React.Fragment key={hour}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hour.toString().padStart(2, '0')}:00`}
</div>
<div
className={`h-20 relative ${
isCurrentDay ? 'bg-primary/5/30' : 'bg-white'
}`}
onClick={
parentView
? undefined
: () => {
const date = new Date(currentDate);
date.setHours(hour);
date.setMinutes(0);
onDateClick(date);
}
}
>
{dayEvents
.filter((e) => new Date(e.start).getHours() === hour)
.map((event) => {
const scheduleColor = getScheduleColor(event);
const classLevelLabel = getScheduleClassLevelLabel(event);
return (
<div
key={event.id}
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
style={calculateEventStyle(event, dayEvents)}
onClick={
parentView
? undefined
: (e) => {
e.stopPropagation();
onEventClick(event);
}
}
>
{classLevelLabel && (
<div
className="px-1 py-0.5 border-t-2"
style={{
borderTopColor: scheduleColor,
backgroundColor: `${scheduleColor}22`,
}}
>
<span
className="text-[10px] font-semibold uppercase tracking-wide truncate block text-center"
style={{ color: scheduleColor }}
>
{classLevelLabel}
</span>
</div>
)}
<div className="p-1">
<div
className="font-semibold text-xs truncate flex items-center gap-1"
style={{ color: event.color }}
>
<span
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: event.color }}
/>
<span className="truncate flex-1">{event.title}</span>
</div>
<div
className="text-xs"
style={{ color: event.color, opacity: 0.75 }}
>
{format(new Date(event.start), 'HH:mm')} {' '}
{format(new Date(event.end), 'HH:mm')}
</div>
{event.location && (
<div
className="text-xs truncate"
style={{ color: event.color, opacity: 0.75 }}
>
{event.location}
</div>
)}
</div>
</div>
);
})}
</div>
</React.Fragment>
))}
</div>
</div>
</div>
);
};
export default DayView;