mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Gestion des absences du jour [#16]
This commit is contained in:
@ -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()}"
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
@ -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)
|
||||
|
||||
@ -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()}"
|
||||
@ -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:
|
||||
|
||||
@ -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"),
|
||||
|
||||
]
|
||||
@ -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'
|
||||
]
|
||||
|
||||
50
Back-End/Subscriptions/views/absences_views.py
Normal file
50
Back-End/Subscriptions/views/absences_views.py
Normal 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)
|
||||
@ -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
|
||||
/>
|
||||
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
});
|
||||
};
|
||||
|
||||
@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user