From 98763dc90a3f717e837b7221285765a196315e54 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 25 May 2025 19:18:17 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20c=C3=A2blage=20des=20absences/retard?= =?UTF-8?q?=20dans=20le=20suivi=20p=C3=A9dagogique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Subscriptions/models.py | 8 +- .../src/app/[locale]/admin/grades/page.js | 79 +++++++++++- .../structure/SchoolClassManagement/page.js | 20 ++- Front-End/src/components/Grades/Attendance.js | 117 ++++++++++++++---- 4 files changed, 193 insertions(+), 31 deletions(-) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 4699d00..1c73588 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -418,7 +418,7 @@ class AbsenceReason(models.IntegerChoices): UNJUSTIFIED_LATE = 4, 'Unjustified Late' class AbsenceManagement(models.Model): - day = models.DateField() + day = models.DateField(blank=True, null=True) moment = models.IntegerField( choices=AbsenceMoment.choices, default=AbsenceMoment.TOTAL @@ -430,9 +430,11 @@ class AbsenceManagement(models.Model): student = models.ForeignKey( Student, 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): return f"{self.student} - {self.day} - {self.get_moment_display()} - {self.get_reason_display()}" \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js index edba3b3..3170d8b 100644 --- a/Front-End/src/app/[locale]/admin/grades/page.js +++ b/Front-End/src/app/[locale]/admin/grades/page.js @@ -20,6 +20,9 @@ import { fetchStudents, fetchStudentCompetencies, searchStudents, + fetchAbsences, + editAbsences, + deleteAbsences, } from '@/app/actions/subscriptionAction'; import { useEstablishment } from '@/context/EstablishmentContext'; import { useClasses } from '@/context/ClassesContext'; @@ -28,9 +31,11 @@ import SectionHeader from '@/components/SectionHeader'; import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; import InputText from '@/components/InputText'; import dayjs from 'dayjs'; +import { useCsrfToken } from '@/context/CsrfContext'; export default function Page() { const router = useRouter(); + const csrfToken = useCsrfToken(); const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment(); const { getNiveauLabel } = useClasses(); @@ -43,6 +48,7 @@ export default function Page() { const [grades, setGrades] = useState({}); const [searchTerm, setSearchTerm] = useState(''); const [selectedPeriod, setSelectedPeriod] = useState(null); + const [allAbsences, setAllAbsences] = useState([]); // Définir les périodes selon la fréquence 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 = [ { date: '2023-09-10', @@ -196,6 +197,32 @@ export default function Page() { } }, [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 function getPeriodString(selectedPeriod, frequency) { 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 ''; } + // 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 (
- +
diff --git a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js index 36315bc..01fe220 100644 --- a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js +++ b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js @@ -345,6 +345,7 @@ export default function Page() { reason: reason, moment: absenceData.moment, establishment: selectedEstablishmentId, + commentaire: absenceData.commentaire, }; if (absenceData.id) { @@ -574,7 +575,7 @@ export default function Page() { Motif d'absence
-
+
{/* Select Absence/Retard */} + {/* Nouveau champ commentaire */} + + setFormAbsences((prev) => ({ + ...prev, + [row.id]: { + ...prev[row.id], + commentaire: e.target.value, + }, + })) + } + /> + {/* Checkbox Justifié */}
{}); + + const { showNotification } = useNotification(); -export default function Attendance({ absences }) { return (

Présence et assiduité

-
    - {absences.map((absence, idx) => ( -
  1. -
    - -
    - {absence.type} - - {absence.justified ? 'Justifiée' : 'Non justifiée'} - -
    -
    {absence.reason}
    -
  2. - ))} -
+ {absences.length === 0 ? ( +
+ Aucune absence enregistrée 🎉 +
+ ) : ( +
    + {absences.map((absence, idx) => ( +
  1. +
    +
    + {/* Infos principales à gauche */} +
    + +
    + {absence.type} + + {absence.justified ? 'Justifiée' : 'Non justifiée'} + +
    +
    + {absence.commentaire} +
    +
    + {/* Actions à droite */} +
    + onToggleJustify(absence)} + label="Justifiée" + /> +
    +
    +
  2. + ))} +
+ )} + setRemovePopupVisible(false)} + />
); }