feat: Gestion des profils des enseignants / Visualisation d'une classe [#4]

This commit is contained in:
N3WT DE COMPET
2024-11-23 20:02:51 +01:00
parent af0cd1c840
commit 81d1dfa9a7
26 changed files with 792 additions and 178 deletions

View File

@ -1,4 +1,5 @@
from django.db import models
from GestionLogin.models import Profil
class Specialite(models.Model):
nom = models.CharField(max_length=100)
@ -13,6 +14,7 @@ class Enseignant(models.Model):
prenom = models.CharField(max_length=100)
mail = models.EmailField(unique=True)
specialite = models.ForeignKey(Specialite, on_delete=models.SET_NULL, null=True, blank=True, related_name='enseignants')
profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return f"{self.nom} {self.prenom}"

View File

@ -1,6 +1,10 @@
from rest_framework import serializers
from .models import Enseignant, Specialite, Classe
from N3wtSchool import settings
from GestionInscriptions.models import FicheInscription
from GestionInscriptions.serializers import EleveSerializer
from GestionLogin.serializers import ProfilSerializer
from GestionLogin.models import Profil
from N3wtSchool import settings, bdd
from django.utils import timezone
import pytz
@ -23,10 +27,11 @@ class ClasseSerializer(serializers.ModelSerializer):
dateCreation_formattee = serializers.SerializerMethodField()
enseignant_principal = serializers.SerializerMethodField()
enseignant_principal_id = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), source='enseignant_principal', write_only=False, read_only=False)
eleves = serializers.SerializerMethodField()
class Meta:
model = Classe
fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'specialites', 'specialites_ids', 'enseignant_principal', 'enseignant_principal_id', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee']
fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'specialites', 'specialites_ids', 'enseignant_principal', 'enseignant_principal_id', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee', 'eleves']
def get_enseignant_principal(self, obj):
from .serializers import EnseignantDetailSerializer
@ -59,22 +64,45 @@ class ClasseSerializer(serializers.ModelSerializer):
return local_time.strftime("%d-%m-%Y %H:%M")
def get_eleves(self, obj):
elevesList = obj.eleves.all()
filtered_eleves = []
for eleve in elevesList:
ficheInscription=bdd.getObject(FicheInscription, "eleve__id", eleve.id)
if ficheInscription.etat == ficheInscription.EtatDossierInscription.DI_VALIDE:
filtered_eleves.append(eleve)
return EleveSerializer(filtered_eleves, many=True, read_only=True).data
class EnseignantSerializer(serializers.ModelSerializer):
specialite = SpecialiteSerializer(read_only=True)
specialite_id = serializers.PrimaryKeyRelatedField(queryset=Specialite.objects.all(), source='specialite', write_only=False, read_only=False)
profilAssocie_id = serializers.PrimaryKeyRelatedField(queryset=Profil.objects.all(), source='profilAssocie', write_only=False, read_only=False)
classes_principal = ClasseSerializer(many=True, read_only=True)
profilAssocie = ProfilSerializer(read_only=True)
DroitLabel = serializers.SerializerMethodField()
DroitValue = serializers.SerializerMethodField()
class Meta:
model = Enseignant
fields = ['id', 'nom', 'prenom', 'mail', 'specialite', 'specialite_id', 'classes_principal']
fields = ['id', 'nom', 'prenom', 'mail', 'specialite', 'specialite_id', 'classes_principal', 'profilAssocie', 'profilAssocie_id', 'DroitLabel', 'DroitValue']
def create(self, validated_data):
specialite = validated_data.pop('specialite', None)
profilAssocie = validated_data.pop('profilAssocie', None)
enseignant = Enseignant.objects.create(**validated_data)
enseignant.specialite = specialite
if specialite:
enseignant.specialite = specialite
if profilAssocie:
enseignant.profilAssocie = profilAssocie
enseignant.save()
return enseignant
def get_DroitLabel(self, obj):
return obj.profilAssocie.get_droit_display() if obj.profilAssocie else None
def get_DroitValue(self, obj):
return obj.profilAssocie.droit if obj.profilAssocie else None
class EnseignantDetailSerializer(serializers.ModelSerializer):
specialite = SpecialiteSerializer(read_only=True)

View File

@ -47,10 +47,15 @@ class EnseignantView(APIView):
def delete(self, request, _id):
enseignant = bdd.getObject(_objectName=Enseignant, _columnName='id', _value=_id)
if enseignant != None:
if enseignant is not None:
if enseignant.profilAssocie:
print('Suppression du profil associé')
enseignant.profilAssocie.delete()
enseignant.delete()
return JsonResponse("La suppression de la spécialité a été effectuée avec succès", safe=False)
return JsonResponse({'message': 'La suppression de l\'enseignant a été effectuée avec succès'}, safe=False)
else:
return JsonResponse({'erreur': 'L\'enseignant n\'a pas été trouvé'}, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
@ -175,6 +180,10 @@ class ClasseView(APIView):
def delete(self, request, _id):
classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id)
if classe != None:
for eleve in classe.eleves.all():
print(f'eleve a retirer la classe : {eleve}')
eleve.classeAssociee = None
eleve.save()
classe.delete()
return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False)

View File

@ -4,6 +4,27 @@ from django.conf import settings
from django.utils.translation import gettext_lazy as _
from GestionLogin.models import Profil
from GestionEnseignants.models import Classe
from datetime import datetime
class FraisInscription(models.Model):
class OptionsPaiements(models.IntegerChoices):
PAIEMENT_1_FOIS = 0, _('Paiement en une seule fois')
PAIEMENT_MENSUEL = 1, _('Paiement mensuel')
PAIEMENT_TRIMESTRIEL = 2, _('Paiement trimestriel')
nom = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
montant_de_base = models.DecimalField(max_digits=10, decimal_places=2)
reductions = models.JSONField(blank=True, null=True)
supplements = models.JSONField(blank=True, null=True)
date_debut_validite = models.DateField()
date_fin_validite = models.DateField()
options_paiement = models.IntegerField(choices=OptionsPaiements, default=OptionsPaiements.PAIEMENT_1_FOIS)
def __str__(self):
return self.nom
class Langue(models.Model):
id = models.AutoField(primary_key=True)
@ -59,7 +80,7 @@ class Eleve(models.Model):
niveau = models.IntegerField(choices=NiveauEleve, default=NiveauEleve.NONE, blank=True)
nationalite = models.CharField(max_length=200, default="", blank=True)
adresse = models.CharField(max_length=200, default="", blank=True)
dateNaissance = models.CharField(max_length=200, default="", blank=True)
dateNaissance = models.DateField(null=True, blank=True)
lieuNaissance = models.CharField(max_length=200, default="", blank=True)
codePostalNaissance = models.IntegerField(default=0, blank=True)
medecinTraitant = models.CharField(max_length=200, default="", blank=True)
@ -77,6 +98,9 @@ class Eleve(models.Model):
# Relation N-N
languesParlees = models.ManyToManyField(Langue, blank=True)
# Relation 1-N
classeAssociee = models.ForeignKey(Classe, on_delete=models.SET_NULL, null=True, blank=True, related_name='eleves')
def __str__(self):
return self.nom + "_" + self.prenom
@ -98,6 +122,31 @@ class Eleve(models.Model):
def getNbFreres(self):
return self.freres.count()
@property
def age(self):
if self.dateNaissance:
today = datetime.today()
years = today.year - self.dateNaissance.year
months = today.month - self.dateNaissance.month
if today.day < self.dateNaissance.day:
months -= 1
if months < 0:
years -= 1
months += 12
# Déterminer le format de l'âge
if months >= 6 and months <= 12:
return f"{years} ans 1/2"
else:
return f"{years} ans"
return None
@property
def dateNaissance_formattee(self):
if self.dateNaissance:
return self.dateNaissance.strftime('%d-%m-%Y')
return None
class FicheInscription(models.Model):
class EtatDossierInscription(models.IntegerChoices):

View File

@ -1,5 +1,6 @@
from rest_framework import serializers
from GestionInscriptions.models import FicheInscription, Eleve, Responsable, Frere, Langue
from GestionInscriptions.models import FicheInscription, Eleve, Responsable, Frere, Langue, FraisInscription
from GestionEnseignants.models import Classe
from GestionLogin.models import Profil
from GestionLogin.serializers import ProfilSerializer
from GestionMessagerie.models import Messagerie
@ -7,6 +8,13 @@ from GestionNotification.models import Notification
from N3wtSchool import settings
from django.utils import timezone
import pytz
from datetime import datetime
class FraisInscriptionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = FraisInscription
fields = '__all__'
class LanguesSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
@ -34,7 +42,13 @@ class EleveSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
responsables = ResponsableSerializer(many=True, required=False)
freres = FrereSerializer(many=True, required=False)
langues = LanguesSerializer(many=True, required=False)
langues = LanguesSerializer(many=True, required=False)
classeAssocie_id = serializers.PrimaryKeyRelatedField(queryset=Classe.objects.all(), source='classeAssociee', required=False, write_only=False, read_only=False)
age = serializers.SerializerMethodField()
dateNaissance_formattee = serializers.SerializerMethodField()
dateNaissance = serializers.DateField(input_formats=['%d-%m-%Y', '%Y-%m-%d'], required=False, allow_null=True)
classeAssocieeName = serializers.SerializerMethodField()
class Meta:
model = Eleve
fields = '__all__'
@ -89,6 +103,15 @@ class EleveSerializer(serializers.ModelSerializer):
return instance
def get_age(self, obj):
return obj.age
def get_dateNaissance_formattee(self, obj):
return obj.dateNaissance_formattee
def get_classeAssocieeName(self, obj):
return obj.classeAssociee.nom_ambiance if obj.classeAssociee else None
class FicheInscriptionSerializer(serializers.ModelSerializer):
eleve = EleveSerializer(many=False, required=True)
fichierInscription = serializers.FileField(required=False)

View File

@ -1,7 +1,7 @@
from django.urls import path, re_path
from . import views
from GestionInscriptions.views import ListFichesInscriptionView, FicheInscriptionView, EleveView, ResponsableView, ListeEnfantsView, ListeElevesView
from GestionInscriptions.views import ListFichesInscriptionView, FicheInscriptionView, EleveView, ResponsableView, ListeEnfantsView, ListeElevesView, FraisInscriptionView
urlpatterns = [
re_path(r'^fichesInscription/([a-zA-z]+)$', ListFichesInscriptionView.as_view(), name="listefichesInscriptions"),
@ -26,6 +26,9 @@ urlpatterns = [
# Page PARENT - Liste des enfants
re_path(r'^enfants/([0-9]+)$', ListeEnfantsView.as_view(), name="enfants"),
# Page INSCRIPTION - Liste des élèves
# Page INSCRIPTION - Liste des élèves
re_path(r'^eleves$', ListeElevesView.as_view(), name="enfants"),
# Frais d'inscription
re_path(r'^tarifsInscription$', FraisInscriptionView.as_view(), name="fraisInscription"),
]

View File

@ -179,3 +179,23 @@ def getArgFromRequest(_argument, _request):
data=JSONParser().parse(_request)
resultat = data[_argument]
return resultat
def diToPDF(ficheEleve):
# Ajout du fichier d'inscriptions
data = {
'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom,
'dateSignature': convertToStr(_now(), '%d-%m-%Y'),
'heureSignature': convertToStr(_now(), '%H:%M'),
'eleve':ficheEleve.eleve,
}
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom)
pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF)
if os.path.exists(str(pathFichier)):
print(f'File exists : {str(pathFichier)}')
os.remove(str(pathFichier))
receipt_file = BytesIO(pdf.content)
ficheEleve.fichierInscription = File(receipt_file, nomFichierPDF)

View File

@ -17,10 +17,10 @@ from io import BytesIO
import GestionInscriptions.mailManager as mailer
import GestionInscriptions.util as util
from GestionInscriptions.serializers import FicheInscriptionSerializer, EleveSerializer, FicheInscriptionByParentSerializer, EleveByDICreationSerializer
from GestionInscriptions.serializers import FicheInscriptionSerializer, EleveSerializer, FicheInscriptionByParentSerializer, EleveByDICreationSerializer, FraisInscriptionSerializer
from GestionInscriptions.pagination import CustomPagination
from GestionInscriptions.signals import clear_cache
from .models import Eleve, Responsable, FicheInscription
from .models import Eleve, Responsable, FicheInscription, FraisInscription
from GestionInscriptions.automate import Automate_DI_Inscription, load_config, getStateMachineObjectState, updateStateMachine
from GestionLogin.models import Profil
@ -30,85 +30,57 @@ from N3wtSchool import settings, renderers, bdd
class ListFichesInscriptionView(APIView):
pagination_class = CustomPagination
def get_fiche_inscriptions(self, _filter, search=None):
"""
Récupère les fiches d'inscriptions en fonction du filtre passé.
_filter: Filtre pour déterminer l'état des fiches ('pending', 'archived', 'subscribed')
search: Terme de recherche (optionnel)
"""
if _filter == 'pending':
exclude_states = [FicheInscription.EtatDossierInscription.DI_VALIDE, FicheInscription.EtatDossierInscription.DI_ARCHIVE]
return bdd.searchObjects(FicheInscription, search, _excludeStates=exclude_states)
elif _filter == 'archived':
return bdd.getObjects(FicheInscription, 'etat', FicheInscription.EtatDossierInscription.DI_ARCHIVE)
elif _filter == 'subscribed':
return bdd.getObjects(FicheInscription, 'etat', FicheInscription.EtatDossierInscription.DI_VALIDE)
return None
def get(self, request, _filter):
if _filter == 'all':
# Récupération des paramètres
search = request.GET.get('search', '').strip()
page_size = request.GET.get('page_size', None)
# Gestion du page_size
if page_size is not None:
try:
page_size = int(page_size)
except ValueError:
page_size = settings.NB_RESULT_PER_PAGE
# Récupération des paramètres
search = request.GET.get('search', '').strip()
page_size = request.GET.get('page_size', None)
cached_page_size = cache.get('N3WT_page_size')
if cached_page_size != page_size:
clear_cache()
cache.set('N3WT_page_size', page_size)
# Gestion du page_size
if page_size is not None:
try:
page_size = int(page_size)
except ValueError:
page_size = settings.NB_RESULT_PER_PAGE
# Définir le cache_key en fonction du filtre
page_number = request.GET.get('page', 1)
cache_key = f'N3WT_ficheInscriptions_{_filter}_page_{page_number}_search_{search if _filter == "pending" else ""}'
cached_page = cache.get(cache_key)
if cached_page:
return JsonResponse(cached_page, safe=False)
# Gestion du cache
page_number = request.GET.get('page', 1)
cache_key = f'N3WT_ficheInscriptions_page_{page_number}_search_{search}'
cached_page = cache.get(cache_key)
if cached_page:
return JsonResponse(cached_page, safe=False)
# Récupérer les fiches d'inscriptions en fonction du filtre
ficheInscriptions_List = self.get_fiche_inscriptions(_filter, search)
# Filtrage des résultats
if search:
# Utiliser la nouvelle fonction de recherche
ficheInscriptions_List = bdd.searchObjects(
FicheInscription,
search,
_excludeState=6 # Exclure les fiches archivées
)
else:
# Récupère toutes les fiches non archivées
ficheInscriptions_List = bdd.getObjects(FicheInscription, 'etat', 6, _reverseCondition=True)
if not ficheInscriptions_List:
return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False)
# Pagination
paginator = self.pagination_class()
page = paginator.paginate_queryset(ficheInscriptions_List, request)
if page is not None:
ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True)
response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data)
cache.set(cache_key, response_data, timeout=60*15)
return JsonResponse(response_data, safe=False)
# Pagination
paginator = self.pagination_class()
page = paginator.paginate_queryset(ficheInscriptions_List, request)
if page is not None:
ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True)
response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data)
cache.set(cache_key, response_data, timeout=60*15)
return JsonResponse(response_data, safe=False)
elif _filter == 'archived' :
page_size = request.GET.get('page_size', None)
if page_size is not None:
try:
page_size = int(page_size)
except ValueError:
page_size = settings.NB_RESULT_PER_PAGE
cached_page_size = cache.get('N3WT_archived_page_size')
# Comparer avec le nouveau page_size
if cached_page_size != page_size:
# Appeler cached_page() et mettre à jour le cache
clear_cache()
cache.set('N3WT_archived_page_size',page_size)
page_number = request.GET.get('page', 1)
cache_key_page = f'N3WT_ficheInscriptions_archives_page_{page_number}'
cached_page = cache.get(cache_key_page)
if cached_page:
return JsonResponse(cached_page, safe=False)
ficheInscriptions_List=bdd.getObjects(FicheInscription, 'etat', 6)
paginator = self.pagination_class()
page = paginator.paginate_queryset(ficheInscriptions_List, request)
if page is not None:
ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True)
response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data)
cache.set(cache_key_page, response_data, timeout=60*15)
return JsonResponse(response_data, safe=False)
return JsonResponse(status=status.HTTP_404_NOT_FOUND)
return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False)
def post(self, request):
fichesEleve_data=JSONParser().parse(request)
@ -168,39 +140,28 @@ class FicheInscriptionView(APIView):
def put(self, request, id):
ficheEleve_data=JSONParser().parse(request)
admin = ficheEleve_data.pop('admin', 1)
etat = ficheEleve_data.pop('etat', 0)
ficheEleve_data["dateMAJ"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'))
ficheEleve = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
currentState = getStateMachineObjectState(ficheEleve.etat)
if admin == 0 and currentState == FicheInscription.EtatDossierInscription.DI_ENVOYE:
if etat == FicheInscription.EtatDossierInscription.DI_EN_VALIDATION:
# Le parent a complété le dossier d'inscription, il est soumis à validation par l'école
print('EN VALIDATION')
json.dumps(ficheEleve_data)
# Ajout du fichier d'inscriptions
data = {
'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom,
'dateSignature': util.convertToStr(util._now(), '%d-%m-%Y'),
'heureSignature': util.convertToStr(util._now(), '%H:%M'),
'eleve':ficheEleve.eleve,
}
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom)
pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF)
if os.path.exists(str(pathFichier)):
print(f'File exists : {str(pathFichier)}')
os.remove(str(pathFichier))
receipt_file = BytesIO(pdf.content)
ficheEleve.fichierInscription = File(receipt_file, nomFichierPDF)
util.diToPDF(ficheEleve)
# Mise à jour de l'automate
updateStateMachine(di, 'saisiDI')
updateStateMachine(ficheEleve, 'saisiDI')
elif etat == FicheInscription.EtatDossierInscription.DI_VALIDE:
# L'école a validé le dossier d'inscription
# Mise à jour de l'automate
print('VALIDATION')
updateStateMachine(ficheEleve, 'valideDI')
ficheEleve_serializer = FicheInscriptionSerializer(ficheEleve, data=ficheEleve_data)
if ficheEleve_serializer.is_valid():
di = ficheEleve_serializer.save()
return JsonResponse("Updated Successfully", safe=False)
ficheEleve_serializer.save()
return JsonResponse(ficheEleve_serializer.data, safe=False)
return JsonResponse(ficheEleve_serializer.errors, safe=False)
@ -287,3 +248,10 @@ class ListeElevesView(APIView):
students = bdd.getAllObjects(_objectName=Eleve)
students_serializer = EleveByDICreationSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False)
# API utilisée pour la vue de personnalisation des frais d'inscription pour la structure
class FraisInscriptionView(APIView):
def get(self, request):
tarifs = bdd.getAllObjects(FraisInscription)
tarifs_serializer = FraisInscriptionSerializer(tarifs, many=True)
return JsonResponse(tarifs_serializer.data, safe=False)

View File

@ -5,10 +5,10 @@ from django.core.validators import EmailValidator
class Profil(AbstractUser):
class Droits(models.IntegerChoices):
PROFIL_UNDEFINED = -1, _('Profil non défini')
PROFIL_ECOLE = 0, _('Profil école')
PROFIL_PARENT = 1, _('Profil parent')
PROFIL_ADMIN = 2, _('Profil administrateur')
PROFIL_UNDEFINED = -1, _('NON DEFINI')
PROFIL_ECOLE = 0, _('ECOLE')
PROFIL_PARENT = 1, _('PARENT')
PROFIL_ADMIN = 2, _('ADMIN')
email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()])

View File

@ -26,3 +26,26 @@ class ProfilSerializer(serializers.ModelSerializer):
ret = super().to_representation(instance)
ret['password'] = '********'
return ret
class ProfilUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Profil
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active']
extra_kwargs = {
'password': {'write_only': True, 'required': False}
}
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
instance = super().update(instance, validated_data)
if password:
instance.set_password(password)
instance.save()
return instance
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['password'] = '********'
return ret

View File

@ -17,8 +17,8 @@ import json
from . import validator
from .models import Profil
from GestionLogin.serializers import ProfilSerializer, ProfilUpdateSerializer
from GestionInscriptions.models import FicheInscription
from GestionInscriptions.serializers import ProfilSerializer
from GestionInscriptions.signals import clear_cache
import GestionInscriptions.mailManager as mailer
import GestionInscriptions.util as util
@ -83,7 +83,7 @@ class ProfilView(APIView):
def put(self, request, _id):
data=JSONParser().parse(request)
profil = Profil.objects.get(id=_id)
profil_serializer = ProfilSerializer(profil, data=data)
profil_serializer = ProfilUpdateSerializer(profil, data=data)
if profil_serializer.is_valid():
profil_serializer.save()
return JsonResponse("Updated Successfully", safe=False)
@ -143,6 +143,7 @@ class LoginView(APIView):
'errorFields':errorFields,
'errorMessage':retour,
'profil':user.id if user else -1,
'droit':user.droit if user else -1,
#'jwtToken':jwt_token if profil != -1 else ''
}, safe=False)

View File

@ -56,19 +56,19 @@ def getLastId(_object):
logging.warning("Aucun résultat n'a été trouvé - ")
return result
def searchObjects(_objectName, _searchTerm, _excludeState=None):
def searchObjects(_objectName, _searchTerm=None, _excludeStates=None):
"""
Recherche générique sur les objets avec possibilité d'exclure certains états
_objectName: Classe du modèle
_searchTerm: Terme de recherche
_excludeState: État à exclure de la recherche (optionnel)
_excludeStates: Liste d'état à exclure de la recherche (optionnel)
"""
try:
query = _objectName.objects.all()
# Si on a un état à exclure
if _excludeState is not None:
query = query.filter(etat__lt=_excludeState)
if _excludeStates is not None:
query = query.exclude(etat__in=_excludeStates)
# Si on a un terme de recherche
if _searchTerm and _searchTerm.strip():
@ -83,4 +83,4 @@ def searchObjects(_objectName, _searchTerm, _excludeState=None):
except _objectName.DoesNotExist:
logging.error(f"Aucun résultat n'a été trouvé - {_objectName.__name__} (recherche: {_searchTerm})")
return None
return None

View File

@ -3,6 +3,7 @@
"addStudent": "New",
"allStudents": "All Students",
"pending": "Pending Registrations",
"subscribed": "Subscribed",
"archived": "Archived",
"name": "Name",
"class": "Class",
@ -25,6 +26,7 @@
"mainContactMail":"Main contact email",
"phone":"Phone",
"lastUpdateDate":"Last update",
"classe":"Class",
"registrationFileStatus":"Registration file status",
"files":"Files"
}

View File

@ -3,6 +3,7 @@
"addStudent": "Nouveau",
"allStudents": "Tous les élèves",
"pending": "Inscriptions en attente",
"subscribed": "Inscrits",
"archived": "Archivés",
"name": "Nom",
"class": "Classe",
@ -25,6 +26,7 @@
"mainContactMail":"Email de contact principal",
"phone":"Téléphone",
"lastUpdateDate":"Dernière mise à jour",
"classe":"Classe",
"registrationFileStatus":"État du dossier d'inscription",
"files":"Fichiers"
}

View File

@ -4,6 +4,8 @@ import React, { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
import Loader from '@/components/Loader';
import { BK_GESTIONINSCRIPTION_CLASSES_URL } from '@/utils/Url';
import ClasseDetails from '@/components/ClasseDetails';
// Composant StatCard pour afficher une statistique
const StatCard = ({ title, value, icon, change, color = "blue" }) => (
@ -54,7 +56,23 @@ export default function DashboardPage() {
}
});
const [classes, setClasses] = useState([]);
const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`)
.then(response => response.json())
.then(data => {
setClasses(data);
})
.catch(error => {
console.error('Error fetching classes:', error);
});
};
useEffect(() => {
// Fetch data for classes
fetchClasses();
// Simulation de chargement des données
setTimeout(() => {
setStats({
@ -120,7 +138,7 @@ export default function DashboardPage() {
</div>
{/* Événements et KPIs */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* Graphique des inscriptions */}
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<h2 className="text-lg font-semibold mb-4">{t('inscriptionTrends')}</h2>
@ -138,6 +156,14 @@ export default function DashboardPage() {
))}
</div>
</div>
<div className="flex flex-wrap">
{classes.map((classe) => (
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100 mr-4">
<ClasseDetails key={classe.id} classe={classe} />
</div>
))}
</div>
</div>
);
}

View File

@ -1,10 +1,8 @@
'use client'
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import SpecialitiesSection from '@/components/SpecialitiesSection'
import ClassesSection from '@/components/ClassesSection'
import TeachersSection from '@/components/TeachersSection';
import { User, School } from 'lucide-react'
import { BK_GESTIONINSCRIPTION_SPECIALITES_URL,
BK_GESTIONINSCRIPTION_CLASSES_URL,
BK_GESTIONINSCRIPTION_SPECIALITE_URL,
@ -66,7 +64,6 @@ export default function Page() {
};
const handleCreate = (url, newData, setDatas) => {
console.log('SEND POST :', JSON.stringify(newData));
fetch(url, {
method: 'POST',
headers: {

View File

@ -13,19 +13,29 @@ import Button from '@/components/Button';
import DropdownMenu from "@/components/DropdownMenu";
import { swapFormatDate } from '@/utils/Date';
import { formatPhoneNumber } from '@/utils/Telephone';
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus, CheckCircle } from 'lucide-react';
import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm'
import AffectationClasseForm from '@/components/AffectationClasseForm'
import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, BK_GESTIONINSCRIPTION_SEND_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, BK_GESTIONINSCRIPTION_ARCHIVE_URL } from '@/utils/Url';
import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL,
BK_GESTIONINSCRIPTION_SEND_URL,
FR_ADMIN_STUDENT_EDIT_SUBSCRIBE,
BK_GESTIONINSCRIPTION_ARCHIVE_URL,
BK_GESTIONINSCRIPTION_CLASSES_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page({ params: { locale } }) {
const t = useTranslations('students');
const [ficheInscriptions, setFicheInscriptions] = useState([]);
const [ficheInscriptionsData, setFicheInscriptionsData] = useState([]);
const [fichesInscriptionsDataArchivees, setFicheInscriptionsDataArchivees] = useState([]);
const [fichesInscriptionsDataEnCours, setFichesInscriptionsDataEnCours] = useState([]);
const [fichesInscriptionsDataInscrits, setichesInscriptionsDataInscrits] = useState([]);
const [fichesInscriptionsDataArchivees, setFichesInscriptionsDataArchivees] = useState([]);
// const [filter, setFilter] = useState('*');
const [searchTerm, setSearchTerm] = useState('');
const [alertPage, setAlertPage] = useState(false);
@ -33,22 +43,32 @@ export default function Page({ params: { locale } }) {
const [ficheArchivee, setFicheArchivee] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [popup, setPopup] = useState({ visible: false, message: '', onConfirm: null });
const [activeTab, setActiveTab] = useState('all');
const [activeTab, setActiveTab] = useState('pending');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalStudents, setTotalStudents] = useState(0);
const [totalPending, setTotalPending] = useState(0);
const [totalSubscribed, setTotalSubscribed] = useState(0);
const [totalArchives, setTotalArchives] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page
const [isOpen, setIsOpen] = useState(false);
const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false);
const [eleve, setEleve] = useState('');
const [classes, setClasses] = useState([]);
const csrfToken = useCsrfToken();
const openModal = () => {
setIsOpen(true);
}
const openModalAssociationEleve = (eleveSelected) => {
setIsOpenAffectationClasse(true);
setEleve(eleveSelected);
}
// Modifier la fonction fetchData pour inclure le terme de recherche
const fetchData = (page, pageSize, search = '') => {
const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/all?page=${page}&page_size=${pageSize}&search=${search}`;
const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/pending?page=${page}&page_size=${pageSize}&search=${search}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
@ -58,11 +78,33 @@ export default function Page({ params: { locale } }) {
setIsLoading(false);
if (data) {
const { fichesInscriptions, count } = data;
setFicheInscriptionsData(fichesInscriptions);
setFichesInscriptionsDataEnCours(fichesInscriptions);
const calculatedTotalPages = Math.ceil(count / pageSize);
setTotalStudents(count);
setTotalPending(count);
setTotalPages(calculatedTotalPages);
}
console.log('Success PENDING:', data);
})
.catch(error => {
console.error('Error fetching data:', error);
setIsLoading(false);
});
};
const fetchDataSubscribed = () => {
fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/subscribed`, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
setIsLoading(false);
if (data) {
const { fichesInscriptions, count } = data;
setTotalSubscribed(count);
setichesInscriptionsDataInscrits(fichesInscriptions);
}
console.log('Success SUBSCRIBED:', data);
})
.catch(error => {
console.error('Error fetching data:', error);
@ -81,7 +123,7 @@ export default function Page({ params: { locale } }) {
if (data) {
const { fichesInscriptions, count } = data;
setTotalArchives(count);
setFicheInscriptionsDataArchivees(fichesInscriptions);
setFichesInscriptionsDataArchivees(fichesInscriptions);
}
console.log('Success ARCHIVED:', data);
})
@ -91,14 +133,31 @@ export default function Page({ params: { locale } }) {
});
};
const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`)
.then(response => response.json())
.then(data => {
setClasses(data);
console.log("classes : ", data)
})
.catch(error => {
console.error('Error fetching classes:', error);
});
};
useEffect(() => {
fetchClasses();
}, []);
useEffect(() => {
const fetchDataAndSetState = () => {
if (!useFakeData) {
fetchData(currentPage, itemsPerPage, searchTerm);
fetchDataSubscribed();
fetchDataArchived();
} else {
setTimeout(() => {
setFicheInscriptionsData(mockFicheInscription);
setFichesInscriptionsDataEnCours(mockFicheInscription);
setIsLoading(false);
}, 1000);
}
@ -183,13 +242,37 @@ export default function Page({ params: { locale } }) {
fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici
};
const validateAndAssociate = (updatedData) => {
fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${eleve.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('Succès :', data);
})
.catch(error => {
console.error('Erreur :', error);
});
}
const columns = [
{ name: t('studentName'), transform: (row) => row.eleve.nom },
{ name: t('studentFistName'), transform: (row) => row.eleve.prenom },
{ name: t('mainContactMail'), transform: (row) => row.eleve.responsables[0].mail },
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.eleve.responsables[0].telephone) },
{ name: t('lastUpdateDate'), transform: (row) => swapFormatDate(row.dateMAJ, "DD-MM-YYYY hh:mm:ss", "DD/MM/YYYY hh:mm") },
{ name: t('registrationFileStatus'), transform: (row) => <StatusLabel etat={row.etat} onChange={(newStatus) => updateStatusAction(row.eleve.id, newStatus)} /> },
{ name: t('lastUpdateDate'), transform: (row) => row.dateMAJ_formattee},
{ name: t('registrationFileStatus'), transform: (row) => (
<div className="flex justify-center items-center h-full">
<StatusLabel etat={row.etat} onChange={(newStatus) => updateStatusAction(row.eleve.id, newStatus)} showDropdown={false} />
</div>
)
},
{ name: t('files'), transform: (row) => (
<ul>
{row.fichiers?.map((fichier, fileIndex) => (
@ -228,6 +311,22 @@ const columns = [
),
onClick: () => window.location.href = `${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?idEleve=${row.eleve.id}&id=1`,
}] : []),
...(row.etat === 3 ? [{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Valider
</>
),
onClick: () => openModalAssociationEleve(row.eleve),
}] : []),
...(row.etat === 5 ? [{
label: (
<>
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.eleve),
}] : []),
...(row.etat !== 6 ? [{
label: (
<>
@ -242,6 +341,53 @@ const columns = [
/>
) },
];
const columnsSubscribed = [
{ name: t('studentName'), transform: (row) => row.eleve.nom },
{ name: t('studentFistName'), transform: (row) => row.eleve.prenom },
{ name: t('lastUpdateDate'), transform: (row) => row.dateMAJ_formattee},
{ name: t('class'), transform: (row) => row.eleve.classeAssocieeName},
{ name: t('registrationFileStatus'), transform: (row) => (
<div className="flex justify-center items-center h-full">
<StatusLabel etat={row.etat} onChange={(newStatus) => updateStatusAction(row.eleve.id, newStatus)} showDropdown={false} />
</div>
)
},
{ name: t('files'), transform: (row) => (
<ul>
{row.fichiers?.map((fichier, fileIndex) => (
<li key={fileIndex} className="flex items-center gap-2">
<FileText size={16} />
<a href={fichier.url}>{fichier.nom}</a>
</li>
))}
</ul>
) },
{ name: 'Actions', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
{ label: (
<>
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.eleve)
},
{ label: (
<>
<Trash2 size={16} className="mr-2 text-red-700" /> Archiver
</>
),
onClick: () => archiveFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom),
}
]}
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"
/>
) },
];
if (isLoading) {
@ -262,22 +408,22 @@ const columns = [
<div className='p-8'>
<div className="border-b border-gray-200 mb-6">
<div className="flex gap-8">
<Tab
text={<>
{t('allStudents')}
<span className="ml-2 text-sm text-gray-400">({totalStudents})</span>
</>}
active={activeTab === 'all'}
onClick={() => setActiveTab('all')}
/>
<Tab
text={<>
{t('pending')}
<span className="ml-2 text-sm text-gray-400">({12})</span>
<span className="ml-2 text-sm text-gray-400">({totalPending})</span>
</>}
active={activeTab === 'pending'}
onClick={() => setActiveTab('pending')}
/>
<Tab
text={<>
{t('subscribed')}
<span className="ml-2 text-sm text-gray-400">({totalSubscribed})</span>
</>}
active={activeTab === 'subscribed'}
onClick={() => setActiveTab('subscribed')}
/>
<Tab
text={<>
{t('archived')}
@ -309,10 +455,21 @@ const columns = [
</div>
</div>
<DjangoCSRFToken csrfToken={csrfToken} />
<Table
key={`${currentPage}-${searchTerm}`}
data={(activeTab === 'all' || activeTab === 'pending') ? ficheInscriptionsData : fichesInscriptionsDataArchivees}
columns={columns}
data={
activeTab === 'pending'
? fichesInscriptionsDataEnCours
: activeTab === 'subscribed'
? fichesInscriptionsDataInscrits
: fichesInscriptionsDataArchivees
}
columns={
activeTab === 'subscribed'
? columnsSubscribed
: columns
}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
@ -327,6 +484,20 @@ const columns = [
}}
onCancel={() => setPopup({ ...popup, visible: false })}
/>
{isOpenAffectationClasse && (
<Modal
isOpen={isOpenAffectationClasse}
setIsOpen={setIsOpenAffectationClasse}
title="Affectation à une classe"
ContentComponent={() => (
<AffectationClasseForm
eleve={eleve}
onSubmit={validateAndAssociate}
classes={classes}
/>
)}
/>
)}
</div>
);
}

View File

@ -8,7 +8,7 @@ import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { BK_LOGIN_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, FR_PARENTS_HOME_URL, FR_USERS_NEW_PASSWORD_URL, FR_USERS_SUBSCRIBE_URL } from '@/utils/Url';
import { BK_LOGIN_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, FR_ADMIN_STUDENT_URL, FR_PARENTS_HOME_URL, FR_USERS_NEW_PASSWORD_URL, FR_USERS_SUBSCRIBE_URL } from '@/utils/Url';
import useLocalStorage from '@/hooks/useLocalStorage';
import useCsrfToken from '@/hooks/useCsrfToken';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
@ -78,7 +78,19 @@ export default function Page() {
setErrorMessage("")
if(isOK(data)){
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
router.push(`${FR_PARENTS_HOME_URL}`);
if (data.droit == 0) {
// Vue ECOLE
} else if (data.droit == 1) {
// Vue PARENT
router.push(`${FR_PARENTS_HOME_URL}`);
} else if (data.droit == 2) {
// Vue ADMIN
router.push(`${FR_ADMIN_STUDENT_URL}`);
} else {
// Cas anormal
}
} else {
if(data.errorFields){
setUserFieldError(data.errorFields.email)

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react';
const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
const [formData, setFormData] = useState({
classeAssocie_id: eleve.classeAssocie_id || null,
});
const handleChange = (e) => {
const { name, value, type } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: parseInt(value, 10),
}));
};
const handleSubmit = () => {
onSubmit({
eleve: {
...formData
},
etat:5
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Classes
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{classes.map(classe => (
<div key={classe.id} className="flex items-center">
<input
type="radio"
id={`classe-${classe.id}`}
name="classeAssocie_id"
value={classe.id}
checked={formData.classeAssocie_id === classe.id}
onChange={handleChange}
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
/>
<label htmlFor={`classe-${classe.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
{classe.nom_ambiance}
</label>
</div>
))}
</div>
</div>
<div className="flex justify-end mt-4 space-x-4">
<button
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(!formData.classeAssocie_id )
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(!formData.classeAssocie_id)}
>
Associer
</button>
</div>
</form>
);
};
export default AffectationClasseForm;

View File

@ -0,0 +1,72 @@
import React from 'react';
import Table from '@/components/Table';
import { GraduationCap } from 'lucide-react';
const ClasseDetails = ({ classe }) => {
if (!classe) return null;
const nombreElevesInscrits = classe.eleves.length;
const capaciteTotale = classe.nombre_eleves;
const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100);
const getColor = (pourcentage) => {
if (pourcentage < 50) return 'bg-emerald-500';
if (pourcentage < 75) return 'bg-orange-500';
return 'bg-red-500';
};
return (
<div className="p-4">
<div className="mb-4 flex justify-between items-center">
{/* Section Enseignant Principal */}
<div className="flex items-center space-x-4">
<div className="bg-gray-100 p-3 rounded-lg shadow-md flex items-center space-x-4">
<GraduationCap className="w-10 h-10 text-gray-600" />
<div>
<p className="italic text-gray-600">Enseignant Principal :</p>
<p className="font-bold text-gray-800">
{classe.enseignant_principal ? (
`${classe.enseignant_principal.nom} ${classe.enseignant_principal.prenom}`
) : (
<i>Non assigné</i>
)}
</p>
</div>
</div>
</div>
{/* Section Capacité de la Classe */}
<div className="flex items-center space-x-4">
<div className="flex items-center">
<span className="font-bold text-gray-700 mr-4">
{nombreElevesInscrits}/{capaciteTotale}
</span>
<div className="w-32 bg-gray-200 rounded-full h-6 shadow-inner">
<div
className={`h-full rounded-full ${getColor(pourcentage)}`}
style={{ width: `${pourcentage}%` }}
></div>
</div>
<span className="ml-4 font-bold text-gray-700">
{pourcentage}%
</span>
</div>
</div>
</div>
<h3 className="text-xl font-semibold mb-4">Liste des élèves</h3>
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
<Table
columns={[
{ name: 'NOM', transform: (row) => row.nom },
{ name: 'PRENOM', transform: (row) => row.prenom },
{ name: 'AGE', transform: (row) => `${row.age}` }
]}
data={classe.eleves}
/>
</div>
</div>
);
};
export default ClasseDetails;

View File

@ -1,13 +1,15 @@
import { School, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal';
import ClassForm from '@/components/ClassForm';
import ClasseDetails from '@/components/ClasseDetails';
const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => {
const [isOpen, setIsOpen] = useState(false);
const [isOpenDetails, setIsOpenDetails] = useState(false);
const [editingClass, setEditingClass] = useState(null);
const openEditModal = (classe) => {
@ -15,11 +17,21 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE
setEditingClass(classe);
}
const openEditModalDetails = (classe) => {
setIsOpenDetails(true);
setEditingClass(classe);
}
const closeEditModal = () => {
setIsOpen(false);
setEditingClass(null);
};
const closeEditModalDetails = () => {
setIsOpenDetails(false);
setEditingClass(null);
};
const handleModalSubmit = (updatedData) => {
if (editingClass) {
handleEdit(editingClass.id, updatedData);
@ -29,15 +41,11 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE
closeEditModal();
};
const handleInspect = (data) => {
console.log('inspect classe : ', data)
}
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<School className="w-8 h-8 mr-2" />
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
<button
@ -82,7 +90,7 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
{ label: 'Inspecter', icon: ZoomIn, onClick: () => handleInspect(row) },
{ label: 'Inspecter', icon: ZoomIn, onClick: () => openEditModalDetails(row) },
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
]
@ -104,6 +112,25 @@ const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleE
)}
/>
)}
{isOpenDetails && (
<Modal
isOpen={isOpenDetails}
setIsOpen={setIsOpenDetails}
title={(
<div className="flex items-center">
<Users className="w-8 h-8 mr-2" />
{editingClass ? (
<>
{editingClass.nom_ambiance} - {editingClass.tranche_age[0]} à {editingClass.tranche_age[1]} ans
</>
) : ''}
</div>
)}
ContentComponent={() => (
<ClasseDetails classe={editingClass} />
)}
/>
)}
</div>
);
};

View File

@ -16,7 +16,7 @@ const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => {
<div className="mt-4 flex justify-end">
<Dialog.Close asChild>
<button
className="inline-flex justify-center px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-gray-300 text-gray-700 hover:bg-gray-400"
onClick={() => setIsOpen(false)}
>
Fermer

View File

@ -31,7 +31,7 @@ function Sidebar({ currentPage, items }) {
{/* Sidebar */}
<div className="w-64 bg-white border-r border-gray-200 py-6 px-4">
<div className="flex items-center mb-8 px-2">
<div className="text-xl font-semibold">Collège Saint-Joseph</div>
<div className="text-xl font-semibold">Ecole NEWT</div>
</div>
<nav className="space-y-1">

View File

@ -20,7 +20,7 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
{data?.map((row, rowIndex) => (
<tr key={rowIndex} className={` ${rowIndex % 2 === 0 ? 'bg-emerald-50' : ''}`}>
{columns.map((column, colIndex) => (
<td key={colIndex} className="py-2 px-4 border-b border-gray-200 text-center text-sm text-gray-700">

View File

@ -1,17 +1,25 @@
import React, { useState } from 'react';
const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
const profils = [
{ value: 0, label: "École" },
{ value: 2, label: "Administrateur" },
];
const [formData, setFormData] = useState({
nom: teacher.nom || '',
prenom: teacher.prenom || '',
mail: teacher.mail || '',
specialite_id: teacher.specialite_id || 1,
classes: teacher.classes || []
specialite_id: teacher.specialite_id || '',
classes: teacher.classes || [],
profilAssocie_id: teacher.profilAssocie_id || [],
DroitValue: teacher.DroitValue || 0
});
const handleChange = (e) => {
const { name, value, type } = e.target;
const newValue = type === 'radio' ? parseInt(value) : value;
const newValue = type === 'radio' ? parseInt(value, 10) : value;
console.log(`Name: ${name}, Value: ${newValue}`);
setFormData((prevState) => ({
...prevState,
[name]: newValue,
@ -91,15 +99,41 @@ const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Types de profil
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{profils.map((profil) => (
<div key={profil.value} className="flex items-center">
<input
type="radio"
id={`profil-${profil.value}`}
name="DroitValue"
value={profil.value}
checked={formData.DroitValue === profil.value}
onChange={handleChange}
className="form-radio h-4 w-4 text-emerald-600 focus:ring-emerald-500"
/>
<label
htmlFor={`profil-${profil.value}`}
className="ml-2 block text-sm text-gray-900"
>
{profil.label}
</label>
</div>
))}
</div>
</div>
<div className="flex justify-end mt-4 space-x-4">
<button
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(!formData.nom || !formData.prenom || !formData.mail)
(!formData.nom || !formData.prenom || !formData.mail || !formData.specialite_id)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(!formData.nom || !formData.prenom || !formData.mail)}
disabled={(!formData.nom || !formData.prenom || !formData.mail || !formData.specialite_id)}
>
Soumettre
</button>

View File

@ -1,14 +1,19 @@
import { School, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { GraduationCap, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal';
import TeacherForm from '@/components/TeacherForm';
import {BK_PROFILE_URL} from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => {
const [isOpen, setIsOpen] = useState(false);
const [editingTeacher, setEditingTeacher] = useState(null);
const csrfToken = useCsrfToken();
const openEditModal = (teacher) => {
setIsOpen(true);
setEditingTeacher(teacher);
@ -21,9 +26,70 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
const handleModalSubmit = (updatedData) => {
if (editingTeacher) {
handleEdit(editingTeacher.id, updatedData);
// Modification du profil
const request = new Request(
`${BK_PROFILE_URL}/${updatedData.profilAssocie_id}`,
{
method:'PUT',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
email: updatedData.mail,
username: updatedData.mail,
droit:updatedData.DroitValue
}),
}
);
fetch(request).then(response => response.json())
.then(response => {
console.log('Success:', response);
console.log('UpdateData:', updatedData);
handleEdit(editingTeacher.id, updatedData);
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
} else {
handleCreate(updatedData);
// Création d'un profil associé à l'adresse mail du responsable saisie
// Le profil est inactif
const request = new Request(
`${BK_PROFILE_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
email: updatedData.mail,
password: 'Provisoire01!',
username: updatedData.mail,
is_active: 1, // On rend le profil actif : on considère qu'au moment de la configuration de l'école un abonnement a été souscrit
droit:updatedData.DroitValue
}),
}
);
fetch(request).then(response => response.json())
.then(response => {
console.log('Success:', response);
console.log('UpdateData:', updatedData);
if (response.id) {
let idProfil = response.id;
updatedData.profilAssocie_id = idProfil;
handleCreate(updatedData);
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
closeEditModal();
};
@ -32,7 +98,7 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-7xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<School className="w-8 h-8 mr-2" />
<GraduationCap className="w-8 h-8 mr-2" />
Enseignants
</h2>
<button
@ -49,7 +115,9 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
{ name: 'PRENOM', transform: (row) => row.prenom },
{ name: 'MAIL', transform: (row) => row.mail },
{ name: 'SPECIALITE',
transform: (row) => (
transform: (row) => {
return row.specialite
?
<div key={row.id} className="flex justify-center items-center space-x-2">
<span
key={row.specialite.id}
@ -57,8 +125,19 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
style={{ backgroundColor: row.specialite.codeCouleur }}
title={row.specialite.nom}
></span>
</div>
)
</div>
: <i>Non définie</i>;
}
},
{ name: 'TYPE PROFIL',
transform: (row) => {
return row.profilAssocie
?
<div key={row.id} className="flex justify-center items-center space-x-2">
{row.DroitLabel}
</div>
: <i>Non définie</i>;
}
},
{ name: 'ACTIONS', transform: (row) => (
<DropdownMenu
@ -72,8 +151,6 @@ const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, spe
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"
/>
)}
// { name: 'SPECIALITE', transform: (row) => row.specialite_id },
// { name: 'CLASSES', transform: (row) => row.classe },
]}
data={teachers}
/>