chore: câblage des absences/retard dans le suivi pédagogique

This commit is contained in:
N3WT DE COMPET
2025-05-25 19:18:17 +02:00
parent fd6348fd6b
commit 98763dc90a
4 changed files with 193 additions and 31 deletions

View File

@ -418,7 +418,7 @@ class AbsenceReason(models.IntegerChoices):
UNJUSTIFIED_LATE = 4, 'Unjustified Late' UNJUSTIFIED_LATE = 4, 'Unjustified Late'
class AbsenceManagement(models.Model): class AbsenceManagement(models.Model):
day = models.DateField() day = models.DateField(blank=True, null=True)
moment = models.IntegerField( moment = models.IntegerField(
choices=AbsenceMoment.choices, choices=AbsenceMoment.choices,
default=AbsenceMoment.TOTAL default=AbsenceMoment.TOTAL
@ -430,9 +430,11 @@ class AbsenceManagement(models.Model):
student = models.ForeignKey( student = models.ForeignKey(
Student, Student,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='absences' related_name='absences',
blank=True, null=True
) )
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='absences') establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='absences', blank=True, null=True)
commentaire = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
return f"{self.student} - {self.day} - {self.get_moment_display()} - {self.get_reason_display()}" return f"{self.student} - {self.day} - {self.get_moment_display()} - {self.get_reason_display()}"

View File

@ -20,6 +20,9 @@ import {
fetchStudents, fetchStudents,
fetchStudentCompetencies, fetchStudentCompetencies,
searchStudents, searchStudents,
fetchAbsences,
editAbsences,
deleteAbsences,
} from '@/app/actions/subscriptionAction'; } from '@/app/actions/subscriptionAction';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext'; import { useClasses } from '@/context/ClassesContext';
@ -28,9 +31,11 @@ import SectionHeader from '@/components/SectionHeader';
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
import InputText from '@/components/InputText'; import InputText from '@/components/InputText';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCsrfToken } from '@/context/CsrfContext';
export default function Page() { export default function Page() {
const router = useRouter(); const router = useRouter();
const csrfToken = useCsrfToken();
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
useEstablishment(); useEstablishment();
const { getNiveauLabel } = useClasses(); const { getNiveauLabel } = useClasses();
@ -43,6 +48,7 @@ export default function Page() {
const [grades, setGrades] = useState({}); const [grades, setGrades] = useState({});
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedPeriod, setSelectedPeriod] = useState(null); const [selectedPeriod, setSelectedPeriod] = useState(null);
const [allAbsences, setAllAbsences] = useState([]);
// Définir les périodes selon la fréquence // Définir les périodes selon la fréquence
const getPeriods = () => { const getPeriods = () => {
@ -99,11 +105,6 @@ export default function Page() {
}, },
]; ];
const absences = [
{ date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
{ date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
];
const remarks = [ const remarks = [
{ {
date: '2023-09-10', date: '2023-09-10',
@ -196,6 +197,32 @@ export default function Page() {
} }
}, [formData.selectedStudent, selectedPeriod]); }, [formData.selectedStudent, selectedPeriod]);
useEffect(() => {
if (selectedEstablishmentId) {
fetchAbsences(selectedEstablishmentId)
.then((data) => setAllAbsences(data))
.catch((error) =>
logger.error('Erreur lors du fetch des absences:', error)
);
}
}, [selectedEstablishmentId]);
// Transforme les absences backend pour l'élève sélectionné
const absences = React.useMemo(() => {
if (!formData.selectedStudent) return [];
return allAbsences
.filter((a) => a.student === formData.selectedStudent)
.map((a) => ({
id: a.id,
date: a.day,
type: [2, 1].includes(a.reason) ? 'Absence' : 'Retard',
reason: a.reason, // tu peux mapper le code vers un label si besoin
justified: [1, 3].includes(a.reason), // 1 et 3 = justifié
moment: a.moment,
commentaire: a.commentaire,
}));
}, [allAbsences, formData.selectedStudent]);
// Fonction utilitaire pour convertir la période sélectionnée en string backend // Fonction utilitaire pour convertir la période sélectionnée en string backend
function getPeriodString(selectedPeriod, frequency) { function getPeriodString(selectedPeriod, frequency) {
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre
@ -207,6 +234,42 @@ export default function Page() {
return ''; return '';
} }
// Callback pour justifier/non justifier une absence
const handleToggleJustify = (absence) => {
// Inverser l'état justifié (1/3 = justifié, 2/4 = non justifié)
const newReason =
absence.type === 'Absence'
? absence.justified
? 2 // Absence non justifiée
: 1 // Absence justifiée
: absence.justified
? 4 // Retard non justifié
: 3; // Retard justifié
editAbsences(absence.id, { ...absence, reason: newReason }, csrfToken)
.then(() => {
setAllAbsences((prev) =>
prev.map((a) =>
a.id === absence.id ? { ...a, reason: newReason } : a
)
);
})
.catch((e) => {
logger.error('Erreur lors du changement de justification', e);
});
};
// Callback pour supprimer une absence
const handleDeleteAbsence = (absence) => {
return deleteAbsences(absence.id, csrfToken)
.then(() => {
setAllAbsences((prev) => prev.filter((a) => a.id !== absence.id));
})
.catch((e) => {
logger.error("Erreur lors de la suppression de l'absence", e);
});
};
return ( return (
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
<SectionHeader <SectionHeader
@ -392,7 +455,11 @@ export default function Page() {
<div className="flex flex-col gap-8 w-full justify-center items-stretch"> <div className="flex flex-col gap-8 w-full justify-center items-stretch">
<div className="w-full flex flex-row items-stretch gap-4"> <div className="w-full flex flex-row items-stretch gap-4">
<div className="flex-1 flex items-stretch justify-center h-full"> <div className="flex-1 flex items-stretch justify-center h-full">
<Attendance absences={absences} /> <Attendance
absences={absences}
onToggleJustify={handleToggleJustify}
onDelete={handleDeleteAbsence}
/>
</div> </div>
<div className="flex-1 flex items-stretch justify-center h-full"> <div className="flex-1 flex items-stretch justify-center h-full">
<GradesStatsCircle grades={grades} /> <GradesStatsCircle grades={grades} />

View File

@ -345,6 +345,7 @@ export default function Page() {
reason: reason, reason: reason,
moment: absenceData.moment, moment: absenceData.moment,
establishment: selectedEstablishmentId, establishment: selectedEstablishmentId,
commentaire: absenceData.commentaire,
}; };
if (absenceData.id) { if (absenceData.id) {
@ -574,7 +575,7 @@ export default function Page() {
Motif d&apos;absence Motif d&apos;absence
</span> </span>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 items-center"> <div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
{/* Select Absence/Retard */} {/* Select Absence/Retard */}
<SelectChoice <SelectChoice
name={`type-${row.id}`} name={`type-${row.id}`}
@ -619,6 +620,23 @@ export default function Page() {
)} )}
/> />
{/* 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é */} {/* Checkbox Justifié */}
<div className="flex items-center gap-2 justify-center"> <div className="flex items-center gap-2 justify-center">
<CheckBox <CheckBox

View File

@ -1,28 +1,103 @@
import React from 'react'; import React, { useState } from 'react';
import { Trash2 } from 'lucide-react';
import ToggleSwitch from '@/components/ToggleSwitch';
import Button from '@/components/Button';
import Popup from '@/components/Popup';
import { useNotification } from '@/context/NotificationContext';
export default function Attendance({ absences, onToggleJustify, onDelete }) {
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { showNotification } = useNotification();
export default function Attendance({ absences }) {
return ( return (
<div className="w-full bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200"> <div className="w-full bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2> <h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2>
<ol className="relative border-l border-emerald-200"> {absences.length === 0 ? (
{absences.map((absence, idx) => ( <div className="text-center text-emerald-600 font-medium py-8">
<li key={idx} className="mb-6 ml-4"> Aucune absence enregistrée 🎉
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" /> </div>
<time className="mb-1 text-xs font-normal leading-none text-gray-400"> ) : (
{absence.date} <ol className="relative border-l border-emerald-200">
</time> {absences.map((absence, idx) => (
<div className="flex items-center gap-2"> <li key={idx} className="mb-6 ml-4">
<span className="font-medium">{absence.type}</span> <div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
<span <div className="flex items-center justify-between gap-4">
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`} {/* Infos principales à gauche */}
> <div className="flex flex-col">
{absence.justified ? 'Justifiée' : 'Non justifiée'} <time className="mb-1 text-xs font-normal leading-none text-gray-400">
</span> {absence.date}
</div> </time>
<div className="text-sm text-gray-500">{absence.reason}</div> <div className="flex items-center gap-2">
</li> <span className="font-medium">{absence.type}</span>
))} <span
</ol> className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
>
{absence.justified ? 'Justifiée' : 'Non justifiée'}
</span>
</div>
<div className="text-sm text-gray-500">
{absence.commentaire}
</div>
</div>
{/* Actions à droite */}
<div className="flex flex-col items-end gap-2 min-w-[110px]">
<ToggleSwitch
name={`justified-${idx}`}
checked={absence.justified}
onChange={() => onToggleJustify(absence)}
label="Justifiée"
/>
<Button
primary
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
"Attention ! \nVous êtes sur le point de supprimer l'absence enregistrée.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
);
setRemovePopupOnConfirm(() => () => {
onDelete(absence)
.then((data) => {
showNotification(
'Opération effectuée avec succès.',
'success',
'Succès'
);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
showNotification(
'Erreur lors de la suppression de l\absence',
'error',
'Erreur'
);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
icon={<Trash2 className="w-6 h-6" />}
text="Supprimer"
title="Evaluez l'élève"
/>
</div>
</div>
</li>
))}
</ol>
)}
<Popup
isOpen={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div> </div>
); );
} }