From 830d9a48c003e1cca469b1cf4082305e16685181 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 11 Jan 2025 19:37:29 +0100 Subject: [PATCH] feat: Configuration et gestion du planning [#2] --- Back-End/GestionEnseignants/models.py | 40 ++- Back-End/GestionEnseignants/serializers.py | 145 ++++---- Back-End/GestionEnseignants/views.py | 26 +- Front-End/package-lock.json | 7 + Front-End/package.json | 1 + .../src/app/[locale]/admin/structure/page.js | 20 +- Front-End/src/components/CheckBoxList.js | 4 +- .../src/components/CustomLabels/LevelLabel.js | 16 + .../components/CustomLabels/TeacherLabel.js | 16 + Front-End/src/components/SelectChoice.js | 59 +-- .../Structure/Configuration/ClassForm.js | 77 ++-- .../Structure/Configuration/ClassesSection.js | 29 +- .../Configuration/PlanningConfiguration.js | 6 +- .../Structure/Configuration/TabsStructure.js | 8 +- .../Structure/Planning/ClassesInfo.js | 81 ----- .../Structure/Planning/ClassesInformation.js | 30 ++ .../Structure/Planning/ClassesList.js | 76 ++++ .../Structure/Planning/DraggableSpeciality.js | 40 +-- .../Structure/Planning/DropTargetCell.js | 59 +-- .../Planning/PlanningClassView copy.js | 249 ------------- .../Structure/Planning/PlanningClassView.js | 264 +++++++++----- .../Structure/Planning/ScheduleManagement.js | 335 +++++++++--------- .../Structure/Planning/SpecialitiesList.js | 21 ++ .../Planning/SpecialityEventModal.js | 304 +++++++++------- Front-End/src/context/ClasseFormContext.js | 110 +++--- Front-End/src/context/ClassesContext.js | 211 +++++++---- 26 files changed, 1163 insertions(+), 1071 deletions(-) create mode 100644 Front-End/src/components/CustomLabels/LevelLabel.js create mode 100644 Front-End/src/components/CustomLabels/TeacherLabel.js delete mode 100644 Front-End/src/components/Structure/Planning/ClassesInfo.js create mode 100644 Front-End/src/components/Structure/Planning/ClassesInformation.js create mode 100644 Front-End/src/components/Structure/Planning/ClassesList.js delete mode 100644 Front-End/src/components/Structure/Planning/PlanningClassView copy.js create mode 100644 Front-End/src/components/Structure/Planning/SpecialitiesList.js diff --git a/Back-End/GestionEnseignants/models.py b/Back-End/GestionEnseignants/models.py index 49bc16d..8a18bb5 100644 --- a/Back-End/GestionEnseignants/models.py +++ b/Back-End/GestionEnseignants/models.py @@ -1,10 +1,21 @@ from django.db import models 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 +NIVEAU_CHOICES = [ + (1, 'Très Petite Section (TPS)'), + (2, 'Petite Section (PS)'), + (3, 'Moyenne Section (MS)'), + (4, 'Grande Section (GS)'), + (5, 'Cours Préparatoire (CP)'), + (6, 'Cours Élémentaire 1 (CE1)'), + (7, 'Cours Élémentaire 2 (CE2)'), + (8, 'Cours Moyen 1 (CM1)'), + (9, 'Cours Moyen 2 (CM2)') +] + class Specialite(models.Model): nom = models.CharField(max_length=100) dateCreation = models.DateTimeField(auto_now=True) @@ -25,6 +36,12 @@ class Enseignant(models.Model): return f"{self.nom} {self.prenom}" class Classe(models.Model): + PLANNING_TYPE_CHOICES = [ + (1, 'Annuel'), + (2, 'Semestriel'), + (3, 'Trimestriel') + ] + nom_ambiance = models.CharField(max_length=255, null=True, blank=True) tranche_age = models.JSONField() nombre_eleves = models.PositiveIntegerField() @@ -32,23 +49,20 @@ class Classe(models.Model): 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) + niveaux = ArrayField(models.IntegerField(choices=NIVEAU_CHOICES), default=list) + type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) + plage_horaire = models.JSONField(default=list) + jours_ouverture = ArrayField(models.IntegerField(), 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') + niveau = models.IntegerField(choices=NIVEAU_CHOICES, null=True, blank=True) + classe = models.ForeignKey(Classe, null=True, blank=True, related_name='plannings', on_delete=models.CASCADE) 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}' + return f'Planning de {self.niveau} pour {self.classe.nom_ambiance}' + + diff --git a/Back-End/GestionEnseignants/serializers.py b/Back-End/GestionEnseignants/serializers.py index 37da853..979adda 100644 --- a/Back-End/GestionEnseignants/serializers.py +++ b/Back-End/GestionEnseignants/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Enseignant, Specialite, Classe, Planning +from .models import Enseignant, Specialite, Classe, Planning, NIVEAU_CHOICES from GestionInscriptions.models import FicheInscription from GestionInscriptions.serializers import EleveSerializer from GestionLogin.serializers import ProfilSerializer @@ -75,52 +75,52 @@ class EnseignantSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") class PlanningSerializer(serializers.ModelSerializer): - emploiDuTemps = serializers.SerializerMethodField() + # emploiDuTemps = serializers.SerializerMethodField() class Meta: model = Planning - fields = ['id', 'emploiDuTemps', 'type', 'plageHoraire', 'joursOuverture'] + fields = ['id', 'niveau', 'emploiDuTemps'] - 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 + # 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}") + # # 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 - ) + # if obj.classe.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 - ) + # elif obj.classe.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 + # return emploi_du_temps def to_internal_value(self, data): internal_value = super().to_internal_value(data) @@ -132,47 +132,51 @@ class ClasseSerializer(serializers.ModelSerializer): enseignants = EnseignantSerializer(many=True, read_only=True) enseignants_ids = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), many=True, source='enseignants') eleves = serializers.SerializerMethodField() - planning = PlanningSerializer() + niveaux = serializers.ListField(child=serializers.ChoiceField(choices=NIVEAU_CHOICES)) + plannings_read = serializers.SerializerMethodField() + plannings = PlanningSerializer(many=True, write_only=True) class Meta: model = Classe fields = [ 'id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'enseignants', 'enseignants_ids', 'annee_scolaire', 'dateCreation', - 'dateCreation_formattee', 'eleves', 'planning', 'niveaux' + 'dateCreation_formattee', 'eleves', 'niveaux', 'type', 'plage_horaire', + 'jours_ouverture', 'plannings', 'plannings_read' ] def create(self, validated_data): - planning_data = validated_data.pop('planning', None) enseignants_data = validated_data.pop('enseignants', []) niveaux_data = validated_data.pop('niveaux', []) - + plannings_data = validated_data.pop('plannings', []) + 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 + niveaux=niveaux_data, + type=validated_data.get('type', 1), # Ajouté ici + plage_horaire=validated_data.get('plage_horaire', ['08:30', '17:30']), # Ajouté ici + jours_ouverture=validated_data.get('jours_ouverture', [1, 2, 4, 5]) # Ajouté ici ) classe.enseignants.set(enseignants_data) - if planning_data and not hasattr(classe, 'planning'): + for planning_data in plannings_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', []) + niveau=planning_data['niveau'], + emploiDuTemps=planning_data.get('emploiDuTemps', {}) ) 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', []) + plannings_data = validated_data.pop('plannings', []) instance.nom_ambiance = validated_data.get('nom_ambiance', instance.nom_ambiance) instance.tranche_age = validated_data.get('tranche_age', instance.tranche_age) @@ -180,23 +184,34 @@ class ClasseSerializer(serializers.ModelSerializer): 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.type = validated_data.get('type', instance.type) # Ajouté ici + instance.plage_horaire = validated_data.get('plage_horaire', instance.plage_horaire) # Ajouté ici + instance.jours_ouverture = validated_data.get('jours_ouverture', instance.jours_ouverture) # Ajouté ici instance.save() instance.enseignants.set(enseignants_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) + existing_plannings = {planning.niveau: planning for planning in instance.plannings.all()} - planning.save() + for planning_data in plannings_data: + niveau = planning_data['niveau'] + if niveau in existing_plannings: + # Mettre à jour le planning existant + planning = existing_plannings[niveau] + planning.emploiDuTemps = planning_data.get('emploiDuTemps', planning.emploiDuTemps) + planning.save() + else: + # Créer un nouveau planning si niveau non existant + Planning.objects.create( + classe=instance, + niveau=niveau, + emploiDuTemps=planning_data.get('emploiDuTemps', {}) + ) return instance def get_dateCreation_formattee(self, obj): - utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale + utc_time = timezone.localtime(obj.dateCreation) local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) return local_time.strftime("%d-%m-%Y %H:%M") @@ -210,8 +225,12 @@ class ClasseSerializer(serializers.ModelSerializer): filtered_eleves.append(eleve) return EleveSerializer(filtered_eleves, many=True, read_only=True).data - def get_planning(self, obj): - from .serializers import PlanningSerializer - if obj.planning: - return PlanningSerializer(obj.planning).data - return None + def get_plannings_read(self, obj): + plannings = obj.plannings.all() + niveaux_dict = {niveau: {'niveau': niveau, 'planning': None} for niveau in obj.niveaux} + + for planning in plannings: + if planning.niveau in niveaux_dict: + niveaux_dict[planning.niveau]['planning'] = PlanningSerializer(planning).data + + return list(niveaux_dict.values()) diff --git a/Back-End/GestionEnseignants/views.py b/Back-End/GestionEnseignants/views.py index c1facc4..9246da2 100644 --- a/Back-End/GestionEnseignants/views.py +++ b/Back-End/GestionEnseignants/views.py @@ -181,15 +181,24 @@ class ClasseView(APIView): def delete(self, request, _id): classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id) - if classe != None: + if classe is not None: + # Supprimer les plannings associés à la classe + for planning in classe.plannings.all(): + print(f'Planning à supprimer : {planning}') + planning.delete() + + # Retirer la classe des élèves associés for eleve in classe.eleves.all(): - print(f'eleve a retirer la classe : {eleve}') + print(f'Eleve à retirer de la classe : {eleve}') eleve.classeAssociee = None eleve.save() + + # Supprimer la classe classe.delete() - + 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): @@ -218,8 +227,15 @@ class PlanningView(APIView): 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_data = JSONParser().parse(request) + + try: + planning = Planning.objects.get(id=_id) + except Planning.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + except Planning.MultipleObjectsReturned: + return JsonResponse({'error': 'Multiple objects found'}, status=400) + planning_serializer = PlanningSerializer(planning, data=planning_data) if planning_serializer.is_valid(): diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index 6940276..48831dc 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -13,6 +13,7 @@ "date-fns": "^4.1.0", "framer-motion": "^11.11.11", "ics": "^3.8.1", + "lodash": "^4.17.21", "lucide-react": "^0.453.0", "next": "14.2.11", "next-intl": "^3.24.0", @@ -3704,6 +3705,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, diff --git a/Front-End/package.json b/Front-End/package.json index 09f5c06..c880101 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -15,6 +15,7 @@ "date-fns": "^4.1.0", "framer-motion": "^11.11.11", "ics": "^3.8.1", + "lodash": "^4.17.21", "lucide-react": "^0.453.0", "next": "14.2.11", "next-intl": "^3.24.0", diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 74beeb5..06ec2f1 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -10,6 +10,7 @@ import { BK_GESTIONENSEIGNANTS_SPECIALITES_URL, BK_GESTIONENSEIGNANTS_PLANNINGS_URL } from '@/utils/Url'; import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; +import { ClassesProvider } from '@/context/ClassesContext'; export default function Page() { const [specialities, setSpecialities] = useState([]); @@ -121,8 +122,8 @@ export default function Page() { }); }; - const handleUpdatePlanning = (url, id, updatedData) => { - fetch(`${url}/${id}`, { + const handleUpdatePlanning = (url, planningId, updatedData) => { + fetch(`${url}/${planningId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -140,6 +141,7 @@ export default function Page() { console.error('Erreur :', error); }); }; + const handleDelete = (url, id, setDatas) => { fetch(`${url}/${id}`, { @@ -184,14 +186,12 @@ export default function Page() { )} {activeTab === 'Schedule' && ( - + + + )} ); diff --git a/Front-End/src/components/CheckBoxList.js b/Front-End/src/components/CheckBoxList.js index c7a9d75..9514f99 100644 --- a/Front-End/src/components/CheckBoxList.js +++ b/Front-End/src/components/CheckBoxList.js @@ -32,7 +32,7 @@ const CheckBoxList = ({