Files
n3wt-school/Front-End/src/components/Structure/Configuration/ClassesSection.js

588 lines
18 KiB
JavaScript

import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import InputText from '@/components/Form/InputText';
import SelectChoice from '@/components/Form/SelectChoice';
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
import MultiSelect from '@/components/Form/MultiSelect';
import LevelLabel from '@/components/CustomLabels/LevelLabel';
import { DndProvider, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
import { usePlanning } from '@/context/PlanningContext';
import { useClasses } from '@/context/ClassesContext';
import { useRouter } from 'next/navigation';
import AlertMessage from '@/components/AlertMessage';
const ItemTypes = {
TEACHER: 'teacher',
};
const TeachersDropZone = ({
classe,
handleTeachersChange,
teachers,
isEditing,
}) => {
const [localTeachers, setLocalTeachers] = useState(
classe.teachers_details || []
);
useEffect(() => {}, [teachers]);
useEffect(() => {
setLocalTeachers(classe.teachers_details || []);
}, [classe.teachers_details]);
useEffect(() => {
handleTeachersChange(localTeachers.map((teacher) => teacher.id));
}, [localTeachers]);
const [{ isOver, canDrop }, drop] = useDrop({
accept: ItemTypes.TEACHER,
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 handleRemoveTeacher = (id) => {
setLocalTeachers((prevTeachers) => {
const updatedTeachers = prevTeachers.filter(
(teacher) => teacher.id !== id
);
return updatedTeachers;
});
};
return (
<div
ref={drop}
className={`p-2 rounded-md flex flex-col items-center ${isEditing ? 'border-2 border-dashed border-blue-500 bg-blue-50' : ''} ${
isOver && canDrop ? 'border-2 border-solid border-blue-300' : ''
}`}
>
{isEditing && (
<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 */}
<span>Déposez un enseignant ici</span>
</div>
)}
{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>
);
};
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 [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId, profileRole } = useEstablishment();
const { addSchedule, reloadPlanning, reloadEvents } = usePlanning();
const { getNiveauxLabels, allNiveaux } = useClasses();
const router = useRouter();
// 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;
};
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
};
const handleAddClass = () => {
setNewClass({
id: Date.now(),
atmosphere_name: '',
age_range: '',
levels: [],
number_of_students: null,
school_year: '',
teachers: [],
establishment: selectedEstablishmentId,
});
setFormData({
atmosphere_name: '',
age_range: '',
levels: [],
number_of_students: null,
school_year: '',
teachers: [],
establishment: selectedEstablishmentId,
});
};
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 &&
newClass.levels.length > 0 &&
newClass.school_year
) {
handleCreate(newClass)
.then((createdClass) => {
setClasses((prevClasses) => [createdClass, ...classes]);
setNewClass(null);
setLocalErrors({});
// Creation des plannings associé à la classe
createdClass.levels.forEach((level) => {
const levelName = allNiveaux.find((lvl) => lvl.id === level)?.name;
const planningName = `${createdClass.atmosphere_name} - ${levelName}`;
const newPlanning = {
name: planningName,
color: '#FF5733', // Couleur par défaut
school_class: createdClass.id,
};
addSchedule(newPlanning);
});
})
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {
setPopupMessage('Tous les champs doivent être remplis et valides');
setPopupVisible(true);
}
};
const handleUpdateClass = (id, updatedData) => {
if (
updatedData.atmosphere_name &&
updatedData.levels.length > 0 &&
updatedData.school_year
) {
handleEdit(id, updatedData)
.then((updatedClass) => {
setClasses((prevClasses) =>
prevClasses.map((classe) =>
classe.id === id ? updatedClass : classe
)
);
setEditingClass(null);
setFormData({});
setLocalErrors({});
})
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {
setPopupMessage('Tous les champs doivent être remplis et valides');
setPopupVisible(true);
}
};
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={getError('atmosphere_name')}
/>
);
case "TRANCHE D'AGE":
return (
<InputText
name="age_range"
value={currentData.age_range}
onChange={handleChange}
placeholder="Tranche d'âge (ex: 3-6)"
errorMsg={getError('age_range')}
/>
);
case 'NIVEAUX':
return (
<MultiSelect
name="levels"
label="Sélection de niveaux"
options={allNiveaux}
selectedOptions={
currentData.levels
? currentData.levels.map((levelId) =>
allNiveaux.find((level) => level.id === levelId)
)
: []
}
onChange={handleMultiSelectChange}
errorMsg={getError('levels')}
/>
);
case 'CAPACITE':
return (
<InputText
name="number_of_students"
type="number"
value={currentData.number_of_students}
onChange={handleChange}
placeholder="Capacité"
errorMsg={getError('number_of_students')}
/>
);
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={getError('school_year')}
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.sort((a, b) => a - b)) // Trier les niveaux par ordre croissant
: [];
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':
// Affichage des actions en mode affichage (hors édition/création)
if (profileRole === 0) {
// Si professeur, uniquement le bouton ZoomIn
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => {
const url = `${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${classe.id}`;
router.push(`${url}`);
}}
className="text-gray-500 hover:text-gray-700"
>
<ZoomIn className="w-5 h-5" />
</button>
</div>
);
}
// Sinon, toutes les actions (admin)
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={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
'Attention ! \nVous êtes sur le point de supprimer la classe ' +
classe.atmosphere_name +
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
);
setRemovePopupOnConfirm(() => () => {
handleDelete(classe.id)
.then((data) => {
logger.debug('Success:', data);
setPopupMessage(
'La classe ' +
classe.atmosphere_name +
' a été correctement supprimée'
);
setPopupVisible(true);
setRemovePopupVisible(false);
reloadPlanning();
reloadEvents();
})
.catch((error) => {
logger.error('Error archiving data:', error);
setPopupMessage(
'Erreur lors de la suppression de la classe ' +
classe.atmosphere_name
);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => {
const url = `${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${classe.id}`;
router.push(`${url}`);
}}
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">
<SectionHeader
icon={Users}
title="Liste des classes"
description="Gérez les classes de votre école"
button={profileRole !== 0}
onClick={handleAddClass}
/>
<Table
data={newClass ? [newClass, ...classes] : classes}
columns={columns}
renderCell={renderClassCell}
emptyMessage={
<AlertMessage
type="warning"
title="Aucune classe enregistrée"
message="Veuillez procéder à la création d'une nouvelle classe."
/>
}
/>
<Popup
isOpen={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
isOpen={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
</DndProvider>
);
};
export default ClassesSection;