mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-30 00:13:21 +00:00
refactor: Création de nouveaux composants / update formulaire de
création de classe (#2)
This commit is contained in:
252
Front-End/src/components/Structure/Configuration/ClassForm.js
Normal file
252
Front-End/src/components/Structure/Configuration/ClassForm.js
Normal file
@ -0,0 +1,252 @@
|
||||
import React, { useState } from 'react';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Button from '@/components/Button';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import CheckBoxList from '@/components/CheckBoxList';
|
||||
import PlanningConfiguration from '@/components/Structure/Configuration/PlanningConfiguration';
|
||||
import TeachersSelectionConfiguration from '@/components/Structure/Configuration/TeachersSelectionConfiguration';
|
||||
import { Users, Maximize2, Calendar, UserPlus } from 'lucide-react';
|
||||
import { useClasseForm } from '@/context/ClasseFormContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
|
||||
const ClassForm = ({ onSubmit, isNew, teachers }) => {
|
||||
|
||||
const { formData, setFormData } = useClasseForm();
|
||||
const { schoolYears, getNiveauxLabels, generateAgeToNiveaux, niveauxPremierCycle, niveauxSecondCycle, niveauxTroisiemeCycle, typeEmploiDuTemps, updatePlanning } = useClasses();
|
||||
const [selectedTeachers, setSelectedTeachers] = useState(formData.enseignants_ids);
|
||||
|
||||
const handleTeacherSelection = (teacher) => {
|
||||
setSelectedTeachers(prevState =>
|
||||
prevState.includes(teacher.id)
|
||||
? prevState.filter(id => id !== teacher.id)
|
||||
: [...prevState, teacher.id]
|
||||
);
|
||||
setFormData(prevState => ({
|
||||
...prevState,
|
||||
enseignants_ids: prevState.enseignants_ids.includes(teacher.id)
|
||||
? prevState.enseignants_ids.filter(id => id !== teacher.id)
|
||||
: [...prevState.enseignants_ids, teacher.id]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTimeChange = (e, index) => {
|
||||
const { value } = e.target;
|
||||
setFormData(prevState => {
|
||||
const updatedTimes = [...prevState.plage_horaire];
|
||||
updatedTimes[index] = value;
|
||||
|
||||
const updatedFormData = {
|
||||
...prevState,
|
||||
plage_horaire: updatedTimes,
|
||||
};
|
||||
|
||||
updatedFormData.planning = updatePlanning(updatedFormData);
|
||||
|
||||
return updatedFormData;
|
||||
});
|
||||
};
|
||||
|
||||
const handleJoursChange = (e) => {
|
||||
const { value, checked } = e.target;
|
||||
const dayId = parseInt(value, 10);
|
||||
|
||||
setFormData((prevState) => {
|
||||
const updatedJoursOuverture = checked
|
||||
? [...prevState.jours_ouverture, dayId]
|
||||
: prevState.jours_ouverture.filter((id) => id !== dayId);
|
||||
|
||||
const updatedFormData = {
|
||||
...prevState,
|
||||
jours_ouverture: updatedJoursOuverture,
|
||||
};
|
||||
|
||||
updatedFormData.planning = updatePlanning(updatedFormData);
|
||||
|
||||
return updatedFormData;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
const { name, value, type, checked } = e.target;
|
||||
|
||||
setFormData(prevState => {
|
||||
let updatedFormData = { ...prevState };
|
||||
|
||||
if (type === 'checkbox') {
|
||||
const newValues = checked
|
||||
? [...(prevState[name] || []), parseInt(value)]
|
||||
: (prevState[name] || []).filter(v => v !== parseInt(value));
|
||||
updatedFormData[name] = newValues;
|
||||
} else if (name === 'tranche_age') {
|
||||
const [minAgeStr, maxAgeStr] = value.split('-');
|
||||
const minAge = minAgeStr ? parseInt(minAgeStr) : null;
|
||||
const maxAge = minAgeStr ? parseInt(maxAgeStr) : null;
|
||||
const selectedNiveaux = generateAgeToNiveaux(minAge, maxAge);
|
||||
const niveauxLabels = getNiveauxLabels(selectedNiveaux);
|
||||
|
||||
updatedFormData = {
|
||||
...prevState,
|
||||
[name]: value,
|
||||
niveaux: selectedNiveaux.length > 0 ? selectedNiveaux : [],
|
||||
niveaux_label: niveauxLabels.length > 0 ? niveauxLabels : []
|
||||
};
|
||||
} else if (type === 'radio') {
|
||||
updatedFormData[name] = parseInt(value, 10);
|
||||
} else {
|
||||
updatedFormData[name] = value;
|
||||
}
|
||||
|
||||
console.log('Updated formData:', updatedFormData);
|
||||
|
||||
updatedFormData.planning = updatePlanning(updatedFormData);
|
||||
|
||||
console.log('Final formData:', updatedFormData);
|
||||
return updatedFormData;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
const [minAge, maxAge] = formData.tranche_age.length === 2 ? formData.tranche_age : [null, null];
|
||||
const selectedAgeGroup = generateAgeToNiveaux(minAge, maxAge);
|
||||
|
||||
return (
|
||||
<div className="h-[80vh] overflow-y-auto">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-8">
|
||||
|
||||
<div className="flex justify-between space-x-4">
|
||||
{/* Section Ambiance */}
|
||||
<div className="w-1/2 space-y-4">
|
||||
<label className="block text-lg font-medium text-gray-700">Ambiance <i>(optionnel)</i></label>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name="nom_ambiance"
|
||||
type="text"
|
||||
IconItem={Users}
|
||||
placeholder="Nom de l'ambiance"
|
||||
value={formData.nom_ambiance}
|
||||
onChange={handleChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name="tranche_age"
|
||||
type="text"
|
||||
IconItem={Maximize2}
|
||||
placeholder="Tranche d'âge (ex: 3-6)"
|
||||
value={formData.tranche_age}
|
||||
onChange={handleChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Niveau */}
|
||||
<div className="w-1/2 space-y-2">
|
||||
<label className="block text-lg font-medium text-gray-700">Niveaux</label>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<CheckBoxList
|
||||
items={niveauxPremierCycle}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
fieldName="niveaux"
|
||||
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
|
||||
className="w-full"
|
||||
/>
|
||||
<CheckBoxList
|
||||
items={niveauxSecondCycle}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
fieldName="niveaux"
|
||||
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
|
||||
className="w-full"
|
||||
/>
|
||||
<CheckBoxList
|
||||
items={niveauxTroisiemeCycle}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
fieldName="niveaux"
|
||||
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between space-x-4">
|
||||
{/* Section Capacité */}
|
||||
<div className="w-1/2 space-y-4">
|
||||
<label className="block text-lg font-medium text-gray-700">Capacité</label>
|
||||
<div className="space-y-4">
|
||||
<InputTextIcon
|
||||
name="nombre_eleves"
|
||||
type="number"
|
||||
IconItem={UserPlus}
|
||||
placeholder="Capacité max"
|
||||
value={formData.nombre_eleves}
|
||||
onChange={handleChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Année scolaire */}
|
||||
<div className="w-1/2 space-y-4">
|
||||
<label className="block text-lg font-medium text-gray-700">Année scolaire</label>
|
||||
<div className="space-y-4">
|
||||
<SelectChoice
|
||||
name="annee_scolaire"
|
||||
placeholder="Sélectionner l'année scolaire"
|
||||
selected={formData.annee_scolaire}
|
||||
callback={handleChange}
|
||||
choices={schoolYears}
|
||||
IconItem={Calendar}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Enseignants */}
|
||||
<TeachersSelectionConfiguration formData={formData}
|
||||
teachers={teachers}
|
||||
handleTeacherSelection={handleTeacherSelection}
|
||||
selectedTeachers={selectedTeachers}
|
||||
/>
|
||||
|
||||
{/* Section Emploi du temps */}
|
||||
<PlanningConfiguration formData={formData}
|
||||
handleChange={handleChange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleJoursChange={handleJoursChange}
|
||||
typeEmploiDuTemps={typeEmploiDuTemps}
|
||||
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<Button
|
||||
text={`${isNew ? "Créer" : "Modifier"}`}
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0)
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
primary
|
||||
disabled={(formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0)}
|
||||
type="submit"
|
||||
name="Create"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassForm;
|
||||
@ -0,0 +1,179 @@
|
||||
import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import ClassForm from '@/components/Structure/Configuration/ClassForm';
|
||||
import ClasseDetails from '@/components/ClasseDetails';
|
||||
import { ClasseFormProvider } from '@/context/ClasseFormContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
|
||||
|
||||
const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
const { getNiveauxLabels } = useClasses();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpenDetails, setIsOpenDetails] = useState(false);
|
||||
const [editingClass, setEditingClass] = useState(null);
|
||||
|
||||
const openEditModal = (classe) => {
|
||||
setIsOpen(true);
|
||||
setEditingClass(classe);
|
||||
}
|
||||
|
||||
const openEditModalDetails = (classe) => {
|
||||
setIsOpenDetails(true);
|
||||
setEditingClass(classe);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingClass(null);
|
||||
};
|
||||
|
||||
const closeEditModalDetails = () => {
|
||||
setIsOpenDetails(false);
|
||||
setEditingClass(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingClass) {
|
||||
handleEdit(editingClass.id, updatedData);
|
||||
} else {
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<Users className="w-8 h-8 mr-2" />
|
||||
Classes
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'AMBIANCE',
|
||||
transform: (row) => {
|
||||
const ambiance = row.nom_ambiance ? row.nom_ambiance : '';
|
||||
const trancheAge = row.tranche_age ? `${row.tranche_age} ans` : '';
|
||||
|
||||
if (ambiance && trancheAge) {
|
||||
return `${ambiance} (${trancheAge})`;
|
||||
} else if (ambiance) {
|
||||
return ambiance;
|
||||
} else if (trancheAge) {
|
||||
return trancheAge;
|
||||
} else {
|
||||
return 'Non spécifié';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'NIVEAUX',
|
||||
transform: (row) => {
|
||||
const niveauxLabels = Array.isArray(row.niveaux) ? getNiveauxLabels(row.niveaux) : [];
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center items-center space-x-2">
|
||||
{niveauxLabels.length > 0
|
||||
? niveauxLabels.map((label, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`px-3 py-1 rounded-md shadow-sm ${
|
||||
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
|
||||
} border border-gray-200 text-gray-700`}>
|
||||
{label}
|
||||
</div>
|
||||
))
|
||||
: 'Aucun niveau'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ name: 'CAPACITÉ MAX', transform: (row) => row.nombre_eleves },
|
||||
{ name: 'ANNÉE SCOLAIRE', transform: (row) => row.annee_scolaire },
|
||||
{
|
||||
name: 'ENSEIGNANTS',
|
||||
transform: (row) => (
|
||||
<div key={row.id} className="flex flex-wrap justify-center items-center space-x-2">
|
||||
{row.enseignants.map((teacher, index) => (
|
||||
<div
|
||||
key={teacher.id}
|
||||
className={`px-3 py-1 rounded-md shadow-sm ${
|
||||
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
|
||||
} border border-gray-200 text-gray-700`}
|
||||
>
|
||||
<span className="font-bold">{teacher.nom} {teacher.prenom}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
|
||||
{
|
||||
name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Inspecter', icon: ZoomIn, onClick: () => openEditModalDetails(row) },
|
||||
{ label: 'Modifier', icon: Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
data={classes}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ClasseFormProvider initialClasse={editingClass || {}}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingClass ? "Modification de la classe" : "Création d'une nouvelle classe"}
|
||||
size='sm:w-1/2'
|
||||
ContentComponent={() => (
|
||||
<ClassForm classe={editingClass || {}} onSubmit={handleModalSubmit} isNew={!editingClass} teachers={teachers} />
|
||||
)}
|
||||
/>
|
||||
</ClasseFormProvider>
|
||||
)}
|
||||
|
||||
{isOpenDetails && (
|
||||
<Modal
|
||||
isOpen={isOpenDetails}
|
||||
setIsOpen={setIsOpenDetails}
|
||||
title={(
|
||||
<div className="flex items-center">
|
||||
<Users className="w-8 h-8 mr-2" />
|
||||
{editingClass ? (
|
||||
<>
|
||||
{editingClass.nom_ambiance} - {editingClass.tranche_age[0]} à {editingClass.tranche_age[1]} ans
|
||||
</>
|
||||
) : ''}
|
||||
</div>
|
||||
)}
|
||||
ContentComponent={() => (
|
||||
<ClasseDetails classe={editingClass} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassesSection;
|
||||
@ -0,0 +1,38 @@
|
||||
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;
|
||||
@ -0,0 +1,113 @@
|
||||
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.jours_ouverture.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="planning_type"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Plage horaire */}
|
||||
<div className="w-1/2">
|
||||
<TimeRange
|
||||
startTime={formData.plage_horaire[0]}
|
||||
endTime={formData.plage_horaire[1]}
|
||||
onStartChange={(e) => handleTimeChange(e, 0)}
|
||||
onEndChange={(e) => handleTimeChange(e, 1)}
|
||||
/>
|
||||
|
||||
{/* CheckBoxList */}
|
||||
<CheckBoxList
|
||||
items={daysOfWeek}
|
||||
formData={formData}
|
||||
handleChange={handleJoursChange}
|
||||
fieldName="jours_ouverture"
|
||||
horizontal={true}
|
||||
labelAttenuated={isLabelAttenuated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DateRange */}
|
||||
<div className="space-y-4 w-full">
|
||||
{formData.planning_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.planning_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;
|
||||
@ -0,0 +1,96 @@
|
||||
import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import SpecialityForm from '@/components/Structure/Configuration/SpecialityForm';
|
||||
import { SpecialityFormProvider } from '@/context/SpecialityFormContext';
|
||||
|
||||
const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingSpeciality, setEditingSpeciality] = useState(null);
|
||||
|
||||
const openEditModal = (speciality) => {
|
||||
setIsOpen(true);
|
||||
setEditingSpeciality(speciality);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingSpeciality(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingSpeciality) {
|
||||
handleEdit(editingSpeciality.id, updatedData);
|
||||
} else {
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<BookOpen className="w-8 h-8 mr-2" />
|
||||
Spécialités
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-4xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'INTITULÉ',
|
||||
transform: (row) => (
|
||||
<div
|
||||
className="inline-block px-3 py-1 rounded-full font-bold text-white"
|
||||
style={{ backgroundColor: row.codeCouleur }}
|
||||
title={row.codeCouleur}
|
||||
>
|
||||
<span className="font-bold text-white">{row.nom.toUpperCase()}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
|
||||
{ name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)}
|
||||
]}
|
||||
data={specialities}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<SpecialityFormProvider initialSpeciality={editingSpeciality || {}}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingSpeciality ? "Modification de la spécialité" : "Création d'une nouvelle spécialité"}
|
||||
size='sm:w-1/6'
|
||||
ContentComponent={() => (
|
||||
<SpecialityForm onSubmit={handleModalSubmit} isNew={!editingSpeciality} />
|
||||
)}
|
||||
/>
|
||||
</SpecialityFormProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialitiesSection;
|
||||
@ -0,0 +1,66 @@
|
||||
import { useState } from 'react';
|
||||
import { BookOpen, Palette } from 'lucide-react';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import InputColorIcon from '@/components/InputColorIcon';
|
||||
import Button from '@/components/Button';
|
||||
import { useSpecialityForm } from '@/context/SpecialityFormContext';
|
||||
|
||||
const SpecialityForm = ({ onSubmit, isNew }) => {
|
||||
const { formData, setFormData } = useSpecialityForm();
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 mt-8">
|
||||
<div>
|
||||
<InputTextIcon
|
||||
type="text"
|
||||
name="nom"
|
||||
IconItem={BookOpen}
|
||||
placeholder="Nom de la spécialité"
|
||||
value={formData.nom}
|
||||
onChange={handleChange}
|
||||
className="w-full mt-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<InputColorIcon
|
||||
type="color"
|
||||
name="codeCouleur"
|
||||
IconItem={Palette}
|
||||
placeholder="Nom de la spécialité"
|
||||
value={formData.codeCouleur}
|
||||
onChange={handleChange}
|
||||
className="w-full mt-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<Button text={`${isNew ? "Créer" : "Modifier"}`}
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!formData.nom
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
primary
|
||||
disabled={!formData.nom}
|
||||
type="submit"
|
||||
name="Create" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecialityForm;
|
||||
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import SpecialitiesSection from '@/components/Structure/Configuration/SpecialitiesSection';
|
||||
import TeachersSection from '@/components/Structure/Configuration/TeachersSection';
|
||||
import ClassesSection from '@/components/Structure/Configuration/ClassesSection';
|
||||
import { ClassesProvider } from '@/context/ClassesContext';
|
||||
|
||||
import { BK_GESTIONENSEIGNANTS_SPECIALITE_URL,
|
||||
BK_GESTIONENSEIGNANTS_TEACHER_URL,
|
||||
BK_GESTIONENSEIGNANTS_CLASSE_URL } from '@/utils/Url';
|
||||
|
||||
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => {
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<ClassesProvider>
|
||||
<SpecialitiesSection
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, newData, setSpecialities)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, id, updatedData, setSpecialities)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, id, setSpecialities)}
|
||||
/>
|
||||
|
||||
<TeachersSection
|
||||
teachers={teachers}
|
||||
specialities={specialities}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, newData, setTeachers)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, id, updatedData, setTeachers)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, id, setTeachers)}
|
||||
/>
|
||||
|
||||
<ClassesSection
|
||||
classes={classes}
|
||||
specialities={specialities}
|
||||
teachers={teachers}
|
||||
handleCreate={(newData) => handleCreate(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, newData, setClasses)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, id, updatedData, setClasses)}
|
||||
handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, id, setClasses)}
|
||||
/>
|
||||
</ClassesProvider>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default StructureManagement;
|
||||
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { School, Calendar } from 'lucide-react';
|
||||
|
||||
const TabsStructure = ({ activeTab, setActiveTab, tabs }) => {
|
||||
return (
|
||||
<div className="flex justify-center mb-8">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab px-4 py-2 mx-2 flex items-center space-x-2 ${activeTab === tab.id ? 'bg-emerald-600 text-white shadow-lg' : 'bg-emerald-200 text-emerald-600'} rounded-full`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
<tab.icon className="w-5 h-5" />
|
||||
<span>{tab.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabsStructure;
|
||||
122
Front-End/src/components/Structure/Configuration/TeacherForm.js
Normal file
122
Front-End/src/components/Structure/Configuration/TeacherForm.js
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useState } from 'react';
|
||||
import { GraduationCap, Mail, BookOpen, Check } from 'lucide-react';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Button from '@/components/Button';
|
||||
import CheckBoxList from '@/components/CheckBoxList';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch'
|
||||
import { useTeacherForm } from '@/context/TeacherFormContext';
|
||||
|
||||
const TeacherForm = ({ onSubmit, isNew, specialities }) => {
|
||||
const { formData, setFormData } = useTeacherForm();
|
||||
|
||||
const handleToggleChange = () => {
|
||||
setFormData({ ...formData, droit: 1-formData.droit });
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const target = e.target || e.currentTarget;
|
||||
const { name, value, type, checked } = target;
|
||||
|
||||
if (type === 'checkbox') {
|
||||
setFormData((prevState) => {
|
||||
const newValues = checked
|
||||
? [...(prevState[name] || []), parseInt(value, 10)]
|
||||
: (prevState[name] || []).filter((v) => v !== parseInt(value, 10));
|
||||
return {
|
||||
...prevState,
|
||||
[name]: newValues,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[name]: type === 'radio' ? parseInt(value, 10) : value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(formData, isNew);
|
||||
};
|
||||
|
||||
const getSpecialityLabel = (speciality) => {
|
||||
return `${speciality.nom}`;
|
||||
};
|
||||
|
||||
const isLabelAttenuated = (item) => {
|
||||
return !formData.specialites_ids.includes(parseInt(item.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-8">
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name="nom"
|
||||
type="text"
|
||||
IconItem={GraduationCap}
|
||||
placeholder="Nom de l'enseignant"
|
||||
value={formData.nom}
|
||||
onChange={handleChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name="prenom"
|
||||
type="text"
|
||||
IconItem={GraduationCap}
|
||||
placeholder="Prénom de l'enseignant"
|
||||
value={formData.prenom}
|
||||
onChange={handleChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputTextIcon
|
||||
name="mail"
|
||||
type="email"
|
||||
IconItem={Mail}
|
||||
placeholder="Email de l'enseignant"
|
||||
value={formData.mail}
|
||||
onChange={handleChange}
|
||||
className="w-full mt-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<CheckBoxList
|
||||
items={specialities}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
fieldName="specialites_ids"
|
||||
label="Spécialités"
|
||||
icon={BookOpen}
|
||||
className="w-full mt-4"
|
||||
itemLabelFunc={getSpecialityLabel}
|
||||
labelAttenuated={isLabelAttenuated}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<ToggleSwitch
|
||||
label="Administrateur"
|
||||
checked={formData.droit}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 space-x-4">
|
||||
<Button text={`${isNew ? "Créer" : "Modifier"}`}
|
||||
onClick={handleSubmit}
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
(!formData.nom || !formData.prenom || !formData.mail || formData.specialites_ids.length === 0)
|
||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
||||
}`}
|
||||
primary
|
||||
disabled={(!formData.nom || !formData.prenom || !formData.mail || formData.specialites_ids.length === 0)}
|
||||
type="submit"
|
||||
name="Create" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeacherForm;
|
||||
@ -0,0 +1,186 @@
|
||||
import { GraduationCap, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
import Modal from '@/components/Modal';
|
||||
import TeacherForm from '@/components/Structure/Configuration/TeacherForm';
|
||||
import {BK_PROFILE_URL} from '@/utils/Url';
|
||||
import useCsrfToken from '@/hooks/useCsrfToken';
|
||||
import { TeacherFormProvider } from '@/context/TeacherFormContext';
|
||||
|
||||
const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingTeacher, setEditingTeacher] = useState(null);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const openEditModal = (teacher) => {
|
||||
setIsOpen(true);
|
||||
setEditingTeacher(teacher);
|
||||
}
|
||||
|
||||
const closeEditModal = () => {
|
||||
setIsOpen(false);
|
||||
setEditingTeacher(null);
|
||||
};
|
||||
|
||||
const handleModalSubmit = (updatedData) => {
|
||||
if (editingTeacher) {
|
||||
// Modification du profil
|
||||
const request = new Request(
|
||||
`${BK_PROFILE_URL}/${updatedData.profilAssocie_id}`,
|
||||
{
|
||||
method:'PUT',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify( {
|
||||
email: updatedData.mail,
|
||||
username: updatedData.mail,
|
||||
droit:updatedData.droit
|
||||
}),
|
||||
}
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(response => {
|
||||
console.log('Success:', response);
|
||||
console.log('UpdateData:', updatedData);
|
||||
handleEdit(editingTeacher.id, updatedData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
} else {
|
||||
// Création d'un profil associé à l'adresse mail du responsable saisie
|
||||
// Le profil est inactif
|
||||
const request = new Request(
|
||||
`${BK_PROFILE_URL}`,
|
||||
{
|
||||
method:'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify( {
|
||||
email: updatedData.mail,
|
||||
password: 'Provisoire01!',
|
||||
username: updatedData.mail,
|
||||
is_active: 1, // On rend le profil actif : on considère qu'au moment de la configuration de l'école un abonnement a été souscrit
|
||||
droit:updatedData.droit
|
||||
}),
|
||||
}
|
||||
);
|
||||
fetch(request).then(response => response.json())
|
||||
.then(response => {
|
||||
console.log('Success:', response);
|
||||
console.log('UpdateData:', updatedData);
|
||||
if (response.id) {
|
||||
let idProfil = response.id;
|
||||
updatedData.profilAssocie_id = idProfil;
|
||||
handleCreate(updatedData);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
|
||||
<h2 className="text-3xl text-gray-800 flex items-center">
|
||||
<GraduationCap className="w-8 h-8 mr-2" />
|
||||
Enseignants
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'NOM', transform: (row) => row.nom },
|
||||
{ name: 'PRENOM', transform: (row) => row.prenom },
|
||||
{ name: 'MAIL', transform: (row) => row.mail },
|
||||
{
|
||||
name: 'SPÉCIALITÉS',
|
||||
transform: (row) => (
|
||||
<div key={row.id} className="flex flex-wrap justify-center items-center space-x-2">
|
||||
{row.specialites.map(specialite => (
|
||||
<span
|
||||
key={specialite.id}
|
||||
className="px-3 py-1 rounded-full font-bold text-white"
|
||||
style={{ backgroundColor: specialite.codeCouleur }}
|
||||
title={specialite.nom}
|
||||
>
|
||||
{specialite.nom}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'TYPE PROFIL',
|
||||
transform: (row) => {
|
||||
if (row.profilAssocie) {
|
||||
const badgeClass = row.DroitLabel === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600';
|
||||
return (
|
||||
<div key={row.id} className="flex justify-center items-center space-x-2">
|
||||
<span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>
|
||||
{row.DroitLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <i>Non définie</i>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
|
||||
{ name: 'ACTIONS', transform: (row) => (
|
||||
<DropdownMenu
|
||||
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
|
||||
items={[
|
||||
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
|
||||
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
|
||||
]
|
||||
}
|
||||
buttonClassName="text-gray-400 hover:text-gray-600"
|
||||
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
|
||||
/>
|
||||
)}
|
||||
]}
|
||||
data={teachers}
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<TeacherFormProvider initialTeacher={editingTeacher || {}}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
title={editingTeacher ? "Modification de l'enseignant" : "Création d'un nouvel enseignant"}
|
||||
size='sm:w-1/4'
|
||||
ContentComponent={() => (
|
||||
<TeacherForm teacher={editingTeacher || {}} onSubmit={handleModalSubmit} isNew={!editingTeacher} specialities={specialities} />
|
||||
)}
|
||||
/>
|
||||
</TeacherFormProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeachersSection;
|
||||
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
const TeachersSelectionConfiguration = ({ formData, teachers, handleTeacherSelection, selectedTeachers }) => {
|
||||
return (
|
||||
<div className="mt-4" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<label className="mt-6 block text-2xl font-medium text-gray-700 mb-2">Enseignants</label>
|
||||
<label className={`block text-sm font-medium mb-4`}>Sélection : <span className={`${formData.enseignants_ids.length !== 0 ? 'text-emerald-400' : 'text-red-300'}`}>{formData.enseignants_ids.length}</span></label>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
name: 'Nom',
|
||||
transform: (row) => row.nom,
|
||||
},
|
||||
{
|
||||
name: 'Prénom',
|
||||
transform: (row) => row.prenom,
|
||||
},
|
||||
{
|
||||
name: 'Spécialités',
|
||||
transform: (row) => (
|
||||
<div className="flex flex-wrap items-center">
|
||||
{row.specialites.map(specialite => (
|
||||
<span key={specialite.id} className="flex items-center mr-2 mb-1">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mr-2"
|
||||
style={{ backgroundColor: specialite.codeCouleur }}
|
||||
title={specialite.nom}
|
||||
></div>
|
||||
<span>{specialite.nom}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={teachers}
|
||||
onRowClick={handleTeacherSelection}
|
||||
selectedRows={selectedTeachers}
|
||||
isSelectable={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeachersSelectionConfiguration;
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimeRange = ({ startTime, endTime, onStartChange, onEndChange }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de début</label>
|
||||
<input
|
||||
type="time"
|
||||
name="startTime"
|
||||
value={startTime}
|
||||
onChange={onStartChange}
|
||||
className="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de fin</label>
|
||||
<input
|
||||
type="time"
|
||||
name="endTime"
|
||||
value={endTime}
|
||||
onChange={onEndChange}
|
||||
className="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeRange;
|
||||
81
Front-End/src/components/Structure/Planning/ClassesInfo.js
Normal file
81
Front-End/src/components/Structure/Planning/ClassesInfo.js
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import { Users } from 'lucide-react';
|
||||
import DraggableSpeciality from '@/components/Structure/Planning/DraggableSpeciality';
|
||||
|
||||
const groupSpecialitiesBySubject = (enseignants) => {
|
||||
const groupedSpecialities = {};
|
||||
|
||||
enseignants.forEach(teacher => {
|
||||
teacher.specialites.forEach(specialite => {
|
||||
if (!groupedSpecialities[specialite.id]) {
|
||||
groupedSpecialities[specialite.id] = {
|
||||
...specialite,
|
||||
teachers: [`${teacher.nom} ${teacher.prenom}`],
|
||||
};
|
||||
} else {
|
||||
groupedSpecialities[specialite.id].teachers.push(`${teacher.nom} ${teacher.prenom}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(groupedSpecialities);
|
||||
};
|
||||
|
||||
const ClassesInfo = ({ classes, onClassSelect }) => {
|
||||
const [selectedClass, setSelectedClass] = useState(null); // Nouvelle variable d'état pour la classe sélectionnée
|
||||
|
||||
const handleClassChange = (event) => {
|
||||
const classId = event.target.value;
|
||||
const selectedClass = classes.find(classe => classe.id === parseInt(classId));
|
||||
setSelectedClass(selectedClass);
|
||||
onClassSelect(selectedClass);
|
||||
};
|
||||
|
||||
const classChoices = [
|
||||
{ value: '', label: 'Sélectionner...' },
|
||||
...classes.map(classe => ({
|
||||
value: classe.id,
|
||||
label: classe.nom_ambiance,
|
||||
}))
|
||||
];
|
||||
|
||||
// S'assurer que `selectedClass` n'est pas null avant d'appeler `groupSpecialitiesBySubject`
|
||||
const groupedSpecialities = selectedClass ? groupSpecialitiesBySubject(selectedClass.enseignants) : [];
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white shadow rounded mb-4">
|
||||
{classes.length > 0 ? (
|
||||
<SelectChoice
|
||||
name="classes"
|
||||
label="Classes"
|
||||
IconItem={Users}
|
||||
selected={selectedClass ? selectedClass.id : ''}
|
||||
choices={classChoices}
|
||||
callback={handleClassChange}
|
||||
/>
|
||||
) : (
|
||||
<p>Aucune classe disponible.</p>
|
||||
)}
|
||||
|
||||
{selectedClass && (
|
||||
<div className="specialities mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-4">Spécialités</label>
|
||||
{groupedSpecialities.map((specialite, index) => {
|
||||
// Combiner l'ID de la spécialité avec les IDs des enseignants pour créer une clé unique
|
||||
const uniqueId = `${specialite.id}-${specialite.teachers.map(teacher => teacher.id).join('-')}-${index}`;
|
||||
|
||||
return (
|
||||
<DraggableSpeciality
|
||||
key={uniqueId} // Utilisation de l'ID unique généré
|
||||
specialite={specialite}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassesInfo;
|
||||
@ -0,0 +1,38 @@
|
||||
import { useDrag } from 'react-dnd';
|
||||
|
||||
const DraggableSpeciality = ({ specialite }) => {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: 'SPECIALITY',
|
||||
item: { id: specialite.id,
|
||||
name: specialite.nom,
|
||||
color: specialite.codeCouleur,
|
||||
teachers: specialite.teachers,
|
||||
duree: specialite.duree },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const isColorDark = (color) => {
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={drag}
|
||||
className="speciality-tag p-2 m-1 rounded cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: specialite.codeCouleur,
|
||||
color: isColorDark(specialite.codeCouleur) ? 'white' : 'black',
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{specialite.nom} ({specialite.teachers.join(', ')})
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableSpeciality;
|
||||
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DropTargetCell = ({ day, hour, course, onDrop }) => {
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: 'SPECIALITY',
|
||||
drop: (item) => onDrop(item, hour, day),
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const isColorDark = (color) => {
|
||||
if (!color) return false; // Vérification si color est défini
|
||||
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();
|
||||
};
|
||||
|
||||
const cellBackgroundColor = course ? course.color : 'transparent';
|
||||
const cellTextColor = isColorDark(course?.color) ? '#E5E5E5' : '#333333';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
className={`h-20 relative border-b border-gray-100 cursor-pointer ${isToday(new Date(day)) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''} hover:bg-emerald-100`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: cellBackgroundColor,
|
||||
color: cellTextColor
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{course && (
|
||||
<>
|
||||
<div className="text-base font-bold">{course.matiere}</div>
|
||||
<div className="text-sm">{course.teachers.join(", ")}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DropTargetCell.propTypes = {
|
||||
day: PropTypes.string.isRequired,
|
||||
hour: PropTypes.number.isRequired,
|
||||
course: PropTypes.object,
|
||||
onDrop: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTargetCell;
|
||||
@ -0,0 +1,249 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { isToday } from 'date-fns';
|
||||
import DropTargetCell from '@/components/Structure/Planning/DropTargetCell';
|
||||
import { BK_GESTIONENSEIGNANTS_PLANNING_URL } from '@/utils/Url';
|
||||
|
||||
const PlanningClassView = ({ schedule }) => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const scrollContainerRef = React.useRef(null);
|
||||
const scheduleIdRef = useRef(scheduleId);
|
||||
const eventsRef = useRef(events);
|
||||
|
||||
const [planning, setPlanning] = useState([])
|
||||
|
||||
// Fonction pour récupérer les données du depuis l'API
|
||||
const fetchPlanning = () => {
|
||||
if (schedule) {
|
||||
fetch(`${BK_GESTIONENSEIGNANTS_PLANNING_URL}/${schedule.id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('DATA : ', data);
|
||||
if (!data || data.emploiDuTemps.length === 0) {
|
||||
setEvents([]);
|
||||
setPlanning([]);
|
||||
return;
|
||||
}
|
||||
const planningData = data.emploiDuTemps || {};
|
||||
console.log('succès : ', planningData);
|
||||
|
||||
let events = [];
|
||||
Object.keys(planningData).forEach(day => {
|
||||
if (planningData[day]) {
|
||||
planningData[day].forEach(event => {
|
||||
if (event) {
|
||||
events.push({
|
||||
...event,
|
||||
day: day.toLowerCase(), // Ajouter une clé jour en minuscule pour faciliter le filtrage
|
||||
scheduleId: data.id
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(events) !== JSON.stringify(eventsRef.current)) {
|
||||
setEvents(events);
|
||||
setPlanning(events);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur lors de la récupération du planning :', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEvents([])
|
||||
setPlanning([]);
|
||||
fetchPlanning();
|
||||
}, [scheduleId]);
|
||||
|
||||
useEffect(() => {
|
||||
scheduleIdRef.current = scheduleId;
|
||||
// Mettre à jour la référence chaque fois que scheduleId change
|
||||
}, [scheduleId]);
|
||||
|
||||
/*useEffect(() => {
|
||||
eventsRef.current = events;
|
||||
// Mettre à jour la référence chaque fois que events change
|
||||
}, [events]);*/
|
||||
|
||||
// Déplacer ces déclarations avant leur utilisation
|
||||
const timeSlots = Array.from({ length: 11 }, (_, i) => i + 8);
|
||||
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
|
||||
const weekDays = Array.from({ length: 6 }, (_, i) => addDays(weekStart, i));
|
||||
|
||||
// Maintenant on peut utiliser weekDays
|
||||
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
|
||||
|
||||
const getFilteredEvents = (day, hour) => {
|
||||
const dayName = format(day, 'eeee', { locale: fr }).toLowerCase(); // Obtenir le nom du jour en minuscule pour le filtrage
|
||||
|
||||
if (!Array.isArray(planning)) {
|
||||
console.error("planning n'est pas un tableau:", planning);
|
||||
return [];
|
||||
}
|
||||
|
||||
return planning.filter(event => {
|
||||
const eventDay = event.day.toLowerCase(); // Convertir le jour de l'événement en minuscule
|
||||
|
||||
return (
|
||||
eventDay === dayName &&
|
||||
event.heure.startsWith(hour.toString().padStart(2, '0')) // Comparer l'heure sans les minutes
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/*const handleDrop = (item, hour, day) => {
|
||||
const dateKey = format(new Date(day), 'yyyy-MM-dd');
|
||||
const newEvent = {
|
||||
id: `event-${dateKey}-${hour}`,
|
||||
start: new Date(day).setHours(hour),
|
||||
end: new Date(day).setHours(hour + 1),
|
||||
matiere: item.matiere || item.name, // Assurer que 'matiere' soit défini
|
||||
color: item.color || '#FFFFFF', // Ajouter une couleur par défaut si indéfini
|
||||
teachers: item.teachers || [], // Assurer que 'teachers' soit défini
|
||||
day: format(new Date(day), 'eeee', { locale: fr }).toLowerCase(), // Ajouter le jour
|
||||
heure: hour.toString().padStart(2, '0') + ':00', // Ajouter l'heure ici
|
||||
scheduleId: scheduleIdRef.current // Utiliser la référence à scheduleId ici
|
||||
};
|
||||
|
||||
// Utiliser la référence pour accéder aux événements actuels
|
||||
const existingEvents = eventsRef.current.filter(event => {
|
||||
const eventHour = event.heure;
|
||||
const eventDay = event.day;
|
||||
|
||||
console.log("DEBUG : " + event);
|
||||
console.log("DEBUG : " + eventHour + " - " + hour.toString().padStart(2, '0') + ':00');
|
||||
console.log("DEBUG : " + eventDay + " - " + format(new Date(day), 'eeee', { locale: fr }).toLowerCase());
|
||||
console.log("DEBUG : " + event.scheduleId + " - " + scheduleIdRef.current);
|
||||
console.log("----");
|
||||
// Comparer la date, l'heure et le scheduleId pour trouver les événements correspondants
|
||||
return eventHour === hour.toString().padStart(2, '0') + ':00' && eventDay === format(new Date(day), 'eeee', { locale: fr }).toLowerCase() && event.scheduleId === scheduleIdRef.current;
|
||||
});
|
||||
|
||||
console.log("existingEvents :", existingEvents);
|
||||
if (existingEvents.length > 0) {
|
||||
existingEvents.forEach(event => {
|
||||
deleteEvent(event.id); // Supprimer l'événement existant
|
||||
});
|
||||
}
|
||||
|
||||
// Ajouter l'événement à l'état global des événements
|
||||
addEvent(newEvent);
|
||||
|
||||
setPlanning(prevEvents => {
|
||||
// Filtrer les événements pour conserver ceux qui n'ont pas le même jour et créneau horaire
|
||||
const updatedEvents = prevEvents.filter(event =>
|
||||
!(event.day === newEvent.day && event.heure === newEvent.heure)
|
||||
);
|
||||
|
||||
// Ajouter le nouvel événement
|
||||
updatedEvents.push(newEvent);
|
||||
|
||||
return updatedEvents;
|
||||
});
|
||||
};*/
|
||||
|
||||
// Mettre à jour la position de la ligne toutes les minutes
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Modifier l'useEffect pour l'auto-scroll
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current && isCurrentWeek) {
|
||||
const currentHour = new Date().getHours();
|
||||
const scrollPosition = currentHour * 80;
|
||||
// Ajout d'un délai pour laisser le temps au DOM de se mettre à jour
|
||||
setTimeout(() => {
|
||||
scrollContainerRef.current.scrollTop = scrollPosition - 200;
|
||||
}, 0);
|
||||
}
|
||||
}, [currentDate, isCurrentWeek]); // Ajout de currentDate dans les dépendances
|
||||
|
||||
|
||||
// Calculer la position de la ligne de temps
|
||||
const getCurrentTimePosition = () => {
|
||||
const hours = currentTime.getHours();
|
||||
const minutes = currentTime.getMinutes();
|
||||
|
||||
if (hours < 8 || hours > 18) {
|
||||
return -1; // Hors de la plage horaire
|
||||
}
|
||||
|
||||
const totalMinutes = (hours - 8) * 60 + minutes; // Minutes écoulées depuis 8h
|
||||
const cellHeight = 80; // Hauteur des cellules (par exemple 80px pour 20rem / 24)
|
||||
|
||||
const position = (totalMinutes / 60) * cellHeight;
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
{/* En-tête des jours */}
|
||||
<div className="grid gap-[1px] bg-gray-100 w-full" style={{ gridTemplateColumns: "2.5rem repeat(6, 1fr)" }}>
|
||||
<div className="bg-white h-14"></div>
|
||||
{weekDays.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className={`p-3 text-center border-b ${isToday(day) ? 'bg-emerald-400 border-x border-emerald-600' : 'bg-white'}`} >
|
||||
<div className={`text font-medium ${isToday(day) ? 'text-white' : 'text-gray-500'}`}>
|
||||
{format(day, 'EEEE', { locale: fr })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grille horaire */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
||||
{/* Ligne de temps actuelle */}
|
||||
{isCurrentWeek && (
|
||||
<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-1 w-2 h-2 rounded-full bg-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-[1px] bg-white" style={{ gridTemplateColumns: "2.5rem repeat(6, 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>
|
||||
{weekDays.map(day => {
|
||||
const filteredEvents = getFilteredEvents(day, hour);
|
||||
|
||||
return(
|
||||
<DropTargetCell
|
||||
key={`${hour}-${day}`}
|
||||
hour={hour}
|
||||
day={day}
|
||||
onDrop={handleDrop}
|
||||
onDateClick={onDateClick}
|
||||
filteredEvents={filteredEvents}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanningClassView;
|
||||
130
Front-End/src/components/Structure/Planning/PlanningClassView.js
Normal file
130
Front-End/src/components/Structure/Planning/PlanningClassView.js
Normal file
@ -0,0 +1,130 @@
|
||||
import React, {useRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { format, isToday, isSameDay, startOfWeek, addDays } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import DropTargetCell from './DropTargetCell';
|
||||
|
||||
const PlanningClassView = ({ schedule, onDrop, planningType }) => {
|
||||
const weekDays = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"];
|
||||
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
|
||||
const scrollContainerRef = useRef(null);
|
||||
// Calcul des dates des jours de la semaine
|
||||
const startDate = startOfWeek(new Date(), { weekStartsOn: 1 }); // Début de la semaine (lundi)
|
||||
const weekDayDates = weekDays.map((_, index) => addDays(startDate, index));
|
||||
|
||||
// Fonction pour formater l'heure
|
||||
const formatTime = (time) => {
|
||||
const [hour, minute] = time.split(':');
|
||||
return `${hour}h${minute}`;
|
||||
};
|
||||
|
||||
const renderCells = () => {
|
||||
const cells = [];
|
||||
const timeSlots = Array.from({ length: 12 }, (_, index) => index + 8); // Heures de 08:00 à 19:00
|
||||
|
||||
timeSlots.forEach(hour => {
|
||||
cells.push(
|
||||
<div key={`hour-${hour}`} className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
|
||||
{`${hour.toString().padStart(2, '0')}:00`}
|
||||
</div>
|
||||
);
|
||||
|
||||
weekDays.forEach(day => {
|
||||
const daySchedule = schedule?.emploiDuTemps?.[day] || [];
|
||||
const courses = daySchedule.filter(course => {
|
||||
const courseHour = parseInt(course.heure.split(':')[0], 10);
|
||||
const courseDuration = parseInt(course.duree, 10); // Utiliser la durée comme un nombre
|
||||
return courseHour <= hour && hour < (courseHour + courseDuration);
|
||||
});
|
||||
|
||||
const course = courses.length > 0 ? courses[0] : null;
|
||||
|
||||
cells.push(
|
||||
<DropTargetCell
|
||||
key={`${day}-${hour}`}
|
||||
day={day}
|
||||
hour={hour}
|
||||
course={course}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
|
||||
// Calculer la position de la ligne de temps
|
||||
const getCurrentTimePosition = () => {
|
||||
const hours = currentTime.getHours();
|
||||
const minutes = currentTime.getMinutes();
|
||||
|
||||
if (hours < 8 || hours > 18) {
|
||||
return -1; // Hors de la plage horaire
|
||||
}
|
||||
|
||||
const totalMinutes = (hours - 8) * 60 + minutes; // Minutes écoulées depuis 8h
|
||||
const cellHeight = 80; // Hauteur des cellules (par exemple 80px pour 20rem / 24)
|
||||
|
||||
const position = (totalMinutes / 60) * cellHeight;
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
{/* En-tête des jours */}
|
||||
<div className="grid gap-[1px] bg-gray-100 w-full" style={{ gridTemplateColumns: "2.5rem repeat(6, 1fr)" }}>
|
||||
<div className="bg-white h-14"></div>
|
||||
{weekDayDates.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className={`p-3 text-center border-b ${isToday(day) ? 'bg-emerald-400 border-x border-emerald-600' : 'bg-white'}`} >
|
||||
<div className={`text font-medium ${isToday(day) ? 'text-white' : 'text-gray-500'}`}>
|
||||
{format(day, 'EEEE', { locale: fr })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grille horaire */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
||||
{isCurrentWeek && (
|
||||
<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-1 w-2 h-2 rounded-full bg-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`grid gap-[1px] bg-white`} style={{ gridTemplateColumns: `2.5rem repeat(6, 1fr)` }}>
|
||||
{renderCells()}
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default PlanningClassView;
|
||||
@ -0,0 +1,195 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { AnimatePresence, findSpring, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import PlanningClassView from '@/components/Structure/Planning/PlanningClassView';
|
||||
import SpecialityEventModal from '@/components/Structure/Planning/SpecialityEventModal';
|
||||
import ClassesInfo from '@/components/Structure/Planning/ClassesInfo';
|
||||
import { BK_GESTIONENSEIGNANTS_PLANNING_URL } from '@/utils/Url';
|
||||
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
|
||||
const ScheduleManagement = ({ schedules, setSchedules, handleUpdatePlanning, specialities, teachers, classes }) => {
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
const [schedule, setSchedule] = useState(null);
|
||||
const scheduleRef = useRef(null);
|
||||
const scheduleId = useRef(null);
|
||||
const [planningType, setPlanningType] = useState('TRIMESTRIEL');
|
||||
const [currentPeriod, setCurrentPeriod] = useState('T1');
|
||||
|
||||
const planningChoices = [
|
||||
{ value: 'TRIMESTRIEL', label: 'TRIMESTRIEL' },
|
||||
{ value: 'SEMESTRIEL', label: 'SEMESTRIEL' },
|
||||
{ value: 'ANNUEL', label: 'ANNUEL' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedClass) {
|
||||
scheduleId.current = selectedClass.planning.id;
|
||||
setSchedule(selectedClass.planning);
|
||||
} else {
|
||||
setSchedule(null);
|
||||
}
|
||||
}, [selectedClass]);
|
||||
|
||||
// Synchroniser scheduleRef avec schedule
|
||||
useEffect(() => {
|
||||
scheduleRef.current = schedule;
|
||||
}, [schedule]);
|
||||
|
||||
const handleClassSelect = (cls) => {
|
||||
setSelectedClass(cls);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule) {
|
||||
console.log("Schedule data:", schedule);
|
||||
}
|
||||
}, [schedule]);
|
||||
|
||||
|
||||
// Fonction onDrop dans PlanningClassView ou un composant parent
|
||||
const onDrop = (item, hour, day) => {
|
||||
// Les données de l'élément drag and drop (spécialité par exemple)
|
||||
const { id, name, color, teachers } = item;
|
||||
|
||||
// Créez une nouvelle copie du planning
|
||||
const newSchedule = { ...scheduleRef.current };
|
||||
|
||||
// Vérifiez s'il existe déjà une entrée pour le jour
|
||||
if (!newSchedule.emploiDuTemps[day]) {
|
||||
newSchedule.emploiDuTemps[day] = [];
|
||||
}
|
||||
const courseTime = `${hour.toString().padStart(2, '0')}:00`;
|
||||
|
||||
// Rechercher s'il y a déjà un cours à l'heure spécifiée
|
||||
const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(course =>
|
||||
course.heure === courseTime
|
||||
);
|
||||
|
||||
const newCourse = {
|
||||
duree: '1',
|
||||
heure: courseTime,
|
||||
matiere: name,
|
||||
teachers: teachers,
|
||||
color: color,
|
||||
};
|
||||
|
||||
// S'il existe déjà un cours à cette heure, on le remplace
|
||||
if (existingCourseIndex !== -1) {
|
||||
newSchedule.emploiDuTemps[day][existingCourseIndex] = newCourse;
|
||||
} else {
|
||||
// Sinon on ajoute le nouveau cours
|
||||
newSchedule.emploiDuTemps[day].push(newCourse);
|
||||
}
|
||||
|
||||
// Mettez à jour l'état du planning
|
||||
setSchedule(newSchedule)
|
||||
|
||||
// Appelez la fonction handleUpdatePlanning en dehors de setSchedule
|
||||
handleUpdatePlanning(`${BK_GESTIONENSEIGNANTS_PLANNING_URL}`, scheduleId.current, newSchedule);
|
||||
};
|
||||
|
||||
const getPeriodLabel = (period) => {
|
||||
switch(period) {
|
||||
case 'T1': return '1er trimestre';
|
||||
case 'T2': return '2e trimestre';
|
||||
case 'T3': return '3e trimestre';
|
||||
case 'S1': return '1er semestre';
|
||||
case 'S2': return '2e semestre';
|
||||
default: return period;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeriodChange = (direction) => {
|
||||
if (planningType === 'TRIMESTRIEL') {
|
||||
if (direction === 'prev') {
|
||||
setCurrentPeriod(currentPeriod === 'T1' ? 'T3' : `T${parseInt(currentPeriod.slice(1)) - 1}`);
|
||||
} else {
|
||||
setCurrentPeriod(currentPeriod === 'T3' ? 'T1' : `T${parseInt(currentPeriod.slice(1)) + 1}`);
|
||||
}
|
||||
} else if (planningType === 'SEMESTRIEL') {
|
||||
setCurrentPeriod(currentPeriod === 'S1' ? 'S2' : 'S1');
|
||||
}
|
||||
};
|
||||
|
||||
// Fonctionnalité de gestion des emplois du temps
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ClassesInfo classes={classes} onClassSelect={handleClassSelect}/>
|
||||
|
||||
<div className="flex justify-between items-center p-4 w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<SelectChoice
|
||||
name="planningType"
|
||||
IconItem={Calendar}
|
||||
selected={planningType}
|
||||
choices={planningChoices}
|
||||
callback={(e) => {
|
||||
setPlanningType(e.target.value);
|
||||
setCurrentPeriod(e.target.value === 'TRIMESTRIEL' ? 'T1' : 'S1');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{planningType !== 'ANNUEL' && (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<button
|
||||
onClick={() => handlePeriodChange('prev')}
|
||||
className={`mr-4 p-2 border rounded-lg ${
|
||||
currentPeriod === 'T1' || currentPeriod === 'S1' ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
} transition-colors duration-300`}
|
||||
disabled={currentPeriod === 'T1' || currentPeriod === 'S1'}
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<span className="text-lg font-semibold mx-4">{getPeriodLabel(currentPeriod)}</span>
|
||||
<button
|
||||
onClick={() => handlePeriodChange('next')}
|
||||
className={`ml-4 p-2 border rounded-lg ${
|
||||
(planningType === 'TRIMESTRIEL' && currentPeriod === 'T3') || (planningType === 'SEMESTRIEL' && currentPeriod === 'S2') ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
} transition-colors duration-300`}
|
||||
disabled={(planningType === 'TRIMESTRIEL' && currentPeriod === 'T3') || (planningType === 'SEMESTRIEL' && currentPeriod === 'S2')}
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
|
||||
<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 }}
|
||||
>
|
||||
<PlanningClassView
|
||||
schedule={schedule}
|
||||
onDrop={onDrop}
|
||||
planningType={planningType}
|
||||
currentPeriod={currentPeriod}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</DndProvider>
|
||||
{/* <SpecialityEventModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
eventData={eventData}
|
||||
setEventData={setEventData}
|
||||
selectedClass={selectedClass}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleManagement;
|
||||
@ -0,0 +1,186 @@
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import { format } from 'date-fns';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Users, BookOpen } from 'lucide-react';
|
||||
|
||||
export default function SpecialityEventModal({ isOpen, onClose, eventData, setEventData, selectedClass }) {
|
||||
const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Réinitialiser eventData lorsque la modale se ferme
|
||||
setEventData({
|
||||
scheduleId: '',
|
||||
specialiteId: '',
|
||||
specialities: [],
|
||||
// Réinitialiser d'autres champs si nécessaire
|
||||
});
|
||||
}
|
||||
}, [isOpen, setEventData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && selectedClass) {
|
||||
|
||||
setEventData(prev => ({
|
||||
...prev,
|
||||
scheduleId: selectedClass.id,
|
||||
specialities: Array.from(new Map(
|
||||
selectedClass.enseignants.flatMap(teacher =>
|
||||
teacher.specialites.map(specialite => [specialite.id, {
|
||||
id: specialite.id,
|
||||
nom: specialite.nom,
|
||||
codeCouleur: specialite.codeCouleur
|
||||
}])
|
||||
)
|
||||
).values())
|
||||
}));
|
||||
}
|
||||
}, [isOpen, selectedClass, setEventData]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!eventData.scheduleId) {
|
||||
alert('Veuillez sélectionner une spécialité');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSchedule = schedules.find(s => s.id === eventData.scheduleId);
|
||||
|
||||
if (eventData.id) {
|
||||
updateEvent(eventData.id, {
|
||||
...eventData,
|
||||
scheduleId: eventData.scheduleId,
|
||||
color: eventData.color || selectedSchedule?.color
|
||||
});
|
||||
} else {
|
||||
addEvent({
|
||||
...eventData,
|
||||
id: `event-${Date.now()}`,
|
||||
scheduleId: eventData.scheduleId,
|
||||
color: eventData.color || selectedSchedule?.color
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
|
||||
deleteEvent(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">
|
||||
{/* Sélection de la Spécialité */}
|
||||
<div>
|
||||
{eventData.scheduleId && eventData.specialities && eventData.specialities.length > 0 ? (
|
||||
<SelectChoice
|
||||
name={`spécialités-${eventData.scheduleId}`}
|
||||
label="Spécialités"
|
||||
selected={eventData.specialiteId ? eventData.specialiteId : ''}
|
||||
choices={eventData.specialities.map(specialite => ({
|
||||
value: specialite.id,
|
||||
label: specialite.nom
|
||||
}))}
|
||||
callback={(event) => {
|
||||
const selectedSpecialityId = event.target.value;
|
||||
const selectedSpeciality = eventData.specialities.find(specialite => specialite.id === parseInt(selectedSpecialityId, 10));
|
||||
setEventData({
|
||||
...eventData,
|
||||
specialiteId: selectedSpecialityId,
|
||||
color: selectedSpeciality?.codeCouleur || '#10b981'
|
||||
});
|
||||
}}
|
||||
IconItem={BookOpen}
|
||||
/>
|
||||
) : (
|
||||
<p>Aucune spécialité disponible pour cette classe.</p>
|
||||
)}
|
||||
</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={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={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>
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user