diff --git a/Back-End/School/models.py b/Back-End/School/models.py index d63ea6e..836e436 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -132,3 +132,32 @@ 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()}" \ No newline at end of file diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index b348ec2..371f2a9 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode, AbsenceManagement from Auth.models import Profile, ProfileRole from Subscriptions.models import Student from Establishment.models import Establishment @@ -8,6 +8,11 @@ from N3wtSchool import settings, bdd from django.utils import timezone import pytz +class AbsenceManagementSerializer(serializers.ModelSerializer): + class Meta: + model = AbsenceManagement + fields = '__all__' + class SpecialitySerializer(serializers.ModelSerializer): updated_date_formatted = serializers.SerializerMethodField() diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 8505bfc..f383156 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -16,7 +16,9 @@ from .views import ( PaymentPlanListCreateView, PaymentPlanDetailView, PaymentModeListCreateView, - PaymentModeDetailView + PaymentModeDetailView, + AbsenceManagementListCreateView, + AbsenceManagementDetailView ) urlpatterns = [ @@ -42,5 +44,8 @@ urlpatterns = [ re_path(r'^paymentPlans/(?P[0-9]+)$', PaymentPlanDetailView.as_view(), name="payment_plan_detail"), re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"), - re_path(r'^paymentModes/(?P[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail") + re_path(r'^paymentModes/(?P[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"), + + re_path(r'^absences$', AbsenceManagementListCreateView.as_view(), name="absence_list_create"), + re_path(r'^absences/(?P[0-9]+)$', AbsenceManagementDetailView.as_view(), name="absence_detail"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index a4cb382..c80854a 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -12,7 +12,8 @@ from .models import ( Discount, Fee, PaymentPlan, - PaymentMode + PaymentMode, + AbsenceManagement ) from .serializers import ( TeacherSerializer, @@ -22,7 +23,8 @@ from .serializers import ( DiscountSerializer, FeeSerializer, PaymentPlanSerializer, - PaymentModeSerializer + PaymentModeSerializer, + AbsenceManagementSerializer ) from N3wtSchool.bdd import delete_object, getAllObjects, getObject @@ -411,3 +413,48 @@ class PaymentModeDetailView(APIView): def delete(self, request, id): return delete_object(PaymentMode, id) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class AbsenceManagementListCreateView(APIView): + def get(self, request): + absences = AbsenceManagement.objects.all() + serializer = AbsenceManagementSerializer(absences, many=True) + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + + def post(self, request): + serializer = AbsenceManagementSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED) + return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class AbsenceManagementDetailView(APIView): + def get(self, request, id): + try: + absence = AbsenceManagement.objects.get(id=id) + serializer = AbsenceManagementSerializer(absence) + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + except AbsenceManagement.DoesNotExist: + return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + def put(self, request, id): + try: + absence = AbsenceManagement.objects.get(id=id) + serializer = AbsenceManagementSerializer(absence, data=request.data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + except AbsenceManagement.DoesNotExist: + return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + def delete(self, request, id): + try: + absence = AbsenceManagement.objects.get(id=id) + absence.delete() + return JsonResponse(safe=False, status=status.HTTP_204_NO_CONTENT) + except AbsenceManagement.DoesNotExist: + return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js index 1c3d26a..7c2a980 100644 --- a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js +++ b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js @@ -11,6 +11,8 @@ import { useSearchParams } from 'next/navigation'; import logger from '@/utils/logger'; import { useClasses } from '@/context/ClassesContext'; import { BASE_URL } from '@/utils/Url'; +import Button from '@/components/Button'; +import SelectChoice from '@/components/SelectChoice'; export default function Page() { const searchParams = useSearchParams(); @@ -29,6 +31,33 @@ export default function Page() { 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 + + // AbsenceMoment constants + const AbsenceMoment = { + MORNING: { value: 1, label: 'Morning' }, + AFTERNOON: { value: 2, label: 'Afternoon' }, + TOTAL: { value: 3, label: 'Total' }, + }; + + // 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' }, + }; + + useEffect(() => { + console.log('Absences enregistrées :', absences); + }, [absences]); useEffect(() => { // Initialiser les niveaux sélectionnés avec tous les niveaux disponibles @@ -50,6 +79,17 @@ 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; + }, {}); + setAttendance(initialAttendance); + } + }, [filteredStudents]); + const handleCreateGroup = () => { if (!newGroup.name || !newGroup.level || !newGroup.students.length) { setPopupMessage( @@ -73,6 +113,75 @@ export default function Page() { ); }; + const handleToggleAttendanceMode = () => { + setIsEditingAttendance((prev) => !prev); // Basculer entre mode édition et visualisation + }; + + const handleValidateAttendance = () => { + console.log('Présence validée :', attendance); + console.log('Absences enregistrées :', absences); + + // Exemple : Envoyer les absences à une API + Object.entries(absences).forEach(([studentId, absenceData]) => { + saveAbsence(studentId, absenceData); + }); + + setIsEditingAttendance(false); // Revenir au mode visualisation + }; + + const handleAttendanceChange = (studentId) => { + 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 + if (!updatedAttendance[studentId]) { + setAbsences((prev) => ({ + ...prev, + [studentId]: { + day: '', + reason: null, + moment: null, + }, + })); + } else { + // Si l'élève est recoché, supprimer ses données d'absence + setAbsences((prev) => { + const updatedAbsences = { ...prev }; + delete updatedAbsences[studentId]; + return updatedAbsences; + }); + } + + return updatedAttendance; + }); + }; + + const saveAbsence = async (studentId, absenceData) => { + try { + const response = await fetch('/api/absences', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + studentId, + ...absenceData, + }), + }); + + if (!response.ok) { + throw new Error("Erreur lors de l'enregistrement de l'absence"); + } + + console.log(`Absence pour l'élève ${studentId} enregistrée avec succès.`); + } catch (error) { + console.error('Erreur :', error); + } + }; + const requestErrorHandler = (err) => { logger.error('Error fetching data:', err); }; @@ -151,47 +260,148 @@ export default function Page() { {/* Section Élèves */} -
-

+
+

Élèves

- ( -
- {row.photo ? ( - - {`${row.first_name} - - ) : ( -
- - {row.first_name[0]} - {row.last_name[0]} - -
- )} -
- ), - }, - { name: 'Nom', transform: (row) => row.last_name }, - { name: 'Prénom', transform: (row) => row.first_name }, - { name: 'Niveau', transform: (row) => getNiveauLabel(row.level) }, - ]} - data={filteredStudents} // Utiliser les élèves filtrés - /> + {!isEditingAttendance ? ( +
( +
+ {row.photo ? ( + + {`${row.first_name} + + ) : ( +
+ + {row.first_name[0]} + {row.last_name[0]} + +
+ )} +
+ ), + }, + { 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', + transform: (row) => ( + handleAttendanceChange(row.id)} + className="w-5 h-5" + /> + ), + }, + { + name: "Détails d'absence", + transform: (row) => + !attendance[row.id] && ( +
+ {/* Champ pour le jour */} + + setAbsences((prev) => ({ + ...prev, + [row.id]: { + ...prev[row.id], + day: e.target.value, + }, + })) + } + className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm" + /> + + {/* Champ pour le motif */} + + setAbsences((prev) => ({ + ...prev, + [row.id]: { + ...prev[row.id], + reason: parseInt(e.target.value, 10), + }, + })) + } + choices={Object.values(AbsenceReason).map( + (reason) => ({ + value: reason.value, + label: reason.label, + }) + )} + required + /> + + {/* Champ pour le moment */} + + setAbsences((prev) => ({ + ...prev, + [row.id]: { + ...prev[row.id], + moment: parseInt(e.target.value, 10), + }, + })) + } + choices={Object.values(AbsenceMoment).map( + (moment) => ({ + value: moment.value, + label: moment.label, + }) + )} + required + /> +
+ ), + }, + ] + : []), + ]} + data={filteredStudents} // Utiliser les élèves filtrés + /> {/* Popup */}