From 7acae479da658707fb3e073ebcdfee023d18500b Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 14 Dec 2024 15:28:07 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20Cr=C3=A9ation=20de=20nouveaux=20com?= =?UTF-8?q?posants=20/=20update=20formulaire=20de=20cr=C3=A9ation=20de=20c?= =?UTF-8?q?lasse=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/GestionEnseignants/apps.py | 4 +- Back-End/GestionEnseignants/models.py | 22 +- Back-End/GestionEnseignants/serializers.py | 106 +++++++- Back-End/GestionEnseignants/urls.py | 6 +- Back-End/GestionEnseignants/views.py | 44 ++- Front-End/package-lock.json | 118 +++++++- Front-End/package.json | 4 +- Front-End/src/app/[locale]/admin/page.js | 4 +- .../src/app/[locale]/admin/structure/page.js | 110 +++++--- .../app/[locale]/admin/subscriptions/page.js | 4 +- Front-End/src/app/layout.js | 11 +- Front-End/src/components/CheckBoxList.js | 95 ++++--- Front-End/src/components/ClassForm.js | 180 ------------- Front-End/src/components/EventModal.js | 22 +- Front-End/src/components/InputColorIcon.js | 2 +- Front-End/src/components/RadioList.js | 60 ++--- Front-End/src/components/SelectChoice.js | 4 +- .../Structure/Configuration/ClassForm.js | 252 ++++++++++++++++++ .../Configuration}/ClassesSection.js | 106 +++++--- .../Structure/Configuration/DateRange.js | 38 +++ .../Configuration/PlanningConfiguration.js | 113 ++++++++ .../Configuration}/SpecialitiesSection.js | 29 +- .../Configuration}/SpecialityForm.js | 44 +-- .../Configuration/StructureManagement.js | 46 ++++ .../Structure/Configuration/TabsStructure.js | 21 ++ .../Configuration}/TeacherForm.js | 22 +- .../Configuration}/TeachersSection.js | 44 +-- .../TeachersSelectionConfiguration.js | 46 ++++ .../Structure/Configuration/TimeRange.js | 32 +++ .../Structure/Planning/ClassesInfo.js | 81 ++++++ .../Structure/Planning/DraggableSpeciality.js | 38 +++ .../Structure/Planning/DropTargetCell.js | 64 +++++ .../Planning/PlanningClassView copy.js | 249 +++++++++++++++++ .../Structure/Planning/PlanningClassView.js | 130 +++++++++ .../Structure/Planning/ScheduleManagement.js | 195 ++++++++++++++ .../Planning/SpecialityEventModal.js | 186 +++++++++++++ Front-End/src/components/Table.js | 16 +- Front-End/src/context/ClasseFormContext.js | 70 +++++ Front-End/src/context/ClassesContext.js | 140 ++++++++++ Front-End/src/context/PlanningContext.js | 3 + .../src/context/SpecialityFormContext.js | 18 ++ Front-End/src/context/TeacherFormContext.js | 22 ++ Front-End/src/utils/Url.js | 14 +- 43 files changed, 2374 insertions(+), 441 deletions(-) delete mode 100644 Front-End/src/components/ClassForm.js create mode 100644 Front-End/src/components/Structure/Configuration/ClassForm.js rename Front-End/src/components/{ => Structure/Configuration}/ClassesSection.js (50%) create mode 100644 Front-End/src/components/Structure/Configuration/DateRange.js create mode 100644 Front-End/src/components/Structure/Configuration/PlanningConfiguration.js rename Front-End/src/components/{ => Structure/Configuration}/SpecialitiesSection.js (76%) rename Front-End/src/components/{ => Structure/Configuration}/SpecialityForm.js (59%) create mode 100644 Front-End/src/components/Structure/Configuration/StructureManagement.js create mode 100644 Front-End/src/components/Structure/Configuration/TabsStructure.js rename Front-End/src/components/{ => Structure/Configuration}/TeacherForm.js (89%) rename Front-End/src/components/{ => Structure/Configuration}/TeachersSection.js (81%) create mode 100644 Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js create mode 100644 Front-End/src/components/Structure/Configuration/TimeRange.js create mode 100644 Front-End/src/components/Structure/Planning/ClassesInfo.js create mode 100644 Front-End/src/components/Structure/Planning/DraggableSpeciality.js create mode 100644 Front-End/src/components/Structure/Planning/DropTargetCell.js create mode 100644 Front-End/src/components/Structure/Planning/PlanningClassView copy.js create mode 100644 Front-End/src/components/Structure/Planning/PlanningClassView.js create mode 100644 Front-End/src/components/Structure/Planning/ScheduleManagement.js create mode 100644 Front-End/src/components/Structure/Planning/SpecialityEventModal.js create mode 100644 Front-End/src/context/ClasseFormContext.js create mode 100644 Front-End/src/context/ClassesContext.js create mode 100644 Front-End/src/context/SpecialityFormContext.js create mode 100644 Front-End/src/context/TeacherFormContext.js diff --git a/Back-End/GestionEnseignants/apps.py b/Back-End/GestionEnseignants/apps.py index 02c8cd3..b9397d8 100644 --- a/Back-End/GestionEnseignants/apps.py +++ b/Back-End/GestionEnseignants/apps.py @@ -3,8 +3,8 @@ from django.db.models.signals import post_migrate def create_specialite(sender, **kwargs): from .models import Specialite - if not Specialite.objects.filter(nom='TRANSVERSE').exists(): - Specialite.objects.create(nom='TRANSVERSE', codeCouleur='#FF0000') + if not Specialite.objects.filter(nom='GROUPE').exists(): + Specialite.objects.create(nom='GROUPE', codeCouleur='#FF0000') class GestionenseignantsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' diff --git a/Back-End/GestionEnseignants/models.py b/Back-End/GestionEnseignants/models.py index 3ad84aa..49bc16d 100644 --- a/Back-End/GestionEnseignants/models.py +++ b/Back-End/GestionEnseignants/models.py @@ -3,6 +3,7 @@ from GestionLogin.models import Profil from django.db.models import JSONField from django.db.models.signals import post_save from django.dispatch import receiver +from django.contrib.postgres.fields import ArrayField class Specialite(models.Model): nom = models.CharField(max_length=100) @@ -18,31 +19,36 @@ class Enseignant(models.Model): mail = models.EmailField(unique=True) specialites = models.ManyToManyField(Specialite, related_name='enseignants') profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE, null=True, blank=True) + dateCreation = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.nom} {self.prenom}" class Classe(models.Model): - nom_ambiance = models.CharField(max_length=255) + nom_ambiance = models.CharField(max_length=255, null=True, blank=True) tranche_age = models.JSONField() nombre_eleves = models.PositiveIntegerField() langue_enseignement = models.CharField(max_length=255) annee_scolaire = models.CharField(max_length=9) dateCreation = models.DateTimeField(auto_now_add=True) enseignants = models.ManyToManyField(Enseignant, related_name='classes') + niveaux = models.JSONField(default=list) def __str__(self): return self.nom_ambiance class Planning(models.Model): + PLANNING_TYPE_CHOICES = [ + (1, 'Annuel'), + (2, 'Semestriel'), + (3, 'Trimestriel') + ] + classe = models.OneToOneField(Classe, on_delete=models.SET_NULL, null=True, blank=True, related_name='planning') - emploiDuTemps = JSONField(default=list) + emploiDuTemps = JSONField(default=dict) + type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) + plageHoraire = models.JSONField(default=list) + joursOuverture = ArrayField(models.IntegerField(), default=list) def __str__(self): return f'Planning de {self.classe.nom_ambiance}' - -@receiver(post_save, sender=Classe) -def create_planning(sender, instance, created, **kwargs): - if created and not hasattr(instance, 'planning'): - Planning.objects.create(classe=instance) - diff --git a/Back-End/GestionEnseignants/serializers.py b/Back-End/GestionEnseignants/serializers.py index e22c713..37da853 100644 --- a/Back-End/GestionEnseignants/serializers.py +++ b/Back-End/GestionEnseignants/serializers.py @@ -36,10 +36,11 @@ class EnseignantSerializer(serializers.ModelSerializer): profilAssocie = ProfilSerializer(read_only=True) DroitLabel = serializers.SerializerMethodField() DroitValue = serializers.SerializerMethodField() + dateCreation_formattee = serializers.SerializerMethodField() class Meta: model = Enseignant - fields = ['id', 'nom', 'prenom', 'mail', 'specialites', 'specialites_ids', 'classes_principal', 'profilAssocie', 'profilAssocie_id', 'DroitLabel', 'DroitValue'] + fields = ['id', 'nom', 'prenom', 'mail', 'specialites', 'specialites_ids', 'classes_principal', 'profilAssocie', 'profilAssocie_id', 'DroitLabel', 'DroitValue', 'dateCreation', 'dateCreation_formattee'] def create(self, validated_data): specialites_data = validated_data.pop('specialites', None) @@ -67,63 +68,146 @@ class EnseignantSerializer(serializers.ModelSerializer): def get_DroitValue(self, obj): return obj.profilAssocie.droit if obj.profilAssocie else None + def get_dateCreation_formattee(self, obj): + utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale + local_tz = pytz.timezone(settings.TZ_APPLI) + local_time = utc_time.astimezone(local_tz) + return local_time.strftime("%d-%m-%Y %H:%M") + class PlanningSerializer(serializers.ModelSerializer): - classe_id = serializers.PrimaryKeyRelatedField(queryset=Classe.objects.all(), source='classe') + emploiDuTemps = serializers.SerializerMethodField() class Meta: model = Planning - fields = ['id', 'classe', 'classe_id', 'emploiDuTemps'] + fields = ['id', 'emploiDuTemps', 'type', 'plageHoraire', 'joursOuverture'] + + def get_emploiDuTemps(self, obj): + emploi_du_temps = obj.emploiDuTemps + if obj.classe: + enseignants = obj.classe.enseignants.all() # Récupérer tous les enseignants associés à la classe + + # Dictionnaire pour accéder rapidement aux spécialités des enseignants + specialite_enseignants = {} + for enseignant in enseignants: + for specialite in enseignant.specialites.all(): + if specialite.nom not in specialite_enseignants: + specialite_enseignants[specialite.nom] = [] + specialite_enseignants[specialite.nom].append(f"{enseignant.prenom} {enseignant.nom}") + + if obj.type == 1: # Planning annuel + for day, events in emploi_du_temps.items(): + for event in events: + # Ajouter les enseignants associés à la spécialité + event['teachers'] = specialite_enseignants.get(event['matiere'], []) + # Ajouter la couleur de la spécialité + event['color'] = next( + (specialite.codeCouleur for enseignant in enseignants for specialite in enseignant.specialites.all() if specialite.nom == event['matiere']), + "#FFFFFF" # Couleur par défaut si non trouvée + ) + + elif obj.type in [2, 3]: # Planning semestriel ou trimestriel + for period_key, period_value in emploi_du_temps.items(): + for day, events in period_value.items(): + if day in ['DateDebut', 'DateFin']: + continue # Ignorer les clés DateDebut et DateFin + for event in events: + print(f'event : {event}') + # Ajouter les enseignants associés à la spécialité + event['teachers'] = specialite_enseignants.get(event['matiere'], []) + # Ajouter la couleur de la spécialité + event['color'] = next( + (specialite.codeCouleur for enseignant in enseignants for specialite in enseignant.specialites.all() if specialite.nom == event['matiere']), + "#FFFFFF" # Couleur par défaut si non trouvée + ) + + return emploi_du_temps + + def to_internal_value(self, data): + internal_value = super().to_internal_value(data) + internal_value['emploiDuTemps'] = data.get('emploiDuTemps', {}) + return internal_value class ClasseSerializer(serializers.ModelSerializer): dateCreation_formattee = serializers.SerializerMethodField() enseignants = EnseignantSerializer(many=True, read_only=True) enseignants_ids = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), many=True, source='enseignants') eleves = serializers.SerializerMethodField() - planning = serializers.SerializerMethodField() + planning = PlanningSerializer() class Meta: model = Classe - fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'enseignants', 'enseignants_ids', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee', 'eleves', 'planning'] + fields = [ + 'id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', + 'enseignants', 'enseignants_ids', 'annee_scolaire', 'dateCreation', + 'dateCreation_formattee', 'eleves', 'planning', 'niveaux' + ] def create(self, validated_data): planning_data = validated_data.pop('planning', None) enseignants_data = validated_data.pop('enseignants', []) - classe = Classe.objects.create(**validated_data) + niveaux_data = validated_data.pop('niveaux', []) + + classe = Classe.objects.create( + nom_ambiance=validated_data.get('nom_ambiance', ''), + tranche_age=validated_data.get('tranche_age', []), + nombre_eleves=validated_data.get('nombre_eleves', 0), + langue_enseignement=validated_data.get('langue_enseignement', ''), + annee_scolaire=validated_data.get('annee_scolaire', ''), + niveaux=niveaux_data + ) + classe.enseignants.set(enseignants_data) + if planning_data and not hasattr(classe, 'planning'): - Planning.objects.create(classe=classe, **planning_data) + Planning.objects.create( + classe=classe, + emploiDuTemps=planning_data.get('emploiDuTemps', {}), + type=planning_data.get('type', 1), + plageHoraire=planning_data.get('plageHoraire', []), + joursOuverture=planning_data.get('joursOuverture', []) + ) + return classe def update(self, instance, validated_data): planning_data = validated_data.pop('planning', None) enseignants_data = validated_data.pop('enseignants', []) + niveaux_data = validated_data.pop('niveaux', []) + instance.nom_ambiance = validated_data.get('nom_ambiance', instance.nom_ambiance) instance.tranche_age = validated_data.get('tranche_age', instance.tranche_age) instance.nombre_eleves = validated_data.get('nombre_eleves', instance.nombre_eleves) instance.langue_enseignement = validated_data.get('langue_enseignement', instance.langue_enseignement) instance.annee_scolaire = validated_data.get('annee_scolaire', instance.annee_scolaire) + instance.niveaux = niveaux_data + instance.save() instance.enseignants.set(enseignants_data) - if planning_data: + + if planning_data: planning = instance.planning planning.emploiDuTemps = planning_data.get('emploiDuTemps', planning.emploiDuTemps) + planning.type = planning_data.get('type', planning.type) + planning.plageHoraire = planning_data.get('plageHoraire', planning.plageHoraire) + planning.joursOuverture = planning_data.get('joursOuverture', planning.joursOuverture) + planning.save() + return instance def get_dateCreation_formattee(self, obj): utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") def get_eleves(self, obj): elevesList = obj.eleves.all() filtered_eleves = [] for eleve in elevesList: - ficheInscription=bdd.getObject(FicheInscription, "eleve__id", eleve.id) + ficheInscription = bdd.getObject(FicheInscription, "eleve__id", eleve.id) if ficheInscription.etat == ficheInscription.EtatDossierInscription.DI_VALIDE: - filtered_eleves.append(eleve) + filtered_eleves.append(eleve) return EleveSerializer(filtered_eleves, many=True, read_only=True).data def get_planning(self, obj): diff --git a/Back-End/GestionEnseignants/urls.py b/Back-End/GestionEnseignants/urls.py index 41acf66..cfc4420 100644 --- a/Back-End/GestionEnseignants/urls.py +++ b/Back-End/GestionEnseignants/urls.py @@ -1,6 +1,6 @@ from django.urls import path, re_path -from GestionEnseignants.views import EnseignantsView, EnseignantView, SpecialitesView, SpecialiteView, ClassesView, ClasseView +from GestionEnseignants.views import EnseignantsView, EnseignantView, SpecialitesView, SpecialiteView, ClassesView, ClasseView, PlanningsView, PlanningView urlpatterns = [ re_path(r'^enseignants$', EnseignantsView.as_view(), name="enseignants"), @@ -14,4 +14,8 @@ urlpatterns = [ re_path(r'^classes$', ClassesView.as_view(), name="classes"), re_path(r'^classe$', ClasseView.as_view(), name="classe"), re_path(r'^classe/([0-9]+)$', ClasseView.as_view(), name="classe"), + + re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"), + re_path(r'^planning$', PlanningView.as_view(), name="planning"), + re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), ] \ No newline at end of file diff --git a/Back-End/GestionEnseignants/views.py b/Back-End/GestionEnseignants/views.py index af00734..c1facc4 100644 --- a/Back-End/GestionEnseignants/views.py +++ b/Back-End/GestionEnseignants/views.py @@ -4,8 +4,8 @@ from django.utils.decorators import method_decorator from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache -from .models import Enseignant, Specialite, Classe -from .serializers import EnseignantSerializer, SpecialiteSerializer, ClasseSerializer +from .models import Enseignant, Specialite, Classe, Planning +from .serializers import EnseignantSerializer, SpecialiteSerializer, ClasseSerializer, PlanningSerializer from N3wtSchool import bdd class EnseignantsView(APIView): @@ -188,4 +188,42 @@ class ClasseView(APIView): eleve.save() classe.delete() - return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False) \ No newline at end of file + return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class PlanningsView(APIView): + def get(self, request): + schedulesList=bdd.getAllObjects(Planning) + schedules_serializer=PlanningSerializer(schedulesList, many=True) + return JsonResponse(schedules_serializer.data, safe=False) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class PlanningView(APIView): + def get (self, request, _id): + planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id) + planning_serializer=PlanningSerializer(planning) + + return JsonResponse(planning_serializer.data, safe=False) + + def post(self, request): + planning_data=JSONParser().parse(request) + planning_serializer = PlanningSerializer(data=planning_data) + + if planning_serializer.is_valid(): + planning_serializer.save() + return JsonResponse(planning_serializer.data, safe=False) + + return JsonResponse(planning_serializer.errors, safe=False) + + def put(self, request, _id): + planning_data=JSONParser().parse(request) + planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id) + planning_serializer = PlanningSerializer(planning, data=planning_data) + + if planning_serializer.is_valid(): + planning_serializer.save() + return JsonResponse(planning_serializer.data, safe=False) + + return JsonResponse(planning_serializer.errors, safe=False) diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index 6cf2c1b..6940276 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -18,6 +18,8 @@ "next-intl": "^3.24.0", "react": "^18", "react-cookie": "^7.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18", "react-phone-number-input": "^3.4.8", "react-tooltip": "^5.28.0" @@ -106,6 +108,18 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -897,6 +911,24 @@ } } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, @@ -1720,9 +1752,10 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1733,7 +1766,9 @@ "integrity": "sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow==" }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1934,6 +1969,17 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -2562,7 +2608,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -4396,6 +4441,45 @@ "react": ">= 16.3.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "license": "MIT", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", @@ -4529,6 +4613,15 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "dev": true, @@ -4549,6 +4642,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "dev": true, @@ -5306,12 +5405,13 @@ } }, "node_modules/universal-cookie": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.0.tgz", - "integrity": "sha512-PvcyflJAYACJKr28HABxkGemML5vafHmiL4ICe3e+BEKXRMt0GaFLZhAwgv637kFFnnfiSJ8e6jknrKkMrU+PQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz", + "integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==", + "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", - "cookie": "^0.6.0" + "cookie": "^0.7.2" } }, "node_modules/update-browserslist-db": { diff --git a/Front-End/package.json b/Front-End/package.json index 29d968c..09f5c06 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -20,6 +20,8 @@ "next-intl": "^3.24.0", "react": "^18", "react-cookie": "^7.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18", "react-phone-number-input": "^3.4.8", "react-tooltip": "^5.28.0" @@ -33,4 +35,4 @@ "postcss": "^8.4.47", "tailwindcss": "^3.4.14" } -} \ No newline at end of file +} diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 0a7e5c0..85e18fa 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react'; import Loader from '@/components/Loader'; -import { BK_GESTIONINSCRIPTION_CLASSES_URL } from '@/utils/Url'; +import { BK_GESTIONENSEIGNANTS_CLASSES_URL } from '@/utils/Url'; import ClasseDetails from '@/components/ClasseDetails'; // Composant StatCard pour afficher une statistique @@ -59,7 +59,7 @@ export default function DashboardPage() { const [classes, setClasses] = useState([]); const fetchClasses = () => { - fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) + fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) .then(response => response.json()) .then(data => { setClasses(data); diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index c72a74f..74beeb5 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,14 +1,13 @@ 'use client' import React, { useState, useEffect } from 'react'; -import SpecialitiesSection from '@/components/SpecialitiesSection' -import ClassesSection from '@/components/ClassesSection' -import TeachersSection from '@/components/TeachersSection'; -import { BK_GESTIONINSCRIPTION_SPECIALITES_URL, - BK_GESTIONINSCRIPTION_CLASSES_URL, - BK_GESTIONINSCRIPTION_SPECIALITE_URL, - BK_GESTIONINSCRIPTION_CLASSE_URL, - BK_GESTIONINSCRIPTION_TEACHERS_URL, - BK_GESTIONINSCRIPTION_TEACHER_URL } from '@/utils/Url'; +import { School, Calendar } from 'lucide-react'; +import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; +import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' +import StructureManagement from '@/components/Structure/Configuration/StructureManagement' +import { BK_GESTIONENSEIGNANTS_SPECIALITES_URL, + BK_GESTIONENSEIGNANTS_CLASSES_URL, + BK_GESTIONENSEIGNANTS_TEACHERS_URL, + BK_GESTIONENSEIGNANTS_PLANNINGS_URL } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; @@ -16,6 +15,12 @@ export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); + const [schedules, setSchedules] = useState([]); + const [activeTab, setActiveTab] = useState('Configuration'); + const tabs = [ + { id: 'Configuration', title: "Configuration de l'école", icon: School }, + { id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar }, + ]; const csrfToken = useCsrfToken(); @@ -28,10 +33,13 @@ export default function Page() { // Fetch data for classes fetchClasses(); + + // Fetch data for schedules + fetchSchedules(); }, []); const fetchSpecialities = () => { - fetch(`${BK_GESTIONINSCRIPTION_SPECIALITES_URL}`) + fetch(`${BK_GESTIONENSEIGNANTS_SPECIALITES_URL}`) .then(response => response.json()) .then(data => { setSpecialities(data); @@ -42,7 +50,7 @@ export default function Page() { }; const fetchTeachers = () => { - fetch(`${BK_GESTIONINSCRIPTION_TEACHERS_URL}`) + fetch(`${BK_GESTIONENSEIGNANTS_TEACHERS_URL}`) .then(response => response.json()) .then(data => { setTeachers(data); @@ -53,7 +61,7 @@ export default function Page() { }; const fetchClasses = () => { - fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) + fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) .then(response => response.json()) .then(data => { setClasses(data); @@ -63,6 +71,17 @@ export default function Page() { }); }; + const fetchSchedules = () => { + fetch(`${BK_GESTIONENSEIGNANTS_PLANNINGS_URL}`) + .then(response => response.json()) + .then(data => { + setSchedules(data); + }) + .catch(error => { + console.error('Error fetching classes:', error); + }); + }; + const handleCreate = (url, newData, setDatas) => { fetch(url, { method: 'POST', @@ -102,6 +121,26 @@ export default function Page() { }); }; + const handleUpdatePlanning = (url, id, updatedData) => { + fetch(`${url}/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(updatedData), + credentials: 'include' + }) + .then(response => response.json()) + .then(data => { + console.log('Planning mis à jour avec succès :', data); + //setDatas(data); + }) + .catch(error => { + console.error('Erreur :', error); + }); + }; + const handleDelete = (url, id, setDatas) => { fetch(`${url}/${id}`, { method:'DELETE', @@ -127,30 +166,33 @@ export default function Page() {
- handleCreate(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, newData, setSpecialities)} - handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, updatedData, setSpecialities)} - handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, setSpecialities)} - /> + - handleCreate(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, newData, setTeachers)} - handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, updatedData, setTeachers)} - handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, setTeachers)} - /> + {activeTab === 'Configuration' && ( + <> + + + )} - handleCreate(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, newData, setClasses)} - handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, updatedData, setClasses)} - handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, setClasses)} - /> + {activeTab === 'Schedule' && ( + + )}
); }; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index ab68322..63fbe6c 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -22,7 +22,7 @@ import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, BK_GESTIONINSCRIPTION_SEND_URL, FR_ADMIN_SUBSCRIPTIONS_EDIT_URL, BK_GESTIONINSCRIPTION_ARCHIVE_URL, - BK_GESTIONINSCRIPTION_CLASSES_URL, + BK_GESTIONENSEIGNANTS_CLASSES_URL, BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL, BK_GESTIONINSCRIPTION_ELEVES_URL, BK_PROFILE_URL } from '@/utils/Url'; @@ -147,7 +147,7 @@ export default function Page({ params: { locale } }) { }; const fetchClasses = () => { - fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) + fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`) .then(response => response.json()) .then(data => { setClasses(data); diff --git a/Front-End/src/app/layout.js b/Front-End/src/app/layout.js index eac2cc1..d290675 100644 --- a/Front-End/src/app/layout.js +++ b/Front-End/src/app/layout.js @@ -1,9 +1,9 @@ +import React from 'react'; +import { NextIntlClientProvider } from 'next-intl'; -import {NextIntlClientProvider} from 'next-intl'; import {getMessages} from 'next-intl/server'; import "@/css/tailwind.css"; - export const metadata = { title: "N3WT-SCHOOL", description: "Gestion de l'école", @@ -21,10 +21,7 @@ export const metadata = { }, }; -export default async function RootLayout({ children, params: {locale}}) { - - // Providing all messages to the client - // side is the easiest way to get started +export default async function RootLayout({ children, params: { locale } }) { const messages = await getMessages(); return ( @@ -37,5 +34,3 @@ export default async function RootLayout({ children, params: {locale}}) { ); } - - diff --git a/Front-End/src/components/CheckBoxList.js b/Front-End/src/components/CheckBoxList.js index 470f6f3..c7a9d75 100644 --- a/Front-End/src/components/CheckBoxList.js +++ b/Front-End/src/components/CheckBoxList.js @@ -1,43 +1,70 @@ -const CheckBoxList = ({ items, formData, handleChange, fieldName, label, icon: Icon, className, itemLabelFunc = (item) => item.name }) => { - const handleCheckboxChange = (e) => { - handleChange(e); - }; - - return ( -
- -
- {items.map(item => ( -
+import React from 'react'; + +const CheckBoxList = ({ + items, + formData, + handleChange, + fieldName, + label, + icon: Icon, + className, + itemLabelFunc = (item) => item.name, + labelAttenuated = () => false, + horizontal = false // Ajouter l'option horizontal +}) => { + const handleCheckboxChange = (e) => { + handleChange(e); + }; + + return ( +
+ +
+ {items.map(item => { + const isChecked = formData[fieldName].includes(parseInt(item.id)); + const isAttenuated = labelAttenuated(item) && !isChecked; + return ( +
+ {horizontal && ( + + )} - + {!horizontal && ( + + )}
- ))} -
+ ); + })}
- ); - }; - - export default CheckBoxList; - \ No newline at end of file +
+ ); +}; + +export default CheckBoxList; diff --git a/Front-End/src/components/ClassForm.js b/Front-End/src/components/ClassForm.js deleted file mode 100644 index 4646f78..0000000 --- a/Front-End/src/components/ClassForm.js +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState } from 'react'; -import Slider from '@/components/Slider' -import InputTextIcon from '@/components/InputTextIcon'; -import Button from '@/components/Button'; -import SelectChoice from '@/components/SelectChoice'; -import CheckBoxList from '@/components/CheckBoxList'; -import { Users, Maximize2, Globe, Calendar, GraduationCap } from 'lucide-react'; - -const ClassForm = ({ classe, onSubmit, isNew, specialities, teachers }) => { - const langues = [ - { value:'Français', label: 'Français'}, - { value:'Anglais', label: 'Anglais'}, - { value:'Espagnol', label: 'Espagnol'}, - ]; - - const [formData, setFormData] = useState({ - nom_ambiance: classe.nom_ambiance || '', - tranche_age: classe.tranche_age || [3, 6], - nombre_eleves: classe.nombre_eleves || '', - langue_enseignement: classe.langue_enseignement || 'Français', - annee_scolaire: classe.annee_scolaire || '', - enseignants_ids: classe.enseignants_ids || [], - planning: classe.planning || [] - }); - - const handleChange = (e) => { - const target = e.target || e.currentTarget; - const { name, value, type, checked } = target; - - console.log('type : ', type); - - if (type === 'checkbox') { - setFormData((prevState) => { - const newValues = checked - ? [...(prevState[name] || []), parseInt(value, 10)] - : (prevState[name] || []).filter((v) => v !== parseInt(value, 10)); - return { - ...prevState, - [name]: newValues, - }; - }); - } else { - setFormData((prevState) => ({ - ...prevState, - [name]: type === 'radio' ? parseInt(value, 10) : value, - })); - } - }; - - const handleSliderChange = (value) => { - setFormData(prevFormData => ({ - ...prevFormData, - tranche_age: value - })); - }; - - const handleNumberChange = (e) => { - const { name, value } = e.target; - setFormData(prevFormData => ({ - ...prevFormData, - [name]: Number(value) - })); - }; - - const handleSubmit = () => { - const updatedFormData = { - ...formData, - planning: formData.planning || [] - }; - onSubmit(updatedFormData, isNew); - }; - - const getTeacherLabel = (teacher) => { - return ( -
- {teacher.nom} {teacher.prenom} - - {teacher.specialites.map(specialite => ( - - {specialite.nom} - - - ))} -
- ); - }; - - return ( -
-
- -
-
- - -
-
- -
-
- -
-
- -
-
- -
-
-
-
- ); -}; - -export default ClassForm; diff --git a/Front-End/src/components/EventModal.js b/Front-End/src/components/EventModal.js index bcfc80a..fa2051f 100644 --- a/Front-End/src/components/EventModal.js +++ b/Front-End/src/components/EventModal.js @@ -5,6 +5,17 @@ import React from 'react'; export default function EventModal({ isOpen, onClose, eventData, setEventData }) { const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning(); + // S'assurer que scheduleId est défini lors du premier rendu + React.useEffect(() => { + if (!eventData.scheduleId && schedules.length > 0) { + setEventData(prev => ({ + ...prev, + scheduleId: schedules[0].id, + color: schedules[0].color + })); + } + }, [schedules, eventData.scheduleId]); + if (!isOpen) return null; const recurrenceOptions = [ @@ -25,17 +36,6 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData }) { value: 0, label: 'Dim' } ]; - // S'assurer que scheduleId est défini lors du premier rendu - React.useEffect(() => { - if (!eventData.scheduleId && schedules.length > 0) { - setEventData(prev => ({ - ...prev, - scheduleId: schedules[0].id, - color: schedules[0].color - })); - } - }, [schedules, eventData.scheduleId]); - const handleSubmit = (e) => { e.preventDefault(); diff --git a/Front-End/src/components/InputColorIcon.js b/Front-End/src/components/InputColorIcon.js index 363d0c5..1aa8243 100644 --- a/Front-End/src/components/InputColorIcon.js +++ b/Front-End/src/components/InputColorIcon.js @@ -16,7 +16,7 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) = name={name} value={value} onChange={onChange} - className="flex-1 block rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full p-0 w-8" + className="flex-1 block rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full p-0 w-8 cursor-pointer" />
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/RadioList.js b/Front-End/src/components/RadioList.js index 4b6a861..e50d3b4 100644 --- a/Front-End/src/components/RadioList.js +++ b/Front-End/src/components/RadioList.js @@ -1,39 +1,33 @@ import React from 'react'; -const RadioList = ({ items, formData, handleChange, fieldName, label, icon: Icon, className, itemLabelFunc}) => { - return ( -
-
+ ); }; export default RadioList; diff --git a/Front-End/src/components/SelectChoice.js b/Front-End/src/components/SelectChoice.js index ea839a0..e2420ff 100644 --- a/Front-End/src/components/SelectChoice.js +++ b/Front-End/src/components/SelectChoice.js @@ -3,12 +3,12 @@ export default function SelectChoice({type, name, label, choices, callback, sele <>
-
+
{IconItem && } +
+
+ Au + + +
+
+
+ ); +}; + +export default DateRange; diff --git a/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js b/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js new file mode 100644 index 0000000..99a715f --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/PlanningConfiguration.js @@ -0,0 +1,113 @@ +import React from 'react'; +import RadioList from '@/components/RadioList'; +import DateRange from '@/components/Structure/Configuration/DateRange'; +import TimeRange from '@/components/Structure/Configuration/TimeRange'; +import CheckBoxList from '@/components/CheckBoxList'; + +const PlanningConfiguration = ({ formData, handleChange, handleTimeChange, handleJoursChange, typeEmploiDuTemps }) => { + const daysOfWeek = [ + { id: 1, name: 'lun' }, + { id: 2, name: 'mar' }, + { id: 3, name: 'mer' }, + { id: 4, name: 'jeu' }, + { id: 5, name: 'ven' }, + { id: 6, name: 'sam' }, + ]; + + const isLabelAttenuated = (item) => { + return !formData.jours_ouverture.includes(parseInt(item.id)); + }; + + return ( +
+ + +
+
+ +
+ + {/* Plage horaire */} +
+ handleTimeChange(e, 0)} + onEndChange={(e) => handleTimeChange(e, 1)} + /> + + {/* CheckBoxList */} + +
+
+ + {/* DateRange */} +
+ {formData.planning_type === 2 && ( + <> + + + + )} + + {formData.planning_type === 3 && ( + <> + + + + + )} +
+
+ ); +}; + +export default PlanningConfiguration; diff --git a/Front-End/src/components/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js similarity index 76% rename from Front-End/src/components/SpecialitiesSection.js rename to Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index fc96c08..2cae280 100644 --- a/Front-End/src/components/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -3,7 +3,8 @@ import { useState } from 'react'; import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; import Modal from '@/components/Modal'; -import SpecialityForm from '@/components/SpecialityForm'; +import SpecialityForm from '@/components/Structure/Configuration/SpecialityForm'; +import { SpecialityFormProvider } from '@/context/SpecialityFormContext'; const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => { @@ -46,14 +47,18 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
row.nom.toUpperCase() }, - { name: 'CODE', transform: (row) => ( -
- )}, + { + name: 'INTITULÉ', + transform: (row) => ( +
+ {row.nom.toUpperCase()} +
+ ) + }, { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, { name: 'ACTIONS', transform: (row) => ( {isOpen && ( - + ( - + )} /> + )} ); diff --git a/Front-End/src/components/SpecialityForm.js b/Front-End/src/components/Structure/Configuration/SpecialityForm.js similarity index 59% rename from Front-End/src/components/SpecialityForm.js rename to Front-End/src/components/Structure/Configuration/SpecialityForm.js index 24c4941..5a767a2 100644 --- a/Front-End/src/components/SpecialityForm.js +++ b/Front-End/src/components/Structure/Configuration/SpecialityForm.js @@ -3,51 +3,59 @@ import { BookOpen, Palette } from 'lucide-react'; import InputTextIcon from '@/components/InputTextIcon'; import InputColorIcon from '@/components/InputColorIcon'; import Button from '@/components/Button'; +import { useSpecialityForm } from '@/context/SpecialityFormContext'; -const SpecialityForm = ({ speciality = {}, onSubmit, isNew }) => { - const [nom, setNom] = useState(speciality.nom || ''); - const [codeCouleur, setCodeCouleur] = useState(speciality.codeCouleur || '#FFFFFF'); +const SpecialityForm = ({ onSubmit, isNew }) => { + const { formData, setFormData } = useSpecialityForm(); - const handleSubmit = () => { - const updatedData = { - nom, - codeCouleur, - }; - onSubmit(updatedData, isNew); + const handleChange = (e) => { + const { name, value } = e.target; + + setFormData((prevState) => ({ + ...prevState, + [name]: value, + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit(formData); }; return (
setNom(e.target.value)} + value={formData.nom} + onChange={handleChange} className="w-full mt-4" />
setCodeCouleur(e.target.value)} + value={formData.codeCouleur} + onChange={handleChange} className="w-full mt-4" />
-
diff --git a/Front-End/src/components/Structure/Configuration/StructureManagement.js b/Front-End/src/components/Structure/Configuration/StructureManagement.js new file mode 100644 index 0000000..c1822d6 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/StructureManagement.js @@ -0,0 +1,46 @@ +import React from 'react'; +import SpecialitiesSection from '@/components/Structure/Configuration/SpecialitiesSection'; +import TeachersSection from '@/components/Structure/Configuration/TeachersSection'; +import ClassesSection from '@/components/Structure/Configuration/ClassesSection'; +import { ClassesProvider } from '@/context/ClassesContext'; + +import { BK_GESTIONENSEIGNANTS_SPECIALITE_URL, + BK_GESTIONENSEIGNANTS_TEACHER_URL, + BK_GESTIONENSEIGNANTS_CLASSE_URL } from '@/utils/Url'; + +const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => { + return ( +
+ + handleCreate(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, newData, setSpecialities)} + handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, id, updatedData, setSpecialities)} + handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_SPECIALITE_URL}`, id, setSpecialities)} + /> + + handleCreate(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, newData, setTeachers)} + handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, id, updatedData, setTeachers)} + handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_TEACHER_URL}`, id, setTeachers)} + /> + + handleCreate(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, newData, setClasses)} + handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, id, updatedData, setClasses)} + handleDelete={(id) => handleDelete(`${BK_GESTIONENSEIGNANTS_CLASSE_URL}`, id, setClasses)} + /> + +
+ + + ); +}; + +export default StructureManagement; diff --git a/Front-End/src/components/Structure/Configuration/TabsStructure.js b/Front-End/src/components/Structure/Configuration/TabsStructure.js new file mode 100644 index 0000000..916047e --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/TabsStructure.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { School, Calendar } from 'lucide-react'; + +const TabsStructure = ({ activeTab, setActiveTab, tabs }) => { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +}; + +export default TabsStructure; diff --git a/Front-End/src/components/TeacherForm.js b/Front-End/src/components/Structure/Configuration/TeacherForm.js similarity index 89% rename from Front-End/src/components/TeacherForm.js rename to Front-End/src/components/Structure/Configuration/TeacherForm.js index d529d8e..9d3d9d5 100644 --- a/Front-End/src/components/TeacherForm.js +++ b/Front-End/src/components/Structure/Configuration/TeacherForm.js @@ -4,17 +4,10 @@ import InputTextIcon from '@/components/InputTextIcon'; import Button from '@/components/Button'; import CheckBoxList from '@/components/CheckBoxList'; import ToggleSwitch from '@/components/ToggleSwitch' +import { useTeacherForm } from '@/context/TeacherFormContext'; -const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { - - const [formData, setFormData] = useState({ - nom: teacher.nom || '', - prenom: teacher.prenom || '', - mail: teacher.mail || '', - specialites_ids: teacher.specialites_ids || [], - profilAssocie_id:teacher.profilAssocie_id || '', - droit: teacher.DroitValue || 0 - }); +const TeacherForm = ({ onSubmit, isNew, specialities }) => { + const { formData, setFormData } = useTeacherForm(); const handleToggleChange = () => { setFormData({ ...formData, droit: 1-formData.droit }); @@ -24,8 +17,6 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { const target = e.target || e.currentTarget; const { name, value, type, checked } = target; - console.log('type : ', type); - if (type === 'checkbox') { setFormData((prevState) => { const newValues = checked @@ -52,6 +43,10 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { return `${speciality.nom}`; }; + const isLabelAttenuated = (item) => { + return !formData.specialites_ids.includes(parseInt(item.id)); + }; + return (
@@ -97,6 +92,7 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { icon={BookOpen} className="w-full mt-4" itemLabelFunc={getSpecialityLabel} + labelAttenuated={isLabelAttenuated} />
@@ -107,7 +103,7 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { />
-
-
+
row.nom }, { name: 'PRENOM', transform: (row) => row.prenom }, { name: 'MAIL', transform: (row) => row.mail }, - { name: 'SPECIALITES', + { + name: 'SPÉCIALITÉS', transform: (row) => ( -
+
{row.specialites.map(specialite => ( + > + {specialite.nom} + ))}
) }, - { name: 'TYPE PROFIL', + { + name: 'TYPE PROFIL', transform: (row) => { - return row.profilAssocie - ? -
- {row.DroitLabel} -
- : Non définie; + if (row.profilAssocie) { + const badgeClass = row.DroitLabel === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600'; + return ( +
+ + {row.DroitLabel} + +
+ ); + } else { + return Non définie; + } } }, + { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, { name: 'ACTIONS', transform: (row) => ( } @@ -155,7 +167,8 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe />
{isOpen && ( - + )} /> + )} ); diff --git a/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js b/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js new file mode 100644 index 0000000..c749939 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/TeachersSelectionConfiguration.js @@ -0,0 +1,46 @@ +import React from 'react'; +import Table from '@/components/Table'; + +const TeachersSelectionConfiguration = ({ formData, teachers, handleTeacherSelection, selectedTeachers }) => { + return ( +
+ + +
row.nom, + }, + { + name: 'Prénom', + transform: (row) => row.prenom, + }, + { + name: 'Spécialités', + transform: (row) => ( +
+ {row.specialites.map(specialite => ( + +
+ {specialite.nom} +
+ ))} +
+ ), + }, + ]} + data={teachers} + onRowClick={handleTeacherSelection} + selectedRows={selectedTeachers} + isSelectable={true} + /> + + ); +}; + +export default TeachersSelectionConfiguration; diff --git a/Front-End/src/components/Structure/Configuration/TimeRange.js b/Front-End/src/components/Structure/Configuration/TimeRange.js new file mode 100644 index 0000000..b7d3e95 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/TimeRange.js @@ -0,0 +1,32 @@ +import React from 'react'; + +const TimeRange = ({ startTime, endTime, onStartChange, onEndChange }) => { + return ( +
+
+
+ + +
+
+ + +
+
+
+ ); +}; + +export default TimeRange; diff --git a/Front-End/src/components/Structure/Planning/ClassesInfo.js b/Front-End/src/components/Structure/Planning/ClassesInfo.js new file mode 100644 index 0000000..4e181dd --- /dev/null +++ b/Front-End/src/components/Structure/Planning/ClassesInfo.js @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import SelectChoice from '@/components/SelectChoice'; +import { Users } from 'lucide-react'; +import DraggableSpeciality from '@/components/Structure/Planning/DraggableSpeciality'; + +const groupSpecialitiesBySubject = (enseignants) => { + const groupedSpecialities = {}; + + enseignants.forEach(teacher => { + teacher.specialites.forEach(specialite => { + if (!groupedSpecialities[specialite.id]) { + groupedSpecialities[specialite.id] = { + ...specialite, + teachers: [`${teacher.nom} ${teacher.prenom}`], + }; + } else { + groupedSpecialities[specialite.id].teachers.push(`${teacher.nom} ${teacher.prenom}`); + } + }); + }); + + return Object.values(groupedSpecialities); +}; + +const ClassesInfo = ({ classes, onClassSelect }) => { + const [selectedClass, setSelectedClass] = useState(null); // Nouvelle variable d'état pour la classe sélectionnée + + const handleClassChange = (event) => { + const classId = event.target.value; + const selectedClass = classes.find(classe => classe.id === parseInt(classId)); + setSelectedClass(selectedClass); + onClassSelect(selectedClass); + }; + + const classChoices = [ + { value: '', label: 'Sélectionner...' }, + ...classes.map(classe => ({ + value: classe.id, + label: classe.nom_ambiance, + })) + ]; + + // S'assurer que `selectedClass` n'est pas null avant d'appeler `groupSpecialitiesBySubject` + const groupedSpecialities = selectedClass ? groupSpecialitiesBySubject(selectedClass.enseignants) : []; + + return ( +
+ {classes.length > 0 ? ( + + ) : ( +

Aucune classe disponible.

+ )} + + {selectedClass && ( +
+ + {groupedSpecialities.map((specialite, index) => { + // Combiner l'ID de la spécialité avec les IDs des enseignants pour créer une clé unique + const uniqueId = `${specialite.id}-${specialite.teachers.map(teacher => teacher.id).join('-')}-${index}`; + + return ( + + ); + })} +
+ )} +
+ ); +}; + +export default ClassesInfo; diff --git a/Front-End/src/components/Structure/Planning/DraggableSpeciality.js b/Front-End/src/components/Structure/Planning/DraggableSpeciality.js new file mode 100644 index 0000000..21b8f89 --- /dev/null +++ b/Front-End/src/components/Structure/Planning/DraggableSpeciality.js @@ -0,0 +1,38 @@ +import { useDrag } from 'react-dnd'; + +const DraggableSpeciality = ({ specialite }) => { + const [{ isDragging }, drag] = useDrag(() => ({ + type: 'SPECIALITY', + item: { id: specialite.id, + name: specialite.nom, + color: specialite.codeCouleur, + teachers: specialite.teachers, + duree: specialite.duree }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })); + + const isColorDark = (color) => { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + return ((r * 0.299 + g * 0.587 + b * 0.114) < 150); + }; + + return ( + + {specialite.nom} ({specialite.teachers.join(', ')}) + + ); +}; + +export default DraggableSpeciality; diff --git a/Front-End/src/components/Structure/Planning/DropTargetCell.js b/Front-End/src/components/Structure/Planning/DropTargetCell.js new file mode 100644 index 0000000..8a65974 --- /dev/null +++ b/Front-End/src/components/Structure/Planning/DropTargetCell.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { useDrop } from 'react-dnd'; +import PropTypes from 'prop-types'; + +const DropTargetCell = ({ day, hour, course, onDrop }) => { + const [{ isOver }, drop] = useDrop(() => ({ + accept: 'SPECIALITY', + drop: (item) => onDrop(item, hour, day), + collect: (monitor) => ({ + isOver: monitor.isOver(), + }), + })); + + const isColorDark = (color) => { + if (!color) return false; // Vérification si color est défini + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + return (r * 0.299 + g * 0.587 + b * 0.114) < 150; + }; + + const isToday = (someDate) => { + const today = new Date(); + return someDate.getDate() === today.getDate() && + someDate.getMonth() === today.getMonth() && + someDate.getFullYear() === today.getFullYear(); + }; + + const cellBackgroundColor = course ? course.color : 'transparent'; + const cellTextColor = isColorDark(course?.color) ? '#E5E5E5' : '#333333'; + + return ( +
+
+ {course && ( + <> +
{course.matiere}
+
{course.teachers.join(", ")}
+ + )} +
+
+ ); +}; + +DropTargetCell.propTypes = { + day: PropTypes.string.isRequired, + hour: PropTypes.number.isRequired, + course: PropTypes.object, + onDrop: PropTypes.func.isRequired +}; + +export default DropTargetCell; diff --git a/Front-End/src/components/Structure/Planning/PlanningClassView copy.js b/Front-End/src/components/Structure/Planning/PlanningClassView copy.js new file mode 100644 index 0000000..fc67bba --- /dev/null +++ b/Front-End/src/components/Structure/Planning/PlanningClassView copy.js @@ -0,0 +1,249 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { format, startOfWeek, addDays, isSameDay } from 'date-fns'; +import { fr } from 'date-fns/locale'; +import { isToday } from 'date-fns'; +import DropTargetCell from '@/components/Structure/Planning/DropTargetCell'; +import { BK_GESTIONENSEIGNANTS_PLANNING_URL } from '@/utils/Url'; + +const PlanningClassView = ({ schedule }) => { + const [currentTime, setCurrentTime] = useState(new Date()); + const scrollContainerRef = React.useRef(null); + const scheduleIdRef = useRef(scheduleId); + const eventsRef = useRef(events); + + const [planning, setPlanning] = useState([]) + + // Fonction pour récupérer les données du depuis l'API + const fetchPlanning = () => { + if (schedule) { + fetch(`${BK_GESTIONENSEIGNANTS_PLANNING_URL}/${schedule.id}`) + .then(response => response.json()) + .then(data => { + console.log('DATA : ', data); + if (!data || data.emploiDuTemps.length === 0) { + setEvents([]); + setPlanning([]); + return; + } + const planningData = data.emploiDuTemps || {}; + console.log('succès : ', planningData); + + let events = []; + Object.keys(planningData).forEach(day => { + if (planningData[day]) { + planningData[day].forEach(event => { + if (event) { + events.push({ + ...event, + day: day.toLowerCase(), // Ajouter une clé jour en minuscule pour faciliter le filtrage + scheduleId: data.id + }); + } + }); + } + }); + + if (JSON.stringify(events) !== JSON.stringify(eventsRef.current)) { + setEvents(events); + setPlanning(events); + } + }) + .catch(error => { + console.error('Erreur lors de la récupération du planning :', error); + }); + } + }; + + useEffect(() => { + setEvents([]) + setPlanning([]); + fetchPlanning(); + }, [scheduleId]); + + useEffect(() => { + scheduleIdRef.current = scheduleId; + // Mettre à jour la référence chaque fois que scheduleId change + }, [scheduleId]); + + /*useEffect(() => { + eventsRef.current = events; + // Mettre à jour la référence chaque fois que events change + }, [events]);*/ + + // Déplacer ces déclarations avant leur utilisation + const timeSlots = Array.from({ length: 11 }, (_, i) => i + 8); + const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); + const weekDays = Array.from({ length: 6 }, (_, i) => addDays(weekStart, i)); + + // Maintenant on peut utiliser weekDays + const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date())); + + const getFilteredEvents = (day, hour) => { + const dayName = format(day, 'eeee', { locale: fr }).toLowerCase(); // Obtenir le nom du jour en minuscule pour le filtrage + + if (!Array.isArray(planning)) { + console.error("planning n'est pas un tableau:", planning); + return []; + } + + return planning.filter(event => { + const eventDay = event.day.toLowerCase(); // Convertir le jour de l'événement en minuscule + + return ( + eventDay === dayName && + event.heure.startsWith(hour.toString().padStart(2, '0')) // Comparer l'heure sans les minutes + ); + }); + }; + + /*const handleDrop = (item, hour, day) => { + const dateKey = format(new Date(day), 'yyyy-MM-dd'); + const newEvent = { + id: `event-${dateKey}-${hour}`, + start: new Date(day).setHours(hour), + end: new Date(day).setHours(hour + 1), + matiere: item.matiere || item.name, // Assurer que 'matiere' soit défini + color: item.color || '#FFFFFF', // Ajouter une couleur par défaut si indéfini + teachers: item.teachers || [], // Assurer que 'teachers' soit défini + day: format(new Date(day), 'eeee', { locale: fr }).toLowerCase(), // Ajouter le jour + heure: hour.toString().padStart(2, '0') + ':00', // Ajouter l'heure ici + scheduleId: scheduleIdRef.current // Utiliser la référence à scheduleId ici + }; + + // Utiliser la référence pour accéder aux événements actuels + const existingEvents = eventsRef.current.filter(event => { + const eventHour = event.heure; + const eventDay = event.day; + + console.log("DEBUG : " + event); + console.log("DEBUG : " + eventHour + " - " + hour.toString().padStart(2, '0') + ':00'); + console.log("DEBUG : " + eventDay + " - " + format(new Date(day), 'eeee', { locale: fr }).toLowerCase()); + console.log("DEBUG : " + event.scheduleId + " - " + scheduleIdRef.current); + console.log("----"); + // Comparer la date, l'heure et le scheduleId pour trouver les événements correspondants + return eventHour === hour.toString().padStart(2, '0') + ':00' && eventDay === format(new Date(day), 'eeee', { locale: fr }).toLowerCase() && event.scheduleId === scheduleIdRef.current; + }); + + console.log("existingEvents :", existingEvents); + if (existingEvents.length > 0) { + existingEvents.forEach(event => { + deleteEvent(event.id); // Supprimer l'événement existant + }); + } + + // Ajouter l'événement à l'état global des événements + addEvent(newEvent); + + setPlanning(prevEvents => { + // Filtrer les événements pour conserver ceux qui n'ont pas le même jour et créneau horaire + const updatedEvents = prevEvents.filter(event => + !(event.day === newEvent.day && event.heure === newEvent.heure) + ); + + // Ajouter le nouvel événement + updatedEvents.push(newEvent); + + return updatedEvents; + }); + };*/ + + // Mettre à jour la position de la ligne toutes les minutes + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(new Date()); + }, 60000); + + return () => clearInterval(interval); + }, []); + + // Modifier l'useEffect pour l'auto-scroll + useEffect(() => { + if (scrollContainerRef.current && isCurrentWeek) { + const currentHour = new Date().getHours(); + const scrollPosition = currentHour * 80; + // Ajout d'un délai pour laisser le temps au DOM de se mettre à jour + setTimeout(() => { + scrollContainerRef.current.scrollTop = scrollPosition - 200; + }, 0); + } + }, [currentDate, isCurrentWeek]); // Ajout de currentDate dans les dépendances + + + // Calculer la position de la ligne de temps + const getCurrentTimePosition = () => { + const hours = currentTime.getHours(); + const minutes = currentTime.getMinutes(); + + if (hours < 8 || hours > 18) { + return -1; // Hors de la plage horaire + } + + const totalMinutes = (hours - 8) * 60 + minutes; // Minutes écoulées depuis 8h + const cellHeight = 80; // Hauteur des cellules (par exemple 80px pour 20rem / 24) + + const position = (totalMinutes / 60) * cellHeight; + + return position; + }; + + return ( +
+ {/* En-tête des jours */} +
+
+ {weekDays.map((day) => ( +
+
+ {format(day, 'EEEE', { locale: fr })} +
+
+ ))} +
+ + {/* Grille horaire */} +
+ {/* Ligne de temps actuelle */} + {isCurrentWeek && ( +
+
+
+ )} + +
+ {timeSlots.map(hour => ( + +
+ {`${hour.toString().padStart(2, '0')}:00`} +
+ {weekDays.map(day => { + const filteredEvents = getFilteredEvents(day, hour); + + return( + + ) + })} +
+ ))} +
+
+
+ ); +}; + +export default PlanningClassView; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Planning/PlanningClassView.js b/Front-End/src/components/Structure/Planning/PlanningClassView.js new file mode 100644 index 0000000..e07edc4 --- /dev/null +++ b/Front-End/src/components/Structure/Planning/PlanningClassView.js @@ -0,0 +1,130 @@ +import React, {useRef} from 'react'; +import PropTypes from 'prop-types'; +import { format, isToday, isSameDay, startOfWeek, addDays } from 'date-fns'; +import { fr } from 'date-fns/locale'; +import DropTargetCell from './DropTargetCell'; + +const PlanningClassView = ({ schedule, onDrop, planningType }) => { + const weekDays = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]; + const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date())); + const scrollContainerRef = useRef(null); + // Calcul des dates des jours de la semaine + const startDate = startOfWeek(new Date(), { weekStartsOn: 1 }); // Début de la semaine (lundi) + const weekDayDates = weekDays.map((_, index) => addDays(startDate, index)); + + // Fonction pour formater l'heure + const formatTime = (time) => { + const [hour, minute] = time.split(':'); + return `${hour}h${minute}`; + }; + + const renderCells = () => { + const cells = []; + const timeSlots = Array.from({ length: 12 }, (_, index) => index + 8); // Heures de 08:00 à 19:00 + + timeSlots.forEach(hour => { + cells.push( +
+ {`${hour.toString().padStart(2, '0')}:00`} +
+ ); + + weekDays.forEach(day => { + const daySchedule = schedule?.emploiDuTemps?.[day] || []; + const courses = daySchedule.filter(course => { + const courseHour = parseInt(course.heure.split(':')[0], 10); + const courseDuration = parseInt(course.duree, 10); // Utiliser la durée comme un nombre + return courseHour <= hour && hour < (courseHour + courseDuration); + }); + + const course = courses.length > 0 ? courses[0] : null; + + cells.push( + + ); + }); + }); + + return cells; + }; + + + // Calculer la position de la ligne de temps + const getCurrentTimePosition = () => { + const hours = currentTime.getHours(); + const minutes = currentTime.getMinutes(); + + if (hours < 8 || hours > 18) { + return -1; // Hors de la plage horaire + } + + const totalMinutes = (hours - 8) * 60 + minutes; // Minutes écoulées depuis 8h + const cellHeight = 80; // Hauteur des cellules (par exemple 80px pour 20rem / 24) + + const position = (totalMinutes / 60) * cellHeight; + + return position; + }; + + return ( +
+ {/* En-tête des jours */} +
+
+ {weekDayDates.map((day) => ( +
+
+ {format(day, 'EEEE', { locale: fr })} +
+
+ ))} +
+ + {/* Grille horaire */} +
+ {isCurrentWeek && ( +
+
+
+ )} + +
+ {renderCells()} +
+
+
+ ); +}; + +PlanningClassView.propTypes = { + schedule: PropTypes.shape({ + emploiDuTemps: PropTypes.objectOf( + PropTypes.arrayOf( + PropTypes.shape({ + duree: PropTypes.string.isRequired, + heure: PropTypes.string.isRequired, + matiere: PropTypes.string.isRequired, + teachers: PropTypes.arrayOf(PropTypes.string).isRequired, + color: PropTypes.string.isRequired, + }) + ) + ).isRequired, + }).isRequired, +}; + +export default PlanningClassView; diff --git a/Front-End/src/components/Structure/Planning/ScheduleManagement.js b/Front-End/src/components/Structure/Planning/ScheduleManagement.js new file mode 100644 index 0000000..6b858ee --- /dev/null +++ b/Front-End/src/components/Structure/Planning/ScheduleManagement.js @@ -0,0 +1,195 @@ +'use client' + +import React, { useState, useEffect, useRef } from 'react'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { DndProvider } from 'react-dnd'; +import { AnimatePresence, findSpring, motion } from 'framer-motion'; // Ajouter cet import +import PlanningClassView from '@/components/Structure/Planning/PlanningClassView'; +import SpecialityEventModal from '@/components/Structure/Planning/SpecialityEventModal'; +import ClassesInfo from '@/components/Structure/Planning/ClassesInfo'; +import { BK_GESTIONENSEIGNANTS_PLANNING_URL } from '@/utils/Url'; +import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react' +import SelectChoice from '@/components/SelectChoice'; + +const ScheduleManagement = ({ schedules, setSchedules, handleUpdatePlanning, specialities, teachers, classes }) => { + + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedClass, setSelectedClass] = useState(null); + const [schedule, setSchedule] = useState(null); + const scheduleRef = useRef(null); + const scheduleId = useRef(null); + const [planningType, setPlanningType] = useState('TRIMESTRIEL'); + const [currentPeriod, setCurrentPeriod] = useState('T1'); + + const planningChoices = [ + { value: 'TRIMESTRIEL', label: 'TRIMESTRIEL' }, + { value: 'SEMESTRIEL', label: 'SEMESTRIEL' }, + { value: 'ANNUEL', label: 'ANNUEL' }, + ]; + + useEffect(() => { + if (selectedClass) { + scheduleId.current = selectedClass.planning.id; + setSchedule(selectedClass.planning); + } else { + setSchedule(null); + } + }, [selectedClass]); + + // Synchroniser scheduleRef avec schedule + useEffect(() => { + scheduleRef.current = schedule; + }, [schedule]); + + const handleClassSelect = (cls) => { + setSelectedClass(cls); + }; + + useEffect(() => { + if (schedule) { + console.log("Schedule data:", schedule); + } + }, [schedule]); + + + // Fonction onDrop dans PlanningClassView ou un composant parent + const onDrop = (item, hour, day) => { + // Les données de l'élément drag and drop (spécialité par exemple) + const { id, name, color, teachers } = item; + + // Créez une nouvelle copie du planning + const newSchedule = { ...scheduleRef.current }; + + // Vérifiez s'il existe déjà une entrée pour le jour + if (!newSchedule.emploiDuTemps[day]) { + newSchedule.emploiDuTemps[day] = []; + } + const courseTime = `${hour.toString().padStart(2, '0')}:00`; + + // Rechercher s'il y a déjà un cours à l'heure spécifiée + const existingCourseIndex = newSchedule.emploiDuTemps[day].findIndex(course => + course.heure === courseTime + ); + + const newCourse = { + duree: '1', + heure: courseTime, + matiere: name, + teachers: teachers, + color: color, + }; + + // S'il existe déjà un cours à cette heure, on le remplace + if (existingCourseIndex !== -1) { + newSchedule.emploiDuTemps[day][existingCourseIndex] = newCourse; + } else { + // Sinon on ajoute le nouveau cours + newSchedule.emploiDuTemps[day].push(newCourse); + } + + // Mettez à jour l'état du planning + setSchedule(newSchedule) + + // Appelez la fonction handleUpdatePlanning en dehors de setSchedule + handleUpdatePlanning(`${BK_GESTIONENSEIGNANTS_PLANNING_URL}`, scheduleId.current, newSchedule); + }; + + const getPeriodLabel = (period) => { + switch(period) { + case 'T1': return '1er trimestre'; + case 'T2': return '2e trimestre'; + case 'T3': return '3e trimestre'; + case 'S1': return '1er semestre'; + case 'S2': return '2e semestre'; + default: return period; + } + }; + + const handlePeriodChange = (direction) => { + if (planningType === 'TRIMESTRIEL') { + if (direction === 'prev') { + setCurrentPeriod(currentPeriod === 'T1' ? 'T3' : `T${parseInt(currentPeriod.slice(1)) - 1}`); + } else { + setCurrentPeriod(currentPeriod === 'T3' ? 'T1' : `T${parseInt(currentPeriod.slice(1)) + 1}`); + } + } else if (planningType === 'SEMESTRIEL') { + setCurrentPeriod(currentPeriod === 'S1' ? 'S2' : 'S1'); + } + }; + + // Fonctionnalité de gestion des emplois du temps + return ( +
+ + + +
+
+ { + setPlanningType(e.target.value); + setCurrentPeriod(e.target.value === 'TRIMESTRIEL' ? 'T1' : 'S1'); + }} + /> +
+ + {planningType !== 'ANNUEL' && ( +
+ + {getPeriodLabel(currentPeriod)} + +
+ )} +
+ +
+ + + + + +
+
+ {/* setIsModalOpen(false)} + eventData={eventData} + setEventData={setEventData} + selectedClass={selectedClass} + /> */} +
+ ); +}; + +export default ScheduleManagement; diff --git a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js new file mode 100644 index 0000000..4265b3c --- /dev/null +++ b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js @@ -0,0 +1,186 @@ +import { usePlanning } from '@/context/PlanningContext'; +import SelectChoice from '@/components/SelectChoice'; +import { format } from 'date-fns'; +import React, { useEffect } from 'react'; +import { Users, BookOpen } from 'lucide-react'; + +export default function SpecialityEventModal({ isOpen, onClose, eventData, setEventData, selectedClass }) { + const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning(); + + useEffect(() => { + if (!isOpen) { + // Réinitialiser eventData lorsque la modale se ferme + setEventData({ + scheduleId: '', + specialiteId: '', + specialities: [], + // Réinitialiser d'autres champs si nécessaire + }); + } + }, [isOpen, setEventData]); + + useEffect(() => { + if (isOpen && selectedClass) { + + setEventData(prev => ({ + ...prev, + scheduleId: selectedClass.id, + specialities: Array.from(new Map( + selectedClass.enseignants.flatMap(teacher => + teacher.specialites.map(specialite => [specialite.id, { + id: specialite.id, + nom: specialite.nom, + codeCouleur: specialite.codeCouleur + }]) + ) + ).values()) + })); + } + }, [isOpen, selectedClass, setEventData]); + + if (!isOpen) return null; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!eventData.scheduleId) { + alert('Veuillez sélectionner une spécialité'); + return; + } + + const selectedSchedule = schedules.find(s => s.id === eventData.scheduleId); + + if (eventData.id) { + updateEvent(eventData.id, { + ...eventData, + scheduleId: eventData.scheduleId, + color: eventData.color || selectedSchedule?.color + }); + } else { + addEvent({ + ...eventData, + id: `event-${Date.now()}`, + scheduleId: eventData.scheduleId, + color: eventData.color || selectedSchedule?.color + }); + } + onClose(); + }; + + const handleDelete = () => { + if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) { + deleteEvent(eventData.id); + onClose(); + } + }; + + return ( +
+
+

+ {eventData.id ? 'Modifier l\'événement' : 'Nouvel événement'} +

+ + + {/* Sélection de la Spécialité */} +
+ {eventData.scheduleId && eventData.specialities && eventData.specialities.length > 0 ? ( + ({ + value: specialite.id, + label: specialite.nom + }))} + callback={(event) => { + const selectedSpecialityId = event.target.value; + const selectedSpeciality = eventData.specialities.find(specialite => specialite.id === parseInt(selectedSpecialityId, 10)); + setEventData({ + ...eventData, + specialiteId: selectedSpecialityId, + color: selectedSpeciality?.codeCouleur || '#10b981' + }); + }} + IconItem={BookOpen} + /> + ) : ( +

Aucune spécialité disponible pour cette classe.

+ )} +
+ + {/* Dates */} +
+
+ + setEventData({ ...eventData, start: new Date(e.target.value).toISOString() })} + className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500" + required + /> +
+
+ + setEventData({ ...eventData, end: new Date(e.target.value).toISOString() })} + className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500" + required + /> +
+
+ + {/* Lieu */} +
+ + setEventData({ ...eventData, location: e.target.value })} + className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500" + /> +
+ + {/* Boutons */} +
+
+ {eventData.id && ( + + )} +
+
+ + +
+
+ +
+
+ ); +} diff --git a/Front-End/src/components/Table.js b/Front-End/src/components/Table.js index 82a18ee..d22d378 100644 --- a/Front-End/src/components/Table.js +++ b/Front-End/src/components/Table.js @@ -1,8 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Pagination from '@/components/Pagination'; // Correction du chemin d'importation +import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio, -const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange }) => { +const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false }) => { const handlePageChange = (newPage) => { onPageChange(newPage); }; @@ -21,9 +21,17 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
{data?.map((row, rowIndex) => ( - + isSelectable && onRowClick && onRowClick(row)} + > {columns.map((column, colIndex) => ( - ))} diff --git a/Front-End/src/context/ClasseFormContext.js b/Front-End/src/context/ClasseFormContext.js new file mode 100644 index 0000000..0fcd629 --- /dev/null +++ b/Front-End/src/context/ClasseFormContext.js @@ -0,0 +1,70 @@ +import React, { createContext, useState, useContext } from 'react'; +import { useClasses } from '@/context/ClassesContext' + +const ClasseFormContext = createContext(); + +export const useClasseForm = () => useContext(ClasseFormContext); + +export const ClasseFormProvider = ({ children, initialClasse }) => { + + const { getNiveauxLabels, selectedDays } = useClasses(); + + const [formData, setFormData] = useState(() => { + const planning = initialClasse.planning || {}; + const emploiDuTemps = planning.emploiDuTemps || {}; + + const dateDebutSemestre1 = emploiDuTemps.S1 ? emploiDuTemps.S1.DateDebut : ''; + const dateFinSemestre1 = emploiDuTemps.S1 ? emploiDuTemps.S1.DateFin : ''; + const dateDebutSemestre2 = emploiDuTemps.S2 ? emploiDuTemps.S2.DateDebut : ''; + const dateFinSemestre2 = emploiDuTemps.S2 ? emploiDuTemps.S2.DateFin : ''; + + const dateDebutTrimestre1 = emploiDuTemps.T1 ? emploiDuTemps.T1.DateDebut : ''; + const dateFinTrimestre1 = emploiDuTemps.T1 ? emploiDuTemps.T1.DateFin : ''; + const dateDebutTrimestre2 = emploiDuTemps.T2 ? emploiDuTemps.T2.DateDebut : ''; + const dateFinTrimestre2 = emploiDuTemps.T2 ? emploiDuTemps.T2.DateFin : ''; + const dateDebutTrimestre3 = emploiDuTemps.T3 ? emploiDuTemps.T3.DateDebut : ''; + const dateFinTrimestre3 = emploiDuTemps.T3 ? emploiDuTemps.T3.DateFin : ''; + + return { + nom_ambiance: initialClasse.nom_ambiance || '', + tranche_age: initialClasse.tranche_age || '', + nombre_eleves: initialClasse.nombre_eleves || '', + langue_enseignement: initialClasse.langue_enseignement || 'Français', + annee_scolaire: initialClasse.annee_scolaire || '', + enseignants_ids: initialClasse.enseignants_ids || [], + planning_type: planning.type || 1, + plage_horaire: planning.plageHoraire || ['08:30', '17:30'], + jours_ouverture: planning.joursOuverture || [1, 2, 4, 5], + niveaux: initialClasse.niveaux || [], + niveaux_label: getNiveauxLabels(initialClasse.niveaux || []), + date_debut_semestre_1: dateDebutSemestre1, + date_fin_semestre_1: dateFinSemestre1, + date_debut_semestre_2: dateDebutSemestre2, + date_fin_semestre_2: dateFinSemestre2, + date_debut_trimestre_1: dateDebutTrimestre1, + date_fin_trimestre_1: dateFinTrimestre1, + date_debut_trimestre_2: dateDebutTrimestre2, + date_fin_trimestre_2: dateFinTrimestre2, + date_debut_trimestre_3: dateDebutTrimestre3, + date_fin_trimestre_3: dateFinTrimestre3, + planning: { + type: planning.type || 1, + plageHoraire: planning.plageHoraire || ['08:30', '17:30'], + joursOuverture: planning.joursOuverture || [1, 2, 4, 5], + emploiDuTemps: planning.emploiDuTemps || { + S1: { DateDebut: '', DateFin: '', lundi: [], mardi: [], mercredi: [], jeudi: [], vendredi: [], samedi: [], dimanche: [] }, + S2: { DateDebut: '', DateFin: '', lundi: [], mardi: [], mercredi: [], jeudi: [], vendredi: [], samedi: [], dimanche: [] }, + T1: { DateDebut: '', DateFin: '', lundi: [], mardi: [], mercredi: [], jeudi: [], vendredi: [], samedi: [], dimanche: [] }, + T2: { DateDebut: '', DateFin: '', lundi: [], mardi: [], mercredi: [], jeudi: [], vendredi: [], samedi: [], dimanche: [] }, + T3: { DateDebut: '', DateFin: '', lundi: [], mardi: [], mercredi: [], jeudi: [], vendredi: [], samedi: [], dimanche: [] }, + } + } + }; + }); + + return ( + + {children} + + ); +}; diff --git a/Front-End/src/context/ClassesContext.js b/Front-End/src/context/ClassesContext.js new file mode 100644 index 0000000..7050e2b --- /dev/null +++ b/Front-End/src/context/ClassesContext.js @@ -0,0 +1,140 @@ +import React, { createContext, useContext } from 'react'; + +const ClassesContext = createContext(); + +export const useClasses = () => useContext(ClassesContext); + +export const ClassesProvider = ({ children }) => { + const currentYear = new Date().getFullYear(); + + const schoolYears = [ + { value: '', label: 'Sélectionner une période' }, + { value: `${currentYear}-${currentYear + 1}`, label: `${currentYear}-${currentYear + 1}` }, + { value: `${currentYear + 1}-${currentYear + 2}`, label: `${currentYear + 1}-${currentYear + 2}` }, + { value: `${currentYear + 2}-${currentYear + 3}`, label: `${currentYear + 2}-${currentYear + 3}` }, + ]; + + const niveauxPremierCycle = [ + { id: 1, name: 'TPS', age: 2 }, + { id: 2, name: 'PS', age: 3 }, + { id: 3, name: 'MS', age: 4 }, + { id: 4, name: 'GS', age: 5 }, + ]; + + const niveauxSecondCycle = [ + { id: 5, name: 'CP', age: 6 }, + { id: 6, name: 'CE1', age: 7 }, + { id: 7, name: 'CE2', age: 8 }, + ]; + + const niveauxTroisiemeCycle = [ + { id: 8, name: 'CM1', age: 9 }, + { id: 9, name: 'CM2', age: 10 }, + ]; + + const allNiveaux = [...niveauxPremierCycle, ...niveauxSecondCycle, ...niveauxTroisiemeCycle]; + + const typeEmploiDuTemps = [ + { id: 1, label: 'Annuel' }, + { id: 2, label: 'Semestriel' }, + { id: 3, label: 'Trimestriel' }, + ]; + + const selectedDays = { + 1: 'lundi', + 2: 'mardi', + 3: 'mercredi', + 4: 'jeudi', + 5: 'vendredi', + 6: 'samedi', + 7: 'dimanche' + }; + + const getNiveauxLabels = (niveaux) => { + return niveaux.map(niveauId => { + const niveau = allNiveaux.find(n => n.id === niveauId); + return niveau ? niveau.name : niveauId; + }); + }; + + const generateAgeToNiveaux = (minAge, maxAge) => { + if (minAge === null || isNaN(minAge)) { + return []; + } + + return allNiveaux + .filter(({ age }) => age === minAge || (age >= minAge && (maxAge !== null && !isNaN(maxAge) && age < maxAge))) + .map(({ id }) => id); + }; + + const updatePlanning = (formData) => { + + let updatedPlanning = { ...formData.planning }; + + const emploiDuTemps = formData.jours_ouverture.reduce((acc, dayId) => { + const dayName = selectedDays[dayId]; + if (dayName) { + acc[dayName] = []; + } + return acc; + }, {}); + + if (formData.planning_type === 1) { + updatedPlanning = { + type: 1, + plageHoraire: formData.plage_horaire, + joursOuverture: formData.jours_ouverture, + emploiDuTemps + }; + } else if (formData.planning_type === 2) { + updatedPlanning = { + type: 2, + plageHoraire: formData.plage_horaire, + joursOuverture: formData.jours_ouverture, + emploiDuTemps: { + S1: { + DateDebut: formData.date_debut_semestre_1, + DateFin: formData.date_fin_semestre_1, + ...emploiDuTemps + }, + S2: { + DateDebut: formData.date_debut_semestre_2, + DateFin: formData.date_fin_semestre_2, + ...emploiDuTemps + } + } + }; + } else if (formData.planning_type === 3) { + updatedPlanning = { + type: 3, + plageHoraire: formData.plage_horaire, + joursOuverture: formData.jours_ouverture, + emploiDuTemps: { + T1: { + DateDebut: formData.date_debut_trimestre_1, + DateFin: formData.date_fin_trimestre_1, + ...emploiDuTemps + }, + T2: { + DateDebut: formData.date_debut_trimestre_2, + DateFin: formData.date_fin_trimestre_2, + ...emploiDuTemps + }, + T3: { + DateDebut: formData.date_debut_trimestre_3, + DateFin: formData.date_fin_trimestre_3, + ...emploiDuTemps + } + } + }; + } + + return updatedPlanning; + }; + + return ( + + {children} + + ); +}; diff --git a/Front-End/src/context/PlanningContext.js b/Front-End/src/context/PlanningContext.js index 5a90b61..ebf53e9 100644 --- a/Front-End/src/context/PlanningContext.js +++ b/Front-End/src/context/PlanningContext.js @@ -13,6 +13,9 @@ import { mockEvents, mockSchedules } from '@/data/mockData'; const PlanningContext = createContext(); export function PlanningProvider({ children }) { + // const [events, setEvents] = useState([]); + // const [schedules, setSchedules] = useState([]); + // const [selectedSchedule, setSelectedSchedule] = useState(null); const [events, setEvents] = useState(mockEvents); const [schedules, setSchedules] = useState(mockSchedules); const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id); diff --git a/Front-End/src/context/SpecialityFormContext.js b/Front-End/src/context/SpecialityFormContext.js new file mode 100644 index 0000000..c0ed9fa --- /dev/null +++ b/Front-End/src/context/SpecialityFormContext.js @@ -0,0 +1,18 @@ +import React, { createContext, useState, useContext } from 'react'; + +const SpecialityFormContext = createContext(); + +export const useSpecialityForm = () => useContext(SpecialityFormContext); + +export const SpecialityFormProvider = ({ children, initialSpeciality }) => { + const [formData, setFormData] = useState(() => ({ + nom: initialSpeciality.nom || '', + codeCouleur: initialSpeciality.codeCouleur || '#FFFFFF', + })); + + return ( + + {children} + + ); +}; diff --git a/Front-End/src/context/TeacherFormContext.js b/Front-End/src/context/TeacherFormContext.js new file mode 100644 index 0000000..209151e --- /dev/null +++ b/Front-End/src/context/TeacherFormContext.js @@ -0,0 +1,22 @@ +import React, { createContext, useState, useContext } from 'react'; + +const TeacherFormContext = createContext(); + +export const useTeacherForm = () => useContext(TeacherFormContext); + +export const TeacherFormProvider = ({ children, initialTeacher }) => { + const [formData, setFormData] = useState(() => ({ + nom: initialTeacher.nom || '', + prenom: initialTeacher.prenom || '', + mail: initialTeacher.mail || '', + specialites_ids: initialTeacher.specialites_ids || [], + profilAssocie_id: initialTeacher.profilAssocie_id || '', + droit: initialTeacher.droit || 0 + })); + + return ( + + {children} + + ); +}; diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index c7109c4..48fa8c8 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -19,12 +19,14 @@ export const BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL = `${BASE_URL}/GestionI export const BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL = `${BASE_URL}/GestionInscriptions/ficheInscription` export const BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL = `${BASE_URL}/GestionInscriptions/recupereDernierResponsable` export const BK_GESTIONINSCRIPTION_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` -export const BK_GESTIONINSCRIPTION_SPECIALITES_URL = `${BASE_URL}/GestionEnseignants/specialites` -export const BK_GESTIONINSCRIPTION_SPECIALITE_URL = `${BASE_URL}//GestionEnseignants/specialite` -export const BK_GESTIONINSCRIPTION_CLASSES_URL = `${BASE_URL}/GestionEnseignants/classes` -export const BK_GESTIONINSCRIPTION_CLASSE_URL = `${BASE_URL}/GestionEnseignants/classe` -export const BK_GESTIONINSCRIPTION_TEACHERS_URL = `${BASE_URL}/GestionEnseignants/enseignants` -export const BK_GESTIONINSCRIPTION_TEACHER_URL = `${BASE_URL}/GestionEnseignants/enseignant` +export const BK_GESTIONENSEIGNANTS_SPECIALITES_URL = `${BASE_URL}/GestionEnseignants/specialites` +export const BK_GESTIONENSEIGNANTS_SPECIALITE_URL = `${BASE_URL}//GestionEnseignants/specialite` +export const BK_GESTIONENSEIGNANTS_CLASSES_URL = `${BASE_URL}/GestionEnseignants/classes` +export const BK_GESTIONENSEIGNANTS_CLASSE_URL = `${BASE_URL}/GestionEnseignants/classe` +export const BK_GESTIONENSEIGNANTS_TEACHERS_URL = `${BASE_URL}/GestionEnseignants/enseignants` +export const BK_GESTIONENSEIGNANTS_TEACHER_URL = `${BASE_URL}/GestionEnseignants/enseignant` +export const BK_GESTIONENSEIGNANTS_PLANNINGS_URL = `${BASE_URL}/GestionEnseignants/plannings` +export const BK_GESTIONENSEIGNANTS_PLANNING_URL = `${BASE_URL}/GestionEnseignants/planning` export const BK_GET_CSRF = `${BASE_URL}/GestionLogin/csrf`
+ {renderCell ? renderCell(row, column.name) : column.transform(row)}