diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index 8cd4fd3..1e0a876 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -223,7 +223,7 @@ def makeToken(user): """ try: # Récupérer tous les rôles de l'utilisateur actifs - roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name') + roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency') # Générer le JWT avec la bonne syntaxe datetime access_payload = { diff --git a/Back-End/Establishment/models.py b/Back-End/Establishment/models.py index df5ad80..0440e0c 100644 --- a/Back-End/Establishment/models.py +++ b/Back-End/Establishment/models.py @@ -7,11 +7,17 @@ class StructureType(models.IntegerChoices): PRIMAIRE = 2, _('Primaire') SECONDAIRE = 3, _('Secondaire') +class EvaluationFrequency(models.IntegerChoices): + TRIMESTER = 1, _("Trimestre") + SEMESTER = 2, _("Semestre") + YEAR = 3, _("Année") + class Establishment(models.Model): name = models.CharField(max_length=255, unique=True) address = models.CharField(max_length=255) total_capacity = models.IntegerField() establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices)) + evaluation_frequency = models.IntegerField(choices=EvaluationFrequency.choices, default=EvaluationFrequency.TRIMESTER) licence_code = models.CharField(max_length=100, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 8c5488d..4699d00 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -56,7 +56,23 @@ def registration_photo_upload_to(instance, filename): return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}" def registration_bilan_form_upload_to(instance, filename): - return f"registration_files/dossier_rf_{instance.pk}/bilan/{filename}" + # On récupère le RegistrationForm lié à l'élève + register_form = getattr(instance.student, 'registrationform', None) + if register_form: + pk = register_form.pk + else: + # fallback sur l'id de l'élève si pas de registrationform + pk = instance.student.pk + return f"registration_files/dossier_rf_{pk}/bilan/{filename}" + +class BilanCompetence(models.Model): + student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans') + file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True) + period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.student} - {self.period}" class Student(models.Model): """ @@ -94,7 +110,6 @@ class Student(models.Model): birth_place = models.CharField(max_length=200, default="", blank=True) birth_postal_code = models.IntegerField(default=0, blank=True) attending_physician = models.CharField(max_length=200, default="", blank=True) - bilan_form = models.FileField(null=True,blank=True, upload_to=registration_bilan_form_upload_to) # Many-to-Many Relationship profiles = models.ManyToManyField('Auth.Profile', blank=True) @@ -187,6 +202,15 @@ class Student(models.Model): return self.birth_date.strftime('%d-%m-%Y') return None +class BilanCompetence(models.Model): + student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans') + file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True) + period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.student} - {self.period}" + class RegistrationFileGroup(models.Model): name = models.CharField(max_length=255, default="") description = models.TextField(blank=True, null=True) @@ -331,16 +355,22 @@ class StudentCompetency(models.Model): establishment_competency = models.ForeignKey('School.EstablishmentCompetency', on_delete=models.CASCADE, related_name='student_scores') score = models.IntegerField(null=True, blank=True) comment = models.TextField(blank=True, null=True) + period = models.CharField( + max_length=20, + help_text="Période d'évaluation ex: T1-2024_2025, S1-2024_2025, A-2024_2025", + default="", + blank=True + ) class Meta: - unique_together = ('student', 'establishment_competency') + unique_together = ('student', 'establishment_competency', 'period') indexes = [ - models.Index(fields=['student', 'establishment_competency']), + models.Index(fields=['student', 'establishment_competency', 'period']), ] def __str__(self): - return f"{self.student} - {self.establishment_competency} - Score: {self.score}" + return f"{self.student} - {self.establishment_competency} - Score: {self.score} - Period: {self.period}" ####### Parent files templates (par dossier d'inscription) ####### class RegistrationParentFileTemplate(models.Model): diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index a886840..ea4d524 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -10,18 +10,16 @@ from .models import ( RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate, - AbsenceManagement + AbsenceManagement, + BilanCompetence ) from School.models import SchoolClass, Fee, Discount, FeeType -from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import ProfileRole, Profile from Auth.serializers import ProfileSerializer, ProfileRoleSerializer -from GestionMessagerie.models import Messagerie from GestionNotification.models import Notification from N3wtSchool import settings from django.utils import timezone import pytz -from datetime import datetime import Subscriptions.util as util class AbsenceManagementSerializer(serializers.ModelSerializer): @@ -415,20 +413,26 @@ class GuardianByDICreationSerializer(serializers.ModelSerializer): return obj.profile_role.profile.id # Retourne l'ID du profil associé return None +class BilanCompetenceSerializer(serializers.ModelSerializer): + class Meta: + model = BilanCompetence + fields = ['id', 'file', 'period', 'created_at'] + class StudentByRFCreationSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) guardians = GuardianByDICreationSerializer(many=True, required=False) associated_class_name = serializers.SerializerMethodField() + bilans = BilanCompetenceSerializer(many=True, read_only=True) class Meta: model = Student - fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo'] + fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo', 'bilans'] def __init__(self, *args, **kwargs): super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs) for field in self.fields: self.fields[field].required = False - + def get_associated_class_name(self, obj): return obj.associated_class.atmosphere_name if obj.associated_class else None diff --git a/Back-End/Subscriptions/templates/pdfs/bilan_competences.html b/Back-End/Subscriptions/templates/pdfs/bilan_competences.html index 13478c0..6e64148 100644 --- a/Back-End/Subscriptions/templates/pdfs/bilan_competences.html +++ b/Back-End/Subscriptions/templates/pdfs/bilan_competences.html @@ -33,6 +33,7 @@ Élève : {{ student.last_name }} {{ student.first_name }}
Niveau : {{ student.level }}
Classe : {{ student.class_name }}
+ Période : {{ period }}
Date : {{ date }} @@ -70,5 +71,42 @@ {% endfor %} + +
+
+
+ Appréciation générale / Commentaire : +
+ +
+
+
+
+
+
+
+
+
+
+ Date : +   +
+
+ Signature : +   +
+
+
\ No newline at end of file diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 1d44d50..87e19ec 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -26,6 +26,7 @@ from Subscriptions.models import ( ) from Subscriptions.automate import updateStateMachine from School.models import EstablishmentCompetency +from Establishment.models import Establishment from N3wtSchool import settings, bdd from django.db.models import Q @@ -392,11 +393,28 @@ class RegisterFormWithIdView(APIView): competency__category__domain__cycle=cycle ) establishment_competencies = establishment_competencies.distinct() + + establishment = registerForm.establishment + evaluation_frequency = establishment.evaluation_frequency # 1=Trimestre, 2=Semestre, 3=Année + school_year = registerForm.school_year # ex: "2024_2025" + + establishment_competencies = establishment_competencies.distinct() + + periods = [] + if evaluation_frequency == 1: # Trimestre + periods = [f"T{i+1}_{school_year}" for i in range(3)] + elif evaluation_frequency == 2: # Semestre + periods = [f"S{i+1}_{school_year}" for i in range(2)] + elif evaluation_frequency == 3: # Année + periods = [f"A_{school_year}"] + for ec in establishment_competencies: - StudentCompetency.objects.get_or_create( - student=student, - establishment_competency=ec - ) + for period in periods: + StudentCompetency.objects.get_or_create( + student=student, + establishment_competency=ec, + period=period + ) except Exception as e: logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}") diff --git a/Back-End/Subscriptions/views/student_competencies_views.py b/Back-End/Subscriptions/views/student_competencies_views.py index fad8114..bbfa831 100644 --- a/Back-End/Subscriptions/views/student_competencies_views.py +++ b/Back-End/Subscriptions/views/student_competencies_views.py @@ -6,13 +6,14 @@ from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from django.utils.decorators import method_decorator from Subscriptions.models import StudentCompetency, Student from Common.models import Domain -from django.conf import settings +from Subscriptions.models import BilanCompetence from datetime import date from N3wtSchool.renderers import render_to_pdf from django.core.files import File from io import BytesIO import os import logging +from django.conf import settings logger = logging.getLogger(__name__) @@ -21,6 +22,7 @@ logger = logging.getLogger(__name__) class StudentCompetencyListCreateView(APIView): def get(self, request): student_id = request.GET.get('student_id') + period = request.GET.get('period') if not student_id: return JsonResponse({'error': 'student_id requis'}, status=400) try: @@ -28,7 +30,12 @@ class StudentCompetencyListCreateView(APIView): except Student.DoesNotExist: return JsonResponse({'error': 'Élève introuvable'}, status=404) - student_competencies = StudentCompetency.objects.filter(student=student).select_related( + # Filtrer par student ET period si period est fourni + filter_kwargs = {'student': student} + if period: + filter_kwargs['period'] = period + + student_competencies = StudentCompetency.objects.filter(**filter_kwargs).select_related( 'establishment_competency', 'establishment_competency__competency', 'establishment_competency__competency__category', @@ -64,6 +71,7 @@ class StudentCompetencyListCreateView(APIView): "nom": comp.name, "score": sc.score, "comment": sc.comment or "", + "period":sc.period or "" }) total_competencies += 1 # Cas compétence custom @@ -73,6 +81,7 @@ class StudentCompetencyListCreateView(APIView): "nom": ec.custom_name, "score": sc.score, "comment": sc.comment or "", + "period":sc.period or "" }) total_competencies += 1 if categorie_dict["competences"]: @@ -99,6 +108,7 @@ class StudentCompetencyListCreateView(APIView): comp_id = item.get("competenceId") grade = item.get("grade") student_id = item.get('studentId') + period = item.get('period') if comp_id is None or grade is None: errors.append({"competenceId": comp_id, "error": "champ manquant"}) continue @@ -106,7 +116,8 @@ class StudentCompetencyListCreateView(APIView): # Ajoute le filtre student_id sc = StudentCompetency.objects.get( establishment_competency_id=comp_id, - student_id=student_id + student_id=student_id, + period=period ) sc.score = grade sc.save() @@ -117,8 +128,8 @@ class StudentCompetencyListCreateView(APIView): # Génération du PDF si au moins une compétence a été mise à jour if updated: student = Student.objects.get(id=student_id) - # Reconstituer la structure "domaines" comme dans le GET - student_competencies = StudentCompetency.objects.filter(student=student).select_related( + # Reconstituer la structure "domaines" pour la période concernée uniquement + student_competencies = StudentCompetency.objects.filter(student=student, period=period).select_related( 'establishment_competency', 'establishment_competency__competency', 'establishment_competency__competency__category', @@ -158,7 +169,7 @@ class StudentCompetencyListCreateView(APIView): domaine_dict["categories"].append(categorie_dict) if domaine_dict["categories"]: result.append(domaine_dict) - + context = { "student": { "first_name": student.first_name, @@ -166,37 +177,38 @@ class StudentCompetencyListCreateView(APIView): "level": student.level, "class_name": student.associated_class.atmosphere_name, }, + "period": period, "date": date.today().strftime("%d/%m/%Y"), "domaines": result, } - print('génération du PDF...') + pdf_result = render_to_pdf('pdfs/bilan_competences.html', context) - - # Vérifier si un fichier bilan_form existe déjà et le supprimer - if student.bilan_form and student.bilan_form.name: - if os.path.isabs(student.bilan_form.path): - existing_file_path = student.bilan_form.path - else: - existing_file_path = os.path.join(settings.MEDIA_ROOT, student.bilan_form.name.lstrip('/')) - - if os.path.exists(existing_file_path): - os.remove(existing_file_path) - student.bilan_form.delete(save=False) - logger.info(f"Ancien PDF supprimé : {existing_file_path}") - else: - logger.info(f"File does not exist: {existing_file_path}") + try: - filename = f"bilan_competences_{student.last_name}_{student.first_name}.pdf" - student.bilan_form.save( - os.path.basename(filename), # Utiliser uniquement le nom de fichier + filename = f"bilan_competences_{student.last_name}_{student.first_name}_{period}.pdf" + # Vérifier si un bilan existe déjà pour cet élève et cette période + existing_bilan = BilanCompetence.objects.filter(student=student, period=period).first() + if existing_bilan: + # Supprimer le fichier physique si présent + if existing_bilan.file and existing_bilan.file.name: + file_path = existing_bilan.file.path + if os.path.exists(file_path): + os.remove(file_path) + existing_bilan.delete() + + bilan = BilanCompetence.objects.create( + student=student, + period=period + ) + bilan.file.save( + os.path.basename(filename), File(BytesIO(pdf_result.content)), save=True ) except Exception as e: logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}") raise - # Exemple : retour du PDF dans la réponse HTTP (pour test) return JsonResponse({"updated": updated, "errors": errors}, status=200) @@ -204,18 +216,4 @@ class StudentCompetencyListCreateView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class StudentCompetencySimpleView(APIView): def get(self, request, id): - return JsonResponse("ok", safe=False, status=status.HTTP_200_OK) - - # 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) \ No newline at end of file + return JsonResponse("ok", safe=False, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py index 47ee3b9..6a43ba9 100644 --- a/Back-End/Subscriptions/views/student_views.py +++ b/Back-End/Subscriptions/views/student_views.py @@ -143,7 +143,7 @@ def search_students(request): 'last_name': student.last_name, 'level': getattr(student.level, 'name', ''), 'associated_class_name': student.associated_class.atmosphere_name if student.associated_class else '', - 'photo': student.photo.url if student.photo else None, + 'photo': student.photo.url if student.photo else None } for student in students ] diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index fd016a6..32aaec3 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -12,6 +12,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@tailwindcss/forms": "^0.5.9", "date-fns": "^4.1.0", + "dayjs": "^1.11.13", "framer-motion": "^11.11.11", "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", @@ -1960,6 +1961,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -7897,6 +7904,11 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/Front-End/package.json b/Front-End/package.json index 645b6c1..46c0e26 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@tailwindcss/forms": "^0.5.9", "date-fns": "^4.1.0", + "dayjs": "^1.11.13", "framer-motion": "^11.11.11", "ics": "^3.8.1", "jsonwebtoken": "^9.0.2", diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js index 293c80e..2dfc9e9 100644 --- a/Front-End/src/app/[locale]/admin/grades/page.js +++ b/Front-End/src/app/[locale]/admin/grades/page.js @@ -23,14 +23,16 @@ import { } from '@/app/actions/subscriptionAction'; import { useEstablishment } from '@/context/EstablishmentContext'; import { useClasses } from '@/context/ClassesContext'; -import StudentInput from '@/components/Grades/StudentInput'; -import { Award, BookOpen } from 'lucide-react'; +import { Award, BookOpen, FileText } from 'lucide-react'; import SectionHeader from '@/components/SectionHeader'; import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; +import InputText from '@/components/InputText'; +import dayjs from 'dayjs'; export default function Page() { const router = useRouter(); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = + useEstablishment(); const { getNiveauLabel } = useClasses(); const [formData, setFormData] = useState({ selectedStudent: null, @@ -39,6 +41,48 @@ export default function Page() { const [students, setStudents] = useState([]); const [studentCompetencies, setStudentCompetencies] = useState(null); const [grades, setGrades] = useState({}); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedPeriod, setSelectedPeriod] = useState(null); + + // Définir les périodes selon la fréquence + const getPeriods = () => { + if (selectedEstablishmentEvaluationFrequency === 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 (selectedEstablishmentEvaluationFrequency === 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 (selectedEstablishmentEvaluationFrequency === 3) { + return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }]; + } + return []; + }; + + // Sélection automatique de la période courante + useEffect(() => { + if (!formData.selectedStudent) { + setSelectedPeriod(null); + return; + } + const periods = getPeriods(); + 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 : null); + }, [formData.selectedStudent, selectedEstablishmentEvaluationFrequency]); const academicResults = [ { @@ -122,8 +166,12 @@ export default function Page() { // Charger les compétences et générer les grades à chaque changement d'élève sélectionné useEffect(() => { - if (formData.selectedStudent) { - fetchStudentCompetencies(formData.selectedStudent) + if (formData.selectedStudent && selectedPeriod) { + const periodString = getPeriodString( + selectedPeriod, + selectedEstablishmentEvaluationFrequency + ); + fetchStudentCompetencies(formData.selectedStudent, periodString) .then((data) => { setStudentCompetencies(data); // Générer les grades à partir du retour API @@ -146,105 +194,218 @@ export default function Page() { setGrades({}); setStudentCompetencies(null); } - }, [formData.selectedStudent]); + }, [formData.selectedStudent, selectedPeriod]); + + // Fonction utilitaire pour convertir la période sélectionnée en string backend + function getPeriodString(selectedPeriod, frequency) { + const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre + 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 ''; + } return (
- {/* Sélection de l'élève */} -
- {/* Recherche élève + bouton + fiche élève */} -
+ + {/* Section haute : filtre + bouton + photo élève */} +
+ {/* Colonne gauche : InputText + bouton */} +
- s.id === formData.selectedStudent) || null - } - setSelectedStudent={(student) => - handleChange('selectedStudent', student?.id || '') - } - searchStudents={searchStudents} - establishmentId={selectedEstablishmentId} - required + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + placeholder="Rechercher un élève" + required={false} + enable={true} />
+ {/* Sélecteur de période */} {formData.selectedStudent && ( -
- {(() => { - const student = students.find( - (s) => s.id === formData.selectedStudent - ); - if (!student) return null; - return ( - <> - {student.photo ? ( - {`${student.first_name} - ) : ( -
- {student.first_name?.[0]} - {student.last_name?.[0]} -
- )} -
-
- {student.last_name} {student.first_name} -
-
- Niveau :{' '} - - {getNiveauLabel(student.level)} - -
-
- Classe :{' '} - - {student.associated_class_name} - -
-
- {/* Bouton bilan de compétences en dessous */} -
+ {/* Colonne droite : Photo élève */} +
+ {formData.selectedStudent && + (() => { + const student = students.find( + (s) => s.id === formData.selectedStudent + ); + if (!student) return null; + return ( + <> + {student.photo ? ( + {`${student.first_name} - - ); - })()} + ) : ( +
+ {student.first_name?.[0]} + {student.last_name?.[0]} +
+ )} + + ); + })()} +
+
+ + {/* Section basse : liste élèves + infos */} +
+ {/* Colonne 1 : Liste des élèves */} +
+

+ Liste des élèves +

+
    + {students + .filter( + (student) => + !searchTerm || + `${student.last_name} ${student.first_name}` + .toLowerCase() + .includes(searchTerm.toLowerCase()) + ) + .map((student) => ( +
  • handleChange('selectedStudent', student.id)} + > + {student.photo ? ( + {`${student.first_name} + ) : ( +
    + {student.first_name?.[0]} + {student.last_name?.[0]} +
    + )} +
    +
    + {student.last_name} {student.first_name} +
    +
    + Niveau :{' '} + + {getNiveauLabel(student.level)} + + {' | '} + Classe :{' '} + + {student.associated_class_name} + +
    +
    + {/* Icône PDF si bilan dispo pour la période sélectionnée */} + {selectedPeriod && + student.bilans && + Array.isArray(student.bilans) && + (() => { + // Génère la string de période attendue + const periodString = getPeriodString( + selectedPeriod, + selectedEstablishmentEvaluationFrequency + ); + const bilan = student.bilans.find( + (b) => b.period === periodString && b.file + ); + if (bilan) { + return ( + e.stopPropagation()} // Pour ne pas sélectionner à nouveau l'élève + > + + + ); + } + return null; + })()} +
  • + ))} +
+
+ {/* Colonne 2 : Reste des infos */} +
+ {formData.selectedStudent && ( +
+
+
+ +
+
+ +
+
+
+ +
)}
- {/* Partie basse : stats à gauche, présence à droite, sans bg */} - {formData.selectedStudent && ( -
-
-
- -
-
- -
-
-
- -
-
- )}
); } diff --git a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js index ff75ab3..612faa0 100644 --- a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js +++ b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js @@ -21,9 +21,10 @@ export default function StudentCompetenciesPage() { const [studentCompetencies, setStudentCompetencies] = useState([]); const [grades, setGrades] = useState({}); const studentId = searchParams.get('studentId'); + const period = searchParams.get('period'); useEffect(() => { - fetchStudentCompetencies(studentId) + fetchStudentCompetencies(studentId, period) .then((data) => { setStudentCompetencies(data); }) @@ -64,6 +65,7 @@ export default function StudentCompetenciesPage() { studentId, competenceId, grade: score, + period: period, })); editStudentCompetencies(data, csrfToken) .then(() => { diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js index 18a4ada..7258218 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -42,7 +42,7 @@ import { import { fetchProfiles } from '@/app/actions/authAction'; import { useClasses } from '@/context/ClassesContext'; import { useCsrfToken } from '@/context/CsrfContext'; -import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; +import { FE_ADMIN_SUBSCRIPTIONS_URL, BASE_URL } from '@/utils/Url'; export default function CreateSubscriptionPage() { const [formData, setFormData] = useState({ diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index 7e98b96..04d246b 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -24,16 +24,18 @@ export const editStudentCompetencies = (data, csrfToken) => { return fetch(request).then(requestResponseHandler).catch(errorHandler); }; -export const fetchStudentCompetencies = (id) => { - const request = new Request( - `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ); +export const fetchStudentCompetencies = (id, period) => { + // Si period est vide, ne pas l'ajouter à l'URL + const url = period + ? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}` + : `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`; + + const request = new Request(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); return fetch(request).then(requestResponseHandler).catch(errorHandler); }; diff --git a/Front-End/src/components/FlashNotification.js b/Front-End/src/components/FlashNotification.js index 0f0c1e4..079ef6d 100644 --- a/Front-End/src/components/FlashNotification.js +++ b/Front-End/src/components/FlashNotification.js @@ -22,7 +22,7 @@ const typeStyles = { }; export default function FlashNotification({ - displayPeriod = 3000, + displayPeriod = 5000, title, message, type = 'info', diff --git a/Front-End/src/components/Grades/GradesDomainBarChart.js b/Front-End/src/components/Grades/GradesDomainBarChart.js index 07f459f..c1ac39f 100644 --- a/Front-End/src/components/Grades/GradesDomainBarChart.js +++ b/Front-End/src/components/Grades/GradesDomainBarChart.js @@ -32,7 +32,7 @@ export default function GradesDomainBarChart({ studentCompetencies }) { }; return ( -
+

Moyenne par domaine

{domainStats.map((d) => ( diff --git a/Front-End/src/components/Grades/StudentInput.js b/Front-End/src/components/Grades/StudentInput.js deleted file mode 100644 index 536a1c1..0000000 --- a/Front-End/src/components/Grades/StudentInput.js +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { BASE_URL } from '@/utils/Url'; - -export default function StudentInput({ - label, - selectedStudent, - setSelectedStudent, - searchStudents, - establishmentId, - required = false, -}) { - const [inputValue, setInputValue] = useState(''); - const [suggestions, setSuggestions] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(-1); - - // Désélectionner si l'input ne correspond plus à l'élève sélectionné - useEffect(() => { - if ( - selectedStudent && - inputValue !== - `${selectedStudent.last_name} ${selectedStudent.first_name}` - ) { - setSelectedStudent(null); - } - // eslint-disable-next-line - }, [inputValue]); - - const handleInputChange = async (e) => { - const value = e.target.value; - setInputValue(value); - - if (value.trim() !== '') { - try { - const results = await searchStudents(establishmentId, value); - setSuggestions(results); - } catch { - setSuggestions([]); - } - } else { - setSuggestions([]); - } - }; - - const handleSuggestionClick = (student) => { - setSelectedStudent(student); - setInputValue(`${student.last_name} ${student.first_name}`); - setSuggestions([]); - }; - - const handleKeyDown = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - if (selectedIndex >= 0 && selectedIndex < suggestions.length) { - handleSuggestionClick(suggestions[selectedIndex]); - } - } else if (e.key === 'ArrowDown') { - setSelectedIndex((prev) => - prev < suggestions.length - 1 ? prev + 1 : 0 - ); - } else if (e.key === 'ArrowUp') { - setSelectedIndex((prev) => - prev > 0 ? prev - 1 : suggestions.length - 1 - ); - } - }; - - return ( -
- - - {suggestions.length > 0 && ( -
    - {suggestions.map((student, idx) => ( -
  • handleSuggestionClick(student)} - onMouseEnter={() => setSelectedIndex(idx)} - > - {student.photo ? ( - {`${student.first_name} - ) : ( -
    - {student.first_name?.[0]} - {student.last_name?.[0]} -
    - )} - - {student.last_name} {student.first_name} ({student.level}) -{' '} - {student.associated_class_name} - -
  • - ))} -
- )} -
- ); -} diff --git a/Front-End/src/context/EstablishmentContext.js b/Front-End/src/context/EstablishmentContext.js index 0accf95..30ad723 100644 --- a/Front-End/src/context/EstablishmentContext.js +++ b/Front-End/src/context/EstablishmentContext.js @@ -12,6 +12,15 @@ export const EstablishmentProvider = ({ children }) => { return storedEstablishmentId; } ); + const [ + selectedEstablishmentEvaluationFrequency, + setSelectedEstablishmentEvaluationFrequencyState, + ] = useState(() => { + const storedEstablishmentEvaluationFrequency = +sessionStorage.getItem( + 'selectedEstablishmentEvaluationFrequency' + ); + return storedEstablishmentEvaluationFrequency; + }); const [selectedRoleId, setSelectedRoleIdState] = useState(() => { const storedRoleId = +sessionStorage.getItem('selectedRoleId'); return storedRoleId; @@ -36,6 +45,12 @@ export const EstablishmentProvider = ({ children }) => { sessionStorage.setItem('selectedEstablishmentId', id); }; + const setSelectedEstablishmentEvaluationFrequency = (id) => { + setSelectedEstablishmentEvaluationFrequencyState(id); + logger.debug('setSelectedEstablishmentEvaluationFrequency', id); + sessionStorage.setItem('selectedEstablishmentEvaluationFrequency', id); + }; + const setSelectedRoleId = (id) => { setSelectedRoleIdState(id); sessionStorage.setItem('selectedRoleId', id); @@ -72,6 +87,7 @@ export const EstablishmentProvider = ({ children }) => { const userEstablishments = user.roles.map((role, i) => ({ id: role.establishment__id, name: role.establishment__name, + evaluation_frequency: role.establishment__evaluation_frequency, role_id: i, role_type: role.role_type, })); @@ -85,6 +101,9 @@ export const EstablishmentProvider = ({ children }) => { setSelectedRoleId(roleIndexDefault); if (userEstablishments.length > 0) { setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id); + setSelectedEstablishmentEvaluationFrequency( + userEstablishments[roleIndexDefault].evaluation_frequency + ); setProfileRole(userEstablishments[roleIndexDefault].role_type); } if (endInitFunctionHandler) { @@ -112,6 +131,8 @@ export const EstablishmentProvider = ({ children }) => { clearContext, selectedEstablishmentId, setSelectedEstablishmentId, + selectedEstablishmentEvaluationFrequency, + setSelectedEstablishmentEvaluationFrequency, selectedRoleId, setSelectedRoleId, profileRole,