mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
refactor: Refactoring de la section ClassSection
This commit is contained in:
@ -3,6 +3,7 @@
|
|||||||
# The first instruction is what image we want to base our container on
|
# The first instruction is what image we want to base our container on
|
||||||
# We Use an official Python runtime as a parent image
|
# We Use an official Python runtime as a parent image
|
||||||
FROM python:3.12.7
|
FROM python:3.12.7
|
||||||
|
WORKDIR /Back-End
|
||||||
|
|
||||||
# Allows docker to cache installed dependencies between builds
|
# Allows docker to cache installed dependencies between builds
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
@ -11,7 +12,6 @@ RUN pip install pymupdf
|
|||||||
|
|
||||||
# Mounts the application code to the image
|
# Mounts the application code to the image
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR /Back-End
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
@ -68,11 +68,7 @@ class TeacherSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_droit(self, obj):
|
def get_droit(self, obj):
|
||||||
if obj.associated_profile:
|
if obj.associated_profile:
|
||||||
droit_id = obj.associated_profile.droit
|
return obj.associated_profile.droit
|
||||||
return {
|
|
||||||
"label": obj.associated_profile.get_droit_display(),
|
|
||||||
"id": droit_id
|
|
||||||
}
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_specialities_details(self, obj):
|
def get_specialities_details(self, obj):
|
||||||
@ -163,7 +159,7 @@ class SchoolClassSerializer(serializers.ModelSerializer):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_teachers_details(self, obj):
|
def get_teachers_details(self, obj):
|
||||||
return [{'last_name': teacher.last_name, 'first_name': teacher.first_name} for teacher in obj.teachers.all()]
|
return [{'id': teacher.id, 'last_name': teacher.last_name, 'first_name': teacher.first_name} for teacher in obj.teachers.all()]
|
||||||
|
|
||||||
def get_updated_date_formatted(self, obj):
|
def get_updated_date_formatted(self, obj):
|
||||||
utc_time = timezone.localtime(obj.updated_date)
|
utc_time = timezone.localtime(obj.updated_date)
|
||||||
|
|||||||
84
Front-End/src/components/MultiSelect.js
Normal file
84
Front-End/src/components/MultiSelect.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Check, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const handleSelect = (option) => {
|
||||||
|
const isSelected = selectedOptions.some(selected => selected.id === option.id);
|
||||||
|
let newSelectedOptions;
|
||||||
|
if (isSelected) {
|
||||||
|
newSelectedOptions = selectedOptions.filter(selected => selected.id !== option.id);
|
||||||
|
} else {
|
||||||
|
newSelectedOptions = [...selectedOptions, option];
|
||||||
|
}
|
||||||
|
onChange(newSelectedOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{selectedOptions.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap gap-1 justify-center items-center">
|
||||||
|
{selectedOptions.map(option => (
|
||||||
|
<span key={option.id} className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm">
|
||||||
|
{option.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Sélectionnez les niveaux'
|
||||||
|
)}
|
||||||
|
<ChevronDown className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none h-full w-5" />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<ul className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
{options.map(option => (
|
||||||
|
<li
|
||||||
|
key={option.id}
|
||||||
|
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
|
||||||
|
selectedOptions.some(selected => selected.id === option.id) ? 'text-white bg-emerald-600' : 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelect(option)}
|
||||||
|
>
|
||||||
|
<span className={`block truncate ${selectedOptions.some(selected => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}>
|
||||||
|
{option.name}
|
||||||
|
</span>
|
||||||
|
{selectedOptions.some(selected => selected.id === option.id) && (
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-white">
|
||||||
|
<Check className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultiSelect;
|
||||||
@ -1,36 +1,34 @@
|
|||||||
export default function SelectChoice({ type, name, label,required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
|
export default function SelectChoice({ type, name, label, required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4">
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
{label}
|
||||||
{label}
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
{required && <span className="text-red-500 ml-1">*</span>}
|
</label>
|
||||||
</label>
|
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
|
||||||
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
|
{IconItem &&
|
||||||
{IconItem &&
|
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
|
||||||
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
|
{<IconItem />}
|
||||||
{<IconItem />}
|
</span>
|
||||||
</span>
|
}
|
||||||
}
|
<select
|
||||||
<select
|
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${disabled ? 'bg-gray-100' : ''}`}
|
||||||
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${disabled ? 'bg-gray-100' : ''} ${selected === "" ? 'italic' : ''}`}
|
type={type}
|
||||||
type={type}
|
id={name}
|
||||||
id={name}
|
name={name}
|
||||||
name={name}
|
value={selected}
|
||||||
value={selected}
|
onChange={callback}
|
||||||
onChange={callback}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
>
|
||||||
>
|
<option value="">{placeHolder?.toLowerCase()}</option>
|
||||||
<option value="" className="italic">{placeHolder?.toLowerCase()}</option>
|
{choices.map(({ value, label }, index) => (
|
||||||
{choices.map(({ value, label }, index) => (
|
<option key={value} value={value}>
|
||||||
<option key={value} value={value}>
|
{label}
|
||||||
{label}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
|
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,253 +0,0 @@
|
|||||||
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 { getNiveauNameById, schoolYears, getNiveauxLabels, getNiveauxTabs, generateAgeToNiveaux, niveauxPremierCycle, niveauxSecondCycle, niveauxTroisiemeCycle, typeEmploiDuTemps, updatePlannings } = useClasses();
|
|
||||||
const [selectedTeachers, setSelectedTeachers] = useState(formData.teachers);
|
|
||||||
|
|
||||||
const handleTeacherSelection = (teacher) => {
|
|
||||||
setSelectedTeachers(prevState =>
|
|
||||||
prevState.includes(teacher.id)
|
|
||||||
? prevState.filter(id => id !== teacher.id)
|
|
||||||
: [...prevState, teacher.id]
|
|
||||||
);
|
|
||||||
setFormData(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
teachers: prevState.teachers.includes(teacher.id)
|
|
||||||
? prevState.teachers.filter(id => id !== teacher.id)
|
|
||||||
: [...prevState.teachers, teacher.id]
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeChange = (e, index) => {
|
|
||||||
const { value } = e.target;
|
|
||||||
setFormData(prevState => {
|
|
||||||
const updatedTimes = [...prevState.time_range];
|
|
||||||
updatedTimes[index] = value;
|
|
||||||
|
|
||||||
const updatedFormData = {
|
|
||||||
...prevState,
|
|
||||||
time_range: updatedTimes,
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingPlannings = prevState.plannings || [];
|
|
||||||
updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings);
|
|
||||||
|
|
||||||
return updatedFormData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleJoursChange = (e) => {
|
|
||||||
const { value, checked } = e.target;
|
|
||||||
const dayId = parseInt(value, 10);
|
|
||||||
|
|
||||||
setFormData((prevState) => {
|
|
||||||
const updatedJoursOuverture = checked
|
|
||||||
? [...prevState.opening_days, dayId]
|
|
||||||
: prevState.opening_days.filter((id) => id !== dayId);
|
|
||||||
|
|
||||||
const updatedFormData = {
|
|
||||||
...prevState,
|
|
||||||
opening_days: updatedJoursOuverture,
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingPlannings = prevState.plannings || [];
|
|
||||||
updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings);
|
|
||||||
|
|
||||||
return updatedFormData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { name, value, type, checked } = e.target;
|
|
||||||
|
|
||||||
setFormData(prevState => {
|
|
||||||
|
|
||||||
// Copier l'état précédent
|
|
||||||
let newState = { ...prevState };
|
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
|
||||||
const newValues = checked
|
|
||||||
? [...(prevState[name] || []), parseInt(value)]
|
|
||||||
: (prevState[name] || []).filter(v => v !== parseInt(value));
|
|
||||||
newState[name] = newValues;
|
|
||||||
} else if (name === 'age_range') {
|
|
||||||
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);
|
|
||||||
|
|
||||||
newState = {
|
|
||||||
...prevState,
|
|
||||||
[name]: value,
|
|
||||||
levels: selectedNiveaux.length > 0 ? selectedNiveaux : [],
|
|
||||||
};
|
|
||||||
} else if (type === 'radio') {
|
|
||||||
newState[name] = parseInt(value, 10);
|
|
||||||
} else {
|
|
||||||
newState[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingPlannings = prevState.plannings || [];
|
|
||||||
newState.plannings = updatePlannings(newState, existingPlannings);
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
onSubmit(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [minAge, maxAge] = formData.age_range.length === 2 ? formData.age_range : [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="atmosphere_name"
|
|
||||||
type="text"
|
|
||||||
IconItem={Users}
|
|
||||||
placeholder="Nom de l'ambiance"
|
|
||||||
value={formData.atmosphere_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<InputTextIcon
|
|
||||||
name="age_range"
|
|
||||||
type="text"
|
|
||||||
IconItem={Maximize2}
|
|
||||||
placeholder="Tranche d'âge (ex: 3-6)"
|
|
||||||
value={formData.age_range}
|
|
||||||
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="levels"
|
|
||||||
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<CheckBoxList
|
|
||||||
items={niveauxSecondCycle}
|
|
||||||
formData={formData}
|
|
||||||
handleChange={handleChange}
|
|
||||||
fieldName="levels"
|
|
||||||
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<CheckBoxList
|
|
||||||
items={niveauxTroisiemeCycle}
|
|
||||||
formData={formData}
|
|
||||||
handleChange={handleChange}
|
|
||||||
fieldName="levels"
|
|
||||||
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="number_of_students"
|
|
||||||
type="number"
|
|
||||||
IconItem={UserPlus}
|
|
||||||
placeholder="Capacité max"
|
|
||||||
value={formData.number_of_students}
|
|
||||||
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="school_year"
|
|
||||||
placeHolder="Sélectionner l'année scolaire"
|
|
||||||
selected={formData.school_year}
|
|
||||||
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.levels.length === 0 || !formData.school_year || !formData.number_of_students || formData.teachers.length === 0)
|
|
||||||
? "bg-gray-300 text-gray-700 cursor-not-allowed"
|
|
||||||
: "bg-emerald-500 text-white hover:bg-emerald-600"
|
|
||||||
}`}
|
|
||||||
primary
|
|
||||||
disabled={(formData.levels.length === 0 || !formData.school_year || !formData.number_of_students || formData.teachers.length === 0)}
|
|
||||||
type="submit"
|
|
||||||
name="Create"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClassForm;
|
|
||||||
@ -1,165 +1,427 @@
|
|||||||
import { Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
|
import { Trash2, Edit3, Plus, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DropdownMenu from '@/components/DropdownMenu';
|
import Popup from '@/components/Popup';
|
||||||
import Modal from '@/components/Modal';
|
import InputText from '@/components/InputText';
|
||||||
import ClassForm from '@/components/Structure/Configuration/ClassForm';
|
import SelectChoice from '@/components/SelectChoice';
|
||||||
import ClasseDetails from '@/components/ClasseDetails';
|
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
|
||||||
|
import MultiSelect from '@/components/MultiSelect';
|
||||||
import LevelLabel from '@/components/CustomLabels/LevelLabel';
|
import LevelLabel from '@/components/CustomLabels/LevelLabel';
|
||||||
import TeacherLabel from '@/components/CustomLabels/TeacherLabel';
|
import { DndProvider, HTML5Backend, useDrop } from 'react-dnd';
|
||||||
import { ClasseFormProvider } from '@/context/ClasseFormContext';
|
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
|
||||||
|
|
||||||
|
const ItemTypes = {
|
||||||
|
TEACHER: 'teacher',
|
||||||
|
};
|
||||||
|
|
||||||
const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDelete }) => {
|
const TeachersDropZone = ({ classe, handleTeachersChange, teachers, isEditing }) => {
|
||||||
|
const [localTeachers, setLocalTeachers] = useState(classe.teachers_details || []);
|
||||||
|
|
||||||
const { getNiveauxLabels } = useClasses();
|
useEffect(() => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
}, [teachers]);
|
||||||
const [isOpenDetails, setIsOpenDetails] = useState(false);
|
|
||||||
const [editingClass, setEditingClass] = useState(null);
|
|
||||||
|
|
||||||
const openEditModal = (classe) => {
|
useEffect(() => {
|
||||||
setIsOpen(true);
|
setLocalTeachers(classe.teachers_details || []);
|
||||||
setEditingClass(classe);
|
}, [classe.teachers_details]);
|
||||||
}
|
|
||||||
|
|
||||||
const openEditModalDetails = (classe) => {
|
useEffect(() => {
|
||||||
setIsOpenDetails(true);
|
handleTeachersChange(localTeachers.map(teacher => teacher.id));
|
||||||
setEditingClass(classe);
|
}, [localTeachers]);
|
||||||
}
|
|
||||||
|
|
||||||
const closeEditModal = () => {
|
const [{ isOver, canDrop }, drop] = useDrop({
|
||||||
setIsOpen(false);
|
accept: ItemTypes.TEACHER,
|
||||||
setEditingClass(null);
|
drop: (item) => {
|
||||||
};
|
const teacherDetails = teachers.find(teacher => teacher.id === item.id);
|
||||||
|
const exists = localTeachers.some(teacher => teacher.id === item.id);
|
||||||
|
if (!exists) {
|
||||||
|
setLocalTeachers(prevTeachers => {
|
||||||
|
const updatedTeachers = [
|
||||||
|
...prevTeachers,
|
||||||
|
{ id: item.id, last_name: teacherDetails.last_name, first_name: teacherDetails.first_name }
|
||||||
|
];
|
||||||
|
return updatedTeachers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isOver: !!monitor.isOver(),
|
||||||
|
canDrop: !!monitor.canDrop(),
|
||||||
|
}),
|
||||||
|
canDrop: () => {
|
||||||
|
return isEditing;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const closeEditModalDetails = () => {
|
const handleRemoveTeacher = (id) => {
|
||||||
setIsOpenDetails(false);
|
setLocalTeachers(prevTeachers => {
|
||||||
setEditingClass(null);
|
const updatedTeachers = prevTeachers.filter(teacher => teacher.id !== id);
|
||||||
};
|
return updatedTeachers;
|
||||||
|
});
|
||||||
const handleModalSubmit = (updatedData) => {
|
|
||||||
if (editingClass) {
|
|
||||||
handleEdit(editingClass.id, updatedData);
|
|
||||||
} else {
|
|
||||||
handleCreate(updatedData);
|
|
||||||
}
|
|
||||||
closeEditModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8">
|
<div ref={drop} className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
|
||||||
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
|
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
|
||||||
<h2 className="text-xl font-bold mb-4">Gestion des classes</h2>
|
}`}
|
||||||
<button
|
>
|
||||||
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
|
{isEditing && (
|
||||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
<div className="mb-2 text-blue-500 font-semibold flex items-center space-x-2">
|
||||||
>
|
<Hand className="w-5 h-5" /> {/* Ajoutez l'icône Hand */}
|
||||||
<Plus className="w-5 h-5" />
|
<span>Déposez un enseignant ici</span>
|
||||||
</button>
|
</div>
|
||||||
</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.atmosphere_name ? row.atmosphere_name : '';
|
|
||||||
const trancheAge = row.age_range ? `${row.age_range} 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 levelLabels = Array.isArray(row.levels) ? getNiveauxLabels(row.levels) : [];
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap justify-center items-center space-x-2">
|
|
||||||
{levelLabels.length > 0
|
|
||||||
? levelLabels.map((label, index) => (
|
|
||||||
<LevelLabel key={index} label={label} index={index} />
|
|
||||||
))
|
|
||||||
: 'Aucun niveau'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ name: 'CAPACITÉ MAX', transform: (row) => row.number_of_students },
|
|
||||||
{ name: 'ANNÉE SCOLAIRE', transform: (row) => row.school_year },
|
|
||||||
{
|
|
||||||
name: 'ENSEIGNANTS',
|
|
||||||
transform: (row) => (
|
|
||||||
<div key={row.id} className="flex flex-wrap justify-center items-center space-x-2">
|
|
||||||
{row.teachers_details.map((teacher, index) => (
|
|
||||||
<TeacherLabel key={`${teacher.id}-${index}`} nom={teacher.last_name} prenom={teacher.first_name} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ name: 'DATE DE CREATION', transform: (row) => row.updated_date_formatted },
|
|
||||||
{
|
|
||||||
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.atmosphere_name} - {editingClass.age_range[0]} à {editingClass.age_range[1]} ans
|
|
||||||
</>
|
|
||||||
) : ''}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
ContentComponent={() => (
|
|
||||||
<ClasseDetails classe={editingClass} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
{localTeachers.map((teacher, index) => (
|
||||||
|
<div key={`${teacher.id}-${index}`} className="flex items-center space-x-2 mb-2">
|
||||||
|
<TeacherItem key={teacher.id} teacher={teacher} isDraggable={false}/>
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveTeacher(teacher.id)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ClassesSection;
|
const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdit, handleDelete }) => {
|
||||||
|
const [formData, setFormData] = useState({});
|
||||||
|
const [editingClass, setEditingClass] = useState(null);
|
||||||
|
const [newClass, setNewClass] = useState(null);
|
||||||
|
const [localErrors, setLocalErrors] = useState({});
|
||||||
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
|
const [popupMessage, setPopupMessage] = useState("");
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const currentYear = currentDate.getFullYear();
|
||||||
|
const currentMonth = currentDate.getMonth() + 1; // Les mois sont indexés à partir de 0
|
||||||
|
|
||||||
|
// Si nous sommes avant septembre, l'année scolaire en cours a commencé l'année précédente
|
||||||
|
const startYear = currentMonth >= 9 ? currentYear : currentYear - 1;
|
||||||
|
|
||||||
|
const choices = [];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const year = startYear + i;
|
||||||
|
choices.push({ value: `${year}-${year + 1}`, label: `${year}-${year + 1}` });
|
||||||
|
}
|
||||||
|
return choices;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddClass = () => {
|
||||||
|
setNewClass({ id: Date.now(), atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] });
|
||||||
|
setFormData({ atmosphere_name: '', age_range: '', levels: [], number_of_students: '', school_year: '', teachers: [] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
if (editingClass) {
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
} else if (newClass) {
|
||||||
|
setNewClass((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewClass = () => {
|
||||||
|
if (newClass.atmosphere_name) {
|
||||||
|
handleCreate(newClass)
|
||||||
|
.then((createdClass) => {
|
||||||
|
setClasses((prevClasses) => [createdClass, ...classes]);
|
||||||
|
setNewClass(null);
|
||||||
|
setLocalErrors({});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPopupMessage("Tous les champs doivent être remplis et valides");
|
||||||
|
setPopupVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateClass = (id, updatedData) => {
|
||||||
|
if (!updatedData.atmosphere_name) {
|
||||||
|
setLocalErrors({ atmosphere_name: 'Le nom d\'ambiance est requis.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEdit(id, updatedData)
|
||||||
|
.then((updatedClass) => {
|
||||||
|
setClasses((prevClasses) => prevClasses.map((classe) => (classe.id === id ? updatedClass : classe)));
|
||||||
|
setEditingClass(null);
|
||||||
|
setFormData({});
|
||||||
|
setLocalErrors({});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTeachersChange = (selectedTeachers) => {
|
||||||
|
if (editingClass) {
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
teachers: selectedTeachers,
|
||||||
|
}));
|
||||||
|
} else if (newClass) {
|
||||||
|
setNewClass((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
teachers: selectedTeachers,
|
||||||
|
}));
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
teachers: selectedTeachers,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiSelectChange = (selectedOptions) => {
|
||||||
|
const levels = selectedOptions.map(option => option.id);
|
||||||
|
|
||||||
|
if (editingClass) {
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
levels,
|
||||||
|
}));
|
||||||
|
} else if (newClass) {
|
||||||
|
setNewClass((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
levels,
|
||||||
|
}));
|
||||||
|
setFormData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
levels,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderClassCell = (classe, column) => {
|
||||||
|
const isEditing = editingClass === classe.id;
|
||||||
|
const isCreating = newClass && newClass.id === classe.id;
|
||||||
|
const currentData = isEditing ? formData : newClass || {};
|
||||||
|
|
||||||
|
if (isEditing || isCreating) {
|
||||||
|
switch (column) {
|
||||||
|
case 'AMBIANCE':
|
||||||
|
return (
|
||||||
|
<InputText
|
||||||
|
name="atmosphere_name"
|
||||||
|
value={currentData.atmosphere_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Nom d'ambiance"
|
||||||
|
errorMsg={localErrors && localErrors.atmosphere_name && Array.isArray(localErrors.atmosphere_name) ? localErrors.atmosphere_name[0] : ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'TRANCHE D\'AGE':
|
||||||
|
return (
|
||||||
|
<InputText
|
||||||
|
name="age_range"
|
||||||
|
value={currentData.age_range}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Tranche d'âge (ex: 3-6)"
|
||||||
|
errorMsg={localErrors && localErrors.age_range && Array.isArray(localErrors.age_range) ? localErrors.age_range[0] : ''}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'NIVEAUX':
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
name="levels"
|
||||||
|
options={allNiveaux}
|
||||||
|
selectedOptions={currentData.levels ? currentData.levels.map(levelId => allNiveaux.find(level => level.id === levelId)) : []}
|
||||||
|
onChange={handleMultiSelectChange}
|
||||||
|
errorMsg={localErrors && localErrors.levels && Array.isArray(localErrors.levels) ? localErrors.levels[0] : ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'CAPACITE':
|
||||||
|
return (
|
||||||
|
<InputText
|
||||||
|
name="number_of_students"
|
||||||
|
type="number"
|
||||||
|
value={currentData.number_of_students}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Capacité"
|
||||||
|
errorMsg={localErrors && localErrors.number_of_students && Array.isArray(localErrors.number_of_students) ? localErrors.number_of_students[0] : ''}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'ANNÉE SCOLAIRE' :
|
||||||
|
return (
|
||||||
|
<SelectChoice
|
||||||
|
type="select"
|
||||||
|
name="school_year"
|
||||||
|
placeHolder="Sélectionnez une année scolaire"
|
||||||
|
choices={getSchoolYearChoices()}
|
||||||
|
callback={handleChange}
|
||||||
|
selected={currentData.school_year || ''}
|
||||||
|
errorMsg={localErrors && localErrors.school_year && Array.isArray(localErrors.school_year) ? localErrors.school_year[0] : ''}
|
||||||
|
IconItem={null}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'ENSEIGNANTS':
|
||||||
|
return (
|
||||||
|
<TeachersDropZone classe={currentData} handleTeachersChange={handleTeachersChange} teachers={teachers} isEditing={isEditing || isCreating} />
|
||||||
|
);
|
||||||
|
case 'ACTIONS':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => (isEditing ? handleUpdateClass(editingClass, formData) : handleSaveNewClass())}
|
||||||
|
className="text-green-500 hover:text-green-700"
|
||||||
|
>
|
||||||
|
<Check className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => (isEditing ? setEditingClass(null) : setNewClass(null))}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (column) {
|
||||||
|
case 'AMBIANCE':
|
||||||
|
return classe.atmosphere_name;
|
||||||
|
case 'TRANCHE D\'AGE':
|
||||||
|
return classe.age_range;
|
||||||
|
case 'NIVEAUX':
|
||||||
|
const levelLabels = Array.isArray(classe.levels) ? getNiveauxLabels(classe.levels) : [];
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-center items-center space-x-2">
|
||||||
|
{levelLabels.length > 0
|
||||||
|
? levelLabels.map((label, index) => (
|
||||||
|
<LevelLabel key={index} label={label} index={index} />
|
||||||
|
))
|
||||||
|
: 'Aucun niveau'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'CAPACITE':
|
||||||
|
return classe.number_of_students;
|
||||||
|
case 'ANNÉE SCOLAIRE' :
|
||||||
|
return classe.school_year;
|
||||||
|
case 'ENSEIGNANTS':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-2 flex-wrap">
|
||||||
|
{classe.teachers_details.map((teacher) => (
|
||||||
|
<TeacherItem key={teacher.id} teacher={teacher} isDraggable={false} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'MISE A JOUR':
|
||||||
|
return classe.updated_date_formatted;
|
||||||
|
case 'ACTIONS':
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditingClass(classe.id) || setFormData(classe)}
|
||||||
|
className="text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
<Edit3 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDelete(classe.id)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openEditModalDetails(classe)}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<ZoomIn className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'AMBIANCE', label: 'Nom d\'ambiance' },
|
||||||
|
{ name: 'TRANCHE D\'AGE', label: 'Tranche d\'âge' },
|
||||||
|
{ name: 'NIVEAUX', label: 'Niveaux' },
|
||||||
|
{ name: 'CAPACITE', label: 'Capacité max' },
|
||||||
|
{ name: 'ANNÉE SCOLAIRE', label: 'Année scolaire' },
|
||||||
|
{ name: 'ENSEIGNANTS', label: 'Enseignants' },
|
||||||
|
{ name: 'MISE A JOUR', label: 'Date mise à jour' },
|
||||||
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<Users className="w-6 h-6 text-emerald-500 mr-2" />
|
||||||
|
<h2 className="text-xl font-semibold">Classes</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={handleAddClass} className="text-emerald-500 hover:text-emerald-700">
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
data={newClass ? [newClass, ...classes] : classes}
|
||||||
|
columns={columns}
|
||||||
|
renderCell={renderClassCell}
|
||||||
|
/>
|
||||||
|
<Popup
|
||||||
|
visible={popupVisible}
|
||||||
|
message={popupMessage}
|
||||||
|
onConfirm={() => setPopupVisible(false)}
|
||||||
|
onCancel={() => setPopupVisible(false)}
|
||||||
|
uniqueConfirmButton={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClassesSection;
|
||||||
@ -5,6 +5,24 @@ const ItemTypes = {
|
|||||||
SPECIALITY: 'speciality',
|
SPECIALITY: 'speciality',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lightenColor = (color, percent) => {
|
||||||
|
const num = parseInt(color.slice(1), 16),
|
||||||
|
amt = Math.round(2.55 * percent),
|
||||||
|
R = (num >> 16) + amt,
|
||||||
|
G = (num >> 8 & 0x00FF) + amt,
|
||||||
|
B = (num & 0x0000FF) + amt;
|
||||||
|
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const darkenColor = (color, percent) => {
|
||||||
|
const num = parseInt(color.slice(1), 16),
|
||||||
|
amt = Math.round(2.55 * percent),
|
||||||
|
R = (num >> 16) - amt,
|
||||||
|
G = (num >> 8 & 0x00FF) - amt,
|
||||||
|
B = (num & 0x0000FF) - amt;
|
||||||
|
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
|
||||||
|
};
|
||||||
|
|
||||||
const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
type: ItemTypes.SPECIALITY,
|
type: ItemTypes.SPECIALITY,
|
||||||
@ -18,10 +36,15 @@ const SpecialityItem = ({ speciality, isDraggable = true }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={isDraggable ? drag : null}
|
ref={isDraggable ? drag : null}
|
||||||
className={`inline-block px-3 py-1 rounded-lg font-bold text-white text-center ${isDraggable ? 'cursor-pointer' : ''} transition-transform duration-200 ease-in-out ${
|
className={`inline-block px-3 py-1 rounded-lg font-bold text-white text-center transition-transform duration-200 ease-in-out ${
|
||||||
isDragging ? 'opacity-30' : 'opacity-100'
|
isDragging ? 'opacity-30' : 'opacity-100'
|
||||||
} ${isDraggable ? 'hover:shadow-lg hover:scale-125' : ''}`}
|
} ${isDraggable ? 'cursor-grabbing hover:shadow-lg hover:scale-105' : ''}`}
|
||||||
style={{ backgroundColor: speciality.color_code }}
|
style={{
|
||||||
|
backgroundColor: isDragging ? lightenColor(speciality.color_code, 30) : speciality.color_code,
|
||||||
|
border: `1px solid ${darkenColor(speciality.color_code, 20)}`,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{speciality.name}
|
{speciality.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { BE_SCHOOL_SPECIALITY_URL, BE_SCHOOL_TEACHER_URL, BE_SCHOOL_SCHOOLCLASS_
|
|||||||
|
|
||||||
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => {
|
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
|
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8">
|
||||||
<ClassesProvider>
|
<ClassesProvider>
|
||||||
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md">
|
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md">
|
||||||
<SpecialitiesSection
|
<SpecialitiesSection
|
||||||
@ -28,9 +28,10 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
|||||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)}
|
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md">
|
<div className="w-full p-4 bg-white rounded-lg shadow-md">
|
||||||
<ClassesSection
|
<ClassesSection
|
||||||
classes={classes}
|
classes={classes}
|
||||||
|
setClasses={setClasses}
|
||||||
teachers={teachers}
|
teachers={teachers}
|
||||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)}
|
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_SCHOOLCLASS_URL}`, newData, setClasses)}
|
||||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)}
|
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)}
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useDrag } from 'react-dnd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ItemTypes = {
|
||||||
|
TEACHER: 'teacher',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeacherItem = ({ teacher, isDraggable = true }) => {
|
||||||
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
|
type: ItemTypes.TEACHER,
|
||||||
|
item: { id: teacher.id, name: `${teacher.last_name} ${teacher.first_name}` },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: !!monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
canDrag: () => isDraggable,
|
||||||
|
}), [isDraggable]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isDraggable ? drag : null}
|
||||||
|
className={`inline-block px-3 py-1 rounded-lg font-bold text-white text-center transition-transform duration-200 ease-in-out ${
|
||||||
|
isDragging ? 'opacity-30' : 'opacity-100'
|
||||||
|
} ${isDraggable ? 'cursor-grabbing hover:shadow-lg hover:scale-105' : ''}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isDragging ? '#d1d5db' : isDraggable ? '#10b981' : '#a7f3d0', // Change background color based on dragging state and draggable state
|
||||||
|
border: isDraggable ? '1px solid #10b981' : '1px solid #a7f3d0', // Add a border
|
||||||
|
boxShadow: isDraggable ? '0 2px 4px rgba(0, 0, 0, 0.1)' : 'none' // Add a shadow if draggable
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{teacher.last_name} {teacher.first_name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeacherItem;
|
||||||
@ -9,6 +9,7 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/InputText';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
|
import TeacherItem from './TeacherItem';
|
||||||
|
|
||||||
const ItemTypes = {
|
const ItemTypes = {
|
||||||
SPECIALITY: 'speciality',
|
SPECIALITY: 'speciality',
|
||||||
@ -149,11 +150,11 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateTeacher = (id, updatedData) => {
|
const handleUpdateTeacher = (id, updatedData) => {
|
||||||
console.log('UpdatedData:', updatedData);
|
console.log(updatedData)
|
||||||
const data = {
|
const data = {
|
||||||
email: updatedData.email,
|
email: updatedData.email,
|
||||||
username: updatedData.email,
|
username: updatedData.email,
|
||||||
droit: updatedData.droit.id,
|
droit: updatedData.droit,
|
||||||
};
|
};
|
||||||
updateProfile(updatedData.associated_profile, data, csrfToken)
|
updateProfile(updatedData.associated_profile, data, csrfToken)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -171,9 +172,15 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
let parsedValue = value;
|
let parsedValue = value;
|
||||||
|
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
parsedValue = checked ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`handleChange - name: ${name}, parsedValue: ${parsedValue}, type: ${type}, checked: ${checked}`);
|
||||||
|
|
||||||
if (editingTeacher) {
|
if (editingTeacher) {
|
||||||
setFormData((prevData) => ({
|
setFormData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
@ -216,25 +223,26 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
|
|
||||||
if (isEditing || isCreating) {
|
if (isEditing || isCreating) {
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case 'NOM':
|
case 'NOM - PRENOM':
|
||||||
return (
|
return (
|
||||||
<InputText
|
<div className="flex justify-center space-x-2">
|
||||||
name="last_name"
|
<InputText
|
||||||
value={currentData.last_name}
|
name="last_name"
|
||||||
onChange={handleChange}
|
label="nom"
|
||||||
placeholder="Nom de l'enseignant"
|
value={currentData.last_name}
|
||||||
errorMsg={localErrors && localErrors.last_name && Array.isArray(localErrors.last_name) ? localErrors.last_name[0] : ''}
|
onChange={handleChange}
|
||||||
/>
|
placeholder="Nom de l'enseignant"
|
||||||
);
|
errorMsg={localErrors && localErrors.last_name && Array.isArray(localErrors.last_name) ? localErrors.last_name[0] : ''}
|
||||||
case 'PRENOM':
|
/>
|
||||||
return (
|
<InputText
|
||||||
<InputText
|
name="first_name"
|
||||||
name="first_name"
|
label="prénom"
|
||||||
value={currentData.first_name}
|
value={currentData.first_name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Prénom de l'enseignant"
|
placeholder="Prénom de l'enseignant"
|
||||||
errorMsg={localErrors && localErrors.first_name && Array.isArray(localErrors.firstname) ? localErrors.firstname[0] : ''}
|
errorMsg={localErrors && localErrors.first_name && Array.isArray(localErrors.firstname) ? localErrors.firstname[0] : ''}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
case 'EMAIL':
|
case 'EMAIL':
|
||||||
return (
|
return (
|
||||||
@ -250,14 +258,16 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
return (
|
return (
|
||||||
<SpecialitiesDropZone teacher={currentData} handleSpecialitiesChange={handleSpecialitiesChange} specialities={specialities} isEditing={isEditing || isCreating} />
|
<SpecialitiesDropZone teacher={currentData} handleSpecialitiesChange={handleSpecialitiesChange} specialities={specialities} isEditing={isEditing || isCreating} />
|
||||||
);
|
);
|
||||||
// case 'PROFIL':
|
case 'ADMINISTRATEUR':
|
||||||
// return (
|
return (
|
||||||
// <ToggleSwitch
|
<div className="flex justify-center">
|
||||||
// name="profile"
|
<ToggleSwitch
|
||||||
// checked={currentData.profile}
|
name="droit"
|
||||||
// onChange={handleChange}
|
checked={currentData.droit === 1}
|
||||||
// />
|
onChange={handleChange}
|
||||||
// );
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'ACTIONS':
|
case 'ACTIONS':
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
@ -282,10 +292,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case 'NOM':
|
case 'NOM - PRENOM':
|
||||||
return teacher.last_name;
|
return (
|
||||||
case 'PRENOM':
|
<TeacherItem key={teacher.id} teacher={teacher} />
|
||||||
return teacher.first_name;
|
);
|
||||||
case 'EMAIL':
|
case 'EMAIL':
|
||||||
return teacher.email;
|
return teacher.email;
|
||||||
case 'SPECIALITES':
|
case 'SPECIALITES':
|
||||||
@ -296,13 +306,14 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'PROFIL':
|
case 'ADMINISTRATEUR':
|
||||||
if (teacher.associated_profile) {
|
if (teacher.associated_profile) {
|
||||||
const badgeClass = teacher.droit.label === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600';
|
const badgeClass = teacher.droit === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600';
|
||||||
|
const label = teacher.droit === 1 ? 'OUI' : 'NON';
|
||||||
return (
|
return (
|
||||||
<div key={teacher.id} className="flex justify-center items-center space-x-2">
|
<div key={teacher.id} className="flex justify-center items-center space-x-2">
|
||||||
<span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>
|
<span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>
|
||||||
{teacher.droit.label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -337,11 +348,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'NOM', label: 'Nom' },
|
{ name: 'NOM - PRENOM', label: 'Nom et prénom' },
|
||||||
{ name: 'PRENOM', label: 'Prénom' },
|
|
||||||
{ name: 'EMAIL', label: 'Email' },
|
{ name: 'EMAIL', label: 'Email' },
|
||||||
{ name: 'SPECIALITES', label: 'Spécialités' },
|
{ name: 'SPECIALITES', label: 'Spécialités' },
|
||||||
{ name: 'PROFIL', label: 'Profil' },
|
{ name: 'ADMINISTRATEUR', label: 'Profil' },
|
||||||
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
{ name: 'MISE A JOUR', label: 'Mise à jour' },
|
||||||
{ name: 'ACTIONS', label: 'Actions' }
|
{ name: 'ACTIONS', label: 'Actions' }
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const ToggleSwitch = ({ label, checked, onChange }) => {
|
const ToggleSwitch = ({ name, label, checked, onChange }) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
@ -16,15 +16,15 @@ const ToggleSwitch = ({ label, checked, onChange }) => {
|
|||||||
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="toggle"
|
name={name}
|
||||||
id="toggle"
|
id={name}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0"
|
className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0"
|
||||||
ref={inputRef} // Reference to the input element
|
ref={inputRef} // Reference to the input element
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="toggle"
|
htmlFor={name}
|
||||||
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-emerald-300' : 'bg-gray-300'}`}
|
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-emerald-300' : 'bg-gray-300'}`}
|
||||||
></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user