diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 7ca25eb..9a1531e 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -73,12 +73,15 @@ class SpecialityListCreateView(APIView): def get(self, request): establishment_id = request.GET.get('establishment_id', None) + school_year = request.GET.get('school_year', None) if establishment_id is None: return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) specialities_list = getAllObjects(Speciality) if establishment_id: specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct() + if school_year: + specialities_list = specialities_list.filter(school_year=school_year) specialities_serializer = SpecialitySerializer(specialities_list, many=True) return JsonResponse(specialities_serializer.data, safe=False) diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index a91f6ae..12257a2 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -399,10 +399,11 @@ class RegistrationFormSerializer(serializers.ModelSerializer): class StudentByParentSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) associated_class_name = serializers.SerializerMethodField() + associated_class_id = serializers.SerializerMethodField() class Meta: model = Student - fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name'] + fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name', 'associated_class_id'] def __init__(self, *args, **kwargs): super(StudentByParentSerializer, self).__init__(*args, **kwargs) @@ -412,6 +413,9 @@ class StudentByParentSerializer(serializers.ModelSerializer): def get_associated_class_name(self, obj): return obj.associated_class.atmosphere_name if obj.associated_class else None + def get_associated_class_id(self, obj): + return obj.associated_class.id if obj.associated_class else None + class RegistrationFormByParentSerializer(serializers.ModelSerializer): student = StudentByParentSerializer(many=False, required=True) 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 a8ee078..43ef6fa 100644 --- a/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js +++ b/Front-End/src/app/[locale]/admin/structure/SchoolClassManagement/page.js @@ -205,10 +205,12 @@ export default function Page() { } }, [filteredStudents, fetchedAbsences]); - // Load specialities for evaluations + // Load specialities for evaluations (filtered by current school year) useEffect(() => { if (selectedEstablishmentId) { - fetchSpecialities(selectedEstablishmentId) + const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; + const currentSchoolYear = `${year}-${year + 1}`; + fetchSpecialities(selectedEstablishmentId, currentSchoolYear) .then((data) => setSpecialities(data)) .catch((error) => logger.error('Erreur lors du chargement des matières:', error)); } diff --git a/Front-End/src/app/[locale]/parents/page.js b/Front-End/src/app/[locale]/parents/page.js index 210b8a5..42d497f 100644 --- a/Front-End/src/app/[locale]/parents/page.js +++ b/Front-End/src/app/[locale]/parents/page.js @@ -1,7 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import Table from '@/components/Table'; import { Edit3, Users, @@ -9,6 +8,12 @@ import { Eye, Upload, CalendarDays, + Award, + ChevronDown, + ChevronUp, + BookOpen, + ArrowLeft, + Clock, } from 'lucide-react'; import StatusLabel from '@/components/StatusLabel'; import FileUpload from '@/components/Form/FileUpload'; @@ -16,7 +21,13 @@ import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url'; import { fetchChildren, editRegisterForm, + fetchStudentCompetencies, + fetchAbsences, } from '@/app/actions/subscriptionAction'; +import { + fetchEvaluations, + fetchStudentEvaluations, +} from '@/app/actions/schoolAction'; import { fetchUpcomingEvents } from '@/app/actions/planningAction'; import logger from '@/utils/logger'; import { getSecureFileUrl } from '@/utils/fileUrl'; @@ -27,13 +38,47 @@ import { PlanningProvider, PlanningModes } from '@/context/PlanningContext'; import SectionHeader from '@/components/SectionHeader'; import ParentPlanningSection from '@/components/ParentPlanningSection'; import EventCard from '@/components/EventCard'; +import SelectChoice from '@/components/Form/SelectChoice'; +import dayjs from 'dayjs'; + +// Fonction utilitaire pour générer la chaîne de période +function getPeriodString(selectedPeriod, frequency) { + const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; + const nextYear = (year + 1).toString(); + const schoolYear = `${year}-${nextYear}`; + if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`; + if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`; + if (frequency === 3) return `A_${schoolYear}`; + return ''; +} + +// Fonction pour obtenir les périodes selon la fréquence d'évaluation +function getPeriods(frequency) { + if (frequency === 1) { + return [ + { label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' }, + { label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' }, + { label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' }, + ]; + } + if (frequency === 2) { + return [ + { label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' }, + { label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' }, + ]; + } + if (frequency === 3) { + return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }]; + } + return []; +} export default function ParentHomePage() { const [children, setChildren] = useState([]); - const { user, selectedEstablishmentId } = useEstablishment(); - const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload - const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé - const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant + const { user, selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment(); + const [uploadingStudentId, setUploadingStudentId] = useState(null); + const [uploadedFile, setUploadedFile] = useState(null); + const [uploadState, setUploadState] = useState('off'); const [showPlanning, setShowPlanning] = useState(false); const [planningClassName, setPlanningClassName] = useState(null); const [upcomingEvents, setUpcomingEvents] = useState([]); @@ -42,16 +87,114 @@ export default function ParentHomePage() { const [reloadFetch, setReloadFetch] = useState(false); const { getNiveauLabel } = useClasses(); + // États pour la vue détaillée de l'élève inscrit + const [expandedStudentId, setExpandedStudentId] = useState(null); + const [studentCompetencies, setStudentCompetencies] = useState(null); + const [grades, setGrades] = useState({}); + const [selectedPeriod, setSelectedPeriod] = useState(null); + const [evaluations, setEvaluations] = useState([]); + const [studentEvaluationsData, setStudentEvaluationsData] = useState([]); + const [allAbsences, setAllAbsences] = useState([]); + const [detailLoading, setDetailLoading] = useState(false); + + // Périodes disponibles selon la fréquence d'évaluation + const periods = useMemo( + () => getPeriods(selectedEstablishmentEvaluationFrequency), + [selectedEstablishmentEvaluationFrequency] + ); + + // Auto-sélection de la période courante + useEffect(() => { + if (periods.length > 0 && !selectedPeriod) { + const today = dayjs(); + const current = periods.find((p) => { + const start = dayjs(`${today.year()}-${p.start}`); + const end = dayjs(`${today.year()}-${p.end}`); + return today.isAfter(start.subtract(1, 'day')) && today.isBefore(end.add(1, 'day')); + }); + setSelectedPeriod(current ? current.value : periods[0]?.value); + } + }, [periods, selectedPeriod]); + useEffect(() => { if (user !== null) { const userIdFromSession = user.user_id; fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => { setChildren(data); + // Auto-expand si un seul enfant inscrit + const enrolledChildren = (data || []).filter((c) => c.status === 5); + if (enrolledChildren.length === 1) { + setExpandedStudentId(enrolledChildren[0].student.id); + } }); setReloadFetch(false); } }, [selectedEstablishmentId, reloadFetch, user]); + // Charger les absences + useEffect(() => { + if (selectedEstablishmentId) { + fetchAbsences(selectedEstablishmentId) + .then((data) => setAllAbsences(data || [])) + .catch((error) => logger.error('Erreur fetch absences:', error)); + } + }, [selectedEstablishmentId]); + + // Charger les données détaillées quand un élève est étendu + useEffect(() => { + if (!expandedStudentId || !selectedPeriod || !selectedEstablishmentEvaluationFrequency) { + return; + } + + const expandedChild = children.find((c) => c.student.id === expandedStudentId); + if (!expandedChild || expandedChild.status !== 5) return; + + const loadDetails = async () => { + setDetailLoading(true); + const periodString = getPeriodString(selectedPeriod, selectedEstablishmentEvaluationFrequency); + + try { + // Charger les compétences + const competenciesData = await fetchStudentCompetencies(expandedStudentId, periodString); + setStudentCompetencies(competenciesData); + if (competenciesData?.data) { + const initialGrades = {}; + competenciesData.data.forEach((domaine) => { + domaine.categories.forEach((cat) => { + cat.competences.forEach((comp) => { + initialGrades[comp.competence_id] = comp.score ?? 0; + }); + }); + }); + setGrades(initialGrades); + } + + // Charger les évaluations si l'élève a une classe + if (expandedChild.student.associated_class_id) { + const [evalData, studentEvalData] = await Promise.all([ + fetchEvaluations( + selectedEstablishmentId, + expandedChild.student.associated_class_id, + periodString + ), + fetchStudentEvaluations(expandedStudentId, null, periodString, null) + ]); + setEvaluations(evalData || []); + setStudentEvaluationsData(studentEvalData || []); + } else { + setEvaluations([]); + setStudentEvaluationsData([]); + } + } catch (error) { + logger.error('Erreur lors du chargement des détails:', error); + } finally { + setDetailLoading(false); + } + }; + + loadDetails(); + }, [expandedStudentId, selectedPeriod, selectedEstablishmentEvaluationFrequency, children, selectedEstablishmentId]); + useEffect(() => { if (selectedEstablishmentId) { // Fetch des événements à venir @@ -132,153 +275,6 @@ export default function ParentHomePage() { setShowPlanning(true); }; - const childrenColumns = [ - { - name: 'photo', - transform: (row) => ( -
Aucune compétence évaluée pour cette période.
+ )} +Aucune évaluation pour cette période.
+ )} +Toute l'année scolaire
+