refactor: Création de nouveaux composants / update formulaire de

création de classe (#2)
This commit is contained in:
N3WT DE COMPET
2024-12-14 15:28:07 +01:00
parent cf144310a1
commit 7acae479da
43 changed files with 2374 additions and 441 deletions

View File

@ -3,8 +3,8 @@ from django.db.models.signals import post_migrate
def create_specialite(sender, **kwargs): def create_specialite(sender, **kwargs):
from .models import Specialite from .models import Specialite
if not Specialite.objects.filter(nom='TRANSVERSE').exists(): if not Specialite.objects.filter(nom='GROUPE').exists():
Specialite.objects.create(nom='TRANSVERSE', codeCouleur='#FF0000') Specialite.objects.create(nom='GROUPE', codeCouleur='#FF0000')
class GestionenseignantsConfig(AppConfig): class GestionenseignantsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'

View File

@ -3,6 +3,7 @@ from GestionLogin.models import Profil
from django.db.models import JSONField from django.db.models import JSONField
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.postgres.fields import ArrayField
class Specialite(models.Model): class Specialite(models.Model):
nom = models.CharField(max_length=100) nom = models.CharField(max_length=100)
@ -18,31 +19,36 @@ class Enseignant(models.Model):
mail = models.EmailField(unique=True) mail = models.EmailField(unique=True)
specialites = models.ManyToManyField(Specialite, related_name='enseignants') specialites = models.ManyToManyField(Specialite, related_name='enseignants')
profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE, null=True, blank=True) profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE, null=True, blank=True)
dateCreation = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return f"{self.nom} {self.prenom}" return f"{self.nom} {self.prenom}"
class Classe(models.Model): 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() tranche_age = models.JSONField()
nombre_eleves = models.PositiveIntegerField() nombre_eleves = models.PositiveIntegerField()
langue_enseignement = models.CharField(max_length=255) langue_enseignement = models.CharField(max_length=255)
annee_scolaire = models.CharField(max_length=9) annee_scolaire = models.CharField(max_length=9)
dateCreation = models.DateTimeField(auto_now_add=True) dateCreation = models.DateTimeField(auto_now_add=True)
enseignants = models.ManyToManyField(Enseignant, related_name='classes') enseignants = models.ManyToManyField(Enseignant, related_name='classes')
niveaux = models.JSONField(default=list)
def __str__(self): def __str__(self):
return self.nom_ambiance return self.nom_ambiance
class Planning(models.Model): 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') 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): def __str__(self):
return f'Planning de {self.classe.nom_ambiance}' 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)

View File

@ -36,10 +36,11 @@ class EnseignantSerializer(serializers.ModelSerializer):
profilAssocie = ProfilSerializer(read_only=True) profilAssocie = ProfilSerializer(read_only=True)
DroitLabel = serializers.SerializerMethodField() DroitLabel = serializers.SerializerMethodField()
DroitValue = serializers.SerializerMethodField() DroitValue = serializers.SerializerMethodField()
dateCreation_formattee = serializers.SerializerMethodField()
class Meta: class Meta:
model = Enseignant 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): def create(self, validated_data):
specialites_data = validated_data.pop('specialites', None) specialites_data = validated_data.pop('specialites', None)
@ -67,63 +68,146 @@ class EnseignantSerializer(serializers.ModelSerializer):
def get_DroitValue(self, obj): def get_DroitValue(self, obj):
return obj.profilAssocie.droit if obj.profilAssocie else None 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): class PlanningSerializer(serializers.ModelSerializer):
classe_id = serializers.PrimaryKeyRelatedField(queryset=Classe.objects.all(), source='classe') emploiDuTemps = serializers.SerializerMethodField()
class Meta: class Meta:
model = Planning 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): class ClasseSerializer(serializers.ModelSerializer):
dateCreation_formattee = serializers.SerializerMethodField() dateCreation_formattee = serializers.SerializerMethodField()
enseignants = EnseignantSerializer(many=True, read_only=True) enseignants = EnseignantSerializer(many=True, read_only=True)
enseignants_ids = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), many=True, source='enseignants') enseignants_ids = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), many=True, source='enseignants')
eleves = serializers.SerializerMethodField() eleves = serializers.SerializerMethodField()
planning = serializers.SerializerMethodField() planning = PlanningSerializer()
class Meta: class Meta:
model = Classe 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): def create(self, validated_data):
planning_data = validated_data.pop('planning', None) planning_data = validated_data.pop('planning', None)
enseignants_data = validated_data.pop('enseignants', []) 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) classe.enseignants.set(enseignants_data)
if planning_data and not hasattr(classe, 'planning'): 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 return classe
def update(self, instance, validated_data): def update(self, instance, validated_data):
planning_data = validated_data.pop('planning', None) planning_data = validated_data.pop('planning', None)
enseignants_data = validated_data.pop('enseignants', []) enseignants_data = validated_data.pop('enseignants', [])
niveaux_data = validated_data.pop('niveaux', [])
instance.nom_ambiance = validated_data.get('nom_ambiance', instance.nom_ambiance) instance.nom_ambiance = validated_data.get('nom_ambiance', instance.nom_ambiance)
instance.tranche_age = validated_data.get('tranche_age', instance.tranche_age) instance.tranche_age = validated_data.get('tranche_age', instance.tranche_age)
instance.nombre_eleves = validated_data.get('nombre_eleves', instance.nombre_eleves) instance.nombre_eleves = validated_data.get('nombre_eleves', instance.nombre_eleves)
instance.langue_enseignement = validated_data.get('langue_enseignement', instance.langue_enseignement) instance.langue_enseignement = validated_data.get('langue_enseignement', instance.langue_enseignement)
instance.annee_scolaire = validated_data.get('annee_scolaire', instance.annee_scolaire) instance.annee_scolaire = validated_data.get('annee_scolaire', instance.annee_scolaire)
instance.niveaux = niveaux_data
instance.save() instance.save()
instance.enseignants.set(enseignants_data) instance.enseignants.set(enseignants_data)
if planning_data:
if planning_data:
planning = instance.planning planning = instance.planning
planning.emploiDuTemps = planning_data.get('emploiDuTemps', planning.emploiDuTemps) 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() planning.save()
return instance return instance
def get_dateCreation_formattee(self, obj): def get_dateCreation_formattee(self, obj):
utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale
local_tz = pytz.timezone(settings.TZ_APPLI) local_tz = pytz.timezone(settings.TZ_APPLI)
local_time = utc_time.astimezone(local_tz) local_time = utc_time.astimezone(local_tz)
return local_time.strftime("%d-%m-%Y %H:%M") return local_time.strftime("%d-%m-%Y %H:%M")
def get_eleves(self, obj): def get_eleves(self, obj):
elevesList = obj.eleves.all() elevesList = obj.eleves.all()
filtered_eleves = [] filtered_eleves = []
for eleve in elevesList: 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: 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 return EleveSerializer(filtered_eleves, many=True, read_only=True).data
def get_planning(self, obj): def get_planning(self, obj):

View File

@ -1,6 +1,6 @@
from django.urls import path, re_path 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 = [ urlpatterns = [
re_path(r'^enseignants$', EnseignantsView.as_view(), name="enseignants"), 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'^classes$', ClassesView.as_view(), name="classes"),
re_path(r'^classe$', ClasseView.as_view(), name="classe"), re_path(r'^classe$', ClasseView.as_view(), name="classe"),
re_path(r'^classe/([0-9]+)$', 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"),
] ]

View File

@ -4,8 +4,8 @@ from django.utils.decorators import method_decorator
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from django.core.cache import cache from django.core.cache import cache
from .models import Enseignant, Specialite, Classe from .models import Enseignant, Specialite, Classe, Planning
from .serializers import EnseignantSerializer, SpecialiteSerializer, ClasseSerializer from .serializers import EnseignantSerializer, SpecialiteSerializer, ClasseSerializer, PlanningSerializer
from N3wtSchool import bdd from N3wtSchool import bdd
class EnseignantsView(APIView): class EnseignantsView(APIView):
@ -188,4 +188,42 @@ class ClasseView(APIView):
eleve.save() eleve.save()
classe.delete() classe.delete()
return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False) 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)

View File

@ -18,6 +18,8 @@
"next-intl": "^3.24.0", "next-intl": "^3.24.0",
"react": "^18", "react": "^18",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18", "react-dom": "^18",
"react-phone-number-input": "^3.4.8", "react-phone-number-input": "^3.4.8",
"react-tooltip": "^5.28.0" "react-tooltip": "^5.28.0"
@ -106,6 +108,18 @@
"node": ">=6.0.0" "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": { "node_modules/@babel/template": {
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "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": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"dev": true, "dev": true,
@ -1720,9 +1752,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -1733,7 +1766,9 @@
"integrity": "sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow==" "integrity": "sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow=="
}, },
"node_modules/cross-spawn": { "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", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@ -1934,6 +1969,17 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" "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": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"dev": true, "dev": true,
@ -2562,7 +2608,6 @@
}, },
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
@ -4396,6 +4441,45 @@
"react": ">= 16.3.0" "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": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"license": "MIT", "license": "MIT",
@ -4529,6 +4613,15 @@
"node": ">=8.10.0" "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": { "node_modules/reflect.getprototypeof": {
"version": "1.0.6", "version": "1.0.6",
"dev": true, "dev": true,
@ -4549,6 +4642,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/regexp.prototype.flags": {
"version": "1.5.2", "version": "1.5.2",
"dev": true, "dev": true,
@ -5306,12 +5405,13 @@
} }
}, },
"node_modules/universal-cookie": { "node_modules/universal-cookie": {
"version": "7.2.0", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.0.tgz", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz",
"integrity": "sha512-PvcyflJAYACJKr28HABxkGemML5vafHmiL4ICe3e+BEKXRMt0GaFLZhAwgv637kFFnnfiSJ8e6jknrKkMrU+PQ==", "integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"cookie": "^0.6.0" "cookie": "^0.7.2"
} }
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {

View File

@ -20,6 +20,8 @@
"next-intl": "^3.24.0", "next-intl": "^3.24.0",
"react": "^18", "react": "^18",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18", "react-dom": "^18",
"react-phone-number-input": "^3.4.8", "react-phone-number-input": "^3.4.8",
"react-tooltip": "^5.28.0" "react-tooltip": "^5.28.0"
@ -33,4 +35,4 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.14"
} }
} }

View File

@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react'; import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
import Loader from '@/components/Loader'; 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'; import ClasseDetails from '@/components/ClasseDetails';
// Composant StatCard pour afficher une statistique // Composant StatCard pour afficher une statistique
@ -59,7 +59,7 @@ export default function DashboardPage() {
const [classes, setClasses] = useState([]); const [classes, setClasses] = useState([]);
const fetchClasses = () => { const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
setClasses(data); setClasses(data);

View File

@ -1,14 +1,13 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import SpecialitiesSection from '@/components/SpecialitiesSection' import { School, Calendar } from 'lucide-react';
import ClassesSection from '@/components/ClassesSection' import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
import TeachersSection from '@/components/TeachersSection'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'
import { BK_GESTIONINSCRIPTION_SPECIALITES_URL, import StructureManagement from '@/components/Structure/Configuration/StructureManagement'
BK_GESTIONINSCRIPTION_CLASSES_URL, import { BK_GESTIONENSEIGNANTS_SPECIALITES_URL,
BK_GESTIONINSCRIPTION_SPECIALITE_URL, BK_GESTIONENSEIGNANTS_CLASSES_URL,
BK_GESTIONINSCRIPTION_CLASSE_URL, BK_GESTIONENSEIGNANTS_TEACHERS_URL,
BK_GESTIONINSCRIPTION_TEACHERS_URL, BK_GESTIONENSEIGNANTS_PLANNINGS_URL } from '@/utils/Url';
BK_GESTIONINSCRIPTION_TEACHER_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken' import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken'; import useCsrfToken from '@/hooks/useCsrfToken';
@ -16,6 +15,12 @@ export default function Page() {
const [specialities, setSpecialities] = useState([]); const [specialities, setSpecialities] = useState([]);
const [classes, setClasses] = useState([]); const [classes, setClasses] = useState([]);
const [teachers, setTeachers] = 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(); const csrfToken = useCsrfToken();
@ -28,10 +33,13 @@ export default function Page() {
// Fetch data for classes // Fetch data for classes
fetchClasses(); fetchClasses();
// Fetch data for schedules
fetchSchedules();
}, []); }, []);
const fetchSpecialities = () => { const fetchSpecialities = () => {
fetch(`${BK_GESTIONINSCRIPTION_SPECIALITES_URL}`) fetch(`${BK_GESTIONENSEIGNANTS_SPECIALITES_URL}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
setSpecialities(data); setSpecialities(data);
@ -42,7 +50,7 @@ export default function Page() {
}; };
const fetchTeachers = () => { const fetchTeachers = () => {
fetch(`${BK_GESTIONINSCRIPTION_TEACHERS_URL}`) fetch(`${BK_GESTIONENSEIGNANTS_TEACHERS_URL}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
setTeachers(data); setTeachers(data);
@ -53,7 +61,7 @@ export default function Page() {
}; };
const fetchClasses = () => { const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
setClasses(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) => { const handleCreate = (url, newData, setDatas) => {
fetch(url, { fetch(url, {
method: 'POST', 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) => { const handleDelete = (url, id, setDatas) => {
fetch(`${url}/${id}`, { fetch(`${url}/${id}`, {
method:'DELETE', method:'DELETE',
@ -127,30 +166,33 @@ export default function Page() {
<div className='p-8'> <div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} /> <DjangoCSRFToken csrfToken={csrfToken} />
<SpecialitiesSection <TabsStructure activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} />
specialities={specialities}
setSpecialities={setSpecialities}
handleCreate={(newData) => 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)}
/>
<TeachersSection {activeTab === 'Configuration' && (
teachers={teachers} <>
specialities={specialities} <StructureManagement
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, newData, setTeachers)} specialities={specialities}
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, updatedData, setTeachers)} setSpecialities={setSpecialities}
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, setTeachers)} teachers={teachers}
/> setTeachers={setTeachers}
classes={classes}
setClasses={setClasses}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete} />
</>
)}
<ClassesSection {activeTab === 'Schedule' && (
classes={classes} <ScheduleManagement
specialities={specialities} schedules={schedules}
teachers={teachers} setSchedules={setSchedules}
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, newData, setClasses)} handleUpdatePlanning={handleUpdatePlanning}
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, updatedData, setClasses)} specialities={specialities}
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, setClasses)} teachers={teachers}
/> classes={classes}
/>
)}
</div> </div>
); );
}; };

View File

@ -22,7 +22,7 @@ import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL,
BK_GESTIONINSCRIPTION_SEND_URL, BK_GESTIONINSCRIPTION_SEND_URL,
FR_ADMIN_SUBSCRIPTIONS_EDIT_URL, FR_ADMIN_SUBSCRIPTIONS_EDIT_URL,
BK_GESTIONINSCRIPTION_ARCHIVE_URL, BK_GESTIONINSCRIPTION_ARCHIVE_URL,
BK_GESTIONINSCRIPTION_CLASSES_URL, BK_GESTIONENSEIGNANTS_CLASSES_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL, BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL,
BK_GESTIONINSCRIPTION_ELEVES_URL, BK_GESTIONINSCRIPTION_ELEVES_URL,
BK_PROFILE_URL } from '@/utils/Url'; BK_PROFILE_URL } from '@/utils/Url';
@ -147,7 +147,7 @@ export default function Page({ params: { locale } }) {
}; };
const fetchClasses = () => { const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) fetch(`${BK_GESTIONENSEIGNANTS_CLASSES_URL}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
setClasses(data); setClasses(data);

View File

@ -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 {getMessages} from 'next-intl/server';
import "@/css/tailwind.css"; import "@/css/tailwind.css";
export const metadata = { export const metadata = {
title: "N3WT-SCHOOL", title: "N3WT-SCHOOL",
description: "Gestion de l'école", description: "Gestion de l'école",
@ -21,10 +21,7 @@ export const metadata = {
}, },
}; };
export default async function RootLayout({ children, params: {locale}}) { export default async function RootLayout({ children, params: { locale } }) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages(); const messages = await getMessages();
return ( return (
@ -37,5 +34,3 @@ export default async function RootLayout({ children, params: {locale}}) {
</html> </html>
); );
} }

View File

@ -1,43 +1,70 @@
const CheckBoxList = ({ items, formData, handleChange, fieldName, label, icon: Icon, className, itemLabelFunc = (item) => item.name }) => { import React from 'react';
const handleCheckboxChange = (e) => {
handleChange(e); const CheckBoxList = ({
}; items,
formData,
return ( handleChange,
<div className={`mb-4 ${className}`}> fieldName,
<label className="block text-sm font-medium text-gray-700 flex items-center"> label,
{Icon && <Icon className="w-5 h-5 mr-2" />} icon: Icon,
{label} className,
</label> itemLabelFunc = (item) => item.name,
<div className="mt-2 grid grid-cols-1 gap-4"> labelAttenuated = () => false,
{items.map(item => ( horizontal = false // Ajouter l'option horizontal
<div key={item.id} className="flex items-center"> }) => {
const handleCheckboxChange = (e) => {
handleChange(e);
};
return (
<div className={`mb-4 ${className}`}>
<label className="block text-sm font-medium text-gray-700 flex items-center">
{Icon && <Icon className="w-5 h-5 mr-2" />}
{label}
</label>
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
{items.map(item => {
const isChecked = formData[fieldName].includes(parseInt(item.id));
const isAttenuated = labelAttenuated(item) && !isChecked;
return (
<div key={item.id} className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}>
{horizontal && (
<label
htmlFor={`${fieldName}-${item.id}`}
className={`block text-sm text-center mb-1 ${
isAttenuated ? 'text-gray-200' : 'font-bold text-emerald-600'
}`}
>
{itemLabelFunc(item)}
</label>
)}
<input <input
key={`${item.id}-${Math.random()}`}
type="checkbox" type="checkbox"
id={`${fieldName}-${item.id}`} id={`${fieldName}-${item.id}`}
name={fieldName} name={fieldName}
value={item.id} value={item.id}
checked={formData[fieldName].includes(item.id)} checked={isChecked}
onChange={handleCheckboxChange} onChange={handleCheckboxChange}
className="form-checkbox h-4 w-4 rounded-md text-emerald-600 hover:ring-emerald-400 checked:bg-emerald-600 hover:border-emerald-500 hover:bg-emerald-500 cursor-pointer" className={`form-checkbox h-4 w-4 rounded-mg text-emerald-600 hover:ring-emerald-400 checked:bg-emerald-600 hover:border-emerald-500 hover:bg-emerald-500 cursor-pointer ${horizontal ? 'mt-1' : 'mr-2'}`}
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }} // Remove focus styles style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
/> />
<label htmlFor={`${fieldName}-${item.id}`} className="ml-2 block text-sm text-gray-900 flex items-center"> {!horizontal && (
{itemLabelFunc(item)} <label
{item.codeCouleur && ( htmlFor={`${fieldName}-${item.id}`}
<div className={`block text-sm ${
className="w-4 h-4 rounded-full ml-2 hover:bg-opacity-70 transition duration-200 ease-in-out" isAttenuated ? 'text-gray-200' : 'font-bold text-emerald-600'
style={{ backgroundColor: item.codeCouleur }} }`}
title={item.codeCouleur} >
></div> {itemLabelFunc(item)}
)} </label>
</label> )}
</div> </div>
))} );
</div> })}
</div> </div>
); </div>
}; );
};
export default CheckBoxList;
export default CheckBoxList;

View File

@ -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 (
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{teacher.nom} {teacher.prenom} -</span>
{teacher.specialites.map(specialite => (
<span
key={specialite.id}
style={{ display: 'flex', alignItems: 'center', marginLeft: '8px' }}
>
<span>{specialite.nom}</span>
<span
className="w-4 h-4 rounded-full ml-1"
style={{ backgroundColor: specialite.codeCouleur }}
title={specialite.nom}
></span>
</span>
))}
</div>
);
};
return (
<form onSubmit={handleSubmit} className="space-y-4 mt-8">
<div>
<InputTextIcon
name="nom_ambiance"
type="text"
IconItem={Users}
placeholder="Nom d'ambiance"
value={formData.nom_ambiance}
onChange={handleChange}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Tranche d'âge
</label>
<Slider
min={2}
max={18}
value={formData.tranche_age}
onChange={handleSliderChange}
/>
</div>
<div>
<InputTextIcon
name="nombre_eleves"
type="number"
IconItem={Maximize2}
placeholder="Capacité max"
value={formData.nombre_eleves}
onChange={handleChange}
className="w-full"
/>
</div>
<div>
<SelectChoice
name="langue_enseignement"
label="Langue d'enseignement"
selected={formData.langue_enseignement}
callback={handleChange}
choices={langues}
IconItem={Globe}
required
/>
</div>
<div>
<InputTextIcon
name="annee_scolaire"
type="text"
IconItem={Calendar}
placeholder="Année scolaire (20xx-20xx)"
value={formData.annee_scolaire}
onChange={handleChange}
className="w-full"
/>
</div>
<div className="flex space-x-4">
<CheckBoxList
items={teachers}
formData={formData}
handleChange={handleChange}
fieldName="enseignants_ids"
label="Enseignants"
icon={GraduationCap}
className="w-full mt-4"
itemLabelFunc={getTeacherLabel}
/>
</div>
<div className="flex justify-end mt-4 space-x-4">
<Button text="Créer"
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(!formData.nom_ambiance || !formData.nombre_eleves || !formData.annee_scolaire || formData.enseignants_ids.length === 0)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
primary
disabled={(!formData.nom_ambiance || !formData.nombre_eleves || !formData.annee_scolaire || formData.enseignants_ids.length === 0)}
type="submit"
name="Create" />
</div>
</form>
);
};
export default ClassForm;

View File

@ -5,6 +5,17 @@ import React from 'react';
export default function EventModal({ isOpen, onClose, eventData, setEventData }) { export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning(); 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; if (!isOpen) return null;
const recurrenceOptions = [ const recurrenceOptions = [
@ -25,17 +36,6 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
{ value: 0, label: 'Dim' } { 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) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -16,7 +16,7 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
name={name} name={name}
value={value} value={value}
onChange={onChange} 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"
/> />
</div> </div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,39 +1,33 @@
import React from 'react'; import React from 'react';
const RadioList = ({ items, formData, handleChange, fieldName, label, icon: Icon, className, itemLabelFunc}) => { const RadioList = ({ items, formData, handleChange, fieldName, icon: Icon, className }) => {
return ( return (
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label className="block text-sm font-medium text-gray-700 flex items-center"> <div className="grid grid-cols-1 gap-4">
{Icon && <Icon className="w-5 h-5 mr-2" />} {items.map(item => (
{label} <div key={item.id} className="flex items-center">
<input
key={`${item.id}-${Math.random()}`}
type="radio"
id={`${fieldName}-${item.id}`}
name={fieldName}
value={item.id}
checked={parseInt(formData[fieldName], 10) === item.id}
onChange={handleChange}
className="form-radio h-4 w-4 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 cursor-pointer"
style={{ outline: 'none', boxShadow: 'none' }}
/>
<label
htmlFor={`${fieldName}-${item.id}`}
className="ml-2 block text-sm text-gray-900 flex items-center cursor-pointer"
>
{item.label}
</label> </label>
<div className="mt-2 grid grid-cols-1 gap-4"> </div>
{items.map(item => ( ))}
<div key={item.id} className="flex items-center"> </div>
<input </div>
type="radio" );
id={`${fieldName}-${item.id}`}
name={fieldName}
value={item.id}
checked={formData[fieldName] === item.id}
onChange={handleChange}
className="form-radio h-4 w-4 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600"
/>
<label htmlFor={`${fieldName}-${item.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
{itemLabelFunc(item)}
{item.codeCouleur && (
<div
className="w-4 h-4 rounded-full ml-2"
style={{ backgroundColor: item.codeCouleur }}
title={item.codeCouleur}
></div>
)}
</label>
</div>
))}
</div>
</div>
);
}; };
export default RadioList; export default RadioList;

View File

@ -3,12 +3,12 @@ export default function SelectChoice({type, name, label, choices, callback, sele
<> <>
<div className="mb-4"> <div className="mb-4">
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md hover:border-gray-400 focus-within:border-gray-500 h-8`}> <div className={`flex items-center border-2 border-gray-200 rounded-md hover:border-gray-400 focus-within:border-gray-500 h-8 mt-2`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full"> <span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
{IconItem && <IconItem />} {IconItem && <IconItem />}
</span> </span>
<select <select
className="mt-1 block w-full px-2 py-0 text-base rounded-r-md sm:text-sm border-none focus:ring-0 outline-none" className="mt-1 block w-full px-2 py-0 text-base rounded-r-md sm:text-sm border-none focus:ring-0 outline-none cursor-pointer"
type={type} type={type}
id={name} id={name}
name={name} name={name}

View File

@ -0,0 +1,252 @@
import React, { useState } from 'react';
import InputTextIcon from '@/components/InputTextIcon';
import Button from '@/components/Button';
import SelectChoice from '@/components/SelectChoice';
import CheckBoxList from '@/components/CheckBoxList';
import PlanningConfiguration from '@/components/Structure/Configuration/PlanningConfiguration';
import TeachersSelectionConfiguration from '@/components/Structure/Configuration/TeachersSelectionConfiguration';
import { Users, Maximize2, Calendar, UserPlus } from 'lucide-react';
import { useClasseForm } from '@/context/ClasseFormContext';
import { useClasses } from '@/context/ClassesContext';
const ClassForm = ({ onSubmit, isNew, teachers }) => {
const { formData, setFormData } = useClasseForm();
const { schoolYears, getNiveauxLabels, generateAgeToNiveaux, niveauxPremierCycle, niveauxSecondCycle, niveauxTroisiemeCycle, typeEmploiDuTemps, updatePlanning } = useClasses();
const [selectedTeachers, setSelectedTeachers] = useState(formData.enseignants_ids);
const handleTeacherSelection = (teacher) => {
setSelectedTeachers(prevState =>
prevState.includes(teacher.id)
? prevState.filter(id => id !== teacher.id)
: [...prevState, teacher.id]
);
setFormData(prevState => ({
...prevState,
enseignants_ids: prevState.enseignants_ids.includes(teacher.id)
? prevState.enseignants_ids.filter(id => id !== teacher.id)
: [...prevState.enseignants_ids, teacher.id]
}));
};
const handleTimeChange = (e, index) => {
const { value } = e.target;
setFormData(prevState => {
const updatedTimes = [...prevState.plage_horaire];
updatedTimes[index] = value;
const updatedFormData = {
...prevState,
plage_horaire: updatedTimes,
};
updatedFormData.planning = updatePlanning(updatedFormData);
return updatedFormData;
});
};
const handleJoursChange = (e) => {
const { value, checked } = e.target;
const dayId = parseInt(value, 10);
setFormData((prevState) => {
const updatedJoursOuverture = checked
? [...prevState.jours_ouverture, dayId]
: prevState.jours_ouverture.filter((id) => id !== dayId);
const updatedFormData = {
...prevState,
jours_ouverture: updatedJoursOuverture,
};
updatedFormData.planning = updatePlanning(updatedFormData);
return updatedFormData;
});
};
const handleChange = (e) => {
e.preventDefault();
const { name, value, type, checked } = e.target;
setFormData(prevState => {
let updatedFormData = { ...prevState };
if (type === 'checkbox') {
const newValues = checked
? [...(prevState[name] || []), parseInt(value)]
: (prevState[name] || []).filter(v => v !== parseInt(value));
updatedFormData[name] = newValues;
} else if (name === 'tranche_age') {
const [minAgeStr, maxAgeStr] = value.split('-');
const minAge = minAgeStr ? parseInt(minAgeStr) : null;
const maxAge = minAgeStr ? parseInt(maxAgeStr) : null;
const selectedNiveaux = generateAgeToNiveaux(minAge, maxAge);
const niveauxLabels = getNiveauxLabels(selectedNiveaux);
updatedFormData = {
...prevState,
[name]: value,
niveaux: selectedNiveaux.length > 0 ? selectedNiveaux : [],
niveaux_label: niveauxLabels.length > 0 ? niveauxLabels : []
};
} else if (type === 'radio') {
updatedFormData[name] = parseInt(value, 10);
} else {
updatedFormData[name] = value;
}
console.log('Updated formData:', updatedFormData);
updatedFormData.planning = updatePlanning(updatedFormData);
console.log('Final formData:', updatedFormData);
return updatedFormData;
});
};
const handleSubmit = () => {
onSubmit(formData);
};
const [minAge, maxAge] = formData.tranche_age.length === 2 ? formData.tranche_age : [null, null];
const selectedAgeGroup = generateAgeToNiveaux(minAge, maxAge);
return (
<div className="h-[80vh] overflow-y-auto">
<form onSubmit={handleSubmit} className="space-y-4 mt-8">
<div className="flex justify-between space-x-4">
{/* Section Ambiance */}
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Ambiance <i>(optionnel)</i></label>
<div className="space-y-4">
<div>
<InputTextIcon
name="nom_ambiance"
type="text"
IconItem={Users}
placeholder="Nom de l'ambiance"
value={formData.nom_ambiance}
onChange={handleChange}
className="w-full"
/>
</div>
<div>
<InputTextIcon
name="tranche_age"
type="text"
IconItem={Maximize2}
placeholder="Tranche d'âge (ex: 3-6)"
value={formData.tranche_age}
onChange={handleChange}
className="w-full"
/>
</div>
</div>
</div>
{/* Section Niveau */}
<div className="w-1/2 space-y-2">
<label className="block text-lg font-medium text-gray-700">Niveaux</label>
<div className="grid grid-cols-3 gap-4">
<CheckBoxList
items={niveauxPremierCycle}
formData={formData}
handleChange={handleChange}
fieldName="niveaux"
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
className="w-full"
/>
<CheckBoxList
items={niveauxSecondCycle}
formData={formData}
handleChange={handleChange}
fieldName="niveaux"
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
className="w-full"
/>
<CheckBoxList
items={niveauxTroisiemeCycle}
formData={formData}
handleChange={handleChange}
fieldName="niveaux"
labelAttenuated={(item) => !selectedAgeGroup.includes(parseInt(item.id))}
className="w-full"
/>
</div>
</div>
</div>
<div className="flex justify-between space-x-4">
{/* Section Capacité */}
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Capacité</label>
<div className="space-y-4">
<InputTextIcon
name="nombre_eleves"
type="number"
IconItem={UserPlus}
placeholder="Capacité max"
value={formData.nombre_eleves}
onChange={handleChange}
className="w-full"
/>
</div>
</div>
{/* Année scolaire */}
<div className="w-1/2 space-y-4">
<label className="block text-lg font-medium text-gray-700">Année scolaire</label>
<div className="space-y-4">
<SelectChoice
name="annee_scolaire"
placeholder="Sélectionner l'année scolaire"
selected={formData.annee_scolaire}
callback={handleChange}
choices={schoolYears}
IconItem={Calendar}
className="w-full"
/>
</div>
</div>
</div>
{/* Section Enseignants */}
<TeachersSelectionConfiguration formData={formData}
teachers={teachers}
handleTeacherSelection={handleTeacherSelection}
selectedTeachers={selectedTeachers}
/>
{/* Section Emploi du temps */}
<PlanningConfiguration formData={formData}
handleChange={handleChange}
handleTimeChange={handleTimeChange}
handleJoursChange={handleJoursChange}
typeEmploiDuTemps={typeEmploiDuTemps}
/>
<div className="flex justify-end mt-4 space-x-4">
<Button
text={`${isNew ? "Créer" : "Modifier"}`}
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
primary
disabled={(formData.niveaux.length === 0 || !formData.annee_scolaire || !formData.nombre_eleves || formData.enseignants_ids.length === 0)}
type="submit"
name="Create"
/>
</div>
</form>
</div>
);
};
export default ClassForm;

View File

@ -3,11 +3,15 @@ import { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import ClassForm from '@/components/ClassForm'; import ClassForm from '@/components/Structure/Configuration/ClassForm';
import ClasseDetails from '@/components/ClasseDetails'; import ClasseDetails from '@/components/ClasseDetails';
import { ClasseFormProvider } from '@/context/ClasseFormContext';
import { useClasses } from '@/context/ClassesContext';
const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => { const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => {
const { getNiveauxLabels } = useClasses();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isOpenDetails, setIsOpenDetails] = useState(false); const [isOpenDetails, setIsOpenDetails] = useState(false);
const [editingClass, setEditingClass] = useState(null); const [editingClass, setEditingClass] = useState(null);
@ -58,59 +62,97 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE
<div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0"> <div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
<Table <Table
columns={[ columns={[
{ name: 'AMBIANCE', transform: (row) => row.nom_ambiance }, {
{ name: 'AGE', transform: (row) => `${row.tranche_age[0]} - ${row.tranche_age[1]} ans` }, name: 'AMBIANCE',
{ name: 'NOMBRE D\'ELEVES', transform: (row) => row.nombre_eleves }, transform: (row) => {
{ name: 'LANGUE D\'ENSEIGNEMENT', transform: (row) => row.langue_enseignement }, const ambiance = row.nom_ambiance ? row.nom_ambiance : '';
{ name: 'ANNEE SCOLAIRE', transform: (row) => row.annee_scolaire }, const trancheAge = row.tranche_age ? `${row.tranche_age} ans` : '';
if (ambiance && trancheAge) {
return `${ambiance} (${trancheAge})`;
} else if (ambiance) {
return ambiance;
} else if (trancheAge) {
return trancheAge;
} else {
return 'Non spécifié';
}
}
},
{
name: 'NIVEAUX',
transform: (row) => {
const niveauxLabels = Array.isArray(row.niveaux) ? getNiveauxLabels(row.niveaux) : [];
return (
<div className="flex flex-wrap justify-center items-center space-x-2">
{niveauxLabels.length > 0
? niveauxLabels.map((label, index) => (
<div
key={index}
className={`px-3 py-1 rounded-md shadow-sm ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
} border border-gray-200 text-gray-700`}>
{label}
</div>
))
: 'Aucun niveau'}
</div>
);
}
},
{ name: 'CAPACITÉ MAX', transform: (row) => row.nombre_eleves },
{ name: 'ANNÉE SCOLAIRE', transform: (row) => row.annee_scolaire },
{ {
name: 'ENSEIGNANTS', name: 'ENSEIGNANTS',
transform: (row) => ( transform: (row) => (
<div key={row.id} className="flex justify-center items-center space-x-2"> <div key={row.id} className="flex flex-wrap justify-center items-center space-x-2">
{row.enseignants.map(teacher => ( {row.enseignants.map((teacher, index) => (
<div key={teacher.id} className="flex items-center space-x-1"> <div
<span>{teacher.nom} {teacher.prenom}</span> key={teacher.id}
{teacher.specialites.map(specialite => ( className={`px-3 py-1 rounded-md shadow-sm ${
<span index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
key={specialite.id} } border border-gray-200 text-gray-700`}
className="w-4 h-4 rounded-full" >
style={{ backgroundColor: specialite.codeCouleur }} <span className="font-bold">{teacher.nom} {teacher.prenom}</span>
title={specialite.nom}
></span>
))}
</div> </div>
))} ))}
</div> </div>
) )
}, },
{ name: 'ACTIONS', transform: (row) => ( { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
<DropdownMenu {
name: 'ACTIONS', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />} buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[ items={[
{ label: 'Inspecter', icon: ZoomIn, onClick: () => openEditModalDetails(row) }, { label: 'Inspecter', icon: ZoomIn, onClick: () => openEditModalDetails(row) },
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) }, { label: 'Modifier', icon: Edit3, onClick: () => openEditModal(row) },
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) } { label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
] ]
} }
buttonClassName="text-gray-400 hover:text-gray-600" buttonClassName="text-gray-400 hover:text-gray-600"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center" menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
/> />
)} )
}
]} ]}
data={classes} data={classes}
/> />
</div> </div>
{isOpen && ( {isOpen && (
<Modal <ClasseFormProvider initialClasse={editingClass || {}}>
isOpen={isOpen} <Modal
setIsOpen={setIsOpen} isOpen={isOpen}
title={editingClass ? "Modification de la classe" : "Création d'une nouvelle classe"} setIsOpen={setIsOpen}
size='sm:w-1/4' title={editingClass ? "Modification de la classe" : "Création d'une nouvelle classe"}
ContentComponent={() => ( size='sm:w-1/2'
<ClassForm classe={editingClass || {}} onSubmit={handleModalSubmit} isNew={!editingClass} specialities={specialities} teachers={teachers} /> ContentComponent={() => (
)} <ClassForm classe={editingClass || {}} onSubmit={handleModalSubmit} isNew={!editingClass} teachers={teachers} />
/> )}
/>
</ClasseFormProvider>
)} )}
{isOpenDetails && ( {isOpenDetails && (
<Modal <Modal
isOpen={isOpenDetails} isOpen={isOpenDetails}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Calendar } from 'lucide-react';
const DateRange = ({ nameStart, nameEnd, valueStart, valueEnd, onChange, label }) => {
return (
<div className="space-y-4 mt-4 p-4 border rounded-md shadow-sm bg-white">
<label className="block text-lg font-medium text-gray-700 mb-2">{label}</label>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 items-center">
<div className="relative flex items-center">
<span className="mr-2">Du</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameStart}
value={valueStart}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de début"
/>
</div>
<div className="relative flex items-center">
<span className="mr-2">Au</span>
<Calendar className="w-5 h-5 text-emerald-500 absolute top-3 left-16" />
<input
type="date"
name={nameEnd}
value={valueEnd}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500 hover:ring-emerald-400 ml-8"
placeholder="Date de fin"
/>
</div>
</div>
</div>
);
};
export default DateRange;

View File

@ -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 (
<div className="space-y-4">
<label className="mt-6 block text-2xl font-medium text-gray-700">Emploi du temps</label>
<div className="flex justify-between space-x-4 items-start">
<div className="w-1/2">
<RadioList
items={typeEmploiDuTemps}
formData={formData}
handleChange={handleChange}
fieldName="planning_type"
className="w-full"
/>
</div>
{/* Plage horaire */}
<div className="w-1/2">
<TimeRange
startTime={formData.plage_horaire[0]}
endTime={formData.plage_horaire[1]}
onStartChange={(e) => handleTimeChange(e, 0)}
onEndChange={(e) => handleTimeChange(e, 1)}
/>
{/* CheckBoxList */}
<CheckBoxList
items={daysOfWeek}
formData={formData}
handleChange={handleJoursChange}
fieldName="jours_ouverture"
horizontal={true}
labelAttenuated={isLabelAttenuated}
/>
</div>
</div>
{/* DateRange */}
<div className="space-y-4 w-full">
{formData.planning_type === 2 && (
<>
<DateRange
nameStart="date_debut_semestre_1"
nameEnd="date_fin_semestre_1"
valueStart={formData.date_debut_semestre_1}
valueEnd={formData.date_fin_semestre_1}
onChange={handleChange}
label="Semestre 1"
/>
<DateRange
nameStart="date_debut_semestre_2"
nameEnd="date_fin_semestre_2"
valueStart={formData.date_debut_semestre_2}
valueEnd={formData.date_fin_semestre_2}
onChange={handleChange}
label="Semestre 2"
/>
</>
)}
{formData.planning_type === 3 && (
<>
<DateRange
nameStart="date_debut_trimestre_1"
nameEnd="date_fin_trimestre_1"
valueStart={formData.date_debut_trimestre_1}
valueEnd={formData.date_fin_trimestre_1}
onChange={handleChange}
label="Trimestre 1"
/>
<DateRange
nameStart="date_debut_trimestre_2"
nameEnd="date_fin_trimestre_2"
valueStart={formData.date_debut_trimestre_2}
valueEnd={formData.date_fin_trimestre_2}
onChange={handleChange}
label="Trimestre 2"
/>
<DateRange
nameStart="date_debut_trimestre_3"
nameEnd="date_fin_trimestre_3"
valueStart={formData.date_debut_trimestre_3}
valueEnd={formData.date_fin_trimestre_3}
onChange={handleChange}
label="Trimestre 3"
/>
</>
)}
</div>
</div>
);
};
export default PlanningConfiguration;

View File

@ -3,7 +3,8 @@ import { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal'; 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 }) => { const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => {
@ -46,14 +47,18 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
<div className="bg-white rounded-lg border border-gray-200 max-w-4xl ml-0"> <div className="bg-white rounded-lg border border-gray-200 max-w-4xl ml-0">
<Table <Table
columns={[ columns={[
{ name: 'NOM', transform: (row) => row.nom.toUpperCase() }, {
{ name: 'CODE', transform: (row) => ( name: 'INTITULÉ',
<div transform: (row) => (
className="w-4 h-4 rounded-full mx-auto" <div
style={{ backgroundColor: row.codeCouleur }} className="inline-block px-3 py-1 rounded-full font-bold text-white"
title={row.codeCouleur} style={{ backgroundColor: row.codeCouleur }}
></div> title={row.codeCouleur}
)}, >
<span className="font-bold text-white">{row.nom.toUpperCase()}</span>
</div>
)
},
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee }, { name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
{ name: 'ACTIONS', transform: (row) => ( { name: 'ACTIONS', transform: (row) => (
<DropdownMenu <DropdownMenu
@ -72,15 +77,17 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
/> />
</div> </div>
{isOpen && ( {isOpen && (
<Modal <SpecialityFormProvider initialSpeciality={editingSpeciality || {}}>
<Modal
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
title={editingSpeciality ? "Modification de la spécialité" : "Création d'une nouvelle spécialité"} title={editingSpeciality ? "Modification de la spécialité" : "Création d'une nouvelle spécialité"}
size='sm:w-1/6' size='sm:w-1/6'
ContentComponent={() => ( ContentComponent={() => (
<SpecialityForm speciality={editingSpeciality || {}} onSubmit={handleModalSubmit} isNew={!editingSpeciality} /> <SpecialityForm onSubmit={handleModalSubmit} isNew={!editingSpeciality} />
)} )}
/> />
</SpecialityFormProvider>
)} )}
</div> </div>
); );

View File

@ -3,51 +3,59 @@ import { BookOpen, Palette } from 'lucide-react';
import InputTextIcon from '@/components/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import InputColorIcon from '@/components/InputColorIcon'; import InputColorIcon from '@/components/InputColorIcon';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useSpecialityForm } from '@/context/SpecialityFormContext';
const SpecialityForm = ({ speciality = {}, onSubmit, isNew }) => { const SpecialityForm = ({ onSubmit, isNew }) => {
const [nom, setNom] = useState(speciality.nom || ''); const { formData, setFormData } = useSpecialityForm();
const [codeCouleur, setCodeCouleur] = useState(speciality.codeCouleur || '#FFFFFF');
const handleSubmit = () => { const handleChange = (e) => {
const updatedData = { const { name, value } = e.target;
nom,
codeCouleur, setFormData((prevState) => ({
}; ...prevState,
onSubmit(updatedData, isNew); [name]: value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
}; };
return ( return (
<div className="space-y-4 mt-8"> <div className="space-y-4 mt-8">
<div> <div>
<InputTextIcon <InputTextIcon
type="text" type="text"
name="nom"
IconItem={BookOpen} IconItem={BookOpen}
placeholder="Nom de la spécialité" placeholder="Nom de la spécialité"
value={nom} value={formData.nom}
onChange={(e) => setNom(e.target.value)} onChange={handleChange}
className="w-full mt-4" className="w-full mt-4"
/> />
</div> </div>
<div className="mt-4"> <div className="mt-4">
<InputColorIcon <InputColorIcon
type="color" type="color"
name="codeCouleur"
IconItem={Palette} IconItem={Palette}
placeholder="Nom de la spécialité" placeholder="Nom de la spécialité"
value={codeCouleur} value={formData.codeCouleur}
onChange={(e) => setCodeCouleur(e.target.value)} onChange={handleChange}
className="w-full mt-4" className="w-full mt-4"
/> />
</div> </div>
<div className="flex justify-end mt-4 space-x-4"> <div className="flex justify-end mt-4 space-x-4">
<Button text="Créer" <Button text={`${isNew ? "Créer" : "Modifier"}`}
onClick={handleSubmit} onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${ className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
!nom !formData.nom
? "bg-gray-300 text-gray-700 cursor-not-allowed" ? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600" : "bg-emerald-500 text-white hover:bg-emerald-600"
}`} }`}
primary primary
disabled={!nom} disabled={!formData.nom}
type="submit" type="submit"
name="Create" /> name="Create" />
</div> </div>

View File

@ -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 (
<div className='p-8'>
<ClassesProvider>
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
handleCreate={(newData) => 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)}
/>
<TeachersSection
teachers={teachers}
specialities={specialities}
handleCreate={(newData) => 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)}
/>
<ClassesSection
classes={classes}
specialities={specialities}
teachers={teachers}
handleCreate={(newData) => 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)}
/>
</ClassesProvider>
</div>
);
};
export default StructureManagement;

View File

@ -0,0 +1,21 @@
import React from 'react';
import { School, Calendar } from 'lucide-react';
const TabsStructure = ({ activeTab, setActiveTab, tabs }) => {
return (
<div className="flex justify-center mb-8">
{tabs.map((tab) => (
<button
key={tab.id}
className={`tab px-4 py-2 mx-2 flex items-center space-x-2 ${activeTab === tab.id ? 'bg-emerald-600 text-white shadow-lg' : 'bg-emerald-200 text-emerald-600'} rounded-full`}
onClick={() => setActiveTab(tab.id)}
>
<tab.icon className="w-5 h-5" />
<span>{tab.title}</span>
</button>
))}
</div>
);
};
export default TabsStructure;

View File

@ -4,17 +4,10 @@ import InputTextIcon from '@/components/InputTextIcon';
import Button from '@/components/Button'; import Button from '@/components/Button';
import CheckBoxList from '@/components/CheckBoxList'; import CheckBoxList from '@/components/CheckBoxList';
import ToggleSwitch from '@/components/ToggleSwitch' import ToggleSwitch from '@/components/ToggleSwitch'
import { useTeacherForm } from '@/context/TeacherFormContext';
const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => { const TeacherForm = ({ onSubmit, isNew, specialities }) => {
const { formData, setFormData } = useTeacherForm();
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 handleToggleChange = () => { const handleToggleChange = () => {
setFormData({ ...formData, droit: 1-formData.droit }); setFormData({ ...formData, droit: 1-formData.droit });
@ -24,8 +17,6 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
const target = e.target || e.currentTarget; const target = e.target || e.currentTarget;
const { name, value, type, checked } = target; const { name, value, type, checked } = target;
console.log('type : ', type);
if (type === 'checkbox') { if (type === 'checkbox') {
setFormData((prevState) => { setFormData((prevState) => {
const newValues = checked const newValues = checked
@ -52,6 +43,10 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
return `${speciality.nom}`; return `${speciality.nom}`;
}; };
const isLabelAttenuated = (item) => {
return !formData.specialites_ids.includes(parseInt(item.id));
};
return ( return (
<form onSubmit={handleSubmit} className="space-y-4 mt-8"> <form onSubmit={handleSubmit} className="space-y-4 mt-8">
<div> <div>
@ -97,6 +92,7 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
icon={BookOpen} icon={BookOpen}
className="w-full mt-4" className="w-full mt-4"
itemLabelFunc={getSpecialityLabel} itemLabelFunc={getSpecialityLabel}
labelAttenuated={isLabelAttenuated}
/> />
</div> </div>
<div className='mt-4'> <div className='mt-4'>
@ -107,7 +103,7 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
/> />
</div> </div>
<div className="flex justify-end mt-4 space-x-4"> <div className="flex justify-end mt-4 space-x-4">
<Button text="Créer" <Button text={`${isNew ? "Créer" : "Modifier"}`}
onClick={handleSubmit} onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${ className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(!formData.nom || !formData.prenom || !formData.mail || formData.specialites_ids.length === 0) (!formData.nom || !formData.prenom || !formData.mail || formData.specialites_ids.length === 0)

View File

@ -3,9 +3,10 @@ import { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import TeacherForm from '@/components/TeacherForm'; import TeacherForm from '@/components/Structure/Configuration/TeacherForm';
import {BK_PROFILE_URL} from '@/utils/Url'; import {BK_PROFILE_URL} from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken'; import useCsrfToken from '@/hooks/useCsrfToken';
import { TeacherFormProvider } from '@/context/TeacherFormContext';
const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => { const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => {
@ -96,7 +97,7 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-7xl ml-0"> <div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center"> <h2 className="text-3xl text-gray-800 flex items-center">
<GraduationCap className="w-8 h-8 mr-2" /> <GraduationCap className="w-8 h-8 mr-2" />
Enseignants Enseignants
@ -108,36 +109,47 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
</button> </button>
</div> </div>
<div className="bg-white rounded-lg border border-gray-200 max-w-7xl ml-0"> <div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
<Table <Table
columns={[ columns={[
{ name: 'NOM', transform: (row) => row.nom }, { name: 'NOM', transform: (row) => row.nom },
{ name: 'PRENOM', transform: (row) => row.prenom }, { name: 'PRENOM', transform: (row) => row.prenom },
{ name: 'MAIL', transform: (row) => row.mail }, { name: 'MAIL', transform: (row) => row.mail },
{ name: 'SPECIALITES', {
name: 'SPÉCIALITÉS',
transform: (row) => ( transform: (row) => (
<div key={row.id} className="flex justify-center items-center space-x-2"> <div key={row.id} className="flex flex-wrap justify-center items-center space-x-2">
{row.specialites.map(specialite => ( {row.specialites.map(specialite => (
<span <span
key={specialite.id} key={specialite.id}
className="w-4 h-4 rounded-full" className="px-3 py-1 rounded-full font-bold text-white"
style={{ backgroundColor: specialite.codeCouleur }} style={{ backgroundColor: specialite.codeCouleur }}
title={specialite.nom} title={specialite.nom}
></span> >
{specialite.nom}
</span>
))} ))}
</div> </div>
) )
}, },
{ name: 'TYPE PROFIL', {
name: 'TYPE PROFIL',
transform: (row) => { transform: (row) => {
return row.profilAssocie if (row.profilAssocie) {
? const badgeClass = row.DroitLabel === 'ECOLE' ? 'bg-blue-100 text-blue-600' : 'bg-red-100 text-red-600';
<div key={row.id} className="flex justify-center items-center space-x-2"> return (
{row.DroitLabel} <div key={row.id} className="flex justify-center items-center space-x-2">
</div> <span className={`px-3 py-1 rounded-full font-bold ${badgeClass}`}>
: <i>Non définie</i>; {row.DroitLabel}
</span>
</div>
);
} else {
return <i>Non définie</i>;
}
} }
}, },
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
{ name: 'ACTIONS', transform: (row) => ( { name: 'ACTIONS', transform: (row) => (
<DropdownMenu <DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />} buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
@ -155,7 +167,8 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
/> />
</div> </div>
{isOpen && ( {isOpen && (
<Modal <TeacherFormProvider initialTeacher={editingTeacher || {}}>
<Modal
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
title={editingTeacher ? "Modification de l'enseignant" : "Création d'un nouvel enseignant"} title={editingTeacher ? "Modification de l'enseignant" : "Création d'un nouvel enseignant"}
@ -164,6 +177,7 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
<TeacherForm teacher={editingTeacher || {}} onSubmit={handleModalSubmit} isNew={!editingTeacher} specialities={specialities} /> <TeacherForm teacher={editingTeacher || {}} onSubmit={handleModalSubmit} isNew={!editingTeacher} specialities={specialities} />
)} )}
/> />
</TeacherFormProvider>
)} )}
</div> </div>
); );

View File

@ -0,0 +1,46 @@
import React from 'react';
import Table from '@/components/Table';
const TeachersSelectionConfiguration = ({ formData, teachers, handleTeacherSelection, selectedTeachers }) => {
return (
<div className="mt-4" style={{ maxHeight: '300px', overflowY: 'auto' }}>
<label className="mt-6 block text-2xl font-medium text-gray-700 mb-2">Enseignants</label>
<label className={`block text-sm font-medium mb-4`}>Sélection : <span className={`${formData.enseignants_ids.length !== 0 ? 'text-emerald-400' : 'text-red-300'}`}>{formData.enseignants_ids.length}</span></label>
<Table
columns={[
{
name: 'Nom',
transform: (row) => row.nom,
},
{
name: 'Prénom',
transform: (row) => row.prenom,
},
{
name: 'Spécialités',
transform: (row) => (
<div className="flex flex-wrap items-center">
{row.specialites.map(specialite => (
<span key={specialite.id} className="flex items-center mr-2 mb-1">
<div
className="w-4 h-4 rounded-full mr-2"
style={{ backgroundColor: specialite.codeCouleur }}
title={specialite.nom}
></div>
<span>{specialite.nom}</span>
</span>
))}
</div>
),
},
]}
data={teachers}
onRowClick={handleTeacherSelection}
selectedRows={selectedTeachers}
isSelectable={true}
/>
</div>
);
};
export default TeachersSelectionConfiguration;

View File

@ -0,0 +1,32 @@
import React from 'react';
const TimeRange = ({ startTime, endTime, onStartChange, onEndChange }) => {
return (
<div className="mb-4">
<div className="flex space-x-4">
<div className="w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de début</label>
<input
type="time"
name="startTime"
value={startTime}
onChange={onStartChange}
className="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
<div className="w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Heure de fin</label>
<input
type="time"
name="endTime"
value={endTime}
onChange={onEndChange}
className="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
</div>
);
};
export default TimeRange;

View File

@ -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 (
<div className="p-4 bg-white shadow rounded mb-4">
{classes.length > 0 ? (
<SelectChoice
name="classes"
label="Classes"
IconItem={Users}
selected={selectedClass ? selectedClass.id : ''}
choices={classChoices}
callback={handleClassChange}
/>
) : (
<p>Aucune classe disponible.</p>
)}
{selectedClass && (
<div className="specialities mt-4">
<label className="block text-sm font-medium text-gray-700 mb-4">Spécialités</label>
{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 (
<DraggableSpeciality
key={uniqueId} // Utilisation de l'ID unique généré
specialite={specialite}
/>
);
})}
</div>
)}
</div>
);
};
export default ClassesInfo;

View File

@ -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 (
<span
ref={drag}
className="speciality-tag p-2 m-1 rounded cursor-pointer"
style={{
backgroundColor: specialite.codeCouleur,
color: isColorDark(specialite.codeCouleur) ? 'white' : 'black',
opacity: isDragging ? 0.5 : 1,
}}
>
{specialite.nom} ({specialite.teachers.join(', ')})
</span>
);
};
export default DraggableSpeciality;

View File

@ -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 (
<div
ref={drop}
className={`h-20 relative border-b border-gray-100 cursor-pointer ${isToday(new Date(day)) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''} hover:bg-emerald-100`}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
backgroundColor: cellBackgroundColor,
color: cellTextColor
}}
>
<div className="flex flex-col items-center gap-1">
{course && (
<>
<div className="text-base font-bold">{course.matiere}</div>
<div className="text-sm">{course.teachers.join(", ")}</div>
</>
)}
</div>
</div>
);
};
DropTargetCell.propTypes = {
day: PropTypes.string.isRequired,
hour: PropTypes.number.isRequired,
course: PropTypes.object,
onDrop: PropTypes.func.isRequired
};
export default DropTargetCell;

View File

@ -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 (
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
{/* En-tête des jours */}
<div className="grid gap-[1px] bg-gray-100 w-full" style={{ gridTemplateColumns: "2.5rem repeat(6, 1fr)" }}>
<div className="bg-white h-14"></div>
{weekDays.map((day) => (
<div
key={day}
className={`p-3 text-center border-b ${isToday(day) ? 'bg-emerald-400 border-x border-emerald-600' : 'bg-white'}`} >
<div className={`text font-medium ${isToday(day) ? 'text-white' : 'text-gray-500'}`}>
{format(day, 'EEEE', { locale: fr })}
</div>
</div>
))}
</div>
{/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
{/* Ligne de temps actuelle */}
{isCurrentWeek && (
<div
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
style={{
top: getCurrentTimePosition(),
}}
>
<div
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
/>
</div>
)}
<div className="grid gap-[1px] bg-white" style={{ gridTemplateColumns: "2.5rem repeat(6, 1fr)" }}>
{timeSlots.map(hour => (
<React.Fragment key={hour}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hour.toString().padStart(2, '0')}:00`}
</div>
{weekDays.map(day => {
const filteredEvents = getFilteredEvents(day, hour);
return(
<DropTargetCell
key={`${hour}-${day}`}
hour={hour}
day={day}
onDrop={handleDrop}
onDateClick={onDateClick}
filteredEvents={filteredEvents}
/>
)
})}
</React.Fragment>
))}
</div>
</div>
</div>
);
};
export default PlanningClassView;

View File

@ -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(
<div key={`hour-${hour}`} className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hour.toString().padStart(2, '0')}:00`}
</div>
);
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(
<DropTargetCell
key={`${day}-${hour}`}
day={day}
hour={hour}
course={course}
onDrop={onDrop}
/>
);
});
});
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 (
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
{/* En-tête des jours */}
<div className="grid gap-[1px] bg-gray-100 w-full" style={{ gridTemplateColumns: "2.5rem repeat(6, 1fr)" }}>
<div className="bg-white h-14"></div>
{weekDayDates.map((day) => (
<div
key={day}
className={`p-3 text-center border-b ${isToday(day) ? 'bg-emerald-400 border-x border-emerald-600' : 'bg-white'}`} >
<div className={`text font-medium ${isToday(day) ? 'text-white' : 'text-gray-500'}`}>
{format(day, 'EEEE', { locale: fr })}
</div>
</div>
))}
</div>
{/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
{isCurrentWeek && (
<div
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
style={{
top: getCurrentTimePosition(),
}}
>
<div
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
/>
</div>
)}
<div className={`grid gap-[1px] bg-white`} style={{ gridTemplateColumns: `2.5rem repeat(6, 1fr)` }}>
{renderCells()}
</div>
</div>
</div>
);
};
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;

View File

@ -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 (
<div className="flex flex-col h-full overflow-hidden">
<DndProvider backend={HTML5Backend}>
<ClassesInfo classes={classes} onClassSelect={handleClassSelect}/>
<div className="flex justify-between items-center p-4 w-full">
<div className="flex items-center w-full">
<SelectChoice
name="planningType"
IconItem={Calendar}
selected={planningType}
choices={planningChoices}
callback={(e) => {
setPlanningType(e.target.value);
setCurrentPeriod(e.target.value === 'TRIMESTRIEL' ? 'T1' : 'S1');
}}
/>
</div>
{planningType !== 'ANNUEL' && (
<div className="flex items-center justify-center w-full">
<button
onClick={() => handlePeriodChange('prev')}
className={`mr-4 p-2 border rounded-lg ${
currentPeriod === 'T1' || currentPeriod === 'S1' ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600'
} transition-colors duration-300`}
disabled={currentPeriod === 'T1' || currentPeriod === 'S1'}
>
<ChevronLeft className="h-6 w-6" />
</button>
<span className="text-lg font-semibold mx-4">{getPeriodLabel(currentPeriod)}</span>
<button
onClick={() => handlePeriodChange('next')}
className={`ml-4 p-2 border rounded-lg ${
(planningType === 'TRIMESTRIEL' && currentPeriod === 'T3') || (planningType === 'SEMESTRIEL' && currentPeriod === 'S2') ? 'bg-gray-300 text-gray-700 cursor-not-allowed' : 'bg-emerald-500 text-white hover:bg-emerald-600'
} transition-colors duration-300`}
disabled={(planningType === 'TRIMESTRIEL' && currentPeriod === 'T3') || (planningType === 'SEMESTRIEL' && currentPeriod === 'S2')}
>
<ChevronRight className="h-6 w-6" />
</button>
</div>
)}
</div>
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key="year"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<PlanningClassView
schedule={schedule}
onDrop={onDrop}
planningType={planningType}
currentPeriod={currentPeriod}
/>
</motion.div>
</AnimatePresence>
</div>
</DndProvider>
{/* <SpecialityEventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
eventData={eventData}
setEventData={setEventData}
selectedClass={selectedClass}
/> */}
</div>
);
};
export default ScheduleManagement;

View File

@ -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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? 'Modifier l\'événement' : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Sélection de la Spécialité */}
<div>
{eventData.scheduleId && eventData.specialities && eventData.specialities.length > 0 ? (
<SelectChoice
name={`spécialités-${eventData.scheduleId}`}
label="Spécialités"
selected={eventData.specialiteId ? eventData.specialiteId : ''}
choices={eventData.specialities.map(specialite => ({
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}
/>
) : (
<p>Aucune spécialité disponible pour cette classe.</p>
)}
</div>
{/* Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Début
</label>
<input
type="datetime-local"
value={format(new Date(eventData.start), "yyyy-MM-dd'T'HH:mm")}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin
</label>
<input
type="datetime-local"
value={format(new Date(eventData.end), "yyyy-MM-dd'T'HH:mm")}
onChange={(e) => 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
/>
</div>
</div>
{/* Lieu */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lieu
</label>
<input
type="text"
value={eventData.location || ''}
onChange={(e) => setEventData({ ...eventData, location: e.target.value })}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
</div>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<div>
{eventData.id && (
<button
type="button"
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
>
Supprimer
</button>
)}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</div>
</form>
</div>
</div>
);
}

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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) => { const handlePageChange = (newPage) => {
onPageChange(newPage); onPageChange(newPage);
}; };
@ -21,9 +21,17 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
</thead> </thead>
<tbody> <tbody>
{data?.map((row, rowIndex) => ( {data?.map((row, rowIndex) => (
<tr key={rowIndex} className={` ${rowIndex % 2 === 0 ? 'bg-emerald-50' : ''}`}> <tr
key={rowIndex}
className={`
${isSelectable ? 'cursor-pointer' : ''}
${selectedRows?.includes(row.id) ? 'bg-emerald-500 text-white' : rowIndex % 2 === 0 ? 'bg-emerald-50' : ''}
${isSelectable ? 'hover:bg-emerald-600' : ''}
`}
onClick={() => isSelectable && onRowClick && onRowClick(row)}
>
{columns.map((column, colIndex) => ( {columns.map((column, colIndex) => (
<td key={colIndex} className="py-2 px-4 border-b border-gray-200 text-center text-sm text-gray-700"> <td key={colIndex} className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`} >
{renderCell ? renderCell(row, column.name) : column.transform(row)} {renderCell ? renderCell(row, column.name) : column.transform(row)}
</td> </td>
))} ))}

View File

@ -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 (
<ClasseFormContext.Provider value={{ formData, setFormData }}>
{children}
</ClasseFormContext.Provider>
);
};

View File

@ -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 (
<ClassesContext.Provider value={{ schoolYears, getNiveauxLabels, generateAgeToNiveaux, niveauxPremierCycle, niveauxSecondCycle, niveauxTroisiemeCycle, typeEmploiDuTemps, updatePlanning }}>
{children}
</ClassesContext.Provider>
);
};

View File

@ -13,6 +13,9 @@ import { mockEvents, mockSchedules } from '@/data/mockData';
const PlanningContext = createContext(); const PlanningContext = createContext();
export function PlanningProvider({ children }) { export function PlanningProvider({ children }) {
// const [events, setEvents] = useState([]);
// const [schedules, setSchedules] = useState([]);
// const [selectedSchedule, setSelectedSchedule] = useState(null);
const [events, setEvents] = useState(mockEvents); const [events, setEvents] = useState(mockEvents);
const [schedules, setSchedules] = useState(mockSchedules); const [schedules, setSchedules] = useState(mockSchedules);
const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id); const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id);

View File

@ -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 (
<SpecialityFormContext.Provider value={{ formData, setFormData }}>
{children}
</SpecialityFormContext.Provider>
);
};

View File

@ -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 (
<TeacherFormContext.Provider value={{ formData, setFormData }}>
{children}
</TeacherFormContext.Provider>
);
};

View File

@ -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_FICHEINSCRIPTION_URL = `${BASE_URL}/GestionInscriptions/ficheInscription`
export const BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL = `${BASE_URL}/GestionInscriptions/recupereDernierResponsable` 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_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie`
export const BK_GESTIONINSCRIPTION_SPECIALITES_URL = `${BASE_URL}/GestionEnseignants/specialites` export const BK_GESTIONENSEIGNANTS_SPECIALITES_URL = `${BASE_URL}/GestionEnseignants/specialites`
export const BK_GESTIONINSCRIPTION_SPECIALITE_URL = `${BASE_URL}//GestionEnseignants/specialite` export const BK_GESTIONENSEIGNANTS_SPECIALITE_URL = `${BASE_URL}//GestionEnseignants/specialite`
export const BK_GESTIONINSCRIPTION_CLASSES_URL = `${BASE_URL}/GestionEnseignants/classes` export const BK_GESTIONENSEIGNANTS_CLASSES_URL = `${BASE_URL}/GestionEnseignants/classes`
export const BK_GESTIONINSCRIPTION_CLASSE_URL = `${BASE_URL}/GestionEnseignants/classe` export const BK_GESTIONENSEIGNANTS_CLASSE_URL = `${BASE_URL}/GestionEnseignants/classe`
export const BK_GESTIONINSCRIPTION_TEACHERS_URL = `${BASE_URL}/GestionEnseignants/enseignants` export const BK_GESTIONENSEIGNANTS_TEACHERS_URL = `${BASE_URL}/GestionEnseignants/enseignants`
export const BK_GESTIONINSCRIPTION_TEACHER_URL = `${BASE_URL}/GestionEnseignants/enseignant` 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` export const BK_GET_CSRF = `${BASE_URL}/GestionLogin/csrf`