fix: Mise à jour des plannings

This commit is contained in:
N3WT DE COMPET
2026-04-05 11:09:32 +02:00
parent 1f2a1b88ac
commit 12939fca85
9 changed files with 292 additions and 68 deletions

View File

@ -9,6 +9,7 @@ import EventModal from '@/components/Calendar/EventModal';
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation'; import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
import { useState } from 'react'; import { useState } from 'react';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { usePlanning } from '@/context/PlanningContext';
export default function Page() { export default function Page() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -29,33 +30,36 @@ export default function Page() {
}); });
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId } = useEstablishment();
const initializeNewEvent = (date = new Date()) => { const PlanningContent = ({ isDrawerOpen, setIsDrawerOpen, isModalOpen, setIsModalOpen, eventData, setEventData }) => {
// S'assurer que date est un objet Date valide const { selectedSchedule, schedules } = usePlanning();
const eventDate = date instanceof Date ? date : new Date();
setEventData({ const initializeNewEvent = (date = new Date()) => {
title: '', const eventDate = date instanceof Date ? date : new Date();
description: '',
start: eventDate.toISOString(),
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '',
planning: '', // Ne pas définir de valeur par défaut ici non plus
recursionType: RecurrenceType.NONE,
selectedDays: [],
recursionEnd: new Date(
eventDate.getTime() + 2 * 60 * 60 * 1000
).toISOString(),
customInterval: 1,
customUnit: 'days',
});
setIsModalOpen(true);
};
return ( const selected =
<PlanningProvider schedules.find((schedule) => Number(schedule.id) === Number(selectedSchedule)) ||
establishmentId={selectedEstablishmentId} schedules[0];
modeSet={PlanningModes.PLANNING}
> setEventData({
title: '',
description: '',
start: eventDate.toISOString(),
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '',
planning: selected?.id || '',
color: selected?.color || '',
recursionType: RecurrenceType.NONE,
selectedDays: [],
recursionEnd: new Date(
eventDate.getTime() + 2 * 60 * 60 * 1000
).toISOString(),
customInterval: 1,
customUnit: 'days',
});
setIsModalOpen(true);
};
return (
<div className="flex h-full overflow-hidden"> <div className="flex h-full overflow-hidden">
<ScheduleNavigation <ScheduleNavigation
isOpen={isDrawerOpen} isOpen={isDrawerOpen}
@ -76,6 +80,22 @@ export default function Page() {
setEventData={setEventData} setEventData={setEventData}
/> />
</div> </div>
);
};
return (
<PlanningProvider
establishmentId={selectedEstablishmentId}
modeSet={PlanningModes.PLANNING}
>
<PlanningContent
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
eventData={eventData}
setEventData={setEventData}
/>
</PlanningProvider> </PlanningProvider>
); );
} }

View File

@ -13,7 +13,7 @@ import { getWeekEvents } from '@/utils/events';
import { CalendarDays, ChevronLeft, ChevronRight, Plus } from 'lucide-react'; import { CalendarDays, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => { const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
const { currentDate, setCurrentDate, parentView } = usePlanning(); const { currentDate, setCurrentDate, parentView, schedules } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const scrollRef = useRef(null); const scrollRef = useRef(null);
@ -43,11 +43,28 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
return `${(hours + minutes / 60) * 5}rem`; 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 calculateEventStyle = (event, allDayEvents) => {
const start = new Date(event.start); const start = new Date(event.start);
const end = new Date(event.end); const end = new Date(event.end);
const startMinutes = (start.getMinutes() / 60) * 5; const startMinutes = (start.getMinutes() / 60) * 5;
const duration = ((end - start) / (1000 * 60 * 60)) * 5; const duration = ((end - start) / (1000 * 60 * 60)) * 5;
const scheduleColor = getScheduleColor(event);
const overlapping = allDayEvents.filter((other) => { const overlapping = allDayEvents.filter((other) => {
if (other.id === event.id) return false; if (other.id === event.id) return false;
@ -179,8 +196,11 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
> >
{dayEvents {dayEvents
.filter((e) => new Date(e.start).getHours() === hour) .filter((e) => new Date(e.start).getHours() === hour)
.map((event) => ( .map((event) => {
<div const scheduleColor = getScheduleColor(event);
const classLevelLabel = getScheduleClassLevelLabel(event);
return (
<div
key={event.id} key={event.id}
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg" className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
style={calculateEventStyle(event, dayEvents)} style={calculateEventStyle(event, dayEvents)}
@ -192,32 +212,53 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
onEventClick(event); onEventClick(event);
} }
} }
> >
<div className="p-1"> {classLevelLabel && (
<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 <div
className="text-xs truncate" className="px-1 py-0.5 border-t-2"
style={{ color: event.color, opacity: 0.75 }} style={{
borderTopColor: scheduleColor,
backgroundColor: `${scheduleColor}22`,
}}
> >
{event.location} <span
className="text-[10px] font-semibold uppercase tracking-wide truncate block text-center"
style={{ color: scheduleColor }}
>
{classLevelLabel}
</span>
</div> </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>
</div> );
))} })}
</div> </div>
</React.Fragment> </React.Fragment>
))} ))}

View File

@ -10,20 +10,31 @@ export default function EventModal({
eventData, eventData,
setEventData, setEventData,
}) { }) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = const {
addEvent,
handleUpdateEvent,
handleDeleteEvent,
schedules,
selectedSchedule,
} =
usePlanning(); usePlanning();
const { showNotification } = useNotification(); const { showNotification } = useNotification();
// S'assurer que planning est défini lors du premier rendu // S'assurer que planning est défini lors du premier rendu
React.useEffect(() => { React.useEffect(() => {
if (!eventData?.planning && schedules.length > 0) { if (!eventData?.planning && schedules.length > 0) {
const defaultSchedule =
schedules.find(
(schedule) => Number(schedule.id) === Number(selectedSchedule)
) || schedules[0];
setEventData((prev) => ({ setEventData((prev) => ({
...prev, ...prev,
planning: schedules[0].id, planning: defaultSchedule.id,
color: schedules[0].color, color: defaultSchedule.color,
})); }));
} }
}, [schedules, eventData?.planning]); }, [schedules, selectedSchedule, eventData?.planning]);
if (!isOpen) return null; if (!isOpen) return null;

View File

@ -14,7 +14,24 @@ import { fr } from 'date-fns/locale';
import { getEventsForDate } from '@/utils/events'; import { getEventsForDate } from '@/utils/events';
const MonthView = ({ onDateClick, onEventClick }) => { const MonthView = ({ onDateClick, onEventClick }) => {
const { currentDate, setViewType, setCurrentDate, events } = usePlanning(); const { currentDate, setViewType, setCurrentDate, events, schedules } =
usePlanning();
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;
};
// Obtenir tous les jours du mois actuel // Obtenir tous les jours du mois actuel
const monthStart = startOfMonth(currentDate); const monthStart = startOfMonth(currentDate);
@ -53,8 +70,11 @@ const MonthView = ({ onDateClick, onEventClick }) => {
</span> </span>
</div> </div>
<div className="space-y-1 flex-1"> <div className="space-y-1 flex-1">
{dayEvents.map((event, index) => ( {dayEvents.map((event) => {
<div const scheduleColor = getScheduleColor(event);
const classLevelLabel = getScheduleClassLevelLabel(event);
return (
<div
key={event.id} key={event.id}
className="text-xs p-1 rounded truncate cursor-pointer" className="text-xs p-1 rounded truncate cursor-pointer"
style={{ style={{
@ -67,9 +87,32 @@ const MonthView = ({ onDateClick, onEventClick }) => {
onEventClick(event); onEventClick(event);
}} }}
> >
{event.title} {classLevelLabel && (
<div
className="-mx-1 -mt-1 mb-1 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>
)}
<span className="inline-flex items-center gap-1 max-w-full">
<span
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: event.color }}
/>
<span className="truncate flex-1">{event.title}</span>
</span>
</div> </div>
))} );
})}
</div> </div>
</div> </div>
); );

View File

@ -7,7 +7,7 @@ import { isToday } from 'date-fns';
const WeekView = ({ onDateClick, onEventClick, events }) => { const WeekView = ({ onDateClick, onEventClick, events }) => {
const { currentDate, planningMode, parentView } = usePlanning(); const { currentDate, planningMode, parentView, schedules } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const scrollContainerRef = useRef(null); // Ajouter cette référence const scrollContainerRef = useRef(null); // Ajouter cette référence
@ -71,11 +71,28 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
}); });
}; };
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, dayEvents) => { const calculateEventStyle = (event, dayEvents) => {
const start = new Date(event.start); const start = new Date(event.start);
const end = new Date(event.end); const end = new Date(event.end);
const startMinutes = (start.getMinutes() / 60) * 5; const startMinutes = (start.getMinutes() / 60) * 5;
const duration = ((end - start) / (1000 * 60 * 60)) * 5; const duration = ((end - start) / (1000 * 60 * 60)) * 5;
const scheduleColor = getScheduleColor(event);
// Trouver les événements qui se chevauchent // Trouver les événements qui se chevauchent
const overlappingEvents = findOverlappingEvents(event, dayEvents); const overlappingEvents = findOverlappingEvents(event, dayEvents);
@ -101,6 +118,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
const renderEventInCell = (event, dayEvents) => { const renderEventInCell = (event, dayEvents) => {
const eventStyle = calculateEventStyle(event, dayEvents); const eventStyle = calculateEventStyle(event, dayEvents);
const scheduleColor = getScheduleColor(event);
const classLevelLabel = getScheduleClassLevelLabel(event);
return ( return (
<div <div
@ -116,12 +135,32 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
} }
} }
> >
{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="p-1">
<div <div
className="font-semibold text-xs truncate" className="font-semibold text-xs truncate flex items-center gap-1"
style={{ color: event.color }} style={{ color: event.color }}
> >
{event.title} <span
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: event.color }}
/>
<span className="truncate flex-1">{event.title}</span>
</div> </div>
<div <div
className="text-xs" className="text-xs"

View File

@ -24,6 +24,29 @@ const ItemTypes = {
TEACHER: 'teacher', TEACHER: 'teacher',
}; };
const CLASS_PLANNING_COLORS = [
'#EF4444',
'#3B82F6',
'#F59E0B',
'#8B5CF6',
'#EC4899',
'#14B8A6',
'#F97316',
'#6366F1',
'#06B6D4',
'#84CC16',
];
const getClassPlanningColor = (schoolClass) => {
const numericId = Number(schoolClass?.id);
if (!Number.isFinite(numericId) || numericId <= 0) {
return CLASS_PLANNING_COLORS[0];
}
const index = (numericId - 1) % CLASS_PLANNING_COLORS.length;
return CLASS_PLANNING_COLORS[index];
};
const TeachersDropZone = ({ const TeachersDropZone = ({
classe, classe,
handleTeachersChange, handleTeachersChange,
@ -245,13 +268,14 @@ const ClassesSection = ({
setNewClass(null); setNewClass(null);
setLocalErrors({}); setLocalErrors({});
// Creation des plannings associé à la classe // Creation des plannings associé à la classe
const classPlanningColor = getClassPlanningColor(createdClass);
createdClass.levels.forEach((level) => { createdClass.levels.forEach((level) => {
const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name; const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name;
const planningName = `${createdClass.atmosphere_name} - ${levelName}`; const planningName = `${createdClass.atmosphere_name} - ${levelName}`;
const newPlanning = { const newPlanning = {
name: planningName, name: planningName,
color: '#FF5733', // Couleur par défaut color: classPlanningColor,
school_class: createdClass.id, school_class: createdClass.id,
}; };
addSchedule(newPlanning); addSchedule(newPlanning);

View File

@ -12,13 +12,22 @@ export default function ScheduleEventModal({
teachers, teachers,
classes, classes,
}) { }) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = const {
addEvent,
handleUpdateEvent,
handleDeleteEvent,
schedules,
selectedSchedule,
} =
usePlanning(); usePlanning();
const { showNotification } = useNotification(); const { showNotification } = useNotification();
React.useEffect(() => { React.useEffect(() => {
if (!eventData?.planning && schedules.length > 0) { if (!eventData?.planning && schedules.length > 0) {
const defaultSchedule = schedules[0]; const defaultSchedule =
schedules.find(
(schedule) => Number(schedule.id) === Number(selectedSchedule)
) || schedules[0];
if (eventData?.planning !== defaultSchedule.id) { if (eventData?.planning !== defaultSchedule.id) {
setEventData((prev) => ({ setEventData((prev) => ({
...prev, ...prev,
@ -26,7 +35,7 @@ export default function ScheduleEventModal({
})); }));
} }
} }
}, [schedules, eventData?.planning]); }, [schedules, selectedSchedule, eventData?.planning]);
const handleSpecialityChange = (specialityId) => { const handleSpecialityChange = (specialityId) => {
const selectedSpeciality = specialities.find( const selectedSpeciality = specialities.find(

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { RecurrenceType } from '@/context/PlanningContext'; import { RecurrenceType, usePlanning } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar/Calendar'; import Calendar from '@/components/Calendar/Calendar';
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal'; import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation'; import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
@ -12,6 +12,7 @@ export default function ScheduleManagement({
specialities, specialities,
teachers, teachers,
}) { }) {
const { selectedSchedule, schedules } = usePlanning();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [eventData, setEventData] = useState({ const [eventData, setEventData] = useState({
title: '', title: '',
@ -32,13 +33,18 @@ export default function ScheduleManagement({
// S'assurer que date est un objet Date valide // S'assurer que date est un objet Date valide
const eventDate = date instanceof Date ? date : new Date(); const eventDate = date instanceof Date ? date : new Date();
const selected =
schedules.find((schedule) => Number(schedule.id) === Number(selectedSchedule)) ||
schedules[0];
setEventData({ setEventData({
title: '', title: '',
description: '', description: '',
start: eventDate.toISOString(), start: eventDate.toISOString(),
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(), end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '', location: '',
planning: '', // Ne pas définir de valeur par défaut ici non plus planning: selected?.id || '',
color: selected?.color || '',
recursionType: RecurrenceType.NONE, recursionType: RecurrenceType.NONE,
selectedDays: [], selectedDays: [],
recursionEnd: new Date( recursionEnd: new Date(

View File

@ -36,6 +36,29 @@ export const PlanningModes = Object.freeze({
PLANNING: 'planning' PLANNING: 'planning'
}); });
const CLASS_SCHEDULE_COLORS = [
'#EF4444',
'#3B82F6',
'#F59E0B',
'#8B5CF6',
'#EC4899',
'#14B8A6',
'#F97316',
'#6366F1',
'#06B6D4',
'#84CC16',
];
const getClassScheduleColor = (schoolClassId) => {
const numericId = Number(schoolClassId);
if (!Number.isFinite(numericId) || numericId <= 0) {
return '#10b981';
}
const index = (numericId - 1) % CLASS_SCHEDULE_COLORS.length;
return CLASS_SCHEDULE_COLORS[index];
};
export function PlanningProvider({ export function PlanningProvider({
children, children,
modeSet = PlanningModes.PLANNING, modeSet = PlanningModes.PLANNING,
@ -65,7 +88,15 @@ export function PlanningProvider({
const reloadPlanning = () => { const reloadPlanning = () => {
fetchPlannings(selectedEstablishmentId, planningMode).then((data) => { fetchPlannings(selectedEstablishmentId, planningMode).then((data) => {
setSchedules(data); const normalizedSchedules =
planningMode === PlanningModes.CLASS_SCHEDULE
? data.map((schedule) => ({
...schedule,
color: getClassScheduleColor(schedule.school_class),
}))
: data;
setSchedules(normalizedSchedules);
if (data.length > 0) { if (data.length > 0) {
setSelectedSchedule(data[0].id); setSelectedSchedule(data[0].id);
} }