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):
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 .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()

View File

@ -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<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):
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:
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()}"

View File

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

View File

@ -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<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 .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'
]

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';
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 (
<div className="p-6 space-y-6">
@ -259,146 +365,185 @@ export default function Page() {
</div>
</div>
{/* Section Élèves */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold flex items-center">
<Users className="w-6 h-6 mr-2" />
Élèves
</h2>
{!isEditingAttendance ? (
<Button
text="Faire l'appel"
onClick={handleToggleAttendanceMode}
primary
className="px-4 py-2"
/>
) : (
<Button
text="Valider l'appel"
onClick={handleValidateAttendance}
primary
className="px-4 py-2"
/>
)}
{/* 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: 'photo',
name: 'Nom',
transform: (row) => (
<div className="flex justify-center items-center">
{row.photo ? (
<a
href={`${BASE_URL}${row.photo}`}
target="_blank"
rel="noopener noreferrer"
>
<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"
/>
</a>
) : (
<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>
<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>
),
},
{ 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) => (
<input
type="checkbox"
checked={attendance[row.id] || false}
onChange={() => handleAttendanceChange(row.id)}
className="w-5 h-5"
/>
<div className="flex flex-col gap-2 items-center">
{/* Case à cocher pour la présence */}
<CheckBox
item={{ id: row.id }}
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
/>

View File

@ -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 <Loader />;
}

View File

@ -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',
});
};

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_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`;