feat: Gestion des absences du jour [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-04 12:08:05 +02:00
parent 1bccc85951
commit 030d19d411
13 changed files with 516 additions and 311 deletions

View File

@ -131,33 +131,3 @@ class PaymentMode(models.Model):
def __str__(self): def __str__(self):
return f"{self.get_mode_display()} - {self.get_type_display()}" 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()}"

View File

@ -1,5 +1,5 @@
from rest_framework import serializers 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 Auth.models import Profile, ProfileRole
from Subscriptions.models import Student from Subscriptions.models import Student
from Establishment.models import Establishment from Establishment.models import Establishment
@ -8,11 +8,6 @@ from N3wtSchool import settings, bdd
from django.utils import timezone from django.utils import timezone
import pytz import pytz
class AbsenceManagementSerializer(serializers.ModelSerializer):
class Meta:
model = AbsenceManagement
fields = '__all__'
class SpecialitySerializer(serializers.ModelSerializer): class SpecialitySerializer(serializers.ModelSerializer):
updated_date_formatted = serializers.SerializerMethodField() updated_date_formatted = serializers.SerializerMethodField()

View File

@ -17,8 +17,6 @@ from .views import (
PaymentPlanDetailView, PaymentPlanDetailView,
PaymentModeListCreateView, PaymentModeListCreateView,
PaymentModeDetailView, PaymentModeDetailView,
AbsenceManagementListCreateView,
AbsenceManagementDetailView
) )
urlpatterns = [ urlpatterns = [
@ -45,7 +43,4 @@ urlpatterns = [
re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"), re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"),
re_path(r'^paymentModes/(?P<id>[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"), re_path(r'^paymentModes/(?P<id>[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<id>[0-9]+)$', AbsenceManagementDetailView.as_view(), name="absence_detail"),
] ]

View File

@ -413,48 +413,3 @@ class PaymentModeDetailView(APIView):
def delete(self, request, id): def delete(self, request, id):
return delete_object(PaymentMode, 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)

View File

@ -357,3 +357,34 @@ class RegistrationParentFileTemplate(models.Model):
for reg_file in registration_files: for reg_file in registration_files:
filenames.append(reg_file.file.path) filenames.append(reg_file.file.path)
return filenames 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()}"

View File

@ -1,5 +1,5 @@
from rest_framework import serializers 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.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import ProfileRole, Profile from Auth.models import ProfileRole, Profile
@ -12,6 +12,11 @@ import pytz
from datetime import datetime from datetime import datetime
import Subscriptions.util as util import Subscriptions.util as util
class AbsenceManagementSerializer(serializers.ModelSerializer):
class Meta:
model = AbsenceManagement
fields = '__all__'
class RegistrationSchoolFileMasterSerializer(serializers.ModelSerializer): class RegistrationSchoolFileMasterSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
class Meta: class Meta:

View File

@ -15,7 +15,9 @@ from .views import (
RegistrationParentFileMasterSimpleView, RegistrationParentFileMasterSimpleView,
RegistrationParentFileMasterView, RegistrationParentFileMasterView,
RegistrationParentFileTemplateSimpleView, RegistrationParentFileTemplateSimpleView,
RegistrationParentFileTemplateView RegistrationParentFileTemplateView,
AbsenceManagementListCreateView,
AbsenceManagementDetailView
) )
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
@ -58,4 +60,7 @@ urlpatterns = [
re_path(r'^students/(?P<student_id>[0-9]+)/guardians/(?P<guardian_id>[0-9]+)/dissociate', DissociateGuardianView.as_view(), name='dissociate-guardian'), re_path(r'^students/(?P<student_id>[0-9]+)/guardians/(?P<guardian_id>[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<id>[0-9]+)$', AbsenceManagementDetailView.as_view(), name="absence_detail"),
] ]

View File

@ -12,6 +12,7 @@ from .registration_file_views import (
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView, DissociateGuardianView from .guardian_views import GuardianView, DissociateGuardianView
from .absences_views import AbsenceManagementDetailView, AbsenceManagementListCreateView
__all__ = [ __all__ = [
'RegisterFormView', 'RegisterFormView',
@ -36,5 +37,7 @@ __all__ = [
'StudentListView', 'StudentListView',
'ChildrenListView', 'ChildrenListView',
'GuardianView', 'GuardianView',
'DissociateGuardianView' 'DissociateGuardianView',
'AbsenceManagementDetailView',
'AbsenceManagementListCreateView'
] ]

View File

@ -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)

View File

@ -1,10 +1,8 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; 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 Table from '@/components/Table';
import MultiSelect from '@/components/MultiSelect';
import InputText from '@/components/InputText';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import { fetchClasse } from '@/app/actions/schoolAction'; import { fetchClasse } from '@/app/actions/schoolAction';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -13,6 +11,17 @@ import { useClasses } from '@/context/ClassesContext';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import Button from '@/components/Button'; import Button from '@/components/Button';
import SelectChoice from '@/components/SelectChoice'; 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() { export default function Page() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -20,56 +29,67 @@ export default function Page() {
const [classe, setClasse] = useState([]); const [classe, setClasse] = useState([]);
const { getNiveauxLabels, getNiveauLabel } = useClasses(); 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 [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState(''); const [popupMessage, setPopupMessage] = useState('');
const [selectedLevels, setSelectedLevels] = useState([]); // Par défaut, tous les niveaux sont sélectionnés const [selectedLevels, setSelectedLevels] = useState([]); // Par défaut, tous les niveaux sont sélectionnés
const [filteredStudents, setFilteredStudents] = useState([]); const [filteredStudents, setFilteredStudents] = useState([]);
const [isEditingAttendance, setIsEditingAttendance] = useState(false); // État pour le mode édition const [isEditingAttendance, setIsEditingAttendance] = useState(false); // État pour le mode édition
const [attendance, setAttendance] = useState({}); // État pour les cases cochées const [attendance, setAttendance] = useState({}); // État pour les cases cochées
const [absences, setAbsences] = useState({}); // État pour stocker les absences const [fetchedAbsences, setFetchedAbsences] = useState({}); // Absences récupérées depuis le backend
const [absenceDetails, setAbsenceDetails] = useState({ const [formAbsences, setFormAbsences] = useState({}); // Absences modifiées localement
day: '',
reason: null, const csrfToken = useCsrfToken();
moment: null, const { selectedEstablishmentId } = useEstablishment();
}); // Détails d'absence
// AbsenceMoment constants // AbsenceMoment constants
const AbsenceMoment = { const AbsenceMoment = {
MORNING: { value: 1, label: 'Morning' }, MORNING: { value: 1, label: 'Matinée' },
AFTERNOON: { value: 2, label: 'Afternoon' }, AFTERNOON: { value: 2, label: 'Après-midi' },
TOTAL: { value: 3, label: 'Total' }, TOTAL: { value: 3, label: 'Journée' },
}; };
// AbsenceReason constants // AbsenceReason constants
const AbsenceReason = { const AbsenceReason = {
JUSTIFIED_ABSENCE: { value: 1, label: 'Justified Absence' }, JUSTIFIED_ABSENCE: { value: 1, label: 'Absence justifiée' },
UNJUSTIFIED_ABSENCE: { value: 2, label: 'Unjustified Absence' }, UNJUSTIFIED_ABSENCE: { value: 2, label: 'Absence non justifiée' },
JUSTIFIED_LATE: { value: 3, label: 'Justified Late' }, JUSTIFIED_LATE: { value: 3, label: 'Retard justifié' },
UNJUSTIFIED_LATE: { value: 4, label: 'Unjustified Late' }, UNJUSTIFIED_LATE: { value: 4, label: 'Retard non justifié' },
}; };
useEffect(() => { useEffect(() => {
console.log('Absences enregistrées :', absences); // Récupérer les données de la classe et initialiser les élèves filtrés
}, [absences]); 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(() => { useEffect(() => {
// Initialiser les niveaux sélectionnés avec tous les niveaux disponibles // Récupérer les absences pour l'établissement sélectionné
if (classe?.levels?.length > 0) { if (selectedEstablishmentId) {
const initialLevels = getNiveauxLabels(classe.levels); fetchAbsences(selectedEstablishmentId)
setSelectedLevels(initialLevels); .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(() => { useEffect(() => {
// Filtrer les élèves en fonction des niveaux sélectionnés // 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) => const filtered = classe.students.filter((student) =>
selectedLevels.includes(getNiveauLabel(student.level)) selectedLevels.includes(getNiveauLabel(student.level))
); );
@ -80,29 +100,34 @@ export default function Page() {
}, [selectedLevels, classe]); }, [selectedLevels, classe]);
useEffect(() => { useEffect(() => {
// Initialiser l'état des cases cochées avec tous les élèves présents par défaut // Initialiser `attendance` et `formAbsences` en fonction des élèves filtrés et des absences
if (filteredStudents.length > 0) { if (filteredStudents.length > 0 && fetchedAbsences) {
const initialAttendance = filteredStudents.reduce((acc, student) => { const today = new Date().toISOString().split('T')[0];
acc[student.id] = true; // Tous les élèves sont cochés par défaut
return acc; 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); setAttendance(initialAttendance);
setFormAbsences(initialFormAbsences);
} }
}, [filteredStudents]); }, [filteredStudents, fetchedAbsences]);
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: [] });
};
const handleLevelClick = (label) => { const handleLevelClick = (label) => {
setSelectedLevels( setSelectedLevels(
@ -118,11 +143,17 @@ export default function Page() {
}; };
const handleValidateAttendance = () => { const handleValidateAttendance = () => {
console.log('Présence validée :', attendance); // Filtrer les absences modifiées uniquement pour les étudiants décochés (absents)
console.log('Absences enregistrées :', absences); 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 // Envoyer les absences modifiées à une API
Object.entries(absences).forEach(([studentId, absenceData]) => { absencesToUpdate.forEach(([studentId, absenceData]) => {
console.log('Modification absence élève : ', studentId);
saveAbsence(studentId, absenceData); saveAbsence(studentId, absenceData);
}); });
@ -130,25 +161,70 @@ export default function Page() {
}; };
const handleAttendanceChange = (studentId) => { const handleAttendanceChange = (studentId) => {
const today = new Date().toISOString().split('T')[0]; // Obtenir la date actuelle au format YYYY-MM-DD
setAttendance((prev) => { setAttendance((prev) => {
const updatedAttendance = { const updatedAttendance = {
...prev, ...prev,
[studentId]: !prev[studentId], // Inverser l'état de présence [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]) { if (!updatedAttendance[studentId]) {
setAbsences((prev) => ({ // Vérifier s'il existe une absence pour le jour actuel
...prev, const existingAbsence = Object.values(fetchedAbsences).find(
[studentId]: { (absence) => absence.student === studentId && absence.day === today
day: '', );
reason: null,
moment: null, 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 { } else {
// Si l'élève est recoché, supprimer ses données d'absence // Si l'élève est recoché (présent), supprimer l'absence existante
setAbsences((prev) => { 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 }; const updatedAbsences = { ...prev };
delete updatedAbsences[studentId]; delete updatedAbsences[studentId];
return updatedAbsences; return updatedAbsences;
@ -159,26 +235,64 @@ export default function Page() {
}); });
}; };
const saveAbsence = async (studentId, absenceData) => { const saveAbsence = (studentId, absenceData) => {
try { if (!absenceData.reason || !studentId || !absenceData.moment) {
const response = await fetch('/api/absences', { console.error('Tous les champs requis doivent être fournis.');
method: 'POST', return;
headers: { }
'Content-Type': 'application/json',
},
body: JSON.stringify({
studentId,
...absenceData,
}),
});
if (!response.ok) { const payload = {
throw new Error("Erreur lors de l'enregistrement de l'absence"); 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.`); if (absenceData.id) {
} catch (error) { // Modifier une absence existante
console.error('Erreur :', error); 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); logger.error('Error fetching data:', err);
}; };
useEffect(() => { const today = new Date().toISOString().split('T')[0]; // Obtenez la date actuelle au format YYYY-MM-DD
fetchClasse(schoolClassId)
.then((classeData) => {
logger.debug('Classes récupérées :', classeData);
setClasse(classeData);
setFilteredStudents(classeData.students);
})
.catch(requestErrorHandler);
}, []);
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
@ -259,146 +365,185 @@ export default function Page() {
</div> </div>
</div> </div>
{/* Section Élèves */} {/* Affichage de la date du jour */}
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4 bg-white p-4 rounded-lg shadow-md">
<h2 className="text-xl font-semibold flex items-center"> <div className="flex items-center space-x-3">
<Users className="w-6 h-6 mr-2" /> <div className="flex items-center justify-center w-10 h-10 bg-emerald-100 text-emerald-600 rounded-full">
Élèves <Clock className="w-6 h-6" />
</h2> </div>
{!isEditingAttendance ? ( <h2 className="text-lg font-semibold text-gray-800">
<Button Appel du jour :{' '}
text="Faire l'appel" <span className="ml-2 text-emerald-600">{today}</span>
onClick={handleToggleAttendanceMode} </h2>
primary </div>
className="px-4 py-2" <div className="flex items-center">
/> {!isEditingAttendance ? (
) : ( <Button
<Button text="Faire l'appel"
text="Valider l'appel" onClick={handleToggleAttendanceMode}
onClick={handleValidateAttendance} primary
primary className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
className="px-4 py-2" />
/> ) : (
)} <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> </div>
<Table <Table
columns={[ columns={[
{ {
name: 'photo', name: 'Nom',
transform: (row) => ( transform: (row) => (
<div className="flex justify-center items-center"> <div className="text-center">{row.last_name}</div>
{row.photo ? ( ),
<a },
href={`${BASE_URL}${row.photo}`} {
target="_blank" name: 'Prénom',
rel="noopener noreferrer" transform: (row) => (
> <div className="text-center">{row.first_name}</div>
<img ),
src={`${BASE_URL}${row.photo}`} },
alt={`${row.first_name} ${row.last_name}`} {
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer rounded-full" name: 'Niveau',
/> transform: (row) => (
</a> <div className="text-center">{getNiveauLabel(row.level)}</div>
) : (
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full">
<span className="text-gray-500 text-sm font-semibold">
{row.first_name[0]}
{row.last_name[0]}
</span>
</div>
)}
</div>
), ),
}, },
{ name: 'Nom', transform: (row) => row.last_name },
{ name: 'Prénom', transform: (row) => row.first_name },
{ name: 'Niveau', transform: (row) => getNiveauLabel(row.level) },
...(isEditingAttendance ...(isEditingAttendance
? [ ? [
{ {
name: 'Présent', name: "Gestion de l'appel",
transform: (row) => ( transform: (row) => (
<input <div className="flex flex-col gap-2 items-center">
type="checkbox" {/* Case à cocher pour la présence */}
checked={attendance[row.id] || false} <CheckBox
onChange={() => handleAttendanceChange(row.id)} item={{ id: row.id }}
className="w-5 h-5" formData={{
/> attendance: attendance[row.id] ? [row.id] : [],
}}
handleChange={() => handleAttendanceChange(row.id)}
fieldName="attendance"
/>
{/* Champs pour le motif et le moment */}
{!attendance[row.id] && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
<SelectChoice
name={`reason-${row.id}`}
label=""
placeHolder="Motif"
selected={formAbsences[row.id]?.reason || ''}
callback={(e) =>
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,
})
)}
/>
<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,
})
)}
/>
</div>
)}
</div>
), ),
}, },
{
name: "Détails d'absence",
transform: (row) =>
!attendance[row.id] && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
{/* Champ pour le jour */}
<input
type="date"
value={absences[row.id]?.day || ''}
onChange={(e) =>
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 */}
<SelectChoice
name={`reason-${row.id}`}
label=""
placeHolder="Motif"
selected={absences[row.id]?.reason || ''}
callback={(e) =>
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 */}
<SelectChoice
name={`moment-${row.id}`}
label=""
placeHolder="Moment"
selected={absences[row.id]?.moment || ''}
callback={(e) =>
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
/>
</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 data={filteredStudents} // Utiliser les élèves filtrés
/> />

View File

@ -37,6 +37,7 @@ export default function Page() {
useEffect(() => { useEffect(() => {
if (selectedEstablishmentId) { if (selectedEstablishmentId) {
setIsLoading(true);
fetchClasses(selectedEstablishmentId) fetchClasses(selectedEstablishmentId)
.then((classesData) => { .then((classesData) => {
logger.debug('Classes récupérées :', 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 setClasses(filteredClasses); // Mettre à jour les classes filtrées
setIsLoading(false);
}) })
.catch(requestErrorHandler); .catch(requestErrorHandler);
} }
@ -71,7 +73,7 @@ export default function Page() {
}); });
}; };
if (isLoading || classes.length === 0) { if (isLoading) {
return <Loader />; return <Loader />;
} }

View File

@ -3,6 +3,7 @@ import {
BE_SUBSCRIPTION_CHILDRENS_URL, BE_SUBSCRIPTION_CHILDRENS_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
BE_SUBSCRIPTION_ABSENCES_URL,
} from '@/utils/Url'; } from '@/utils/Url';
export const PENDING = 'pending'; export const PENDING = 'pending';
@ -213,3 +214,50 @@ export const dissociateGuardian = async (studentId, guardianId) => {
return response.json(); 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',
});
};

View File

@ -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_MASTERS_URL = `${BASE_URL}/Subscriptions/registrationParentFileMasters`;
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`; 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_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`;
//GESTION ECOLE //GESTION ECOLE
export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`; export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`;