diff --git a/Back-End/Common/models.py b/Back-End/Common/models.py index 3fd3a1f..990f8da 100644 --- a/Back-End/Common/models.py +++ b/Back-End/Common/models.py @@ -14,22 +14,6 @@ class Category(models.Model): def __str__(self): return self.name -class Competency(models.Model): - name = models.TextField() - end_of_cycle = models.BooleanField(default=False, null=True, blank=True) - level = models.CharField(max_length=50, null=True, blank=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='competencies') - - establishments = models.ManyToManyField( - 'Establishment.Establishment', - through='School.EstablishmentCompetency', - related_name='competencies', - blank=True - ) - - def __str__(self): - return self.name - class PaymentPlanType(models.Model): code = models.CharField(max_length=50, unique=True) label = models.CharField(max_length=255) diff --git a/Back-End/Common/serializers.py b/Back-End/Common/serializers.py index 5b79b63..7d4737f 100644 --- a/Back-End/Common/serializers.py +++ b/Back-End/Common/serializers.py @@ -1,8 +1,7 @@ from rest_framework import serializers from Common.models import ( Domain, - Category, - Competency + Category ) class DomainSerializer(serializers.ModelSerializer): @@ -13,9 +12,4 @@ class DomainSerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category - fields = '__all__' - -class CompetencySerializer(serializers.ModelSerializer): - class Meta: - model = Competency fields = '__all__' \ No newline at end of file diff --git a/Back-End/Common/signals.py b/Back-End/Common/signals.py index 72dbc58..7ffd67c 100644 --- a/Back-End/Common/signals.py +++ b/Back-End/Common/signals.py @@ -2,7 +2,8 @@ import json import os from django.db.models.signals import post_migrate from django.dispatch import receiver -from Common.models import Domain, Category, Competency, PaymentModeType, PaymentPlanType, Cycle, Level +from Common.models import Domain, Category, PaymentModeType, PaymentPlanType, Cycle, Level +from School.models import Competency @receiver(post_migrate) def common_post_migrate(sender, **kwargs): diff --git a/Back-End/Common/urls.py b/Back-End/Common/urls.py index 9c0fd0b..4dd0ba9 100644 --- a/Back-End/Common/urls.py +++ b/Back-End/Common/urls.py @@ -3,7 +3,6 @@ from django.urls import path, re_path from .views import ( DomainListCreateView, DomainDetailView, CategoryListCreateView, CategoryDetailView, - CompetencyListCreateView, CompetencyDetailView, ) urlpatterns = [ @@ -12,7 +11,4 @@ urlpatterns = [ re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"), re_path(r'^categories/(?P[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"), - - re_path(r'^competencies$', CompetencyListCreateView.as_view(), name="competency_list_create"), - re_path(r'^competencies/(?P[0-9]+)$', CompetencyDetailView.as_view(), name="competency_detail"), ] \ No newline at end of file diff --git a/Back-End/Common/views.py b/Back-End/Common/views.py index b650bc4..a8fa8e2 100644 --- a/Back-End/Common/views.py +++ b/Back-End/Common/views.py @@ -3,15 +3,14 @@ from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from django.utils.decorators import method_decorator from rest_framework.parsers import JSONParser from rest_framework.views import APIView +from rest_framework import status from .models import ( Domain, - Category, - Competency + Category ) from .serializers import ( DomainSerializer, - CategorySerializer, - CompetencySerializer + CategorySerializer ) @method_decorator(csrf_protect, name='dispatch') @@ -109,57 +108,3 @@ class CategoryDetailView(APIView): return JsonResponse({'message': 'Deleted'}, safe=False) except Category.DoesNotExist: return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class CompetencyListCreateView(APIView): - def get(self, request): - cycle = request.GET.get('cycle') - if cycle is None: - return JsonResponse({'error': 'cycle est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - competencies_list = getAllObjects(Competency) - competencies_list = competencies_list.filter( - category__domain__cycle=cycle - ).distinct() - serializer = CompetencySerializer(competencies_list, many=True) - return JsonResponse(serializer.data, safe=False) - - def post(self, request): - data = JSONParser().parse(request) - serializer = CompetencySerializer(data=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 CompetencyDetailView(APIView): - def get(self, request, id): - try: - competency = Competency.objects.get(id=id) - serializer = CompetencySerializer(competency) - return JsonResponse(serializer.data, safe=False) - except Competency.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) - - def put(self, request, id): - try: - competency = Competency.objects.get(id=id) - except Competency.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) - data = JSONParser().parse(request) - serializer = CompetencySerializer(competency, data=data, partial=True) - if serializer.is_valid(): - serializer.save() - return JsonResponse(serializer.data, safe=False) - return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, id): - try: - competency = Competency.objects.get(id=id) - competency.delete() - return JsonResponse({'message': 'Deleted'}, safe=False) - except Competency.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Establishment/views.py b/Back-End/Establishment/views.py index 697a0fd..052aa0b 100644 --- a/Back-End/Establishment/views.py +++ b/Back-End/Establishment/views.py @@ -7,8 +7,7 @@ from rest_framework import status from .models import Establishment from .serializers import EstablishmentSerializer from N3wtSchool.bdd import delete_object, getAllObjects -from School.models import EstablishmentCompetency -from Common.models import Competency +from School.models import EstablishmentCompetency, Competency from django.db.models import Q @method_decorator(csrf_protect, name='dispatch') diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 98185c9..6975363 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -116,6 +116,22 @@ class PaymentMode(models.Model): def __str__(self): return f"{self.mode.label} - {self.get_type_display()}" +class Competency(models.Model): + name = models.TextField() + end_of_cycle = models.BooleanField(default=False, null=True, blank=True) + level = models.CharField(max_length=50, null=True, blank=True) + category = models.ForeignKey('Common.Category', on_delete=models.CASCADE, related_name='competencies') + + establishments = models.ManyToManyField( + 'Establishment.Establishment', + through='School.EstablishmentCompetency', + related_name='competencies', + blank=True + ) + + def __str__(self): + return self.name + class EstablishmentCompetency(models.Model): """ Relation entre un établissement et une compétence. @@ -123,7 +139,7 @@ class EstablishmentCompetency(models.Model): """ establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE) competency = models.ForeignKey( - 'Common.Competency', on_delete=models.CASCADE, null=True, blank=True, + 'School.Competency', on_delete=models.CASCADE, null=True, blank=True, help_text="Compétence de référence (optionnelle si custom)" ) custom_name = models.TextField(null=True, blank=True, help_text="Nom de la compétence custom") diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index b0758de..801b1c5 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -9,7 +9,8 @@ from .models import ( Fee, PaymentPlan, PaymentMode, - EstablishmentCompetency + EstablishmentCompetency, + Competency ) from Auth.models import Profile, ProfileRole from Subscriptions.models import Student @@ -19,6 +20,12 @@ from N3wtSchool import settings from django.utils import timezone import pytz + +class CompetencySerializer(serializers.ModelSerializer): + class Meta: + model = Competency + fields = '__all__' + class EstablishmentCompetencySerializer(serializers.ModelSerializer): class Meta: model = EstablishmentCompetency diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 49af99a..9cec102 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -9,6 +9,7 @@ from .views import ( DiscountListCreateView, DiscountDetailView, PaymentPlanListCreateView, PaymentPlanDetailView, PaymentModeListCreateView, PaymentModeDetailView, + CompetencyListCreateView, CompetencyDetailView, EstablishmentCompetencyListCreateView, EstablishmentCompetencyDetailView, ) @@ -37,6 +38,9 @@ urlpatterns = [ 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'^competencies$', CompetencyListCreateView.as_view(), name="competency_list_create"), + re_path(r'^competencies/(?P[0-9]+)$', CompetencyDetailView.as_view(), name="competency_detail"), + re_path(r'^establishmentCompetencies$', EstablishmentCompetencyListCreateView.as_view(), name="establishment_competency_list_create"), re_path(r'^establishmentCompetencies/(?P[0-9]+)$', EstablishmentCompetencyDetailView.as_view(), name="establishment_competency_detail"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 330db63..3cc91d3 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -13,7 +13,8 @@ from .models import ( Fee, PaymentPlan, PaymentMode, - EstablishmentCompetency + EstablishmentCompetency, + Competency ) from .serializers import ( TeacherSerializer, @@ -24,12 +25,14 @@ from .serializers import ( FeeSerializer, PaymentPlanSerializer, PaymentModeSerializer, - EstablishmentCompetencySerializer + EstablishmentCompetencySerializer, + CompetencySerializer ) -from Common.models import Domain, Category, Competency +from Common.models import Domain, Category from N3wtSchool.bdd import delete_object, getAllObjects, getObject from django.db.models import Q from collections import defaultdict +from Subscriptions.models import Student, StudentCompetency @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') @@ -417,6 +420,60 @@ 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 CompetencyListCreateView(APIView): + def get(self, request): + cycle = request.GET.get('cycle') + if cycle is None: + return JsonResponse({'error': 'cycle est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + + competencies_list = getAllObjects(Competency) + competencies_list = competencies_list.filter( + category__domain__cycle=cycle + ).distinct() + serializer = CompetencySerializer(competencies_list, many=True) + return JsonResponse(serializer.data, safe=False) + + def post(self, request): + data = JSONParser().parse(request) + serializer = CompetencySerializer(data=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 CompetencyDetailView(APIView): + def get(self, request, id): + try: + competency = Competency.objects.get(id=id) + serializer = CompetencySerializer(competency) + return JsonResponse(serializer.data, safe=False) + except Competency.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) + + def put(self, request, id): + try: + competency = Competency.objects.get(id=id) + except Competency.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) + data = JSONParser().parse(request) + serializer = CompetencySerializer(competency, data=data, partial=True) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, safe=False) + return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, id): + try: + competency = Competency.objects.get(id=id) + competency.delete() + return JsonResponse({'message': 'Deleted'}, safe=False) + except Competency.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) + @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class EstablishmentCompetencyListCreateView(APIView): @@ -541,6 +598,14 @@ class EstablishmentCompetencyListCreateView(APIView): custom_category=category, is_required=False ) + # Associer à tous les élèves de l'établissement + students = Student.objects.filter(associated_class__establishment_id=establishment_id) + for student in students: + StudentCompetency.objects.get_or_create( + student=student, + establishment_competency=ec + ) + created.append({ "competence_id": ec.id, "nom": ec.custom_name, @@ -558,6 +623,7 @@ class EstablishmentCompetencyListCreateView(APIView): def delete(self, request): """ Supprime une ou plusieurs compétences custom (EstablishmentCompetency) à partir d'une liste d'IDs. + Supprime aussi les StudentCompetency associés. Attendu dans le body : { "ids": [1, 2, 3, ...] @@ -571,6 +637,8 @@ class EstablishmentCompetencyListCreateView(APIView): for ec_id in ids: try: ec = EstablishmentCompetency.objects.get(id=ec_id) + # Supprimer les StudentCompetency associés + StudentCompetency.objects.filter(establishment_competency=ec).delete() ec.delete() deleted.append(ec_id) except EstablishmentCompetency.DoesNotExist: diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 5068ae1..d4e4f1f 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -324,15 +324,19 @@ class RegistrationSchoolFileTemplate(models.Model): class StudentCompetency(models.Model): student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores') - competency = models.ForeignKey('Common.Competency', on_delete=models.CASCADE, related_name='student_scores') + 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) class Meta: - unique_together = ('student', 'competency') + unique_together = ('student', 'establishment_competency') + + indexes = [ + models.Index(fields=['student', 'establishment_competency']), + ] def __str__(self): - return f"{self.student} - {self.competency.name} - Score: {self.score}" + return f"{self.student} - {self.establishment_competency} - Score: {self.score}" ####### Parent files templates (par dossier d'inscription) ####### class RegistrationParentFileTemplate(models.Model): diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 45e7d07..059cf75 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -17,7 +17,6 @@ import Subscriptions.util as util from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer from Subscriptions.pagination import CustomSubscriptionPagination from Subscriptions.models import ( - Student, Guardian, RegistrationForm, RegistrationSchoolFileTemplate, @@ -26,7 +25,7 @@ from Subscriptions.models import ( StudentCompetency ) from Subscriptions.automate import updateStateMachine -from Common.models import Competency +from School.models import EstablishmentCompetency from N3wtSchool import settings, bdd from django.db.models import Q @@ -246,6 +245,7 @@ class RegisterFormWithIdView(APIView): """ studentForm_data = request.data.get('data', '{}') + try: data = json.loads(studentForm_data) except json.JSONDecodeError: @@ -306,13 +306,13 @@ class RegisterFormWithIdView(APIView): # L'école doit désormais valider le dossier d'inscription try: # Génération de la fiche d'inscription au format PDF - base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}") - os.makedirs(base_dir, exist_ok=True) + # base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}") + # os.makedirs(base_dir, exist_ok=True) - # Fichier PDF initial - initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" - registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) - registerForm.save() + # # Fichier PDF initial + # initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + # registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) + # registerForm.save() # Mise à jour de l'automate # Vérification de la présence du fichier SEPA @@ -376,7 +376,6 @@ class RegisterFormWithIdView(APIView): File(merged_pdf_content), save=True ) - # Valorisation des StudentCompetency pour l'élève try: student = registerForm.student @@ -384,15 +383,19 @@ class RegisterFormWithIdView(APIView): if student.level: cycle = student.level.cycle.number if cycle: - competencies = Competency.objects.filter( - category__domain__cycle=cycle - ).filter( - Q(end_of_cycle=True) | Q(level=student.level.name) + # Récupérer les EstablishmentCompetency de l'établissement et du cycle de l'élève + establishment_competencies = EstablishmentCompetency.objects.filter( + establishment=registerForm.establishment, + custom_category__domain__cycle=cycle + ) | EstablishmentCompetency.objects.filter( + establishment=registerForm.establishment, + competency__category__domain__cycle=cycle ) - for comp in competencies: + establishment_competencies = establishment_competencies.distinct() + for ec in establishment_competencies: StudentCompetency.objects.get_or_create( student=student, - competency=comp + establishment_competency=ec ) 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 40e8732..d3f8ce4 100644 --- a/Back-End/Subscriptions/views/student_competencies_views.py +++ b/Back-End/Subscriptions/views/student_competencies_views.py @@ -6,7 +6,8 @@ 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.models import StudentCompetency, Student -from Common.models import Domain, Competency +from Common.models import Domain +from School.models import Competency from N3wtSchool.bdd import delete_object @method_decorator(csrf_protect, name='dispatch') @@ -21,10 +22,14 @@ class StudentCompetencyListCreateView(APIView): except Student.DoesNotExist: return JsonResponse({'error': 'Élève introuvable'}, status=404) - student_competencies = StudentCompetency.objects.filter(student=student).select_related('competency', 'competency__category', 'competency__category__domain') - - # On ne garde que les IDs des compétences de l'élève - student_competency_ids = set(sc.competency_id for sc in student_competencies) + student_competencies = StudentCompetency.objects.filter(student=student).select_related( + 'establishment_competency', + 'establishment_competency__competency', + 'establishment_competency__competency__category', + 'establishment_competency__competency__category__domain', + 'establishment_competency__custom_category', + 'establishment_competency__custom_category__domain', + ) result = [] total_competencies = 0 @@ -44,15 +49,26 @@ class StudentCompetencyListCreateView(APIView): } # On ne boucle que sur les compétences du student pour cette catégorie for sc in student_competencies: - comp = sc.competency - if comp.category_id == categorie.id: + ec = sc.establishment_competency + # Cas compétence de référence + if ec.competency and ec.competency.category_id == categorie.id: + comp = ec.competency categorie_dict["competences"].append({ - "competence_id": comp.id, + "competence_id": ec.id, # <-- retourne l'id de l'EstablishmentCompetency "nom": comp.name, "score": sc.score, "comment": sc.comment or "", }) total_competencies += 1 + # Cas compétence custom + elif ec.competency is None and ec.custom_category_id == categorie.id: + categorie_dict["competences"].append({ + "competence_id": ec.id, # <-- retourne l'id de l'EstablishmentCompetency + "nom": ec.custom_name, + "score": sc.score, + "comment": sc.comment or "", + }) + total_competencies += 1 if categorie_dict["competences"]: domaine_dict["categories"].append(categorie_dict) if domaine_dict["categories"]: @@ -77,14 +93,13 @@ class StudentCompetencyListCreateView(APIView): comp_id = item.get("competenceId") grade = item.get("grade") student_id = item.get('studentId') - print(f'lecture des données : {comp_id} - {grade} - {student_id}') if comp_id is None or grade is None: errors.append({"competenceId": comp_id, "error": "champ manquant"}) continue try: # Ajoute le filtre student_id sc = StudentCompetency.objects.get( - competency_id=comp_id, + establishment_competency_id=comp_id, student_id=student_id ) sc.score = grade diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index bf0aad5..fd016a6 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -23,6 +23,7 @@ "next-logger": "^5.0.1", "pino": "^9.6.0", "react": "^18", + "react-circular-progressbar": "^2.2.0", "react-cookie": "^7.2.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -5015,6 +5016,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-circular-progressbar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz", + "integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==", + "license": "MIT", + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/react-cookie": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz", @@ -10037,6 +10047,12 @@ "loose-envify": "^1.1.0" } }, + "react-circular-progressbar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz", + "integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==", + "requires": {} + }, "react-cookie": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz", diff --git a/Front-End/package.json b/Front-End/package.json index 15a8fdc..645b6c1 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -26,6 +26,7 @@ "next-logger": "^5.0.1", "pino": "^9.6.0", "react": "^18", + "react-circular-progressbar": "^2.2.0", "react-cookie": "^7.2.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js index 8a4e599..efa3c27 100644 --- a/Front-End/src/app/[locale]/admin/grades/page.js +++ b/Front-End/src/app/[locale]/admin/grades/page.js @@ -8,11 +8,15 @@ import WorkPlan from '@/components/Grades/WorkPlan'; import Homeworks from '@/components/Grades/Homeworks'; import SpecificEvaluations from '@/components/Grades/SpecificEvaluations'; import Orientation from '@/components/Grades/Orientation'; +import GradesStatsCircle from '@/components/Grades/GradesStatsCircle'; import Button from '@/components/Button'; +import logger from '@/utils/logger'; import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url'; import { useRouter } from 'next/navigation'; -import { fetchStudents } from '@/app/actions/subscriptionAction'; - +import { + fetchStudents, + fetchStudentCompetencies, +} from '@/app/actions/subscriptionAction'; import { useEstablishment } from '@/context/EstablishmentContext'; import { useClasses } from '@/context/ClassesContext'; @@ -25,6 +29,8 @@ export default function Page() { }); const [students, setStudents] = useState([]); + const [studentCompetencies, setStudentCompetencies] = useState(null); + const [grades, setGrades] = useState({}); const academicResults = [ { @@ -106,6 +112,34 @@ export default function Page() { } }, [selectedEstablishmentId]); + // Charger les compétences et générer les grades à chaque changement d'élève sélectionné + useEffect(() => { + if (formData.selectedStudent) { + fetchStudentCompetencies(formData.selectedStudent) + .then((data) => { + setStudentCompetencies(data); + // Générer les grades à partir du retour API + if (data && data.data) { + const initialGrades = {}; + data.data.forEach((domaine) => { + domaine.categories.forEach((cat) => { + cat.competences.forEach((comp) => { + initialGrades[comp.competence_id] = comp.score ?? 0; + }); + }); + }); + setGrades(initialGrades); + } + }) + .catch((error) => + logger.error('Error fetching studentCompetencies:', error) + ); + } else { + setGrades({}); + setStudentCompetencies(null); + } + }, [formData.selectedStudent]); + return (
{/* Sélection de l'élève */} @@ -145,13 +179,14 @@ export default function Page() { {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 b12b370..0bac7bc 100644 --- a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js +++ b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js @@ -11,18 +11,13 @@ import { import SectionHeader from '@/components/SectionHeader'; import { Award } from 'lucide-react'; import { useCsrfToken } from '@/context/CsrfContext'; - -// À remplacer par un fetch réel des compétences selon l'élève -const mockCompetencies = [ - { id: 1, name: 'Lire un texte court', score: null }, - { id: 2, name: 'Résoudre un problème simple', score: null }, - { id: 3, name: 'Exprimer une idée à l’oral', score: null }, -]; +import { useNotification } from '@/context/NotificationContext'; export default function StudentCompetenciesPage() { const searchParams = useSearchParams(); const router = useRouter(); const csrfToken = useCsrfToken(); + const { showNotification } = useNotification(); const [studentCompetencies, setStudentCompetencies] = useState([]); const [grades, setGrades] = useState({}); const studentId = searchParams.get('studentId'); @@ -70,14 +65,21 @@ export default function StudentCompetenciesPage() { competenceId, grade: score, })); - editStudentCompetencies(data, csrfToken) .then(() => { - alert('Bilan de compétence enregistré !'); + showNotification( + 'Bilan de compétence sauvegardé avec succès', + 'success', + 'Succès' + ); router.back(); }) .catch((error) => { - alert("Erreur lors de l'enregistrement du bilan"); + showNotification( + "Erreur lors de l'enregistrement du bilan de compétence", + 'error', + 'Erreur' + ); }); }; diff --git a/Front-End/src/components/Grades/GradeView.js b/Front-End/src/components/Grades/GradeView.js index 2707246..0442c1e 100644 --- a/Front-End/src/components/Grades/GradeView.js +++ b/Front-End/src/components/Grades/GradeView.js @@ -75,7 +75,9 @@ export default function GradeView({ data, grades, onGradeChange }) { {data.map((domaine) => (
toggleDomain(domaine.domaine_id)} >
diff --git a/Front-End/src/components/Grades/GradesStatsCircle.js b/Front-End/src/components/Grades/GradesStatsCircle.js new file mode 100644 index 0000000..2e1abbe --- /dev/null +++ b/Front-End/src/components/Grades/GradesStatsCircle.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; + +export default function GradesStatsCircle({ grades }) { + // grades : { [competence_id]: grade } + const total = Object.keys(grades).length; + const acquired = Object.values(grades).filter((g) => g === 3).length; + const inProgress = Object.values(grades).filter((g) => g === 2).length; + const notAcquired = Object.values(grades).filter((g) => g === 1).length; + const notEvaluated = Object.values(grades).filter((g) => g === 0).length; + + // Pourcentage d'acquis + const percent = total ? Math.round((acquired / total) * 100) : 0; + + return ( +
+

Statistiques globales

+
+ +
+
+ + {acquired} acquis + + {inProgress} en cours + {notAcquired} non acquis + {notEvaluated} non évalués +
+
+ ); +}