Files
n3wt-school/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js

743 lines
28 KiB
JavaScript

'use client';
import React, { useState, useEffect } from 'react';
import { Users, Layers, CheckCircle, Clock, XCircle } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import { fetchClasse } from '@/app/actions/schoolAction';
import { useSearchParams } from 'next/navigation';
import logger from '@/utils/logger';
import { useClasses } from '@/context/ClassesContext';
import Button from '@/components/Button';
import SelectChoice from '@/components/SelectChoice';
import CheckBox from '@/components/CheckBox';
import {
fetchAbsences,
createAbsences,
editAbsences,
deleteAbsences,
} from '@/app/actions/subscriptionAction';
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
import { useNotification } from '@/context/NotificationContext';
export default function Page() {
const searchParams = useSearchParams();
const { showNotification } = useNotification();
const schoolClassId = searchParams.get('schoolClassId');
const [classe, setClasse] = useState([]);
const { getNiveauxLabels, getNiveauLabel } = useClasses();
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [selectedLevels, setSelectedLevels] = useState([]); // Par défaut, tous les niveaux sont sélectionnés
const [filteredStudents, setFilteredStudents] = useState([]);
const [isEditingAttendance, setIsEditingAttendance] = useState(false); // État pour le mode édition
const [attendance, setAttendance] = useState({}); // État pour les cases cochées
const [fetchedAbsences, setFetchedAbsences] = useState({}); // Absences récupérées depuis le backend
const [formAbsences, setFormAbsences] = useState({}); // Absences modifiées localement
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
// AbsenceMoment constants
const AbsenceMoment = {
MORNING: { value: 1, label: 'Matinée' },
AFTERNOON: { value: 2, label: 'Après-midi' },
TOTAL: { value: 3, label: 'Journée' },
};
// AbsenceReason constants
const AbsenceReason = {
JUSTIFIED_ABSENCE: { value: 1, label: 'Absence justifiée' },
UNJUSTIFIED_ABSENCE: { value: 2, label: 'Absence non justifiée' },
JUSTIFIED_LATE: { value: 3, label: 'Retard justifié' },
UNJUSTIFIED_LATE: { value: 4, label: 'Retard non justifié' },
};
useEffect(() => {
// Récupérer les données de la classe et initialiser les élèves filtrés
if (schoolClassId) {
fetchClasse(schoolClassId)
.then((classeData) => {
logger.debug('Classes récupérées :', classeData);
setClasse(classeData);
setFilteredStudents(classeData.students); // Initialiser les élèves filtrés
setSelectedLevels(getNiveauxLabels(classeData.levels)); // Initialiser les niveaux sélectionnés
})
.catch(requestErrorHandler);
}
}, [schoolClassId]);
useEffect(() => {
// Récupérer les absences pour l'établissement sélectionné
if (selectedEstablishmentId) {
fetchAbsences(selectedEstablishmentId)
.then((data) => {
const absencesById = data.reduce((acc, absence) => {
acc[absence.student] = absence;
return acc;
}, {});
setFetchedAbsences(absencesById);
})
.catch((error) =>
logger.error('Erreur lors de la récupération des absences :', error)
);
}
}, [selectedEstablishmentId]);
useEffect(() => {
// Filtrer les élèves en fonction des niveaux sélectionnés
if (classe && selectedLevels.length > 0) {
const filtered = classe.students.filter((student) =>
selectedLevels.includes(getNiveauLabel(student.level))
);
setFilteredStudents(filtered);
} else {
setFilteredStudents([]); // Aucun élève si aucun niveau n'est sélectionné
}
}, [selectedLevels, classe]);
useEffect(() => {
// Initialiser `attendance` et `formAbsences` en fonction des élèves filtrés et des absences
if (filteredStudents.length > 0 && fetchedAbsences) {
const today = new Date().toISOString().split('T')[0];
const initialAttendance = {};
const initialFormAbsences = {};
filteredStudents.forEach((student) => {
const existingAbsence =
fetchedAbsences[student.id] &&
fetchedAbsences[student.id].day === today
? fetchedAbsences[student.id]
: null;
if (existingAbsence) {
// Si une absence existe pour aujourd'hui, décocher la case et pré-remplir les champs
// Conversion reason -> type/justified
let type = '';
let justified = false;
switch (existingAbsence.reason) {
case AbsenceReason.JUSTIFIED_ABSENCE.value:
type = 'absence';
justified = true;
break;
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
type = 'absence';
justified = false;
break;
case AbsenceReason.JUSTIFIED_LATE.value:
type = 'retard';
justified = true;
break;
case AbsenceReason.UNJUSTIFIED_LATE.value:
type = 'retard';
justified = false;
break;
default:
type = '';
justified = false;
}
initialAttendance[student.id] = false;
initialFormAbsences[student.id] = {
...existingAbsence,
type,
justified,
};
} else {
// Sinon, cocher la case par défaut
initialAttendance[student.id] = true;
}
});
setAttendance(initialAttendance);
setFormAbsences(initialFormAbsences);
}
}, [filteredStudents, fetchedAbsences]);
const handleLevelClick = (label) => {
setSelectedLevels(
(prev) =>
prev.includes(label)
? prev.filter((level) => level !== label) // Retirer le niveau si déjà sélectionné
: [...prev, label] // Ajouter le niveau si non sélectionné
);
};
const handleToggleAttendanceMode = () => {
setIsEditingAttendance((prev) => !prev); // Basculer entre mode édition et visualisation
};
const handleValidateAttendance = () => {
let hasError = false;
// Pour chaque élève filtré (présents et absents)
filteredStudents.forEach((student) => {
const studentId = student.id;
const isPresent = attendance[studentId];
const existingAbsence = fetchedAbsences[studentId];
if (isPresent) {
// Si l'élève est présent et qu'une absence existe, la supprimer
if (existingAbsence) {
deleteAbsences(existingAbsence.id, csrfToken)
.then(() => {
logger.debug(
`Absence pour l'élève ${studentId} supprimée (présent).`
);
setFetchedAbsences((prev) => {
const updatedAbsences = { ...prev };
delete updatedAbsences[studentId];
return updatedAbsences;
});
setFormAbsences((prev) => {
const updatedAbsences = { ...prev };
delete updatedAbsences[studentId];
return updatedAbsences;
});
})
.catch((error) => {
logger.error(
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}:`,
error
);
showNotification(
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}`,
'error',
'Erreur'
);
});
}
// Si tu veux garder une trace de la présence, tu peux ici appeler une API ou enregistrer un "Présent"
} else {
// Si l'élève est absent, créer ou modifier l'absence
const absenceData = formAbsences[studentId];
if (!absenceData || !absenceData.type || !absenceData.moment) {
logger.error(
`Tous les champs requis doivent être fournis pour l'élève ${studentId}.`
);
showNotification(
`Tous les champs requis doivent être fournis pour l'élève ${studentId}.`,
'error',
'Erreur'
);
hasError = true;
// On ne fait pas de return ici, on continue la boucle pour les autres élèves
} else {
saveAbsence(studentId, absenceData);
}
}
});
// On ne quitte le mode édition que s'il n'y a pas d'erreur
if (!hasError) {
setIsEditingAttendance(false);
}
};
const handleAttendanceChange = (studentId) => {
const today = new Date().toISOString().split('T')[0]; // Obtenir la date actuelle au format YYYY-MM-DD
setAttendance((prev) => {
const updatedAttendance = {
...prev,
[studentId]: !prev[studentId], // Inverser l'état de présence
};
// Si l'élève est décoché (absent)
if (!updatedAttendance[studentId]) {
// Vérifier s'il existe une absence pour le jour actuel
const existingAbsence = Object.values(fetchedAbsences).find(
(absence) => absence.student === studentId && absence.day === today
);
if (existingAbsence) {
// Afficher l'absence existante pour le jour actuel
setFormAbsences((prev) => ({
...prev,
[studentId]: {
...existingAbsence,
},
}));
} else {
// Initialiser des champs vides pour créer une nouvelle absence
setFormAbsences((prev) => ({
...prev,
[studentId]: {
day: today,
reason: null,
moment: null,
},
}));
}
} else {
// Si l'élève est recoché (présent), supprimer l'absence existante
const existingAbsence = Object.values(fetchedAbsences).find(
(absence) => absence.student === studentId && absence.day === today
);
if (existingAbsence) {
// Appeler la fonction pour supprimer l'absence
deleteAbsences(existingAbsence.id, csrfToken)
.then(() => {
logger.debug(
`Absence pour l'élève ${studentId} supprimée avec succès.`
);
// Mettre à jour les absences récupérées
setFetchedAbsences((prev) => {
const updatedAbsences = { ...prev };
delete updatedAbsences[studentId];
return updatedAbsences;
});
})
.catch((error) => {
logger.error(
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}:`,
error
);
});
}
// Supprimer les données d'absence dans `formAbsences`
setFormAbsences((prev) => {
const updatedAbsences = { ...prev };
delete updatedAbsences[studentId];
return updatedAbsences;
});
}
return updatedAttendance;
});
};
const getAbsenceReason = (type, justified) => {
if (type === 'absence') {
return justified
? AbsenceReason.JUSTIFIED_ABSENCE.value
: AbsenceReason.UNJUSTIFIED_ABSENCE.value;
} else if (type === 'retard') {
return justified
? AbsenceReason.JUSTIFIED_LATE.value
: AbsenceReason.UNJUSTIFIED_LATE.value;
}
return null;
};
const saveAbsence = (studentId, absenceData) => {
if (!absenceData.type || !studentId || !absenceData.moment) {
logger.error('Tous les champs requis doivent être fournis.');
showNotification(
'Tous les champs requis doivent être fournis.',
'error',
'Erreur'
);
return;
}
const reason = getAbsenceReason(absenceData.type, absenceData.justified);
const payload = {
student: studentId,
day: absenceData.day,
reason: reason,
moment: absenceData.moment,
establishment: selectedEstablishmentId,
commentaire: absenceData.commentaire,
};
if (absenceData.id) {
// Modifier une absence existante
editAbsences(absenceData.id, payload, csrfToken)
.then(() => {
logger.debug(
`Absence pour l'élève ${studentId} modifiée avec succès.`
);
showNotification(
'Opération effectuée avec succès.',
'success',
'Succès'
);
// Mettre à jour fetchedAbsences et formAbsences localement
setFetchedAbsences((prev) => ({
...prev,
[studentId]: { ...prev[studentId], ...payload },
}));
setFormAbsences((prev) => ({
...prev,
[studentId]: { ...prev[studentId], ...payload },
}));
})
.catch((error) => {
logger.error(
`Erreur lors de la modification de l'absence pour l'élève ${studentId}:`,
error
);
});
} else {
// Créer une nouvelle absence
createAbsences(payload, csrfToken)
.then((response) => {
logger.debug(`Absence pour l'élève ${studentId} créée avec succès.`);
showNotification(
'Opération effectuée avec succès.',
'success',
'Succès'
);
// Mettre à jour fetchedAbsences et formAbsences localement
setFetchedAbsences((prev) => ({
...prev,
[studentId]: { id: response.id, ...payload },
}));
setFormAbsences((prev) => ({
...prev,
[studentId]: { id: response.id, ...payload },
}));
})
.catch((error) => {
logger.error(
`Erreur lors de la création de l'absence pour l'élève ${studentId}:`,
error
);
});
}
};
const requestErrorHandler = (err) => {
logger.error('Error fetching data:', err);
};
const today = new Date().toISOString().split('T')[0]; // Obtenez la date actuelle au format YYYY-MM-DD
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">{classe?.atmosphere_name}</h1>
{/* Section Niveaux et Enseignants */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Section Niveaux */}
<div className="bg-white p-4 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 flex items-center">
<Layers className="w-6 h-6 mr-2" />
Niveaux
</h2>
<p className="text-sm text-gray-500 mb-4">
Filtrer les élèves par niveau
</p>
<div className="flex flex-wrap gap-2">
{classe?.levels?.length > 0 ? (
getNiveauxLabels(classe.levels).map((label, index) => (
<span
key={index}
onClick={() => handleLevelClick(label)} // Gérer le clic sur un niveau
className={`px-4 py-2 rounded-full cursor-pointer border transition-all duration-200 ${
selectedLevels.includes(label)
? 'bg-emerald-200 text-emerald-800 border-emerald-300 shadow-md'
: 'bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300'
}`}
>
{selectedLevels.includes(label) ? (
<span className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-emerald-600" />
{label}
</span>
) : (
label
)}
</span>
))
) : (
<span className="text-gray-500">Aucun niveau associé</span>
)}
</div>
</div>
{/* Section Enseignants */}
<div className="bg-white p-4 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4 flex items-center">
<Users className="w-6 h-6 mr-2" />
Enseignants
</h2>
<p className="text-sm text-gray-500 mb-4">Liste des enseignants</p>
<div className="flex flex-wrap gap-2">
{classe?.teachers_details?.map((teacher) => (
<span
key={teacher.id}
className="px-3 py-1 bg-emerald-200 rounded-full text-emerald-800"
>
{teacher.last_name} {teacher.first_name}
</span>
))}
</div>
</div>
</div>
{/* Affichage de la date du jour */}
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-lg shadow-md">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-emerald-100 text-emerald-600 rounded-full">
<Clock className="w-6 h-6" />
</div>
<h2 className="text-lg font-semibold text-gray-800">
Appel du jour :{' '}
<span className="ml-2 text-emerald-600">{today}</span>
</h2>
</div>
<div className="flex items-center">
{!isEditingAttendance ? (
<Button
text="Faire l'appel"
onClick={handleToggleAttendanceMode}
primary
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
/>
) : (
<Button
text="Valider l'appel"
onClick={handleValidateAttendance}
primary
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
/>
)}
</div>
</div>
<Table
columns={[
{
name: 'Nom',
transform: (row) => (
<div className="text-center">{row.last_name}</div>
),
},
{
name: 'Prénom',
transform: (row) => (
<div className="text-center">{row.first_name}</div>
),
},
{
name: 'Niveau',
transform: (row) => (
<div className="text-center">{getNiveauLabel(row.level)}</div>
),
},
...(isEditingAttendance
? [
{
name: "Gestion de l'appel",
transform: (row) => (
<div className="flex flex-col gap-2 items-center">
{/* Présence */}
<div className="flex items-center gap-2">
{attendance[row.id] ? (
<>
<CheckBox
item={{ id: row.id }}
formData={{
attendance: attendance[row.id] ? [row.id] : [],
}}
handleChange={() =>
handleAttendanceChange(row.id)
}
fieldName="attendance"
/>
<span className="text-sm font-medium text-gray-700">
Présent
</span>
</>
) : (
<>
{/* Icône croix pour remettre l'élève en présent */}
<button
type="button"
onClick={() => handleAttendanceChange(row.id)}
className="text-red-500 hover:text-red-700 transition"
title="Annuler l'absence"
>
<XCircle className="w-6 h-6" />
</button>
<span className="text-sm font-medium text-red-600">
Effacer l&apos;absence
</span>
</>
)}
</div>
{/* Détails absence/retard */}
{!attendance[row.id] && (
<div className="w-full bg-emerald-50 border border-emerald-100 rounded-lg p-3 mt-2 shadow-sm">
<div className="flex items-center gap-2 mb-2">
<Clock className="w-4 h-4 text-emerald-500" />
<span className="font-semibold text-emerald-700 text-sm">
Motif d&apos;absence
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
{/* Select Absence/Retard */}
<SelectChoice
name={`type-${row.id}`}
label=""
placeHolder="Type"
selected={formAbsences[row.id]?.type || ''}
callback={(e) =>
setFormAbsences((prev) => ({
...prev,
[row.id]: {
...prev[row.id],
type: e.target.value,
},
}))
}
choices={[
{ value: 'absence', label: 'Absence' },
{ value: 'retard', label: 'Retard' },
]}
/>
{/* Select Moment */}
<SelectChoice
name={`moment-${row.id}`}
label=""
placeHolder="Durée"
selected={formAbsences[row.id]?.moment || ''}
callback={(e) =>
setFormAbsences((prev) => ({
...prev,
[row.id]: {
...prev[row.id],
moment: parseInt(e.target.value, 10),
},
}))
}
choices={Object.values(AbsenceMoment).map(
(moment) => ({
value: moment.value,
label: moment.label,
})
)}
/>
{/* Nouveau champ commentaire */}
<input
type="text"
className="border rounded px-2 py-1 text-sm w-full"
placeholder="Commentaire"
value={formAbsences[row.id]?.commentaire || ''}
onChange={(e) =>
setFormAbsences((prev) => ({
...prev,
[row.id]: {
...prev[row.id],
commentaire: e.target.value,
},
}))
}
/>
{/* Checkbox Justifié */}
<div className="flex items-center gap-2 justify-center">
<CheckBox
item={{ id: `justified-${row.id}` }}
formData={{
justified: !!formAbsences[row.id]?.justified,
}}
handleChange={() =>
setFormAbsences((prev) => ({
...prev,
[row.id]: {
...prev[row.id],
justified: !prev[row.id]?.justified,
},
}))
}
fieldName="justified"
itemLabelFunc={() => 'Justifié'}
/>
</div>
</div>
</div>
)}
</div>
),
},
]
: [
{
name: 'Statut',
transform: (row) => {
const today = new Date().toISOString().split('T')[0];
const absence =
formAbsences[row.id] ||
Object.values(fetchedAbsences).find(
(absence) =>
absence.student === row.id && absence.day === today
);
if (!absence) {
return (
<div className="text-center text-green-500 flex justify-center items-center gap-2">
<CheckCircle className="w-5 h-5" />
Présent
</div>
);
}
switch (absence.reason) {
case AbsenceReason.JUSTIFIED_LATE.value:
return (
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
<Clock className="w-5 h-5" />
Retard justifié
</div>
);
case AbsenceReason.UNJUSTIFIED_LATE.value:
return (
<div className="text-center text-red-500 flex justify-center items-center gap-2">
<Clock className="w-5 h-5" />
Retard non justifié
</div>
);
case AbsenceReason.JUSTIFIED_ABSENCE.value:
return (
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
<CheckCircle className="w-5 h-5" />
Absence justifiée
</div>
);
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
return (
<div className="text-center text-red-500 flex justify-center items-center gap-2">
<CheckCircle className="w-5 h-5" />
Absence non justifiée
</div>
);
default:
return (
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
<CheckCircle className="w-5 h-5" />
Statut inconnu
</div>
);
}
},
},
]),
]}
data={filteredStudents} // Utiliser les élèves filtrés
/>
{/* Popup */}
<Popup
isOpen={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</div>
);
}