mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-06 13:11:25 +00:00
feat(frontend): refonte mobile planning et ameliorations suivi pedagogique [#NEWTS-4]
Fonction PWA et ajout du responsive design Planning mobile : - Nouvelle vue DayView avec bandeau semaine scrollable, date picker natif et navigation integree - ScheduleNavigation converti en drawer overlay sur mobile, sidebar fixe sur desktop - Suppression double barre navigation mobile, controles deplaces dans DayView - Date picker natif via label+input sur mobile Suivi pedagogique : - Refactorisation page grades avec composant Table partage - Colonnes stats par periode, absences, actions (Fiche + Evaluer) - Lien cliquable sur la classe vers SchoolClassManagement feat(backend): ajout associated_class_id dans StudentByRFCreationSerializer [#NEWTS-4] UI global : - Remplacement fleches texte par icones Lucide ChevronDown/ChevronRight - Pagination conditionnelle sur tous les tableaux plats - Layout responsive mobile : cartes separees fond transparent - Table.js : pagination optionnelle, wrapper md uniquement
This commit is contained in:
230
Front-End/src/components/Calendar/DayView.js
Normal file
230
Front-End/src/components/Calendar/DayView.js
Normal file
@ -0,0 +1,230 @@
|
||||
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 } = 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 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 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-emerald-600 text-white rounded-full hover:bg-emerald-700 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-emerald-600 text-white'
|
||||
: isToday(day)
|
||||
? 'border border-emerald-400 text-emerald-600'
|
||||
: '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-emerald-500 border pointer-events-none"
|
||||
style={{ top: getCurrentTimePosition() }}
|
||||
>
|
||||
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
|
||||
</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 border-b border-gray-100 ${
|
||||
isCurrentDay ? 'bg-emerald-50/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) => (
|
||||
<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);
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div
|
||||
className="font-semibold text-xs truncate"
|
||||
style={{ color: event.color }}
|
||||
>
|
||||
{event.title}
|
||||
</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;
|
||||
Reference in New Issue
Block a user