diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 836e436..0ecfeb7 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -130,34 +130,4 @@ class PaymentMode(models.Model): establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='payment_modes') def __str__(self): - return f"{self.get_mode_display()} - {self.get_type_display()}" - -class AbsenceMoment(models.IntegerChoices): - MORNING = 1, 'Morning' - AFTERNOON = 2, 'Afternoon' - TOTAL = 3, 'Total' - -class AbsenceReason(models.IntegerChoices): - JUSTIFIED_ABSENCE = 1, 'Justified Absence' - UNJUSTIFIED_ABSENCE = 2, 'Unjustified Absence' - JUSTIFIED_LATE = 3, 'Justified Late' - UNJUSTIFIED_LATE = 4, 'Unjustified Late' - -class AbsenceManagement(models.Model): - day = models.DateField() - moment = models.IntegerField( - choices=AbsenceMoment.choices, - default=AbsenceMoment.TOTAL - ) - reason = models.IntegerField( - choices=AbsenceReason.choices, - default=AbsenceReason.UNJUSTIFIED_ABSENCE - ) - student = models.ForeignKey( - 'Subscriptions.Student', - on_delete=models.CASCADE, - related_name='absences' - ) - - def __str__(self): - return f"{self.student} - {self.day} - {self.get_moment_display()} - {self.get_reason_display()}" \ No newline at end of file + return f"{self.get_mode_display()} - {self.get_type_display()}" \ No newline at end of file diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 371f2a9..b348ec2 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode, AbsenceManagement +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode from Auth.models import Profile, ProfileRole from Subscriptions.models import Student from Establishment.models import Establishment @@ -8,11 +8,6 @@ from N3wtSchool import settings, bdd from django.utils import timezone import pytz -class AbsenceManagementSerializer(serializers.ModelSerializer): - class Meta: - model = AbsenceManagement - fields = '__all__' - class SpecialitySerializer(serializers.ModelSerializer): updated_date_formatted = serializers.SerializerMethodField() diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index f383156..ab08dad 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -17,8 +17,6 @@ from .views import ( PaymentPlanDetailView, PaymentModeListCreateView, PaymentModeDetailView, - AbsenceManagementListCreateView, - AbsenceManagementDetailView ) urlpatterns = [ @@ -45,7 +43,4 @@ urlpatterns = [ re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"), re_path(r'^paymentModes/(?P[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"), - - re_path(r'^absences$', AbsenceManagementListCreateView.as_view(), name="absence_list_create"), - re_path(r'^absences/(?P[0-9]+)$', AbsenceManagementDetailView.as_view(), name="absence_detail"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index b9934e9..6f98d11 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -413,48 +413,3 @@ class PaymentModeDetailView(APIView): def delete(self, request, id): return delete_object(PaymentMode, id) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class AbsenceManagementListCreateView(APIView): - def get(self, request): - absences = AbsenceManagement.objects.all() - serializer = AbsenceManagementSerializer(absences, many=True) - return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) - - def post(self, request): - serializer = AbsenceManagementSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED) - return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class AbsenceManagementDetailView(APIView): - def get(self, request, id): - try: - absence = AbsenceManagement.objects.get(id=id) - serializer = AbsenceManagementSerializer(absence) - return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) - except AbsenceManagement.DoesNotExist: - return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) - - def put(self, request, id): - try: - absence = AbsenceManagement.objects.get(id=id) - serializer = AbsenceManagementSerializer(absence, data=request.data) - if serializer.is_valid(): - serializer.save() - return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) - return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - except AbsenceManagement.DoesNotExist: - return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) - - def delete(self, request, id): - try: - absence = AbsenceManagement.objects.get(id=id) - absence.delete() - return JsonResponse(safe=False, status=status.HTTP_204_NO_CONTENT) - except AbsenceManagement.DoesNotExist: - return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index cad45a7..c4c8ae2 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -357,3 +357,34 @@ class RegistrationParentFileTemplate(models.Model): for reg_file in registration_files: filenames.append(reg_file.file.path) return filenames + +class AbsenceMoment(models.IntegerChoices): + MORNING = 1, 'Morning' + AFTERNOON = 2, 'Afternoon' + TOTAL = 3, 'Total' + +class AbsenceReason(models.IntegerChoices): + JUSTIFIED_ABSENCE = 1, 'Justified Absence' + UNJUSTIFIED_ABSENCE = 2, 'Unjustified Absence' + JUSTIFIED_LATE = 3, 'Justified Late' + UNJUSTIFIED_LATE = 4, 'Unjustified Late' + +class AbsenceManagement(models.Model): + day = models.DateField() + moment = models.IntegerField( + choices=AbsenceMoment.choices, + default=AbsenceMoment.TOTAL + ) + reason = models.IntegerField( + choices=AbsenceReason.choices, + default=AbsenceReason.UNJUSTIFIED_ABSENCE + ) + student = models.ForeignKey( + Student, + on_delete=models.CASCADE, + related_name='absences' + ) + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='absences') + + 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/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index a264891..864f28e 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate +from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate, AbsenceManagement from School.models import SchoolClass, Fee, Discount, FeeType from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import ProfileRole, Profile @@ -12,6 +12,11 @@ import pytz from datetime import datetime import Subscriptions.util as util +class AbsenceManagementSerializer(serializers.ModelSerializer): + class Meta: + model = AbsenceManagement + fields = '__all__' + class RegistrationSchoolFileMasterSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 00cdc91..2f71bba 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -15,7 +15,9 @@ from .views import ( RegistrationParentFileMasterSimpleView, RegistrationParentFileMasterView, RegistrationParentFileTemplateSimpleView, - RegistrationParentFileTemplateView + RegistrationParentFileTemplateView, + AbsenceManagementListCreateView, + AbsenceManagementDetailView ) from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group @@ -58,4 +60,7 @@ urlpatterns = [ re_path(r'^students/(?P[0-9]+)/guardians/(?P[0-9]+)/dissociate', DissociateGuardianView.as_view(), name='dissociate-guardian'), + re_path(r'^absences$', AbsenceManagementListCreateView.as_view(), name="absence_list_create"), + re_path(r'^absences/(?P[0-9]+)$', AbsenceManagementDetailView.as_view(), name="absence_detail"), + ] \ No newline at end of file diff --git a/Back-End/Subscriptions/views/__init__.py b/Back-End/Subscriptions/views/__init__.py index fb24394..ea8ed98 100644 --- a/Back-End/Subscriptions/views/__init__.py +++ b/Back-End/Subscriptions/views/__init__.py @@ -12,6 +12,7 @@ from .registration_file_views import ( from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .student_views import StudentView, StudentListView, ChildrenListView from .guardian_views import GuardianView, DissociateGuardianView +from .absences_views import AbsenceManagementDetailView, AbsenceManagementListCreateView __all__ = [ 'RegisterFormView', @@ -36,5 +37,7 @@ __all__ = [ 'StudentListView', 'ChildrenListView', 'GuardianView', - 'DissociateGuardianView' + 'DissociateGuardianView', + 'AbsenceManagementDetailView', + 'AbsenceManagementListCreateView' ] diff --git a/Back-End/Subscriptions/views/absences_views.py b/Back-End/Subscriptions/views/absences_views.py new file mode 100644 index 0000000..9d79be0 --- /dev/null +++ b/Back-End/Subscriptions/views/absences_views.py @@ -0,0 +1,50 @@ +from django.http.response import JsonResponse +from rest_framework.views import APIView +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect +from django.utils.decorators import method_decorator +from Subscriptions.serializers import AbsenceManagementSerializer +from Subscriptions.models import AbsenceManagement +from N3wtSchool.bdd import delete_object + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class AbsenceManagementListCreateView(APIView): + def get(self, request): + absences = AbsenceManagement.objects.all() + serializer = AbsenceManagementSerializer(absences, many=True) + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + + def post(self, request): + serializer = AbsenceManagementSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED) + return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class AbsenceManagementDetailView(APIView): + def get(self, request, id): + try: + absence = AbsenceManagement.objects.get(id=id) + serializer = AbsenceManagementSerializer(absence) + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + except AbsenceManagement.DoesNotExist: + return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + def put(self, request, id): + try: + absence = AbsenceManagement.objects.get(id=id) + serializer = AbsenceManagementSerializer(absence, data=request.data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + except AbsenceManagement.DoesNotExist: + return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + def delete(self, request, id): + return delete_object(AbsenceManagement, id) \ No newline at end of file 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 7c2a980..4c6e409 100644 --- a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js +++ b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js @@ -1,10 +1,8 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { Plus, Users, Layers, CheckCircle } from 'lucide-react'; +import { Users, Layers, CheckCircle, Clock } from 'lucide-react'; import Table from '@/components/Table'; -import MultiSelect from '@/components/MultiSelect'; -import InputText from '@/components/InputText'; import Popup from '@/components/Popup'; import { fetchClasse } from '@/app/actions/schoolAction'; import { useSearchParams } from 'next/navigation'; @@ -13,6 +11,17 @@ import { useClasses } from '@/context/ClassesContext'; import { BASE_URL } from '@/utils/Url'; import Button from '@/components/Button'; import SelectChoice from '@/components/SelectChoice'; +import CheckBox from '@/components/CheckBox'; +import InputText from '@/components/InputText'; +import { + fetchAbsences, + createAbsences, + editAbsences, + deleteAbsences, +} from '@/app/actions/subscriptionAction'; + +import { useCsrfToken } from '@/context/CsrfContext'; +import { useEstablishment } from '@/context/EstablishmentContext'; export default function Page() { const searchParams = useSearchParams(); @@ -20,56 +29,67 @@ export default function Page() { const [classe, setClasse] = useState([]); const { getNiveauxLabels, getNiveauLabel } = useClasses(); - const [students, setStudents] = useState([]); - const [groups, setGroups] = useState([]); - const [newGroup, setNewGroup] = useState({ - name: '', - level: null, - students: [], - }); 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 [absences, setAbsences] = useState({}); // État pour stocker les absences - const [absenceDetails, setAbsenceDetails] = useState({ - day: '', - reason: null, - moment: null, - }); // Détails d'absence + 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: 'Morning' }, - AFTERNOON: { value: 2, label: 'Afternoon' }, - TOTAL: { value: 3, label: 'Total' }, + 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: 'Justified Absence' }, - UNJUSTIFIED_ABSENCE: { value: 2, label: 'Unjustified Absence' }, - JUSTIFIED_LATE: { value: 3, label: 'Justified Late' }, - UNJUSTIFIED_LATE: { value: 4, label: 'Unjustified Late' }, + 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(() => { - console.log('Absences enregistrées :', absences); - }, [absences]); + // 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(() => { - // Initialiser les niveaux sélectionnés avec tous les niveaux disponibles - if (classe?.levels?.length > 0) { - const initialLevels = getNiveauxLabels(classe.levels); - setSelectedLevels(initialLevels); + // 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) + ); } - }, [classe]); + }, [selectedEstablishmentId]); useEffect(() => { // Filtrer les élèves en fonction des niveaux sélectionnés - if (selectedLevels.length > 0) { + if (classe && selectedLevels.length > 0) { const filtered = classe.students.filter((student) => selectedLevels.includes(getNiveauLabel(student.level)) ); @@ -80,29 +100,34 @@ export default function Page() { }, [selectedLevels, classe]); useEffect(() => { - // Initialiser l'état des cases cochées avec tous les élèves présents par défaut - if (filteredStudents.length > 0) { - const initialAttendance = filteredStudents.reduce((acc, student) => { - acc[student.id] = true; // Tous les élèves sont cochés par défaut - return acc; - }, {}); + // 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 + initialAttendance[student.id] = false; + initialFormAbsences[student.id] = { ...existingAbsence }; + } else { + // Sinon, cocher la case par défaut + initialAttendance[student.id] = true; + } + }); + setAttendance(initialAttendance); + setFormAbsences(initialFormAbsences); } - }, [filteredStudents]); - - const handleCreateGroup = () => { - if (!newGroup.name || !newGroup.level || !newGroup.students.length) { - setPopupMessage( - 'Tous les champs doivent être remplis pour créer un groupe.' - ); - setPopupVisible(true); - return; - } - - const updatedGroups = [...groups, newGroup]; - setGroups(updatedGroups); - setNewGroup({ name: '', level: null, students: [] }); - }; + }, [filteredStudents, fetchedAbsences]); const handleLevelClick = (label) => { setSelectedLevels( @@ -118,11 +143,17 @@ export default function Page() { }; const handleValidateAttendance = () => { - console.log('Présence validée :', attendance); - console.log('Absences enregistrées :', absences); + // Filtrer les absences modifiées uniquement pour les étudiants décochés (absents) + const absencesToUpdate = Object.entries(formAbsences).filter( + ([studentId, absenceData]) => + !attendance[studentId] && // L'étudiant est décoché (absent) + JSON.stringify(absenceData) !== + JSON.stringify(fetchedAbsences[studentId]) // Les données ont été modifiées + ); - // Exemple : Envoyer les absences à une API - Object.entries(absences).forEach(([studentId, absenceData]) => { + // Envoyer les absences modifiées à une API + absencesToUpdate.forEach(([studentId, absenceData]) => { + console.log('Modification absence élève : ', studentId); saveAbsence(studentId, absenceData); }); @@ -130,25 +161,70 @@ export default function Page() { }; 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), initialiser les champs d'absence + // Si l'élève est décoché (absent) if (!updatedAttendance[studentId]) { - setAbsences((prev) => ({ - ...prev, - [studentId]: { - day: '', - reason: null, - moment: null, - }, - })); + // 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é, supprimer ses données d'absence - setAbsences((prev) => { + // 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(() => { + console.log( + `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) => { + console.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; @@ -159,26 +235,64 @@ export default function Page() { }); }; - const saveAbsence = async (studentId, absenceData) => { - try { - const response = await fetch('/api/absences', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - studentId, - ...absenceData, - }), - }); + const saveAbsence = (studentId, absenceData) => { + if (!absenceData.reason || !studentId || !absenceData.moment) { + console.error('Tous les champs requis doivent être fournis.'); + return; + } - if (!response.ok) { - throw new Error("Erreur lors de l'enregistrement de l'absence"); - } + const payload = { + student: studentId, + day: absenceData.day, + reason: absenceData.reason, + moment: absenceData.moment, + establishment: selectedEstablishmentId, + }; - console.log(`Absence pour l'élève ${studentId} enregistrée avec succès.`); - } catch (error) { - console.error('Erreur :', error); + if (absenceData.id) { + // Modifier une absence existante + editAbsences(absenceData.id, payload, csrfToken) + .then(() => { + console.log( + `Absence pour l'élève ${studentId} modifiée avec succès.` + ); + // Mettre à jour fetchedAbsences et formAbsences localement + setFetchedAbsences((prev) => ({ + ...prev, + [studentId]: { ...prev[studentId], ...payload }, + })); + setFormAbsences((prev) => ({ + ...prev, + [studentId]: { ...prev[studentId], ...payload }, + })); + }) + .catch((error) => { + console.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) => { + console.log(`Absence pour l'élève ${studentId} créée avec 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) => { + console.error( + `Erreur lors de la création de l'absence pour l'élève ${studentId}:`, + error + ); + }); } }; @@ -186,15 +300,7 @@ export default function Page() { logger.error('Error fetching data:', err); }; - useEffect(() => { - fetchClasse(schoolClassId) - .then((classeData) => { - logger.debug('Classes récupérées :', classeData); - setClasse(classeData); - setFilteredStudents(classeData.students); - }) - .catch(requestErrorHandler); - }, []); + const today = new Date().toISOString().split('T')[0]; // Obtenez la date actuelle au format YYYY-MM-DD return (
@@ -259,146 +365,185 @@ export default function Page() {
- {/* Section Élèves */} -
-

- - Élèves -

- {!isEditingAttendance ? ( -
+ ( -
- {row.photo ? ( - - {`${row.first_name} - - ) : ( -
- - {row.first_name[0]} - {row.last_name[0]} - -
- )} -
+
{row.last_name}
+ ), + }, + { + name: 'Prénom', + transform: (row) => ( +
{row.first_name}
+ ), + }, + { + name: 'Niveau', + transform: (row) => ( +
{getNiveauLabel(row.level)}
), }, - { name: 'Nom', transform: (row) => row.last_name }, - { name: 'Prénom', transform: (row) => row.first_name }, - { name: 'Niveau', transform: (row) => getNiveauLabel(row.level) }, ...(isEditingAttendance ? [ { - name: 'Présent', + name: "Gestion de l'appel", transform: (row) => ( - handleAttendanceChange(row.id)} - className="w-5 h-5" - /> +
+ {/* Case à cocher pour la présence */} + handleAttendanceChange(row.id)} + fieldName="attendance" + /> + + {/* Champs pour le motif et le moment */} + {!attendance[row.id] && ( +
+ + setFormAbsences((prev) => ({ + ...prev, + [row.id]: { + ...prev[row.id], + reason: parseInt(e.target.value, 10), + }, + })) + } + choices={Object.values(AbsenceReason).map( + (reason) => ({ + value: reason.value, + label: reason.label, + }) + )} + /> + + + 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, + }) + )} + /> +
+ )} +
), }, - { - name: "Détails d'absence", - transform: (row) => - !attendance[row.id] && ( -
- {/* Champ pour le jour */} - - setAbsences((prev) => ({ - ...prev, - [row.id]: { - ...prev[row.id], - day: e.target.value, - }, - })) - } - className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm" - /> - - {/* Champ pour le motif */} - - setAbsences((prev) => ({ - ...prev, - [row.id]: { - ...prev[row.id], - reason: parseInt(e.target.value, 10), - }, - })) - } - choices={Object.values(AbsenceReason).map( - (reason) => ({ - value: reason.value, - label: reason.label, - }) - )} - required - /> - - {/* Champ pour le moment */} - - setAbsences((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, - }) - )} - required - /> -
- ), - }, ] - : []), + : [ + { + 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 ( +
+ + Présent +
+ ); + } + + switch (absence.reason) { + case AbsenceReason.JUSTIFIED_LATE.value: + return ( +
+ + Retard justifié +
+ ); + case AbsenceReason.UNJUSTIFIED_LATE.value: + return ( +
+ + Retard non justifié +
+ ); + case AbsenceReason.JUSTIFIED_ABSENCE.value: + return ( +
+ + Absence justifiée +
+ ); + case AbsenceReason.UNJUSTIFIED_ABSENCE.value: + return ( +
+ + Absence non justifiée +
+ ); + default: + return ( +
+ + Statut inconnu +
+ ); + } + }, + }, + ]), ]} data={filteredStudents} // Utiliser les élèves filtrés /> diff --git a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js index 223e182..2270ac3 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js @@ -37,6 +37,7 @@ export default function Page() { useEffect(() => { if (selectedEstablishmentId) { + setIsLoading(true); fetchClasses(selectedEstablishmentId) .then((classesData) => { logger.debug('Classes récupérées :', classesData); @@ -47,6 +48,7 @@ export default function Page() { ); setClasses(filteredClasses); // Mettre à jour les classes filtrées + setIsLoading(false); }) .catch(requestErrorHandler); } @@ -71,7 +73,7 @@ export default function Page() { }); }; - if (isLoading || classes.length === 0) { + if (isLoading) { return ; } diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index fa0d0da..4bf8c95 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -3,6 +3,7 @@ import { BE_SUBSCRIPTION_CHILDRENS_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, + BE_SUBSCRIPTION_ABSENCES_URL, } from '@/utils/Url'; export const PENDING = 'pending'; @@ -213,3 +214,50 @@ export const dissociateGuardian = async (studentId, guardianId) => { return response.json(); }; + +export const fetchAbsences = (establishment) => { + return fetch( + `${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}` + ).then(requestResponseHandler); +}; + +export const createAbsences = (data, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'X-CSRFToken': csrfToken, + 'Content-Type': 'application/json', + }, + credentials: 'include', + }).then(requestResponseHandler); +}; + +export const editAbsences = (absenceId, payload, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(payload), // Sérialisez les données en JSON + credentials: 'include', + }).then((response) => { + if (!response.ok) { + return response.json().then((error) => { + throw new Error(error); + }); + } + return response.json(); + }); +}; + +export const deleteAbsences = (id, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }); +}; diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 83a23a7..17ee144 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -31,6 +31,7 @@ export const BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL = `${BASE_UR export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL = `${BASE_URL}/Subscriptions/registrationParentFileMasters`; export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`; export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`; +export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`; //GESTION ECOLE export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`;