mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-04 02:01:28 +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:
@ -4,6 +4,7 @@ import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
import PlanningView from '@/components/Calendar/PlanningView';
|
||||
import DayView from '@/components/Calendar/DayView';
|
||||
import ToggleView from '@/components/ToggleView';
|
||||
import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
@ -11,9 +12,11 @@ import {
|
||||
addWeeks,
|
||||
addMonths,
|
||||
addYears,
|
||||
addDays,
|
||||
subWeeks,
|
||||
subMonths,
|
||||
subYears,
|
||||
subDays,
|
||||
getWeek,
|
||||
setMonth,
|
||||
setYear,
|
||||
@ -22,7 +25,7 @@ import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) => {
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName = '', onOpenDrawer = () => {} }) => {
|
||||
const {
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
@ -35,6 +38,14 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
} = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => setIsMobile(window.innerWidth < 768);
|
||||
check();
|
||||
window.addEventListener('resize', check);
|
||||
return () => window.removeEventListener('resize', check);
|
||||
}, []);
|
||||
|
||||
// Ajouter ces fonctions pour la gestion des mois et années
|
||||
const months = Array.from({ length: 12 }, (_, i) => ({
|
||||
@ -68,7 +79,12 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
|
||||
const navigateDate = (direction) => {
|
||||
const getNewDate = () => {
|
||||
switch (viewType) {
|
||||
const effectiveView = isMobile ? 'day' : viewType;
|
||||
switch (effectiveView) {
|
||||
case 'day':
|
||||
return direction === 'next'
|
||||
? addDays(currentDate, 1)
|
||||
: subDays(currentDate, 1);
|
||||
case 'week':
|
||||
return direction === 'next'
|
||||
? addWeeks(currentDate, 1)
|
||||
@ -91,116 +107,114 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateDate('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{format(
|
||||
currentDate,
|
||||
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
|
||||
{ locale: fr }
|
||||
)}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
<div className="p-2 border-b">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{months.map((month) => (
|
||||
<button
|
||||
key={month.value}
|
||||
onClick={() => handleMonthSelect(month.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
{/* Header uniquement sur desktop */}
|
||||
<div className="hidden md:flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
<>
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
<div className="p-2 border-b">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{months.map((month) => (
|
||||
<button key={month.value} onClick={() => handleMonthSelect(month.value)} className="p-2 text-sm hover:bg-gray-100 rounded-md">
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{years.map((year) => (
|
||||
<button key={year.value} onClick={() => handleYearSelect(year.value)} className="p-2 text-sm hover:bg-gray-100 rounded-md">
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year.value}
|
||||
onClick={() => handleYearSelect(year.value)}
|
||||
className="p-2 text-sm hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex justify-center">
|
||||
{((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
{getWeek(currentDate, { weekStartsOn: 1 })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{parentView && (
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-base font-semibold">
|
||||
{planningClassName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centre : numéro de semaine ou classe/niveau */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
{getWeek(currentDate, { weekStartsOn: 1 })}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
)}
|
||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{parentView && (
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-base font-semibold">
|
||||
{/* À adapter selon les props disponibles */}
|
||||
{planningClassName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
)}
|
||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
{/* Contenu scrollable */}
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewType === 'week' && (
|
||||
{isMobile && (
|
||||
<motion.div
|
||||
key="day"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<DayView
|
||||
onDateClick={onDateClick}
|
||||
onEventClick={onEventClick}
|
||||
events={visibleEvents}
|
||||
onOpenDrawer={onOpenDrawer}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{!isMobile && viewType === 'week' && (
|
||||
<motion.div
|
||||
key="week"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -216,7 +230,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'month' && (
|
||||
{!isMobile && viewType === 'month' && (
|
||||
<motion.div
|
||||
key="month"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -231,7 +245,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'year' && (
|
||||
{!isMobile && viewType === 'year' && (
|
||||
<motion.div
|
||||
key="year"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@ -242,7 +256,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
|
||||
<YearView onDateClick={onDateClick} events={visibleEvents} />
|
||||
</motion.div>
|
||||
)}
|
||||
{viewType === 'planning' && (
|
||||
{!isMobile && viewType === 'planning' && (
|
||||
<motion.div
|
||||
key="planning"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
||||
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;
|
||||
@ -253,7 +253,7 @@ export default function EventModal({
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Début
|
||||
|
||||
@ -75,22 +75,35 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const dayLabels = [
|
||||
{ short: 'L', long: 'Lun' },
|
||||
{ short: 'M', long: 'Mar' },
|
||||
{ short: 'M', long: 'Mer' },
|
||||
{ short: 'J', long: 'Jeu' },
|
||||
{ short: 'V', long: 'Ven' },
|
||||
{ short: 'S', long: 'Sam' },
|
||||
{ short: 'D', long: 'Dim' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white">
|
||||
{/* En-tête des jours de la semaine */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="p-2 text-center text-sm font-medium text-gray-500"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Grille des jours */}
|
||||
<div className="flex-1 grid grid-cols-7 grid-rows-[repeat(6,1fr)]">
|
||||
{days.map((day) => renderDay(day))}
|
||||
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white overflow-x-auto">
|
||||
<div className="min-w-[280px]">
|
||||
{/* En-tête des jours de la semaine */}
|
||||
<div className="grid grid-cols-7 border-b">
|
||||
{dayLabels.map((day, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-1 sm:p-2 text-center text-xs sm:text-sm font-medium text-gray-500"
|
||||
>
|
||||
<span className="sm:hidden">{day.short}</span>
|
||||
<span className="hidden sm:inline">{day.long}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Grille des jours */}
|
||||
<div className="grid grid-cols-7 grid-rows-[repeat(6,1fr)]">
|
||||
{days.map((day) => renderDay(day))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -32,7 +32,7 @@ const PlanningView = ({ events, onEventClick }) => {
|
||||
|
||||
return (
|
||||
<div className="bg-white h-full overflow-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<table className="min-w-full border-collapse">
|
||||
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
|
||||
|
||||
@ -3,7 +3,7 @@ import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
|
||||
export default function ScheduleNavigation({ classes, modeSet = 'event', isOpen = false, onClose = () => {} }) {
|
||||
const {
|
||||
schedules,
|
||||
selectedSchedule,
|
||||
@ -62,22 +62,10 @@ export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="w-64 border-r p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold">
|
||||
{planningMode === PlanningModes.CLASS_SCHEDULE
|
||||
? 'Emplois du temps'
|
||||
: 'Plannings'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
const title = planningMode === PlanningModes.CLASS_SCHEDULE ? 'Emplois du temps' : 'Plannings';
|
||||
|
||||
const listContent = (
|
||||
<>
|
||||
{isAddingNew && (
|
||||
<div className="mb-4 p-2 border rounded">
|
||||
<input
|
||||
@ -251,6 +239,50 @@ export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop : sidebar fixe */}
|
||||
<nav className="hidden md:flex flex-col w-64 border-r p-4 h-full overflow-y-auto shrink-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold">{title}</h2>
|
||||
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{listContent}
|
||||
</nav>
|
||||
|
||||
{/* Mobile : drawer en overlay */}
|
||||
<div
|
||||
className={`md:hidden fixed inset-0 z-50 transition-opacity duration-200 ${
|
||||
isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
|
||||
<div
|
||||
className={`absolute left-0 top-0 bottom-0 w-72 bg-white shadow-xl flex flex-col transition-transform duration-200 ${
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
||||
<h2 className="font-semibold">{title}</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{listContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ const YearView = ({ onDateClick }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4 p-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||
{months.map((month) => (
|
||||
<MonthCard
|
||||
key={month.getTime()}
|
||||
|
||||
Reference in New Issue
Block a user