feat: Gestion du planning [3]

This commit is contained in:
Luc SORIGNET
2025-05-03 15:12:17 +02:00
parent cb4fe74a9e
commit 58144ba0d0
39 changed files with 939 additions and 1864 deletions

View File

@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
import logger from '@/utils/logger';
const Calendar = ({ onDateClick, onEventClick }) => {
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
const {
currentDate,
setCurrentDate,

View File

@ -1,4 +1,4 @@
import { usePlanning } from '@/context/PlanningContext';
import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
@ -13,23 +13,23 @@ export default function EventModal({
// S'assurer que planning est défini lors du premier rendu
React.useEffect(() => {
if (!eventData.planning && schedules.length > 0) {
if (!eventData?.planning && schedules.length > 0) {
setEventData((prev) => ({
...prev,
planning: schedules[0].id,
color: schedules[0].color,
}));
}
}, [schedules, eventData.planning]);
}, [schedules, eventData?.planning]);
if (!isOpen) return null;
const recurrenceOptions = [
{ value: 'none', label: 'Aucune' },
{ value: 'daily', label: 'Quotidienne' },
{ value: 'weekly', label: 'Hebdomadaire' },
{ value: 'monthly', label: 'Mensuelle' },
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option
{ value: RecurrenceType.NONE, label: 'Aucune' },
{ value: RecurrenceType.DAILY, label: 'Quotidienne' },
{ value: RecurrenceType.WEEKLY, label: 'Hebdomadaire' },
{ value: RecurrenceType.MONTHLY, label: 'Mensuelle' },
/* { value: RecurrenceType.CUSTOM, label: 'Personnalisée' }, */
];
const daysOfWeek = [
@ -171,10 +171,13 @@ export default function EventModal({
Récurrence
</label>
<select
value={eventData.recurrence || 'none'}
onChange={(e) =>
setEventData({ ...eventData, recurrence: e.target.value })
}
value={eventData.recursionType || RecurrenceType.NONE}
onChange={(e) => {
return setEventData({
...eventData,
recursionType: e.target.value,
});
}}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
{recurrenceOptions.map((option) => (
@ -186,46 +189,7 @@ export default function EventModal({
</div>
{/* Paramètres de récurrence personnalisée */}
{eventData.recurrence === 'custom' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Répéter tous les
</label>
<div className="flex gap-2">
<input
type="number"
min="1"
value={eventData.customInterval || 1}
onChange={(e) =>
setEventData({
...eventData,
customInterval: parseInt(e.target.value) || 1,
})
}
className="w-20 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
<select
value={eventData.customUnit || 'days'}
onChange={(e) =>
setEventData({
...eventData,
customUnit: e.target.value,
})
}
className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
<option value="days">Jours</option>
<option value="weeks">Semaines</option>
<option value="months">Mois</option>
</select>
</div>
</div>
</div>
)}
{/* Jours de la semaine (pour récurrence hebdomadaire) */}
{eventData.recurrence === 'weekly' && (
{eventData.recursionType == RecurrenceType.CUSTOM && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Jours de répétition
@ -256,16 +220,25 @@ export default function EventModal({
)}
{/* Date de fin de récurrence */}
{eventData.recurrence !== 'none' && (
{eventData.recursionType != RecurrenceType.NONE && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin de récurrence
</label>
<input
type="date"
value={eventData.recurrenceEnd || ''}
value={
eventData.recursionEnd
? format(new Date(eventData.recursionEnd), 'yyyy-MM-dd')
: ''
}
onChange={(e) =>
setEventData({ ...eventData, recurrenceEnd: e.target.value })
setEventData({
...eventData,
recursionEnd: e.target.value
? new Date(e.target.value).toISOString()
: null,
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>

View File

@ -0,0 +1,249 @@
import { useState } from 'react';
import { usePlanning,PlanningModes } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
export default function ScheduleNavigation({classes, modeSet='event'}) {
const {
schedules,
selectedSchedule,
setSelectedSchedule,
hiddenSchedules,
toggleScheduleVisibility,
addSchedule,
updateSchedule,
planningMode,
} = usePlanning();
const [editingId, setEditingId] = useState(null);
const [editedName, setEditedName] = useState('');
const [editedColor, setEditedColor] = useState('');
const [editedSchoolClass, setEditedSchoolClass] = useState(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [newSchedule, setNewSchedule] = useState({
name: '',
color: '#10b981',
school_class: '', // Ajout du champ pour la classe
});
const handleEdit = (schedule) => {
setEditingId(schedule.id);
setEditedName(schedule.name);
setEditedColor(schedule.color);
setEditedSchoolClass(schedule.school_class);
};
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find((s) => s.id === editingId),
name: editedName,
color: editedColor,
school_class: editedSchoolClass, // Ajout de l'ID de la classe
});
setEditingId(null);
}
};
const handleAddNew = () => {
if (newSchedule.name) {
let payload = {
name: newSchedule.name,
color: newSchedule.color,
};
if (planningMode === PlanningModes.CLASS_SCHEDULE) {
payload.school_class = newSchedule.school_class; // Ajout de l'ID de la classe
}
addSchedule({
id: `schedule-${Date.now()}`,
...payload,
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981', school_class: '' });
}
};
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>
{isAddingNew && (
<div className="mb-4 p-2 border rounded">
<input
type="text"
value={newSchedule.name}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full p-1 mb-2 border rounded"
placeholder={(planningMode===PlanningModes.CLASS_SCHEDULE)?"Nom de l'emplois du temps":"Nom du planning"}
/>
<div className="flex gap-2 items-center mb-2">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={newSchedule.color}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
}
className="w-8 h-8"
/>
</div>
{planningMode === PlanningModes.CLASS_SCHEDULE&& (
<div className="mb-2">
<label className="text-sm">Classe (optionnel):</label>
<select
value={newSchedule.school_class}
onChange={(e) =>
setNewSchedule((prev) => ({
...prev,
school_class: e.target.value,
}))
}
className="w-full p-1 border rounded"
>
<option value="">Aucune</option>
{classes.map((classe) => { console.log({classe});
return (
<option key={classe.id} value={classe.id}>
{classe.atmosphere_name}
</option>
)}
)}
</select>
</div>
)}
<div className="flex justify-end gap-2">
<button
onClick={() => setIsAddingNew(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleAddNew}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
)}
<ul className="space-y-2">
{schedules
.map((schedule) => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id
? 'bg-gray-100'
: 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
<div className="space-y-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="w-full p-1 border rounded"
/>
<div className="flex gap-2 items-center">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={editedColor}
onChange={(e) => setEditedColor(e.target.value)}
className="w-8 h-8"
/>
</div>
{planningMode === PlanningModes.CLASS_SCHEDULE && (
<div className="mb-2">
<label className="text-sm">Classe:</label>
<select
value={editedSchoolClass}
onChange={(e) => setEditedSchoolClass(e.target.value)}
className="w-full p-1 border rounded"
>
<option value="">Aucune</option>
{classes.map((classe) => (
<option key={classe.id} value={classe.id}>
{classe.atmosphere_name}
</option>
))}
</select>
</div>
)}
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingId(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleSave}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => setSelectedSchedule(schedule.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span
className={
hiddenSchedules.includes(schedule.id)
? 'text-gray-400'
: ''
}
>
{schedule.name}
</span>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la propagation du clic
toggleScheduleVisibility(schedule.id);
}}
className="p-1 hover:bg-gray-100 rounded"
>
{hiddenSchedules.includes(schedule.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(schedule)}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@ -1,12 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import {
format,
startOfWeek,
addDays,
differenceInMinutes,
isSameDay,
} from 'date-fns';
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getWeekEvents } from '@/utils/events';
import { isToday } from 'date-fns';
@ -49,7 +43,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
const getCurrentTimePosition = () => {
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
return `${(hours + minutes / 60) * 5}rem`;
const rowHeight = 5; // Hauteur des lignes en rem (h-20 = 5rem)
return `${((hours + minutes / 60) * rowHeight)}rem`;
};
// Utiliser les événements déjà filtrés passés en props
@ -144,17 +139,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
};
return (
<div className="flex flex-col h-full overflow-hidden">
<div className="flex flex-col h-full overflow-y-auto">
{/* En-tête des jours */}
<div
className="grid gap-[1px] bg-gray-100 pr-[17px]"
className="grid gap-[1px] w-full bg-gray-100"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
>
<div className="bg-white h-14"></div>
{weekDays.map((day) => (
<div
key={day}
className={`p-2 text-center border-b
className={`h-14 p-2 text-center border-b
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
>
@ -172,7 +167,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
</div>
{/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
<div ref={scrollContainerRef} className="flex-1 relative">
{/* Ligne de temps actuelle */}
{isCurrentWeek && (
<div
@ -181,12 +176,12 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
top: getCurrentTimePosition(),
}}
>
<div className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500" />
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
</div>
)}
<div
className="grid gap-[1px] bg-gray-100"
className="grid gap-[1px] w-full bg-gray-100"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
>
{timeSlots.map((hour) => (
@ -209,9 +204,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
onDateClick(date);
}}
>
<div className="flex gap-1">
{' '}
{/* Ajout de gap-1 */}
<div className="grid gap-1">
{dayEvents
.filter((event) => {
const eventStart = new Date(event.start);

View File

@ -2,7 +2,7 @@ import Logo from '@/components/Logo';
export default function Footer({ softwareName, softwareVersion }) {
return (
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
<footer className="absolute bottom-0 left-0 right-0 h-16 bg-white border-t border-gray-200 flex items-center justify-center box-border">
<div className="text-sm font-light">
<span>
&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.

View File

@ -1,193 +0,0 @@
import { useState } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
export default function ScheduleNavigation() {
const {
schedules,
selectedSchedule,
setSelectedSchedule,
hiddenSchedules,
toggleScheduleVisibility,
addSchedule,
updateSchedule,
} = usePlanning();
const [editingId, setEditingId] = useState(null);
const [editedName, setEditedName] = useState('');
const [editedColor, setEditedColor] = useState('');
const [isAddingNew, setIsAddingNew] = useState(false);
const [newSchedule, setNewSchedule] = useState({
name: '',
color: '#10b981',
});
const handleEdit = (schedule) => {
setEditingId(schedule.id);
setEditedName(schedule.name);
setEditedColor(schedule.color);
};
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find((s) => s.id === editingId),
name: editedName,
color: editedColor,
});
setEditingId(null);
}
};
const handleAddNew = () => {
if (newSchedule.name) {
addSchedule({
id: `schedule-${Date.now()}`,
...newSchedule,
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981' });
}
};
return (
<nav className="w-64 border-r p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">Plannings</h2>
<button
onClick={() => setIsAddingNew(true)}
className="p-1 hover:bg-gray-100 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{isAddingNew && (
<div className="mb-4 p-2 border rounded">
<input
type="text"
value={newSchedule.name}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full p-1 mb-2 border rounded"
placeholder="Nom du planning"
/>
<div className="flex gap-2 items-center mb-2">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={newSchedule.color}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsAddingNew(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleAddNew}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
)}
<ul className="space-y-2">
{schedules.map((schedule) => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id
? 'bg-gray-100'
: 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
<div className="space-y-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="w-full p-1 border rounded"
/>
<div className="flex gap-2 items-center">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={editedColor}
onChange={(e) => setEditedColor(e.target.value)}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingId(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleSave}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => setSelectedSchedule(schedule.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span
className={
hiddenSchedules.includes(schedule.id)
? 'text-gray-400'
: ''
}
>
{schedule.name}
</span>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la propagation du clic
toggleScheduleVisibility(schedule.id);
}}
className="p-1 hover:bg-gray-100 rounded"
>
{hiddenSchedules.includes(schedule.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(schedule)}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@ -4,8 +4,8 @@ const SidebarTabs = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return (
<div className="w-full">
<div className="flex border-b-2 border-gray-200">
<>
<div className="flex h-14 border-b-2 border-gray-200">
{tabs.map((tab) => (
<button
key={tab.id}
@ -20,17 +20,15 @@ const SidebarTabs = ({ tabs }) => {
</button>
))}
</div>
<div className="p-4">
{tabs.map((tab) => (
<div
key={tab.id}
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
>
{tab.content}
</div>
))}
</div>
</div>
</>
);
};

View File

@ -12,8 +12,10 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext';
import { useRouter } from 'next/navigation';
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
import { usePlanning } from '@/context/PlanningContext';
import { useClasses } from '@/context/ClassesContext';
const ItemTypes = {
TEACHER: 'teacher',
@ -117,6 +119,7 @@ const ClassesSection = ({
handleCreate,
handleEdit,
handleDelete,
}) => {
const [formData, setFormData] = useState({});
const [editingClass, setEditingClass] = useState(null);
@ -129,41 +132,10 @@ const ClassesSection = ({
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
const [selectedClass, setSelectedClass] = useState(null);
const router = useRouter();
const { selectedEstablishmentId } = useEstablishment();
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
const{ getNiveauxLabels, allNiveaux } = useClasses();
const niveauxPremierCycle = [
{ id: 1, name: 'TPS', age: 2 },
{ id: 2, name: 'PS', age: 3 },
{ id: 3, name: 'MS', age: 4 },
{ id: 4, name: 'GS', age: 5 },
];
const niveauxSecondCycle = [
{ id: 5, name: 'CP', age: 6 },
{ id: 6, name: 'CE1', age: 7 },
{ id: 7, name: 'CE2', age: 8 },
];
const niveauxTroisiemeCycle = [
{ id: 8, name: 'CM1', age: 9 },
{ id: 9, name: 'CM2', age: 10 },
];
const allNiveaux = [
...niveauxPremierCycle,
...niveauxSecondCycle,
...niveauxTroisiemeCycle,
];
const getNiveauxLabels = (levels) => {
return levels.map((niveauId) => {
const niveau = allNiveaux.find((n) => n.id === niveauId);
return niveau ? niveau.name : niveauId;
});
};
// Fonction pour générer les années scolaires
const getSchoolYearChoices = () => {
@ -241,6 +213,19 @@ const ClassesSection = ({
setClasses((prevClasses) => [createdClass, ...classes]);
setNewClass(null);
setLocalErrors({});
// Creation des plannings associé à la classe
createdClass.levels.forEach((level) => {
const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name;
const planningName = `${createdClass.atmosphere_name} - ${levelName}`;
const newPlanning = {
name: planningName,
color: '#FF5733', // Couleur par défaut
school_class: createdClass.id,
}
addSchedule(newPlanning)
});
})
.catch((error) => {
logger.error('Error:', error.message);
@ -505,6 +490,8 @@ const ClassesSection = ({
);
setPopupVisible(true);
setRemovePopupVisible(false);
reloadPlanning();
reloadEvents();
})
.catch((error) => {
logger.error('Error archiving data:', error);

View File

@ -1,47 +0,0 @@
import React from 'react';
import { Calendar } from 'lucide-react';
const DateRange = ({
nameStart,
nameEnd,
valueStart,
valueEnd,
onChange,
label,
}) => {
return (
<div className="space-y-4 mt-4 p-4 border rounded-md shadow-sm bg-white">
<label className="block text-lg font-medium text-gray-700 mb-2">
{label}
</label>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 items-center">
<div className="relative flex items-center">
<span className="mr-2">Du</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameStart}
value={valueStart}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de début"
/>
</div>
<div className="relative flex items-center">
<span className="mr-2">Au</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameEnd}
value={valueEnd}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de fin"
/>
</div>
</div>
</div>
);
};
export default DateRange;

View File

@ -1,121 +0,0 @@
import React from 'react';
import RadioList from '@/components/RadioList';
import DateRange from '@/components/Structure/Configuration/DateRange';
import TimeRange from '@/components/Structure/Configuration/TimeRange';
import CheckBoxList from '@/components/CheckBoxList';
const PlanningConfiguration = ({
formData,
handleChange,
handleTimeChange,
handleJoursChange,
typeEmploiDuTemps,
}) => {
const daysOfWeek = [
{ id: 1, name: 'lun' },
{ id: 2, name: 'mar' },
{ id: 3, name: 'mer' },
{ id: 4, name: 'jeu' },
{ id: 5, name: 'ven' },
{ id: 6, name: 'sam' },
];
const isLabelAttenuated = (item) => {
return !formData.opening_days.includes(parseInt(item.id));
};
return (
<div className="space-y-4">
<label className="mt-6 block text-2xl font-medium text-gray-700">
Emploi du temps
</label>
<div className="flex justify-between space-x-4 items-start">
<div className="w-1/2">
<RadioList
items={typeEmploiDuTemps}
formData={formData}
handleChange={handleChange}
fieldName="type"
className="w-full"
/>
</div>
{/* Plage horaire */}
<div className="w-1/2">
<TimeRange
startTime={formData.time_range[0]}
endTime={formData.time_range[1]}
onStartChange={(e) => handleTimeChange(e, 0)}
onEndChange={(e) => handleTimeChange(e, 1)}
/>
{/* CheckBoxList */}
<CheckBoxList
items={daysOfWeek}
formData={formData}
handleChange={handleJoursChange}
fieldName="opening_days"
horizontal={true}
labelAttenuated={isLabelAttenuated}
/>
</div>
</div>
{/* DateRange */}
<div className="space-y-4 w-full">
{formData.type === 2 && (
<>
<DateRange
nameStart="date_debut_semestre_1"
nameEnd="date_fin_semestre_1"
valueStart={formData.date_debut_semestre_1}
valueEnd={formData.date_fin_semestre_1}
onChange={handleChange}
label="Semestre 1"
/>
<DateRange
nameStart="date_debut_semestre_2"
nameEnd="date_fin_semestre_2"
valueStart={formData.date_debut_semestre_2}
valueEnd={formData.date_fin_semestre_2}
onChange={handleChange}
label="Semestre 2"
/>
</>
)}
{formData.type === 3 && (
<>
<DateRange
nameStart="date_debut_trimestre_1"
nameEnd="date_fin_trimestre_1"
valueStart={formData.date_debut_trimestre_1}
valueEnd={formData.date_fin_trimestre_1}
onChange={handleChange}
label="Trimestre 1"
/>
<DateRange
nameStart="date_debut_trimestre_2"
nameEnd="date_fin_trimestre_2"
valueStart={formData.date_debut_trimestre_2}
valueEnd={formData.date_fin_trimestre_2}
onChange={handleChange}
label="Trimestre 2"
/>
<DateRange
nameStart="date_debut_trimestre_3"
nameEnd="date_fin_trimestre_3"
valueStart={formData.date_debut_trimestre_3}
valueEnd={formData.date_fin_trimestre_3}
onChange={handleChange}
label="Trimestre 3"
/>
</>
)}
</div>
</div>
);
};
export default PlanningConfiguration;

View File

@ -22,7 +22,7 @@ const StructureManagement = ({
handleDelete,
}) => {
return (
<div className="w-full mx-auto mt-6">
<div className="w-full p-4 mx-auto mt-6">
<ClassesProvider>
<div className="mt-8 w-2/5">
<SpecialitiesSection

View File

@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
}
return (
<div className="w-full mx-auto mt-6">
<div className="w-full p-4 mx-auto mt-6">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}

View File

@ -1,40 +0,0 @@
import React from 'react';
import TeacherLabel from '@/components/CustomLabels/TeacherLabel';
const ClassesInformation = ({ selectedClass, isPastYear }) => {
if (!selectedClass) {
return null;
}
return (
<div
className={`w-full p-6 shadow-lg rounded-full border relative ${isPastYear ? 'bg-gray-200 border-gray-600' : 'bg-emerald-200 border-emerald-500'}`}
>
<div
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
>
<p className="text-gray-700 text-center">
<strong>{selectedClass.age_range} ans</strong>
</p>
</div>
<div
className={`border-b pb-4 ${isPastYear ? 'border-gray-600' : 'border-emerald-500'}`}
>
<div className="flex flex-wrap justify-center space-x-4">
{selectedClass.teachers.map((teacher) => (
<div key={teacher.id} className="relative group mt-4">
<TeacherLabel nom={teacher.nom} prenom={teacher.prenom} />
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-full mb-2 w-max px-4 py-2 text-white bg-gray-800 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-sm">
{teacher.nom} {teacher.prenom}
</p>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default ClassesInformation;

View File

@ -1,89 +0,0 @@
import React from 'react';
import { History, Clock, Users } from 'lucide-react';
import logger from '@/utils/logger';
const ClassesList = ({ classes, onClassSelect, selectedClassId }) => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const currentSchoolYearStart =
currentMonth >= 8 ? currentYear : currentYear - 1;
const handleClassClick = (classe) => {
logger.debug(
`Classe sélectionnée: ${classe.atmosphere_name}, Année scolaire: ${classe.school_year}`
);
onClassSelect(classe);
};
const categorizedClasses = classes.reduce((acc, classe) => {
const { school_year } = classe;
const [startYear] = school_year.split('-').map(Number);
const category =
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(classe);
return acc;
}, {});
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h3 className="text-xl font-semibold mb-2 text-emerald-600 flex items-center space-x-2">
<Clock className="inline-block mr-2 w-5 h-5" /> Actives
</h3>
<div className="flex flex-col">
{categorizedClasses['Actives']?.map((classe) => (
<div
key={classe.id}
className={`flex items-center ${selectedClassId === classe.id ? 'bg-emerald-600 text-white' : 'bg-emerald-100 text-emerald-600'} border border-emerald-300 rounded-lg shadow-lg overflow-hidden hover:bg-emerald-300 hover:text-emerald-700 cursor-pointer p-4 mb-4`}
onClick={() => handleClassClick(classe)}
style={{ maxWidth: '400px' }}
>
<div className="flex-1 text-sm font-medium">
{classe.atmosphere_name}
</div>
<div className="flex-1 text-sm font-medium">
{classe.school_year}
</div>
</div>
))}
</div>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 text-gray-600 flex items-center space-x-2">
<History className="inline-block mr-2 w-5 h-5" /> Anciennes
</h3>
<div className="flex flex-col">
{categorizedClasses['Anciennes']?.map((classe) => (
<div
key={classe.id}
className={`flex items-center ${selectedClassId === classe.id ? 'bg-gray-400 text-white' : 'bg-gray-100 text-gray-600'} border border-gray-300 rounded-lg shadow-lg overflow-hidden hover:bg-gray-300 hover:text-gray-700 cursor-pointer p-4 mb-4`}
onClick={() => handleClassClick(classe)}
style={{ maxWidth: '400px' }}
>
<div className="flex-1 text-sm font-medium">
{classe.atmosphere_name}
</div>
<div className="flex-1 text-sm font-medium">
{classe.school_year}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default ClassesList;

View File

@ -1,40 +0,0 @@
import React from 'react';
import { useDrag } from 'react-dnd';
import { UserIcon } from 'lucide-react'; // Assure-toi d'importer l'icône que tu souhaites utiliser
const DraggableSpeciality = ({ speciality }) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: 'SPECIALITY',
item: {
id: speciality.id,
name: speciality.nom,
color: speciality.codeCouleur,
teachers: speciality.teachers,
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return (
<span
ref={drag}
key={speciality.id}
className={`relative flex items-center px-4 py-2 rounded-full font-bold text-white text-center shadow-lg cursor-pointer transition-transform duration-200 ease-in-out transform ${isDragging ? 'opacity-50 scale-95' : 'scale-100 hover:scale-105 hover:shadow-xl'}`}
style={{
backgroundColor: speciality.codeCouleur,
minWidth: '200px',
maxWidth: '400px',
}}
title={speciality.nom}
>
{speciality.nom}
<span className="absolute top-0 right-0 mt-1 mr-1 flex items-center justify-center text-xs bg-black bg-opacity-50 rounded-full px-2 py-1">
<UserIcon size={16} className="ml-1" />
{speciality.teachers.length}
</span>
</span>
);
};
export default DraggableSpeciality;

View File

@ -1,87 +0,0 @@
import React from 'react';
import { useDrop } from 'react-dnd';
import PropTypes from 'prop-types';
// Définition du composant DropTargetCell
const DropTargetCell = ({ day, hour, courses, onDrop, onClick }) => {
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
accept: 'SPECIALITY',
drop: (item) => onDrop(item, hour, day),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[hour, day]
);
const isColorDark = (color) => {
if (!color) return false;
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
return r * 0.299 + g * 0.587 + b * 0.114 < 150;
};
const isToday = (someDate) => {
const today = new Date();
return (
someDate.getDate() === today.getDate() &&
someDate.getMonth() === today.getMonth() &&
someDate.getFullYear() === today.getFullYear()
);
};
// Vérifie si c'est une heure pleine
const isFullHour = parseInt(hour.split(':')[1], 10) === 0;
return (
<div
ref={drop}
onClick={() => onClick(hour, day)}
className={`relative cursor-pointer
${isToday(new Date(day)) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}
hover:bg-emerald-100 h-10 border-b
${isFullHour ? 'border-emerald-200' : 'border-gray-300'}
${isOver && canDrop ? 'bg-emerald-200' : ''}`} // Ajouté pour indiquer le drop
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
width: '100%',
}}
>
{courses.map((course) => (
<div
key={course.matiere}
className="flex flex-row items-center justify-center gap-2"
style={{
backgroundColor: course.color,
color: isColorDark(course.color) ? '#E5E5E5' : '#333333',
width: '100%',
height: '100%',
}}
>
<div style={{ textAlign: 'center', fontWeight: 'bold' }}>
{course.matiere}
</div>
<div style={{ fontStyle: 'italic', textAlign: 'center' }}>
{course.teachers.join(', ')}
</div>
</div>
))}
</div>
);
};
DropTargetCell.propTypes = {
day: PropTypes.string.isRequired,
hour: PropTypes.string.isRequired,
courses: PropTypes.array.isRequired,
onDrop: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
formData: PropTypes.object.isRequired,
};
export default DropTargetCell;

View File

@ -1,274 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { format, addDays, startOfWeek } from 'date-fns';
import { fr } from 'date-fns/locale';
import DropTargetCell from '@/components/Structure/Planning/DropTargetCell';
import { useClasseForm } from '@/context/ClasseFormContext';
import { useClasses } from '@/context/ClassesContext';
import { Calendar } from 'lucide-react';
import SpecialityEventModal from '@/components/Structure/Planning/SpecialityEventModal'; // Assurez-vous du bon chemin d'importation
const PlanningClassView = ({
schedule,
onDrop,
selectedLevel,
handleUpdatePlanning,
classe,
}) => {
const { formData } = useClasseForm();
const { determineInitialPeriod } = useClasses();
const [currentPeriod, setCurrentPeriod] = useState(
schedule?.emploiDuTemps
? determineInitialPeriod(schedule.emploiDuTemps)
: null
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCell, setSelectedCell] = useState(null);
const [existingEvent, setExistingEvent] = useState(null);
useEffect(() => {
if (schedule?.emploiDuTemps) {
setCurrentPeriod(determineInitialPeriod(schedule.emploiDuTemps));
}
}, [schedule]);
if (!schedule || !schedule.emploiDuTemps) {
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Calendar className="w-8 h-8 mr-2" />
Planning
</h2>
</div>
</div>
);
}
const emploiDuTemps =
schedule.emploiDuTemps[currentPeriod] || schedule.emploiDuTemps;
const joursOuverture = Object.keys(emploiDuTemps);
const currentWeekDays = joursOuverture
.map((day) => {
switch (day.toLowerCase()) {
case 'lundi':
return 1;
case 'mardi':
return 2;
case 'mercredi':
return 3;
case 'jeudi':
return 4;
case 'vendredi':
return 5;
case 'samedi':
return 6;
case 'dimanche':
return 7;
default:
return 0;
}
})
.sort((a, b) => a - b) // Trier les jours dans l'ordre croissant
.map((day) =>
addDays(startOfWeek(new Date(), { weekStartsOn: 1 }), day - 1)
); // Calculer les dates à partir du lundi
const getFilteredEvents = (day, time, level) => {
const [hour, minute] = time.split(':').map(Number);
const startTime = hour + minute / 60; // Convertir l'heure en fraction d'heure
return (
emploiDuTemps[day.toLowerCase()]?.filter((event) => {
const [eventHour, eventMinute] = event.heure.split(':').map(Number);
const eventStartTime = eventHour + eventMinute / 60;
const eventEndTime = eventStartTime + parseFloat(event.duree);
// Filtrer en fonction du selectedLevel
return (
schedule.niveau === level &&
startTime >= eventStartTime &&
startTime < eventEndTime
);
}) || []
);
};
const handleCellClick = (hour, day) => {
const cellEvents = getFilteredEvents(day, hour, selectedLevel);
setSelectedCell({ hour, day, selectedLevel });
setExistingEvent(cellEvents.length ? cellEvents[0] : null);
setIsModalOpen(true);
};
const renderTimeSlots = () => {
const timeSlots = [];
for (
let hour = parseInt(formData.time_range[0], 10);
hour <= parseInt(formData.time_range[1], 10);
hour++
) {
const hourString = hour.toString().padStart(2, '0');
timeSlots.push(
<React.Fragment key={`${hourString}:00-${Math.random()}`}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hourString}:00`}
</div>
{currentWeekDays.map((date, index) => {
const day = format(date, 'iiii', { locale: fr }).toLowerCase();
const uniqueKey = `${hourString}:00-${day}-${index}`;
return (
<div key={uniqueKey} className="flex flex-col">
<DropTargetCell
hour={`${hourString}:00`}
day={day}
courses={getFilteredEvents(
day,
`${hourString}:00`,
selectedLevel
)}
onDrop={onDrop}
onClick={(hour, day) => handleCellClick(hour, day)}
/>
<DropTargetCell
hour={`${hourString}:30`}
day={day}
courses={getFilteredEvents(
day,
`${hourString}:30`,
selectedLevel
)}
onDrop={onDrop}
onClick={(hour, day) => handleCellClick(hour, day)}
/>
</div>
);
})}
</React.Fragment>
);
}
return timeSlots;
};
return (
<div className="w-full p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Calendar className="w-8 h-8 mr-2" />
Planning
</h2>
{schedule.emploiDuTemps.S1 && schedule.emploiDuTemps.S2 && (
<div>
<button
onClick={() => setCurrentPeriod('S1')}
className={`px-4 py-2 ${currentPeriod === 'S1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Semestre 1
</button>
<button
onClick={() => setCurrentPeriod('S2')}
className={`px-4 py-2 ${currentPeriod === 'S2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Semestre 2
</button>
</div>
)}
{schedule.emploiDuTemps.T1 &&
schedule.emploiDuTemps.T2 &&
schedule.emploiDuTemps.T3 && (
<div>
<button
onClick={() => setCurrentPeriod('T1')}
className={`px-4 py-2 ${currentPeriod === 'T1' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 1
</button>
<button
onClick={() => setCurrentPeriod('T2')}
className={`px-4 py-2 ${currentPeriod === 'T2' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 2
</button>
<button
onClick={() => setCurrentPeriod('T3')}
className={`px-4 py-2 ${currentPeriod === 'T3' ? 'bg-emerald-600 text-white' : 'bg-gray-200 text-gray-800'}`}
>
Trimestre 3
</button>
</div>
)}
</div>
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
{/* En-tête des jours */}
<div
className="grid w-full"
style={{
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
}}
>
<div className="bg-gray-50 h-14"></div>
{currentWeekDays.map((date, index) => (
<div
key={`${date}-${index}`}
className="p-3 text-center bg-emerald-100 text-emerald-800 border-r border-emerald-200"
>
<div className="text font-semibold">
{format(date, 'EEEE', { locale: fr })}
</div>
</div>
))}
</div>
{/* Contenu du planning */}
<div
className="flex-1 overflow-y-auto relative"
style={{ maxHeight: 'calc(100vh - 300px)' }}
>
<div
className="grid bg-white relative"
style={{
gridTemplateColumns: `2.5rem repeat(${currentWeekDays.length}, 1fr)`,
}}
>
{renderTimeSlots()}
</div>
</div>
</div>
<SpecialityEventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
selectedCell={selectedCell}
existingEvent={existingEvent}
handleUpdatePlanning={handleUpdatePlanning}
classe={classe}
/>
</div>
);
};
PlanningClassView.propTypes = {
schedule: PropTypes.shape({
emploiDuTemps: PropTypes.objectOf(
PropTypes.arrayOf(
PropTypes.shape({
duree: PropTypes.string.isRequired,
heure: PropTypes.string.isRequired,
matiere: PropTypes.string.isRequired,
teachers: PropTypes.arrayOf(PropTypes.string).isRequired,
color: PropTypes.string.isRequired,
})
)
).isRequired,
plageHoraire: PropTypes.shape({
startHour: PropTypes.number.isRequired,
endHour: PropTypes.number.isRequired,
}).isRequired,
joursOuverture: PropTypes.arrayOf(PropTypes.number).isRequired,
}).isRequired,
};
export default PlanningClassView;

View File

@ -0,0 +1,299 @@
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
export default function ScheduleEventModal({
isOpen,
onClose,
eventData,
setEventData,
specialities,
teachers,
classes
}) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning();
React.useEffect(() => {
if (!eventData?.planning && schedules.length > 0) {
const defaultSchedule = schedules[0];
if (eventData?.planning !== defaultSchedule.id) {
setEventData((prev) => ({
...prev,
planning: defaultSchedule.id,
}));
}
}
}, [schedules, eventData?.planning]);
const handleSpecialityChange = (specialityId) => {
const selectedSpeciality = specialities.find((s) => s.id === parseInt(specialityId, 10));
if (selectedSpeciality) {
setEventData((prev) => ({
...prev,
speciality: selectedSpeciality.id,
title: selectedSpeciality.name, // Définit la matière
color: selectedSpeciality.color_code, // Définit la couleur associée
}));
}
};
const handleTeacherChange = (teacherId) => {
const selectedTeacher = teachers.find((t) => t.id === parseInt(teacherId, 10));
if (selectedTeacher) {
setEventData((prev) => ({
...prev,
teacher: selectedTeacher.id,
description: `${selectedTeacher.first_name} ${selectedTeacher.last_name}`, // Définit le nom du professeur
}));
}
};
const handlePlanningChange = (planningId) => {
const selectedSchedule = schedules.find((s) => s.id === parseInt(planningId, 10));
if (selectedSchedule) {
setEventData((prev) => ({
...prev,
planning: selectedSchedule.id,
}));
}
};
const handleColorChange = (color) => {
setEventData((prev) => ({
...prev,
color, // Permet de changer manuellement la couleur
}));
};
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.speciality) {
alert('Veuillez sélectionner une matière');
return;
}
if (!eventData.teacher) {
alert('Veuillez sélectionner un professeur');
return;
}
if (!eventData.planning) {
alert('Veuillez sélectionner un planning');
return;
}
if (!eventData.location) {
alert('Veuillez saisir un lieu');
return;
}
if (eventData.id) {
handleUpdateEvent(eventData.id, eventData);
} else {
addEvent({
...eventData,
id: `event-${Date.now()}`,
});
}
onClose();
};
const handleDelete = () => {
if (
eventData.id &&
confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')
) {
handleDeleteEvent(eventData.id);
onClose();
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Planning */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Planning
</label>
<select
value={eventData.planning || ''}
onChange={(e) => handlePlanningChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez un planning
</option>
{schedules.map((schedule) => (
<option key={schedule.id} value={schedule.id}>
{schedule.name}
</option>
))}
</select>
</div>
{/* Matière */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Matière
</label>
<select
value={eventData.speciality || ''}
onChange={(e) => handleSpecialityChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez une matière
</option>
{specialities.map((speciality) => (
<option key={speciality.id} value={speciality.id}>
{speciality.name}
</option>
))}
</select>
</div>
{/* Professeur */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Professeur
</label>
<select
value={eventData.teacher || ''}
onChange={(e) => handleTeacherChange(e.target.value)}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
<option value="" disabled>
Sélectionnez un professeur
</option>
{teachers.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{`${teacher.first_name} ${teacher.last_name}`}
</option>
))}
</select>
</div>
{/* Lieu */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lieu
</label>
<input
type="text"
value={eventData.location || ''}
onChange={(e) =>
setEventData({ ...eventData, location: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="Saisissez un lieu"
required
/>
</div>
{/* Couleur */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Couleur
</label>
<input
type="color"
value={eventData.color || '#10b981'}
onChange={(e) => handleColorChange(e.target.value)}
className="w-full h-10 p-1 rounded border"
/>
</div>
{/* Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Début
</label>
<input
type="datetime-local"
value={
eventData.start
? format(new Date(eventData.start), "yyyy-MM-dd'T'HH:mm")
: ''
}
onChange={(e) =>
setEventData({
...eventData,
start: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin
</label>
<input
type="datetime-local"
value={
eventData.end
? format(new Date(eventData.end), "yyyy-MM-dd'T'HH:mm")
: ''
}
onChange={(e) =>
setEventData({
...eventData,
end: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
</div>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<div>
{eventData.id && (
<button
type="button"
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
>
Supprimer
</button>
)}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</div>
</form>
</div>
</div>
);
}

View File

@ -1,211 +1,74 @@
'use client';
import React, { useState, useEffect } from 'react';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { AnimatePresence, motion } from 'framer-motion';
import PlanningClassView from '@/components/Structure/Planning/PlanningClassView';
import SpecialitiesList from '@/components/Structure/Planning/SpecialitiesList';
import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import { useClasses } from '@/context/ClassesContext';
import { ClasseFormProvider } from '@/context/ClasseFormContext';
import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
import { Bookmark, Users, BookOpen, Newspaper } from 'lucide-react';
import React, { useState } from 'react';
import logger from '@/utils/logger';
import { RecurrenceType } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar/Calendar';
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
const ScheduleManagement = ({ handleUpdatePlanning, classes }) => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const currentSchoolYearStart =
currentMonth >= 8 ? currentYear : currentYear - 1;
const [selectedClass, setSelectedClass] = useState(null);
const [selectedLevel, setSelectedLevel] = useState('');
const [schedule, setSchedule] = useState(null);
const { getNiveauxTabs } = useClasses();
const niveauxLabels = Array.isArray(selectedClass?.levels)
? getNiveauxTabs(selectedClass.levels)
: [];
export default function ScheduleManagement({classes,specialities,teachers}) {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleOpenModal = () => setIsModalOpen(true);
const handleCloseModal = () => setIsModalOpen(false);
const [eventData, setEventData] = useState({
title: '',
description: '',
start: '',
end: '',
location: '',
planning: '', // Enlever la valeur par défaut ici
recursionType: RecurrenceType.NONE,
selectedDays: [],
recursionEnd: '',
customInterval: 1,
customUnit: 'days',
viewType: 'week', // Ajouter la vue semaine par défaut
});
useEffect(() => {
if (selectedClass) {
const defaultLevel = niveauxLabels.length > 0 ? niveauxLabels[0].id : '';
const niveau = selectedLevel || defaultLevel;
const initializeNewEvent = (date = new Date()) => {
// S'assurer que date est un objet Date valide
const eventDate = date instanceof Date ? date : new Date();
setSelectedLevel(niveau);
const currentPlanning = selectedClass.plannings_read?.find(
(planning) => planning.niveau === niveau
);
setSchedule(currentPlanning ? currentPlanning.planning : {});
}
}, [selectedClass, niveauxLabels]);
useEffect(() => {
if (selectedClass && selectedLevel) {
const currentPlanning = selectedClass.plannings_read?.find(
(planning) => planning.niveau === selectedLevel
);
setSchedule(currentPlanning ? currentPlanning.planning : {});
}
}, [selectedClass, selectedLevel]);
const handleLevelSelect = (niveau) => {
setSelectedLevel(niveau);
setEventData({
title: '',
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);
};
const handleClassSelect = (classId) => {
const selectedClasse = categorizedClasses['Actives'].find(
(classe) => classe.id === classId
);
setSelectedClass(selectedClasse);
setSelectedLevel('');
};
const onDrop = (item, hour, day) => {
const { id, name, color, teachers } = item;
const newSchedule = {
...schedule,
emploiDuTemps: schedule.emploiDuTemps || {},
};
if (!newSchedule.emploiDuTemps[day]) {
newSchedule.emploiDuTemps[day] = [];
}
const courseTime = `${hour.toString().padStart(2, '0')}:00`;
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(
(course) => course.heure === courseTime
);
const newCourse = {
duree: '1',
heure: courseTime,
matiere: name,
teachers: teachers,
color: color,
};
if (existingCourseIndex !== -1) {
newSchedule.emploiDuTemps[day][existingCourseIndex] = newCourse;
} else {
newSchedule.emploiDuTemps[day].push(newCourse);
}
// Mettre à jour scheduleRef
setSchedule(newSchedule);
// Utiliser `handleUpdatePlanning` pour mettre à jour le planning du niveau de la classe
const planningId = selectedClass.plannings_read.find(
(planning) => planning.niveau === selectedLevel
)?.planning.id;
if (planningId) {
logger.debug('newSchedule : ', newSchedule);
handleUpdatePlanning(BE_SCHOOL_PLANNINGS_URL, planningId, newSchedule);
}
};
const categorizedClasses = classes.reduce((acc, classe) => {
const { school_year } = classe;
const [startYear] = school_year.split('-').map(Number);
const category =
startYear >= currentSchoolYearStart ? 'Actives' : 'Anciennes';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(classe);
return acc;
}, {});
return (
<div className="flex flex-col h-full">
<DndProvider backend={HTML5Backend}>
<div className="p-4 bg-gray-100 border-b">
<div className="grid grid-cols-3 gap-4">
{/* Colonne Classes */}
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
</div>
{categorizedClasses['Actives'] && (
<TabsStructure
activeTab={selectedClass?.id}
setActiveTab={handleClassSelect}
tabs={categorizedClasses['Actives'].map((classe) => ({
id: classe.id,
title: classe.atmosphere_name,
icon: Users,
}))}
/>
)}
</div>
{/* Colonne Niveaux */}
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<Bookmark className="w-8 h-8 mr-2" />
Niveaux
</h2>
</div>
{niveauxLabels && (
<TabsStructure
activeTab={selectedLevel}
setActiveTab={handleLevelSelect}
tabs={niveauxLabels}
/>
)}
</div>
{/* Colonne Spécialités */}
<div className="p-4 bg-gray-50 rounded-lg shadow-inner">
<div className="flex justify-between items-center mb-4">
<h2 className="text-3xl text-gray-800 flex items-center">
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
</div>
<SpecialitiesList
teachers={selectedClass ? selectedClass.teachers : []}
/>
</div>
</div>
</div>
<div className="flex-1 p-4 overflow-y-auto">
<AnimatePresence mode="wait">
<motion.div
key="year"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="flex-1 relative"
>
<ClasseFormProvider initialClasse={selectedClass || {}}>
<PlanningClassView
schedule={schedule}
onDrop={onDrop}
selectedLevel={selectedLevel}
handleUpdatePlanning={handleUpdatePlanning}
classe={selectedClass}
/>
</ClasseFormProvider>
</motion.div>
</AnimatePresence>
</div>
</DndProvider>
</div>
<div className="flex h-full overflow-hidden">
<ScheduleNavigation classes={classes} />
<Calendar
onDateClick={initializeNewEvent}
onEventClick={(event) => {
setEventData(event);
setIsModalOpen(true);
}}
/>
<ScheduleEventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
eventData={eventData}
setEventData={setEventData}
specialities={specialities}
teachers={teachers}
classes={classes}
/>
</div>
);
};
}
export default ScheduleManagement;

View File

@ -1,19 +0,0 @@
import React from 'react';
import { useClasses } from '@/context/ClassesContext';
import DraggableSpeciality from '@/components/Structure/Planning/DraggableSpeciality';
const SpecialitiesList = ({ teachers }) => {
const { groupSpecialitiesBySubject } = useClasses();
return (
<div className="flex justify-center items-center w-full">
<div className="flex flex-wrap gap-2 mt-4 justify-center">
{groupSpecialitiesBySubject(teachers).map((speciality) => (
<DraggableSpeciality key={speciality.id} speciality={speciality} />
))}
</div>
</div>
);
};
export default SpecialitiesList;

View File

@ -1,258 +0,0 @@
import React, { useState, useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice';
import { useClasses } from '@/context/ClassesContext';
import { useClasseForm } from '@/context/ClasseFormContext';
import { BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import { BookOpen, Users } from 'lucide-react';
import logger from '@/utils/logger';
const SpecialityEventModal = ({
isOpen,
onClose,
selectedCell,
existingEvent,
handleUpdatePlanning,
classe,
}) => {
const { formData, setFormData } = useClasseForm();
const { groupSpecialitiesBySubject } = useClasses();
const [selectedSpeciality, setSelectedSpeciality] = useState('');
const [selectedTeacher, setSelectedTeacher] = useState('');
const [eventData, setEventData] = useState({
specialiteId: '',
teacherId: '',
start: '',
duration: '1h',
});
useEffect(() => {
if (!isOpen) {
// Réinitialiser eventData lorsque la modale se ferme
setEventData({
specialiteId: '',
teacherId: '',
start: '',
duration: '1h',
});
setSelectedSpeciality('');
setSelectedTeacher('');
}
}, [isOpen]);
useEffect(() => {
if (isOpen) {
logger.debug('debug : ', selectedCell);
if (existingEvent) {
// Mode édition
setEventData(existingEvent);
setSelectedSpeciality(existingEvent.specialiteId);
setSelectedTeacher(existingEvent.teacherId);
} else {
// Mode création
setEventData((prev) => ({
...prev,
start: selectedCell.hour,
duration: '1h',
}));
setSelectedSpeciality('');
setSelectedTeacher('');
}
}
}, [isOpen, existingEvent, selectedCell]);
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.specialiteId) {
alert('Veuillez sélectionner une spécialité');
return;
}
if (!eventData.teacherId) {
alert('Veuillez sélectionner un enseignant');
return;
}
// Transformer eventData pour correspondre au format du planning
const selectedTeacherData = formData.teachers.find(
(teacher) => teacher.id === parseInt(eventData.teacherId, 10)
);
const newCourse = {
color: '#FF0000', // Vous pouvez définir la couleur de manière dynamique si nécessaire
teachers: selectedTeacherData
? [`${selectedTeacherData.nom} ${selectedTeacherData.prenom}`]
: [],
heure: `${eventData.start}:00`,
duree: eventData.duration.replace('h', ''), // Supposons que '1h' signifie 1
matiere: 'GROUPE',
};
// Mettre à jour le planning
const updatedPlannings = classe.plannings_read.map((planning) => {
if (planning.niveau === selectedCell.selectedLevel) {
const newEmploiDuTemps = { ...planning.emploiDuTemps };
if (!newEmploiDuTemps[selectedCell.day]) {
newEmploiDuTemps[selectedCell.day] = [];
}
const courseTime = newCourse.heure;
const existingCourseIndex = newEmploiDuTemps[
selectedCell.day
].findIndex((course) => course.heure === courseTime);
if (existingCourseIndex !== -1) {
newEmploiDuTemps[selectedCell.day][existingCourseIndex] = newCourse;
} else {
newEmploiDuTemps[selectedCell.day].push(newCourse);
}
return {
...planning,
emploiDuTemps: newEmploiDuTemps,
};
}
return planning;
});
const updatedPlanning = updatedPlannings.find(
(planning) => planning.niveau === selectedCell.selectedLevel
);
setFormData((prevFormData) => ({
...prevFormData,
plannings: updatedPlannings,
}));
// Appeler handleUpdatePlanning avec les arguments appropriés
const planningId = updatedPlanning ? updatedPlanning.planning.id : null;
logger.debug('id : ', planningId);
if (planningId) {
handleUpdatePlanning(
BE_SCHOOL_PLANNINGS_URL,
planningId,
updatedPlanning.emploiDuTemps
);
}
onClose();
};
const filteredTeachers = selectedSpeciality
? formData.teachers.filter((teacher) =>
teacher.specialites.includes(parseInt(selectedSpeciality, 10))
)
: formData.teachers;
const handleSpecialityChange = (e) => {
const specialityId = e.target.value;
setSelectedSpeciality(specialityId);
// Mettre à jour eventData
setEventData((prev) => ({
...prev,
specialiteId: specialityId,
}));
};
const handleTeacherChange = (e) => {
const teacherId = e.target.value;
setSelectedTeacher(teacherId);
// Mettre à jour eventData
setEventData((prev) => ({
...prev,
teacherId: teacherId,
}));
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Sélection de la Spécialité */}
<div>
<SelectChoice
name="specialites"
placeHolder="Spécialités"
selected={selectedSpeciality}
choices={[
{ value: '', label: 'Sélectionner une spécialité' },
...groupSpecialitiesBySubject(formData.teachers).map(
(speciality) => ({
value: speciality.id,
label: speciality.nom,
})
),
]}
callback={handleSpecialityChange}
IconItem={BookOpen}
/>
</div>
{/* Sélection de l'enseignant */}
<div>
<SelectChoice
name="teachers"
placeHolder="Enseignants"
selected={selectedTeacher}
choices={[
{ value: '', label: 'Sélectionner un enseignant' },
...filteredTeachers.map((teacher) => ({
value: teacher.id,
label: `${teacher.nom} ${teacher.prenom}`,
})),
]}
callback={handleTeacherChange}
IconItem={Users}
disabled={!selectedSpeciality} // Désactive le sélecteur si aucune spécialité n'est sélectionnée
/>
</div>
{/* Durée */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Durée
</label>
<input
type="text"
value={eventData.duration}
onChange={(e) =>
setEventData((prev) => ({
...prev,
duration: e.target.value,
}))
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
</div>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</form>
</div>
</div>
);
};
export default SpecialityEventModal;

View File

@ -50,7 +50,7 @@ const FeesManagement = ({
};
return (
<div className="w-full mx-auto mt-6">
<div className="w-full p-4 mx-auto mt-6">
<div className="w-4/5 mx-auto flex items-center mt-8">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">