mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-06 05:01:25 +00:00
272 lines
10 KiB
JavaScript
272 lines
10 KiB
JavaScript
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;
|