diff --git a/Back-End/Auth/models.py b/Back-End/Auth/models.py index bb2cb9c..cbbceac 100644 --- a/Back-End/Auth/models.py +++ b/Back-End/Auth/models.py @@ -2,25 +2,31 @@ from django.contrib.auth.models import AbstractUser from django.db import models from django.utils.translation import gettext_lazy as _ from django.core.validators import EmailValidator +from Establishment.models import Establishment class Profile(AbstractUser): - class Droits(models.IntegerChoices): - PROFIL_UNDEFINED = -1, _('NON DEFINI') - PROFIL_ECOLE = 0, _('ECOLE') - PROFIL_ADMIN = 1, _('ADMIN') - PROFIL_PARENT = 2, _('PARENT') - email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()]) USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ('password', ) code = models.CharField(max_length=200, default="", blank=True) datePeremption = models.CharField(max_length=200, default="", blank=True) - droit = models.IntegerField(choices=Droits, default=Droits.PROFIL_UNDEFINED) - estConnecte = models.BooleanField(default=False, blank=True) - establishment = models.ForeignKey('School.Establishment', on_delete=models.PROTECT, related_name='profile', null=True, blank=True) def __str__(self): - return self.email + " - " + str(self.droit) + return self.email + +class ProfileRole(models.Model): + class RoleType(models.IntegerChoices): + PROFIL_UNDEFINED = -1, _('NON DEFINI') + PROFIL_ECOLE = 0, _('ECOLE') + PROFIL_ADMIN = 1, _('ADMIN') + PROFIL_PARENT = 2, _('PARENT') + + profile = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='roles') + role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED) + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='profile_roles') + is_active = models.BooleanField(default=False) + + def __str__(self): + return f"{self.profile.email} - {self.get_role_type_display()} - {self.establishment.name}" \ No newline at end of file diff --git a/Back-End/Auth/serializers.py b/Back-End/Auth/serializers.py index 67ce120..c35569a 100644 --- a/Back-End/Auth/serializers.py +++ b/Back-End/Auth/serializers.py @@ -1,48 +1,97 @@ from rest_framework import serializers -from Auth.models import Profile +from Auth.models import Profile, ProfileRole from django.core.exceptions import ValidationError +class ProfileRoleSerializer(serializers.ModelSerializer): + class Meta: + model = ProfileRole + fields = ['role_type', 'establishment', 'is_active', 'profile'] + class ProfileSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) password = serializers.CharField(write_only=True) + roles = ProfileRoleSerializer(many=True, required=False) class Meta: model = Profile - fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active', 'establishment'] + fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles'] extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): + roles_data = validated_data.pop('roles', []) user = Profile( username=validated_data['username'], email=validated_data['email'], - is_active=validated_data['is_active'], - droit=validated_data['droit'], - establishment=validated_data.get('establishment') + code=validated_data.get('code', ''), + datePeremption=validated_data.get('datePeremption', '') ) user.set_password(validated_data['password']) + user.full_clean() user.save() + + for role_data in roles_data: + ProfileRole.objects.create(profile=user, **role_data) + return user - def to_representation(self, instance): - ret = super().to_representation(instance) - ret['password'] = '********' - return ret - -class ProfilUpdateSerializer(serializers.ModelSerializer): - class Meta: - model = Profile - 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): + roles_data = validated_data.pop('roles', []) password = validated_data.pop('password', None) instance = super().update(instance, validated_data) if password: instance.set_password(password) - instance.save() + + instance.full_clean() + instance.save() + + for role_data in roles_data: + ProfileRole.objects.update_or_create( + profile=instance, + establishment_id=role_data.get('establishment_id'), + defaults={ + 'role_type': role_data.get('role_type'), + 'is_active': role_data.get('is_active', True) + } + ) + + return instance + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['password'] = '********' + return ret + +class ProfilUpdateSerializer(serializers.ModelSerializer): + roles = ProfileRoleSerializer(many=True, required=False) + + class Meta: + model = Profile + fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles'] + extra_kwargs = { + 'password': {'write_only': True, 'required': False} + } + + def update(self, instance, validated_data): + roles_data = validated_data.pop('roles', []) + password = validated_data.pop('password', None) + instance = super().update(instance, validated_data) + + if password: + instance.set_password(password) + + instance.full_clean() + instance.save() + + for role_data in roles_data: + ProfileRole.objects.update_or_create( + profile=instance, + establishment_id=role_data.get('establishment_id'), + defaults={ + 'role_type': role_data.get('role_type'), + 'is_active': role_data.get('is_active', True) + } + ) return instance diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index a8b4de8..ca3351a 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -19,7 +19,7 @@ from jwt.exceptions import ExpiredSignatureError, InvalidTokenError import json from . import validator -from .models import Profile +from .models import Profile, ProfileRole from rest_framework.decorators import action, api_view from Auth.serializers import ProfileSerializer, ProfilUpdateSerializer @@ -56,7 +56,10 @@ class SessionView(APIView): 'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={ 'id': openapi.Schema(type=openapi.TYPE_INTEGER), 'email': openapi.Schema(type=openapi.TYPE_STRING), - 'role': openapi.Schema(type=openapi.TYPE_STRING) + 'roles': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_OBJECT, properties={ + 'role_type': openapi.Schema(type=openapi.TYPE_STRING), + 'establishment': openapi.Schema(type=openapi.TYPE_STRING) + })) }) })), 401: openapi.Response('Session invalide') @@ -67,15 +70,16 @@ class SessionView(APIView): try: decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) - print(f'decode : {decoded_token}') - userid = decoded_token.get('id') + userid = decoded_token.get('user_id') user = Profile.objects.get(id=userid) + roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name') + response_data = { 'user': { 'id': user.id, 'email': user.email, - 'role': user.droit, # Assure-toi que le champ 'droit' existe et contient le rôle + 'roles': list(roles) } } return JsonResponse(response_data, status=status.HTTP_200_OK) @@ -103,13 +107,11 @@ class ProfileView(APIView): } ) def post(self, request): - profil_data=JSONParser().parse(request) - print(f'{profil_data}') + profil_data = JSONParser().parse(request) profil_serializer = ProfileSerializer(data=profil_data) if profil_serializer.is_valid(): - profil_serializer.save() - + profil = profil_serializer.save() return JsonResponse(profil_serializer.data, safe=False) return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) @@ -122,8 +124,8 @@ class ProfileSimpleView(APIView): responses={200: ProfileSerializer} ) def get(self, request, id): - profil=bdd.getObject(Profile, "id", id) - profil_serializer=ProfileSerializer(profil) + profil = bdd.getObject(Profile, "id", id) + profil_serializer = ProfileSerializer(profil) return JsonResponse(profil_serializer.data, safe=False) @swagger_auto_schema( @@ -135,12 +137,12 @@ class ProfileSimpleView(APIView): } ) def put(self, request, id): - data=JSONParser().parse(request) + data = JSONParser().parse(request) profil = Profile.objects.get(id=id) profil_serializer = ProfilUpdateSerializer(profil, data=data) if profil_serializer.is_valid(): profil_serializer.save() - return JsonResponse("Updated Successfully", safe=False) + return JsonResponse(profil_serializer.data, safe=False) return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) @@ -157,10 +159,11 @@ class LoginView(APIView): operation_description="Connexion utilisateur", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, - required=['email', 'password'], + required=['email', 'password', 'role_type'], properties={ 'email': openapi.Schema(type=openapi.TYPE_STRING), - 'password': openapi.Schema(type=openapi.TYPE_STRING) + 'password': openapi.Schema(type=openapi.TYPE_STRING), + 'role_type': openapi.Schema(type=openapi.TYPE_STRING) } ), responses={ @@ -193,40 +196,45 @@ class LoginView(APIView): password=data.get('password'), ) if user is not None: - if user.is_active: - login(request, user) - user.estConnecte = True - user.save() - clear_cache() - retour = '' - # Générer le JWT avec la bonne syntaxe datetime - access_payload = { - 'user_id': user.id, - 'email': user.email, - 'droit': user.droit, - 'establishment': user.establishment.id, - 'type': 'access', - 'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], - 'iat': datetime.utcnow(), - } + role_type = data.get('role_type') + primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first() - access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM']) - # Générer le Refresh Token (exp: 7 jours) - refresh_payload = { - 'user_id': user.id, - 'type': 'refresh', - 'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], - 'iat': datetime.utcnow(), - } - refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM']) + if not primary_role: + return JsonResponse({"errorMessage": "Role not assigned to the user"}, status=status.HTTP_401_UNAUTHORIZED) - return JsonResponse({ - 'token': access_token, - 'refresh': refresh_token - }, safe=False) + login(request, user) + user.save() + clear_cache() + retour = '' + + # Récupérer tous les rôles de l'utilisateur avec le type spécifié + roles = ProfileRole.objects.filter(profile=user, role_type=role_type).values('role_type', 'establishment__id', 'establishment__name') + + # Générer le JWT avec la bonne syntaxe datetime + access_payload = { + 'user_id': user.id, + 'email': user.email, + 'roles': list(roles), + 'type': 'access', + 'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], + 'iat': datetime.utcnow(), + } + + access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM']) + # Générer le Refresh Token (exp: 7 jours) + refresh_payload = { + 'user_id': user.id, + 'type': 'refresh', + 'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], + 'iat': datetime.utcnow(), + } + refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM']) + + return JsonResponse({ + 'token': access_token, + 'refresh': refresh_token + }, safe=False) - else: - retour = error.returnMessage[error.PROFIL_INACTIVE] else: retour = error.returnMessage[error.WRONG_ID] @@ -235,7 +243,6 @@ class LoginView(APIView): 'errorMessage': retour, }, safe=False, status=status.HTTP_400_BAD_REQUEST) - class RefreshJWTView(APIView): @swagger_auto_schema( operation_description="Rafraîchir le token d'accès", @@ -295,13 +302,20 @@ class RefreshJWTView(APIView): # Récupérer les informations utilisateur user = Profile.objects.get(id=payload['user_id']) + role_type = payload.get('role_type') + + # Récupérer le rôle principal de l'utilisateur + primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type).first() + + if not primary_role: + return JsonResponse({'errorMessage': 'No role assigned to the user'}, status=400) # Générer un nouveau Access Token avec les informations complètes new_access_payload = { 'user_id': user.id, 'email': user.email, - 'droit': user.droit, - 'establishment': user.establishment.id, + 'role_type': primary_role.get_role_type_display(), + 'establishment': primary_role.establishment.id, 'type': 'access', 'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], 'iat': datetime.utcnow(), @@ -311,6 +325,7 @@ class RefreshJWTView(APIView): new_refresh_payload = { 'user_id': user.id, + 'role_type': role_type, 'type': 'refresh', 'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], 'iat': datetime.utcnow(), @@ -335,6 +350,14 @@ class RefreshJWTView(APIView): class SubscribeView(APIView): @swagger_auto_schema( operation_description="Inscription utilisateur", + manual_parameters=[ + openapi.Parameter( + 'establishment_id', openapi.IN_QUERY, + description="ID de l'établissement", + type=openapi.TYPE_INTEGER, + required=True + ) + ], request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['email', 'password1', 'password2'], @@ -359,37 +382,54 @@ class SubscribeView(APIView): def post(self, request): retourErreur = error.returnMessage[error.BAD_URL] retour = '' - newProfilConnection=JSONParser().parse(request) + newProfilConnection = JSONParser().parse(request) + establishment_id = request.GET.get('establishment_id') + + if not establishment_id: + return JsonResponse({'message': retour, 'errorMessage': 'establishment_id manquant', "errorFields": {}, "id": -1}, safe=False, status=status.HTTP_400_BAD_REQUEST) validatorSubscription = validator.ValidatorSubscription(data=newProfilConnection) validationOk, errorFields = validatorSubscription.validate() - if validationOk: - # On vérifie que l'email existe : si ce n'est pas le cas, on retourne une erreur profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email')) - if profil == None: + if profil is None: retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS] else: - if profil.is_active: - retourErreur=error.returnMessage[error.PROFIL_ACTIVE] - return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False) + # Vérifier si le profil a déjà un rôle actif pour l'établissement donné + active_roles = ProfileRole.objects.filter(profile=profil, establishment_id=establishment_id, is_active=True) + if active_roles.exists(): + retourErreur = error.returnMessage[error.PROFIL_ACTIVE] + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False) else: try: profil.set_password(newProfilConnection.get('password1')) - profil.is_active = True profil.full_clean() profil.save() + + # Utiliser le sérialiseur ProfileRoleSerializer pour créer ou mettre à jour le rôle + role_data = { + 'profile': profil.id, + 'establishment_id': establishment_id, + 'role_type': ProfileRole.RoleType.PROFIL_PARENT, + 'is_active': True + } + role_serializer = ProfileRoleSerializer(data=role_data) + if role_serializer.is_valid(): + role_serializer.save() + else: + return JsonResponse(role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + clear_cache() retour = error.returnMessage[error.MESSAGE_ACTIVATION_PROFILE] - retourErreur='' - return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False) + retourErreur = '' + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False) except ValidationError as e: retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT] - return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields}, safe=False) + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False) - return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields, "id":-1}, safe=False) + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": -1}, safe=False) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') @@ -417,26 +457,29 @@ class NewPasswordView(APIView): def post(self, request): retourErreur = error.returnMessage[error.BAD_URL] retour = '' - newProfilConnection=JSONParser().parse(request) + newProfilConnection = JSONParser().parse(request) validatorNewPassword = validator.ValidatorNewPassword(data=newProfilConnection) validationOk, errorFields = validatorNewPassword.validate() if validationOk: - profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email')) - if profil == None: + if profil is None: retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS] else: - # Génération d'une URL provisoire pour modifier le mot de passe - profil.code = util.genereRandomCode(12) - profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS) - profil.save() - clear_cache() - retourErreur = '' - retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD]%(newProfilConnection.get('email')) - mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code) + try: + # Génération d'une URL provisoire pour modifier le mot de passe + profil.code = util.genereRandomCode(12) + profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS) + profil.save() + clear_cache() + retourErreur = '' + retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD] % (newProfilConnection.get('email')) + mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code) + except ValidationError as e: + retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT] + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False) - return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields}, safe=False) + return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') @@ -465,7 +508,7 @@ class ResetPasswordView(APIView): def post(self, request, code): retourErreur = error.returnMessage[error.BAD_URL] retour = '' - newProfilConnection=JSONParser().parse(request) + newProfilConnection = JSONParser().parse(request) validatorResetPassword = validator.ValidatorResetPassword(data=newProfilConnection) validationOk, errorFields = validatorResetPassword.validate() @@ -474,16 +517,15 @@ class ResetPasswordView(APIView): if profil: if datetime.strptime(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'), '%d-%m-%Y %H:%M') > datetime.strptime(profil.datePeremption, '%d-%m-%Y %H:%M'): - retourErreur = error.returnMessage[error.EXPIRED_URL]%(_uuid) + retourErreur = error.returnMessage[error.EXPIRED_URL] % (_uuid) elif validationOk: retour = error.returnMessage[error.PASSWORD_CHANGED] profil.set_password(newProfilConnection.get('password1')) profil.code = '' profil.datePeremption = '' - profil.is_active = True profil.save() clear_cache() - retourErreur='' + retourErreur = '' - return JsonResponse({'message':retour, "errorMessage":retourErreur, "errorFields":errorFields}, safe=False) + return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False) diff --git a/Back-End/Establishment/__init__.py b/Back-End/Establishment/__init__.py new file mode 100644 index 0000000..95d2d94 --- /dev/null +++ b/Back-End/Establishment/__init__.py @@ -0,0 +1 @@ +default_app_config = 'Establishment.apps.EstablishmentConfig' diff --git a/Back-End/Establishment/admin.py b/Back-End/Establishment/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Back-End/Establishment/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Back-End/Establishment/apps.py b/Back-End/Establishment/apps.py new file mode 100644 index 0000000..ee46399 --- /dev/null +++ b/Back-End/Establishment/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + +class EstablishmentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'Establishment' + + diff --git a/Back-End/Establishment/models.py b/Back-End/Establishment/models.py new file mode 100644 index 0000000..df5ad80 --- /dev/null +++ b/Back-End/Establishment/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.contrib.postgres.fields import ArrayField +from django.utils.translation import gettext_lazy as _ + +class StructureType(models.IntegerChoices): + MATERNELLE = 1, _('Maternelle') + PRIMAIRE = 2, _('Primaire') + SECONDAIRE = 3, _('Secondaire') + +class Establishment(models.Model): + name = models.CharField(max_length=255, unique=True) + address = models.CharField(max_length=255) + total_capacity = models.IntegerField() + establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices)) + licence_code = models.CharField(max_length=100, blank=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/Back-End/Establishment/serializers.py b/Back-End/Establishment/serializers.py new file mode 100644 index 0000000..990038c --- /dev/null +++ b/Back-End/Establishment/serializers.py @@ -0,0 +1,91 @@ +from rest_framework import serializers +from .models import Establishment +from School.models import SchoolClass, Teacher, Speciality, Fee, Discount, PaymentMode, PaymentPlan +from Subscriptions.models import RegistrationForm, RegistrationFileGroup +from Auth.models import Profile + +class EstablishmentSerializer(serializers.ModelSerializer): + profile_count = serializers.SerializerMethodField() + profiles = serializers.SerializerMethodField() + school_class_count = serializers.SerializerMethodField() + school_classes = serializers.SerializerMethodField() + teacher_count = serializers.SerializerMethodField() + teachers = serializers.SerializerMethodField() + speciality_count = serializers.SerializerMethodField() + specialities = serializers.SerializerMethodField() + fee_count = serializers.SerializerMethodField() + fees = serializers.SerializerMethodField() + discount_count = serializers.SerializerMethodField() + discounts = serializers.SerializerMethodField() + active_payment_mode_count = serializers.SerializerMethodField() + active_payment_modes = serializers.SerializerMethodField() + active_payment_plan_count = serializers.SerializerMethodField() + active_payment_plans = serializers.SerializerMethodField() + file_group_count = serializers.SerializerMethodField() + file_groups = serializers.SerializerMethodField() + registration_form_count = serializers.SerializerMethodField() + registration_forms = serializers.SerializerMethodField() + + class Meta: + model = Establishment + fields = '__all__' + + def get_profile_count(self, obj): + return Profile.objects.filter(roles__establishment=obj).distinct().count() + + def get_profiles(self, obj): + return list(Profile.objects.filter(roles__establishment=obj).distinct().values_list('email', flat=True)) + + def get_school_class_count(self, obj): + return SchoolClass.objects.filter(establishment=obj).distinct().count() + + def get_school_classes(self, obj): + return list(SchoolClass.objects.filter(establishment=obj).distinct().values_list('atmosphere_name', flat=True)) + + def get_teacher_count(self, obj): + return Teacher.objects.filter(profile_role__establishment=obj).distinct().count() + + def get_teachers(self, obj): + return list(Teacher.objects.filter(profile_role__establishment=obj).distinct().values_list('last_name', 'first_name')) + + def get_speciality_count(self, obj): + return Speciality.objects.filter(establishment=obj).distinct().count() + + def get_specialities(self, obj): + return list(Speciality.objects.filter(establishment=obj).distinct().values_list('name', flat=True)) + + def get_fee_count(self, obj): + return Fee.objects.filter(establishment=obj).distinct().count() + + def get_fees(self, obj): + return list(Fee.objects.filter(establishment=obj).distinct().values_list('name', flat=True)) + + def get_discount_count(self, obj): + return Discount.objects.filter(establishment=obj).distinct().count() + + def get_discounts(self, obj): + return list(Discount.objects.filter(establishment=obj).distinct().values_list('name', flat=True)) + + def get_active_payment_mode_count(self, obj): + return PaymentMode.objects.filter(establishment=obj, is_active=True).distinct().count() + + def get_active_payment_modes(self, obj): + return list(PaymentMode.objects.filter(establishment=obj, is_active=True).distinct().values_list('mode', flat=True)) + + def get_active_payment_plan_count(self, obj): + return PaymentPlan.objects.filter(establishment=obj, is_active=True).distinct().count() + + def get_active_payment_plans(self, obj): + return list(PaymentPlan.objects.filter(establishment=obj, is_active=True).distinct().values_list('frequency', flat=True)) + + def get_file_group_count(self, obj): + return RegistrationFileGroup.objects.filter(establishment=obj).distinct().count() + + def get_file_groups(self, obj): + return list(RegistrationFileGroup.objects.filter(establishment=obj).distinct().values_list('name', flat=True)) + + def get_registration_form_count(self, obj): + return RegistrationForm.objects.filter(establishment=obj).distinct().count() + + def get_registration_forms(self, obj): + return list(RegistrationForm.objects.filter(establishment=obj).distinct().values_list('student__last_name', 'student__first_name')) \ No newline at end of file diff --git a/Back-End/Establishment/urls.py b/Back-End/Establishment/urls.py new file mode 100644 index 0000000..de40903 --- /dev/null +++ b/Back-End/Establishment/urls.py @@ -0,0 +1,7 @@ +from django.urls import path, re_path +from .views import EstablishmentListCreateView, EstablishmentDetailView + +urlpatterns = [ + re_path(r'^establishments$', EstablishmentListCreateView.as_view(), name='establishment_list_create'), + re_path(r'^establishments/(?P[0-9]+)$', EstablishmentDetailView.as_view(), name="establishment_detail"), +] \ No newline at end of file diff --git a/Back-End/Establishment/views.py b/Back-End/Establishment/views.py new file mode 100644 index 0000000..df48324 --- /dev/null +++ b/Back-End/Establishment/views.py @@ -0,0 +1,51 @@ +from django.http.response import JsonResponse +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect +from django.utils.decorators import method_decorator +from rest_framework.parsers import JSONParser +from rest_framework.views import APIView +from rest_framework import status +from .models import Establishment +from .serializers import EstablishmentSerializer +from N3wtSchool.bdd import delete_object, getAllObjects, getObject + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class EstablishmentListCreateView(APIView): + def get(self, request): + establishments = getAllObjects(Establishment) + establishments_serializer = EstablishmentSerializer(establishments, many=True) + return JsonResponse(establishments_serializer.data, safe=False, status=status.HTTP_200_OK) + + def post(self, request): + establishment_data = JSONParser().parse(request) + establishment_serializer = EstablishmentSerializer(data=establishment_data) + if establishment_serializer.is_valid(): + establishment_serializer.save() + return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_201_CREATED) + return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class EstablishmentDetailView(APIView): + def get(self, request, id=None): + try: + establishment = Establishment.objects.get(id=id) + establishment_serializer = EstablishmentSerializer(establishment) + return JsonResponse(establishment_serializer.data, safe=False) + except Establishment.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) + + def put(self, request, id): + establishment_data = JSONParser().parse(request) + try: + establishment = Establishment.objects.get(id=id) + except Establishment.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) + establishment_serializer = EstablishmentSerializer(establishment, data=establishment_data, partial=True) + if establishment_serializer.is_valid(): + establishment_serializer.save() + return JsonResponse(establishment_serializer.data, safe=False) + return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, id): + return delete_object(Establishment, id) \ No newline at end of file diff --git a/Back-End/GestionNotification/signals.py b/Back-End/GestionNotification/signals.py index 20e78f3..4e3fb0a 100644 --- a/Back-End/GestionNotification/signals.py +++ b/Back-End/GestionNotification/signals.py @@ -4,20 +4,20 @@ from .models import Notification, TypeNotif from GestionMessagerie.models import Messagerie from Subscriptions.models import RegistrationForm -@receiver(post_save, sender=Messagerie) -def notification_MESSAGE(sender, instance, created, **kwargs): - if created: - Notification.objects.create( - user=instance.destinataire, - message=(TypeNotif.NOTIF_MESSAGE).label, - typeNotification=TypeNotif.NOTIF_MESSAGE - ) +# @receiver(post_save, sender=Messagerie) +# def notification_MESSAGE(sender, instance, created, **kwargs): +# if created: +# Notification.objects.create( +# user=instance.destinataire, +# message=(TypeNotif.NOTIF_MESSAGE).label, +# typeNotification=TypeNotif.NOTIF_MESSAGE +# ) -@receiver(post_save, sender=RegistrationForm) -def notification_DI(sender, instance, created, **kwargs): - for responsable in instance.student.guardians.all(): - Notification.objects.create( - user=responsable.associated_profile, - message=(TypeNotif.NOTIF_DI).label, - typeNotification=TypeNotif.NOTIF_DI - ) +# @receiver(post_save, sender=RegistrationForm) +# def notification_DI(sender, instance, created, **kwargs): +# for responsable in instance.student.guardians.all(): +# Notification.objects.create( +# user=responsable.associated_profile, +# message=(TypeNotif.NOTIF_DI).label, +# typeNotification=TypeNotif.NOTIF_DI +# ) diff --git a/Back-End/N3wtSchool/settings.py b/Back-End/N3wtSchool/settings.py index b17dd31..ab74235 100644 --- a/Back-End/N3wtSchool/settings.py +++ b/Back-End/N3wtSchool/settings.py @@ -44,6 +44,7 @@ INSTALLED_APPS = [ 'GestionNotification.apps.GestionNotificationConfig', 'School.apps.SchoolConfig', 'Planning.apps.PlanningConfig', + 'Establishment.apps.EstablishmentConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/Back-End/N3wtSchool/urls.py b/Back-End/N3wtSchool/urls.py index 8a90e6f..048a202 100644 --- a/Back-End/N3wtSchool/urls.py +++ b/Back-End/N3wtSchool/urls.py @@ -46,6 +46,7 @@ urlpatterns = [ path("School/", include(("School.urls", 'School'), namespace='School')), path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')), path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')), + path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')), # Documentation Api re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), diff --git a/Back-End/Planning/models.py b/Back-End/Planning/models.py index 1ee967f..b36882c 100644 --- a/Back-End/Planning/models.py +++ b/Back-End/Planning/models.py @@ -3,7 +3,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.conf import settings -from School.models import Establishment +from Establishment.models import Establishment class RecursionType(models.IntegerChoices): RECURSION_NONE = 0, _('Aucune') diff --git a/Back-End/School/apps.py b/Back-End/School/apps.py index 8e7c210..fe33f19 100644 --- a/Back-End/School/apps.py +++ b/Back-End/School/apps.py @@ -1,14 +1,5 @@ from django.apps import AppConfig -from django.db.models.signals import post_migrate - -def create_speciality(sender, **kwargs): - from .models import Speciality - if not Speciality.objects.filter(name='GROUPE').exists(): - Speciality.objects.create(name='GROUPE', color_code='#FF0000') class SchoolConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'School' - - def ready(self): - post_migrate.connect(create_speciality, sender=self) diff --git a/Back-End/School/management/commands/init_mock_datas.py b/Back-End/School/management/commands/init_mock_datas.py index 0d68817..902a55f 100644 --- a/Back-End/School/management/commands/init_mock_datas.py +++ b/Back-End/School/management/commands/init_mock_datas.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand -from django.contrib.auth.models import User from Subscriptions.models import ( RegistrationForm, Student, @@ -10,9 +9,8 @@ from Subscriptions.models import ( RegistrationTemplateMaster, RegistrationTemplate ) -from Auth.models import Profile +from Auth.models import Profile, ProfileRole from School.models import ( - Establishment, FeeType, Speciality, Teacher, @@ -20,8 +18,7 @@ from School.models import ( PaymentMode, PaymentModeType, PaymentPlan, - PaymentPlanType, - StructureType, + PaymentPlanType, DiscountType ) from django.utils import timezone @@ -34,6 +31,19 @@ from faker import Faker import random import json +from School.serializers import ( + FeeSerializer, + DiscountSerializer, + PaymentModeSerializer, + PaymentPlanSerializer, + SpecialitySerializer, + TeacherSerializer, + SchoolClassSerializer +) +from Auth.serializers import ProfileSerializer, ProfileRoleSerializer +from Establishment.serializers import EstablishmentSerializer +from Subscriptions.serializers import RegistrationFormSerializer, GuardianSerializer + # Définir le chemin vers le dossier mock_datas MOCK_DATAS_PATH = os.path.join(settings.BASE_DIR, 'School', 'management', 'mock_datas') @@ -41,176 +51,306 @@ class Command(BaseCommand): help = 'Initialise toutes les données mock' def handle(self, *args, **kwargs): - self.create_or_update_establishments() - self.create_or_update_fees() - self.create_or_update_discounts() - self.create_or_update_payment_modes() - self.create_or_update_payment_plans() - self.create_or_update_specialities() - self.create_or_update_teachers() - self.create_or_update_school_classes() - self.create_or_update_registration_file_group() - self.create_register_form() + self.init_establishments() + self.init_profiles() + self.init_fees() + self.init_discounts() + self.init_payment_modes() + self.init_payment_plans() + self.init_specialities() + self.init_teachers() + self.init_guardians() + self.init_school_classes() + self.init_file_group() + self.init_register_form() def load_data(self, filename): with open(os.path.join(MOCK_DATAS_PATH, filename), 'r') as file: return json.load(file) - def create_or_update_establishments(self): + def init_establishments(self): establishments_data = self.load_data('establishments.json') self.establishments = [] for establishment_data in establishments_data: - establishment, created = Establishment.objects.update_or_create( - name=establishment_data["name"], - defaults=establishment_data - ) - self.establishments.append(establishment) - if created: - self.stdout.write(self.style.SUCCESS(f'Establishment {establishment.name} created successfully')) + serializer = EstablishmentSerializer(data=establishment_data) + if serializer.is_valid(): + establishment = serializer.save() + self.establishments.append(establishment) + self.stdout.write(self.style.SUCCESS(f'Establishment {establishment.name} created or updated successfully')) else: - self.stdout.write(self.style.SUCCESS(f'Establishment {establishment.name} updated successfully')) + self.stdout.write(self.style.ERROR(f'Error in data for establishment: {serializer.errors}')) - def create_or_update_fees(self): + def init_profiles(self): + profiles_data = self.load_data('profiles.json') + + for profile_data in profiles_data: + # Randomize the number of roles to create (between 1 et 3) + num_roles = random.randint(1, 3) + selected_roles = [] + + for _ in range(num_roles): + establishment = random.choice(self.establishments) + role_type = random.choice([ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN, ProfileRole.RoleType.PROFIL_PARENT]) + + # Ensure no duplicate ADMIN role for the same establishment + if role_type == ProfileRole.RoleType.PROFIL_ADMIN: + if any(role['role_type'] == ProfileRole.RoleType.PROFIL_ADMIN and role['establishment'] == establishment.id for role in selected_roles): + continue + + selected_roles.append({ + "role_type": role_type, + "establishment": establishment.id, + "establishment_name": establishment.name + }) + + # Generate email based on the selected roles and establishment + role_types = '-'.join([f"{ProfileRole.RoleType(role['role_type']).name.replace('PROFIL_', '')}_{role['establishment_name'].replace(' ', '')}" for role in selected_roles]) + email = f"{profile_data['username']}-{role_types}@exemple.com" + + # Add email to profile data + profile_data['email'] = email + + serializer = ProfileSerializer(data=profile_data) + if serializer.is_valid(): + profile = serializer.save() + profile.set_password(profile_data["password"]) + profile.save() + self.stdout.write(self.style.SUCCESS(f'Profile {profile.email} created successfully')) + + # Create or update the profile role for each selected role using ProfileRoleSerializer + for role in selected_roles: + role_data = { + "profile": profile.id, + "establishment": role["establishment"], + "role_type": role["role_type"], + "is_active": True + } + role_serializer = ProfileRoleSerializer(data=role_data) + if role_serializer.is_valid(): + role_serializer.save() + else: + self.stdout.write(self.style.ERROR(f'Error in data for profile role: {role_serializer.errors}')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for profile: {serializer.errors}')) + + def init_fees(self): fees_data = self.load_data('fees.json') for fee_data in fees_data: establishment = random.choice(self.establishments) + print(f'establishment : {establishment}') fee_data["name"] = f"{fee_data['name']} - {establishment.name}" - fee_data["establishment"] = establishment - Fee.objects.update_or_create( - name=fee_data["name"], - type=fee_data["type"], - defaults=fee_data - ) + fee_data["establishment"] = establishment.id + fee_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]) - self.stdout.write(self.style.SUCCESS('Fees initialized or updated successfully')) + serializer = FeeSerializer(data=fee_data) + if serializer.is_valid(): + fee = serializer.save() + self.stdout.write(self.style.SUCCESS(f'Fee {fee.name} created successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for fee: {serializer.errors}')) - def create_or_update_discounts(self): + def init_discounts(self): discounts_data = self.load_data('discounts.json') for discount_data in discounts_data: establishment = random.choice(self.establishments) discount_data["name"] = f"{discount_data['name']} - {establishment.name}" - discount_data["establishment"] = establishment - Discount.objects.update_or_create( - name=discount_data["name"], - type=discount_data["type"], - discount_type=discount_data["discount_type"], - defaults=discount_data - ) + discount_data["establishment"] = establishment.id + discount_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]) + discount_data["discount_type"] = random.choice([DiscountType.CURRENCY, DiscountType.PERCENT]) - self.stdout.write(self.style.SUCCESS('Discounts initialized or updated successfully')) + serializer = DiscountSerializer(data=discount_data) + if serializer.is_valid(): + discount = serializer.save() + self.stdout.write(self.style.SUCCESS(f'Discount {discount.name} created successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for discount: {serializer.errors}')) - def create_or_update_payment_modes(self): - payment_modes_data = self.load_data('payment_modes.json') + def init_payment_modes(self): + modes = [PaymentModeType.SEPA, PaymentModeType.TRANSFER, PaymentModeType.CHECK, PaymentModeType.CASH] + types = [FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE] - for payment_mode_data in payment_modes_data: - establishment = random.choice(self.establishments) - payment_mode_data["establishment"] = establishment - PaymentMode.objects.update_or_create( - mode=payment_mode_data["mode"], - type=payment_mode_data["type"], - defaults=payment_mode_data - ) + for establishment in self.establishments: + for mode in modes: + for type in types: + payment_mode_data = { + "mode": mode, + "type": type, + "is_active": random.choice([True, False]), + "establishment": establishment.id + } - self.stdout.write(self.style.SUCCESS('Payment Modes initialized or updated successfully')) + serializer = PaymentModeSerializer(data=payment_mode_data) + if serializer.is_valid(): + payment_mode = serializer.save() + self.stdout.write(self.style.SUCCESS(f'Payment Mode {payment_mode} created successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for payment mode: {serializer.errors}')) - def create_or_update_payment_plans(self): - payment_plans_data = self.load_data('payment_plans.json') + def init_payment_plans(self): + frequencies = [PaymentPlanType.ONE_TIME, PaymentPlanType.THREE_TIMES, PaymentPlanType.TEN_TIMES, PaymentPlanType.TWELVE_TIMES] + types = [FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE] current_date = timezone.now().date() - for payment_plan_data in payment_plans_data: - establishment = random.choice(self.establishments) - payment_plan_data["establishment"] = establishment - payment_plan_data["due_dates"] = [current_date + relativedelta(months=1)] - if payment_plan_data["frequency"] == PaymentPlanType.THREE_TIMES: - payment_plan_data["due_dates"] = [current_date + relativedelta(months=1+4*i) for i in range(3)] - elif payment_plan_data["frequency"] == PaymentPlanType.TEN_TIMES: - payment_plan_data["due_dates"] = [current_date + relativedelta(months=1+i) for i in range(10)] - elif payment_plan_data["frequency"] == PaymentPlanType.TWELVE_TIMES: - payment_plan_data["due_dates"] = [current_date + relativedelta(months=1+i) for i in range(12)] + for establishment in self.establishments: + for frequency in frequencies: + for type in types: + payment_plan_data = { + "frequency": frequency, + "type": type, + "is_active": random.choice([True, False]), + "establishment": establishment.id, + "due_dates": self.generate_due_dates(frequency, current_date) + } - PaymentPlan.objects.update_or_create( - frequency=payment_plan_data["frequency"], - type=payment_plan_data["type"], - defaults=payment_plan_data - ) + serializer = PaymentPlanSerializer(data=payment_plan_data) + if serializer.is_valid(): + payment_plan = serializer.save() + self.stdout.write(self.style.SUCCESS(f'Payment Plan {payment_plan} created successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for payment plan: {serializer.errors}')) - self.stdout.write(self.style.SUCCESS('Payment Plans initialized or updated successfully')) + def generate_due_dates(self, frequency, start_date): + if frequency == PaymentPlanType.ONE_TIME: + return [start_date + relativedelta(months=1)] + elif frequency == PaymentPlanType.THREE_TIMES: + return [start_date + relativedelta(months=1+4*i) for i in range(3)] + elif frequency == PaymentPlanType.TEN_TIMES: + return [start_date + relativedelta(months=1+i) for i in range(10)] + elif frequency == PaymentPlanType.TWELVE_TIMES: + return [start_date + relativedelta(months=1+i) for i in range(12)] - def create_or_update_specialities(self): + def init_specialities(self): specialities_data = self.load_data('specialities.json') for speciality_data in specialities_data: - Speciality.objects.update_or_create( - name=speciality_data["name"], - defaults=speciality_data - ) - self.stdout.write(self.style.SUCCESS('Specialities initialized or updated successfully')) - - def create_or_update_teachers(self): - teachers_data = self.load_data('teachers.json') - - for teacher_data in teachers_data: - specialities = teacher_data.pop("specialities") - email = teacher_data["email"] - droit = teacher_data.pop("droit") - establishment = random.choice(self.establishments) + speciality_data["name"] = f"{speciality_data['name']} - {establishment.name}" + speciality_data["establishment"] = establishment.id - # Create or update the user profile - user, created = Profile.objects.update_or_create( - email=email, - defaults={ - "username": email, - "email": email, - "is_active": True, - "password": "Provisoire01!", - "droit": droit, - "establishment": establishment + serializer = SpecialitySerializer(data=speciality_data) + if serializer.is_valid(): + speciality = serializer.save() + self.stdout.write(self.style.SUCCESS(f'Speciality {speciality.name} created successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for speciality: {serializer.errors}')) + + def init_teachers(self): + fake = Faker() + + # Récupérer tous les profils dont le role_type est soit ECOLE soit ADMIN + profiles = Profile.objects.filter(roles__role_type__in=[ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN]).distinct() + + for profile in profiles: + # Récupérer les rôles associés au profil + profile_roles = ProfileRole.objects.filter(profile=profile, role_type__in=[ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN]) + + for profile_role in profile_roles: + establishment = profile_role.establishment + teacher_data = { + "last_name": fake.last_name(), + "first_name": f"{fake.first_name()} - {establishment.name}", + "profile_role": profile_role.id } - ) - if created: - user.set_password("Provisoire01!") - user.save() - # Create or update the teacher - teacher, created = Teacher.objects.update_or_create( - email=email, - defaults={**teacher_data, "associated_profile_id": user.id} - ) - teacher.specialities.set(Speciality.objects.filter(name__in=specialities)) - teacher.save() + establishment_specialities = list(Speciality.objects.filter(establishment=establishment)) + num_specialities = min(random.randint(1, 3), len(establishment_specialities)) + selected_specialities = random.sample(establishment_specialities, num_specialities) + + # Créer l'enseignant si il n'existe pas + teacher_serializer = TeacherSerializer(data=teacher_data) + if teacher_serializer.is_valid(): + teacher = teacher_serializer.save() + # Associer les spécialités + teacher.specialities.set(selected_specialities) + teacher.save() + self.stdout.write(self.style.SUCCESS(f'Teacher {teacher.last_name} created successfully for establishment {establishment.name}')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for teacher: {teacher_serializer.errors}')) self.stdout.write(self.style.SUCCESS('Teachers initialized or updated successfully')) - - def create_or_update_school_classes(self): + + def init_guardians(self): + fake = Faker() + + # Récupérer tous les profils dont le role_type est PROFIL_PARENT + profiles = Profile.objects.filter(roles__role_type=ProfileRole.RoleType.PROFIL_PARENT).distinct() + + for profile in profiles: + # Récupérer les rôles associés au profil + profile_roles = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT) + + for profile_role in profile_roles: + establishment = profile_role.establishment + guardian_data = { + "last_name": fake.last_name(), + "first_name": f"{fake.first_name()} - {establishment.name}", + "profile_role": profile_role.id, + "birth_date": fake.date_of_birth().strftime('%Y-%m-%d'), # Convertir en chaîne de caractères valide + "address": fake.address(), + "phone": fake.phone_number(), + "profession": fake.job() + } + + # Créer le guardian si il n'existe pas + guardian_serializer = GuardianSerializer(data=guardian_data) + if guardian_serializer.is_valid(): + guardian = guardian_serializer.save() + self.stdout.write(self.style.SUCCESS(f'Guardian {guardian.last_name} created successfully for establishment {establishment.name}')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for guardian: {guardian_serializer.errors}')) + + self.stdout.write(self.style.SUCCESS('Guardians initialized or updated successfully')) + + def init_school_classes(self): school_classes_data = self.load_data('school_classes.json') for index, class_data in enumerate(school_classes_data, start=1): - teachers_ids = class_data.pop("teachers") + # Randomize establishment establishment = random.choice(self.establishments) class_data["atmosphere_name"] = f"Classe {index} - {establishment.name}" - class_data["establishment"] = establishment - school_class, created = SchoolClass.objects.update_or_create( - atmosphere_name=class_data["atmosphere_name"], - school_year=class_data["school_year"], - defaults=class_data - ) - school_class.teachers.set(teachers_ids) - school_class.save() + class_data["establishment"] = establishment.id + + # Randomize levels + class_data["levels"] = random.sample(range(1, 10), random.randint(1, 5)) + + # Randomize teachers + establishment_teachers = list(Teacher.objects.filter(profile_role__establishment=establishment)) + num_teachers = min(random.randint(1, 10), len(establishment_teachers)) + selected_teachers = random.sample(establishment_teachers, num_teachers) + teachers_ids = [teacher.id for teacher in selected_teachers] + + # Use the serializer to create or update the school class + class_data["teachers"] = teachers_ids + serializer = SchoolClassSerializer(data=class_data) + if serializer.is_valid(): + school_class = serializer.save() + self.stdout.write(self.style.SUCCESS(f'SchoolClass {school_class.atmosphere_name} created or updated successfully')) + else: + self.stdout.write(self.style.ERROR(f'Error in data for school class: {serializer.errors}')) self.stdout.write(self.style.SUCCESS('SchoolClasses initialized or updated successfully')) - def create_or_update_registration_file_group(self): - file_groups_data = self.load_data('file_groups.json') + def init_file_group(self): + fake = Faker() - for group_data in file_groups_data: - RegistrationFileGroup.objects.update_or_create(name=group_data["name"], defaults=group_data) - self.stdout.write(self.style.SUCCESS(f'RegistrationFileGroup {group_data["name"]} initialized or updated successfully')) + for establishment in self.establishments: + for i in range(1, 4): # Créer 3 groupes de fichiers par établissement + name = f"Fichiers d'inscription - {fake.word()} - {establishment.name}" + description = fake.sentence() + group_data = { + "name": name, + "description": description, + "establishment": establishment + } + RegistrationFileGroup.objects.update_or_create(name=name, defaults=group_data) + self.stdout.write(self.style.SUCCESS(f'RegistrationFileGroup {name} initialized or updated successfully')) - def create_register_form(self): + self.stdout.write(self.style.SUCCESS('All RegistrationFileGroups initialized or updated successfully')) + + def init_register_form(self): fake = Faker('fr_FR') # Utiliser le locale français pour Faker file_group_count = RegistrationFileGroup.objects.count() @@ -218,40 +358,13 @@ class Command(BaseCommand): for _ in range(50): establishment = random.choice(self.establishments) - # Générer des données fictives pour le profil - profile_data = { - "email": fake.email(), - "droit": 2, - "username": fake.user_name(), - "is_active": True, - "password": "Provisoire01!", - "establishment": establishment - } - - user, created = Profile.objects.update_or_create( - email=profile_data["email"], - defaults={ - "username": profile_data["username"], - "email": profile_data["email"], - "is_active": profile_data["is_active"], - "droit": profile_data["droit"], - "establishment": profile_data["establishment"] - } - ) - if created: - user.set_password(profile_data["password"]) - user.save() - - # Générer des données fictives pour le guardian - guardian_data = { - "associated_profile_id": user.id, - "email": profile_data["email"], - } + # Récupérer un guardian aléatoire déjà créé + guardian = Guardian.objects.order_by('?').first() # Générer des données fictives pour l'étudiant student_data = { - "last_name": f"{fake.last_name()} - {establishment.name}", - "first_name": fake.first_name(), + "last_name": fake.last_name(), + "first_name": f"{fake.first_name()} - {establishment.name}", "address": fake.address(), "birth_date": fake.date_of_birth(), "birth_place": fake.city(), @@ -261,16 +374,12 @@ class Command(BaseCommand): "level": fake.random_int(min=1, max=6) } - # Créer ou mettre à jour l'étudiant et le guardian + # Créer ou mettre à jour l'étudiant student, created = Student.objects.get_or_create( last_name=student_data["last_name"], first_name=student_data["first_name"], defaults=student_data ) - guardian, created = Guardian.objects.get_or_create( - last_name=guardian_data["email"], - defaults=guardian_data - ) student.guardians.add(guardian) # Récupérer les frais et les réductions @@ -279,18 +388,23 @@ class Command(BaseCommand): # Créer les données du formulaire d'inscription register_form_data = { - "student": student, "fileGroup": RegistrationFileGroup.objects.get(id=fake.random_int(min=1, max=file_group_count)), "establishment": establishment, "status": fake.random_int(min=1, max=3) } # Créer ou mettre à jour le formulaire d'inscription - register_form, created = RegistrationForm.objects.get_or_create(student=student, defaults=register_form_data) - register_form.fees.set(fees) - register_form.discounts.set(discounts) - if not created: - register_form.fileGroup = file_group - register_form.save() + register_form, created = RegistrationForm.objects.get_or_create( + student=student, + establishment=establishment, + defaults=register_form_data + ) + + if created: + register_form.fees.set(fees) + register_form.discounts.set(discounts) + self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} created successfully')) + else: + self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} already exists')) self.stdout.write(self.style.SUCCESS('50 RegistrationForms initialized or updated successfully')) \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/discounts.json b/Back-End/School/management/mock_datas/discounts.json index 9643c41..f498b92 100644 --- a/Back-End/School/management/mock_datas/discounts.json +++ b/Back-End/School/management/mock_datas/discounts.json @@ -2,15 +2,41 @@ { "name": "Parrainage", "amount": "10.00", - "description": "Réduction pour parrainage", - "discount_type": 1, - "type": 1 + "description": "Réduction pour parrainage" }, { "name": "Réinscription", "amount": "100.00", - "description": "Réduction pour Réinscription", - "discount_type": 1, - "type": 0 + "description": "Réduction pour Réinscription" + }, + { + "name": "Famille nombreuse", + "amount": "50.00", + "description": "Réduction pour les familles nombreuses" + }, + { + "name": "Excellence académique", + "amount": "200.00", + "description": "Réduction pour les élèves ayant des résultats académiques exceptionnels" + }, + { + "name": "Sportif de haut niveau", + "amount": "150.00", + "description": "Réduction pour les élèves pratiquant un sport de haut niveau" + }, + { + "name": "Artiste talentueux", + "amount": "100.00", + "description": "Réduction pour les élèves ayant des talents artistiques" + }, + { + "name": "Bourse d'études", + "amount": "300.00", + "description": "Réduction pour les élèves bénéficiant d'une bourse d'études" + }, + { + "name": "Réduction spéciale", + "amount": "75.00", + "description": "Réduction spéciale pour des occasions particulières" } ] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/fees.json b/Back-End/School/management/mock_datas/fees.json index b54affc..043afe2 100644 --- a/Back-End/School/management/mock_datas/fees.json +++ b/Back-End/School/management/mock_datas/fees.json @@ -3,35 +3,30 @@ "name": "Frais d'inscription", "base_amount": "150.00", "description": "Montant de base", - "is_active": true, - "type": 0 + "is_active": true }, { "name": "Matériel", "base_amount": "85.00", "description": "Livres / jouets", - "is_active": true, - "type": 0 + "is_active": true }, { "name": "Sorties périscolaires", "base_amount": "120.00", "description": "Sorties", - "is_active": true, - "type": 0 + "is_active": true }, { "name": "Les colibris", "base_amount": "4500.00", "description": "TPS / PS / MS / GS", - "is_active": true, - "type": 1 + "is_active": true }, { "name": "Les butterflies", "base_amount": "5000.00", "description": "CP / CE1 / CE2 / CM1 / CM2", - "is_active": true, - "type": 1 + "is_active": true } ] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/file_groups.json b/Back-End/School/management/mock_datas/file_groups.json deleted file mode 100644 index a188153..0000000 --- a/Back-End/School/management/mock_datas/file_groups.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "name": "Fichiers d'inscription - Classe 1 - Ecole A", - "description": "Fichiers d'inscription pour la Classe 1 de l'école Ecole A" - }, - { - "name": "Fichiers d'inscription - Classe 2 - Ecole B", - "description": "Fichiers d'inscription pour la Classe 2 de l'école Ecole B" - }, - { - "name": "Fichiers d'inscription - Classe 3 - Ecole C", - "description": "Fichiers d'inscription pour la Classe 3 de l'école Ecole C" - }, - { - "name": "Fichiers d'inscription - Classe 4 - Ecole A", - "description": "Fichiers d'inscription pour la Classe 4 de l'école Ecole A" - }, - { - "name": "Fichiers d'inscription - Classe 5 - Ecole B", - "description": "Fichiers d'inscription pour la Classe 5 de l'école Ecole B" - } -] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/payment_modes.json b/Back-End/School/management/mock_datas/payment_modes.json deleted file mode 100644 index fd79623..0000000 --- a/Back-End/School/management/mock_datas/payment_modes.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "mode": 4, - "type": 0, - "is_active": true - }, - { - "mode": 2, - "type": 1, - "is_active": true - } -] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/payment_plans.json b/Back-End/School/management/mock_datas/payment_plans.json deleted file mode 100644 index 96b5a5c..0000000 --- a/Back-End/School/management/mock_datas/payment_plans.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "frequency": 1, - "type": 0, - "is_active": true - }, - { - "frequency": 3, - "type": 1, - "is_active": true - }, - { - "frequency": 10, - "type": 1, - "is_active": true - }, - { - "frequency": 12, - "type": 1, - "is_active": true - } -] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/profiles.json b/Back-End/School/management/mock_datas/profiles.json new file mode 100644 index 0000000..f59adf8 --- /dev/null +++ b/Back-End/School/management/mock_datas/profiles.json @@ -0,0 +1,34 @@ +[ + { + "username": "albus.dumbledore", + "password": "Provisoire01!" + }, + { + "username": "severus.rogue", + "password": "Provisoire01!" + }, + { + "username": "minerva.mcgonagall", + "password": "Provisoire01!" + }, + { + "username": "pomona.chourave", + "password": "Provisoire01!" + }, + { + "username": "rubeus.hagrid", + "password": "Provisoire01!" + }, + { + "username": "filius.flitwick", + "password": "Provisoire01!" + }, + { + "username": "pomona.sprout", + "password": "Provisoire01!" + }, + { + "username": "aurora.sinistra", + "password": "Provisoire01!" + } +] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/school_classes.json b/Back-End/School/management/mock_datas/school_classes.json index eed817c..fe8b388 100644 --- a/Back-End/School/management/mock_datas/school_classes.json +++ b/Back-End/School/management/mock_datas/school_classes.json @@ -7,8 +7,7 @@ "levels": [2, 3, 4], "type": 1, "time_range": ["08:30", "17:30"], - "opening_days": [1, 2, 4, 5], - "teachers": [2] + "opening_days": [1, 2, 4, 5] }, { "age_range": "2-3", @@ -18,8 +17,7 @@ "levels": [1], "type": 1, "time_range": ["08:30", "17:30"], - "opening_days": [1, 2, 4, 5], - "teachers": [3] + "opening_days": [1, 2, 4, 5] }, { "age_range": "6-12", @@ -29,8 +27,7 @@ "levels": [5, 6, 7, 8, 9], "type": 1, "time_range": ["08:30", "17:30"], - "opening_days": [1, 2, 4, 5], - "teachers": [4] + "opening_days": [1, 2, 4, 5] }, { "age_range": "4-6", @@ -40,8 +37,7 @@ "levels": [4, 5], "type": 1, "time_range": ["08:30", "17:30"], - "opening_days": [1, 2, 4, 5], - "teachers": [1] + "opening_days": [1, 2, 4, 5] }, { "age_range": "7-9", @@ -51,7 +47,6 @@ "levels": [6, 7], "type": 1, "time_range": ["08:30", "17:30"], - "opening_days": [1, 2, 4, 5], - "teachers": [2] + "opening_days": [1, 2, 4, 5] } ] \ No newline at end of file diff --git a/Back-End/School/management/mock_datas/teachers.json b/Back-End/School/management/mock_datas/teachers.json deleted file mode 100644 index 18ceadc..0000000 --- a/Back-End/School/management/mock_datas/teachers.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "last_name": "DUMBLEDORE", - "first_name": "Albus", - "email": "albus.dumbledore@gmail.com", - "specialities": ["GROUPE"], - "droit": 1 - }, - { - "last_name": "ROGUE", - "first_name": "Severus", - "email": "severus.rogue@gmail.com", - "specialities": ["ANGLAIS"], - "droit": 2 - }, - { - "last_name": "MC GONAGALL", - "first_name": "Minerva", - "email": "minerva.mcgonagall@gmail.com", - "specialities": ["MATHS", "HISTOIRE"], - "droit": 2 - }, - { - "last_name": "CHOURAVE", - "first_name": "Pomona", - "email": "pomona.chourave@gmail.com", - "specialities": ["MATHS", "FRANCAIS", "SPORT"], - "droit": 1 - }, - { - "last_name": "HAGRID", - "first_name": "Rubeus", - "email": "rubeus.hagrid@gmail.com", - "specialities": ["SCIENCES"], - "droit": 2 - }, - { - "last_name": "FLITWICK", - "first_name": "Filius", - "email": "filius.flitwick@gmail.com", - "specialities": ["MUSIQUE"], - "droit": 1 - }, - { - "last_name": "SPROUT", - "first_name": "Pomona", - "email": "pomona.sprout@gmail.com", - "specialities": ["ART"], - "droit": 2 - }, - { - "last_name": "SINISTRA", - "first_name": "Aurora", - "email": "aurora.sinistra@gmail.com", - "specialities": ["INFORMATIQUE"], - "droit": 2 - } -] \ No newline at end of file diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 9615db9..71d6078 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -1,5 +1,6 @@ from django.db import models -from Auth.models import Profile +from Auth.models import ProfileRole +from Establishment.models import Establishment from django.db.models import JSONField from django.dispatch import receiver from django.contrib.postgres.fields import ArrayField @@ -18,27 +19,11 @@ LEVEL_CHOICES = [ (9, 'Cours Moyen 2 (CM2)') ] -class StructureType(models.IntegerChoices): - MATERNELLE = 1, _('Maternelle') - PRIMAIRE = 2, _('Primaire') - SECONDAIRE = 3, _('Secondaire') - -class Establishment(models.Model): - name = models.CharField(max_length=255, unique=True) - address = models.CharField(max_length=255) - total_capacity = models.IntegerField() - establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices)) - licence_code = models.CharField(max_length=100, blank=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.name - class Speciality(models.Model): name = models.CharField(max_length=100) updated_date = models.DateTimeField(auto_now=True) color_code = models.CharField(max_length=7, default='#FFFFFF') + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='specialities') def __str__(self): return self.name @@ -46,9 +31,8 @@ class Speciality(models.Model): class Teacher(models.Model): last_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100) - email = models.EmailField(unique=True) specialities = models.ManyToManyField(Speciality, blank=True) - associated_profile = models.ForeignKey(Profile, on_delete=models.CASCADE, null=True, blank=True) + profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True) updated_date = models.DateTimeField(auto_now=True) def __str__(self): diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index 9d44e8a..5e17371 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,7 +1,8 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode, Establishment -from Auth.models import Profile +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode +from Auth.models import Profile, ProfileRole from Subscriptions.models import Student +from Establishment.models import Establishment from N3wtSchool import settings, bdd from django.utils import timezone import pytz @@ -30,9 +31,10 @@ class TeacherDetailSerializer(serializers.ModelSerializer): class TeacherSerializer(serializers.ModelSerializer): specialities = serializers.PrimaryKeyRelatedField(queryset=Speciality.objects.all(), many=True, required=False) specialities_details = serializers.SerializerMethodField() - associated_profile = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), required=True) + profile_role = serializers.PrimaryKeyRelatedField(queryset=ProfileRole.objects.all(), required=True) updated_date_formatted = serializers.SerializerMethodField() - droit = serializers.SerializerMethodField() + role_type = serializers.SerializerMethodField() + associated_profile_email = serializers.SerializerMethodField() class Meta: model = Teacher @@ -40,12 +42,12 @@ class TeacherSerializer(serializers.ModelSerializer): def create(self, validated_data): specialities_data = validated_data.pop('specialities', None) - associated_profile = validated_data.pop('associated_profile', None) + profile_role = validated_data.pop('profile_role', None) teacher = Teacher.objects.create(**validated_data) if specialities_data: teacher.specialities.set(specialities_data) - if associated_profile: - teacher.associated_profile = associated_profile + if profile_role: + teacher.profile_role = profile_role teacher.save() return teacher @@ -54,7 +56,7 @@ class TeacherSerializer(serializers.ModelSerializer): instance.last_name = validated_data.get('last_name', instance.last_name) instance.first_name = validated_data.get('first_name', instance.first_name) instance.email = validated_data.get('email', instance.email) - instance.associated_profile = validated_data.get('associated_profile', instance.associated_profile) + instance.profile_role = validated_data.get('profile_role', instance.profile_role) instance.save() if specialities_data: instance.specialities.set(specialities_data) @@ -64,17 +66,18 @@ class TeacherSerializer(serializers.ModelSerializer): utc_time = timezone.localtime(obj.updated_date) # Convert to local time local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") - def get_droit(self, obj): - if obj.associated_profile: - return obj.associated_profile.droit - return None + def get_role_type(self, obj): + profile_role = obj.profile_role + return {'role_type': profile_role.role_type, 'establishment': profile_role.establishment.name} def get_specialities_details(self, obj): return [{'id': speciality.id, 'name': speciality.name, 'color_code': speciality.color_code} for speciality in obj.specialities.all()] + def get_associated_profile_email(self, obj): + return obj.profile_role.profile.email + class PlanningSerializer(serializers.ModelSerializer): class Meta: model = Planning @@ -207,9 +210,4 @@ class PaymentPlanSerializer(serializers.ModelSerializer): class PaymentModeSerializer(serializers.ModelSerializer): class Meta: model = PaymentMode - fields = '__all__' - -class EstablishmentSerializer(serializers.ModelSerializer): - class Meta: - model = Establishment fields = '__all__' \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index dbf34cd..8505bfc 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -16,9 +16,7 @@ from .views import ( PaymentPlanListCreateView, PaymentPlanDetailView, PaymentModeListCreateView, - PaymentModeDetailView, - EstablishmentListCreateView, - EstablishmentDetailView + PaymentModeDetailView ) urlpatterns = [ @@ -44,8 +42,5 @@ urlpatterns = [ re_path(r'^paymentPlans/(?P[0-9]+)$', PaymentPlanDetailView.as_view(), name="payment_plan_detail"), re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"), - re_path(r'^paymentModes/(?P[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"), - - re_path(r'^establishments$', EstablishmentListCreateView.as_view(), name='establishment_list_create'), - re_path(r'^establishments/(?P[0-9]+)$', EstablishmentDetailView.as_view(), name="establishment_detail"), + re_path(r'^paymentModes/(?P[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail") ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index f2e00a2..f211640 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -12,8 +12,7 @@ from .models import ( Discount, Fee, PaymentPlan, - PaymentMode, - Establishment + PaymentMode ) from .serializers import ( TeacherSerializer, @@ -23,8 +22,7 @@ from .serializers import ( DiscountSerializer, FeeSerializer, PaymentPlanSerializer, - PaymentModeSerializer, - EstablishmentSerializer + PaymentModeSerializer ) from N3wtSchool.bdd import delete_object, getAllObjects, getObject @@ -32,8 +30,14 @@ from N3wtSchool.bdd import delete_object, getAllObjects, getObject @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialityListCreateView(APIView): def get(self, request): - specialitiesList = getAllObjects(Speciality) - specialities_serializer = SpecialitySerializer(specialitiesList, many=True) + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + + specialities_list = getAllObjects(Speciality) + if establishment_id: + specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct() + specialities_serializer = SpecialitySerializer(specialities_list, many=True) return JsonResponse(specialities_serializer.data, safe=False) def post(self, request): @@ -51,7 +55,7 @@ class SpecialityListCreateView(APIView): class SpecialityDetailView(APIView): def get(self, request, id): speciality = getObject(_objectName=Speciality, _columnName='id', _value=id) - speciality_serializer = SpecialitySerializer(speciality) + speciality_serializer=SpecialitySerializer(speciality) return JsonResponse(speciality_serializer.data, safe=False) def put(self, request, id): @@ -71,9 +75,14 @@ class SpecialityDetailView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class TeacherListCreateView(APIView): def get(self, request): - teachersList=getAllObjects(Teacher) - teachers_serializer=TeacherSerializer(teachersList, many=True) + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + teachers_list = getAllObjects(Teacher) + if teachers_list: + teachers_list = teachers_list.filter(profile_role__establishment_id=establishment_id) + teachers_serializer = TeacherSerializer(teachers_list, many=True) return JsonResponse(teachers_serializer.data, safe=False) def post(self, request): @@ -107,14 +116,20 @@ class TeacherDetailView(APIView): return JsonResponse(teacher_serializer.errors, safe=False) def delete(self, request, id): - return delete_object(Teacher, id, related_field='associated_profile') + return delete_object(Teacher, id, related_field='profile') @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SchoolClassListCreateView(APIView): - def get(self, request): - classesList=getAllObjects(SchoolClass) - classes_serializer=SchoolClassSerializer(classesList, many=True) + def get(self, request): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + + school_classes_list = getAllObjects(SchoolClass) + if school_classes_list: + school_classes_list = school_classes_list.filter(establishment=establishment_id).distinct() + classes_serializer = SchoolClassSerializer(school_classes_list, many=True) return JsonResponse(classes_serializer.data, safe=False) def post(self, request): @@ -201,9 +216,14 @@ class PlanningDetailView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class FeeListCreateView(APIView): def get(self, request, *args, **kwargs): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + filter = request.GET.get('filter', '').strip() fee_type_value = 0 if filter == 'registration' else 1 - fees = Fee.objects.filter(type=fee_type_value) + + fees = Fee.objects.filter(type=fee_type_value, establishment_id=establishment_id).distinct() fee_serializer = FeeSerializer(fees, many=True) return JsonResponse(fee_serializer.data, safe=False, status=status.HTTP_200_OK) @@ -244,11 +264,16 @@ class FeeDetailView(APIView): @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') -class DiscountListCreateView(APIView): +class DiscountListCreateView(APIView): def get(self, request, *args, **kwargs): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + filter = request.GET.get('filter', '').strip() discount_type_value = 0 if filter == 'registration' else 1 - discounts = Discount.objects.filter(type=discount_type_value) + + discounts = Discount.objects.filter(type=discount_type_value, establishment_id=establishment_id).distinct() discounts_serializer = DiscountSerializer(discounts, many=True) return JsonResponse(discounts_serializer.data, safe=False, status=status.HTTP_200_OK) @@ -291,10 +316,15 @@ class DiscountDetailView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class PaymentPlanListCreateView(APIView): def get(self, request, *args, **kwargs): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + filter = request.GET.get('filter', '').strip() type_value = 0 if filter == 'registration' else 1 - paymentPlans = PaymentPlan.objects.filter(type=type_value) - payment_plans_serializer = PaymentPlanSerializer(paymentPlans, many=True) + + payment_plans = PaymentPlan.objects.filter(type=type_value, establishment_id=establishment_id).distinct() + payment_plans_serializer = PaymentPlanSerializer(payment_plans, many=True) return JsonResponse(payment_plans_serializer.data, safe=False, status=status.HTTP_200_OK) @@ -336,10 +366,15 @@ class PaymentPlanDetailView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class PaymentModeListCreateView(APIView): def get(self, request): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + filter = request.GET.get('filter', '').strip() type_value = 0 if filter == 'registration' else 1 - paymentModes = PaymentMode.objects.filter(type=type_value) - payment_modes_serializer = PaymentModeSerializer(paymentModes, many=True) + + payment_modes = PaymentMode.objects.filter(type=type_value, establishment_id=establishment_id).distinct() + payment_modes_serializer = PaymentModeSerializer(payment_modes, many=True) return JsonResponse(payment_modes_serializer.data, safe=False, status=status.HTTP_200_OK) @@ -376,45 +411,3 @@ class PaymentModeDetailView(APIView): def delete(self, request, id): return delete_object(PaymentMode, id) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class EstablishmentListCreateView(APIView): - def get(self, request): - establishments=getAllObjects(Establishment) - establishments_serializer=EstablishmentSerializer(establishments, many=True) - return JsonResponse(establishments_serializer.data, safe=False, status=status.HTTP_200_OK) - - def post(self, request): - establishment_data = JSONParser().parse(request) - establishment_serializer = EstablishmentSerializer(data=establishment_data) - if establishment_serializer.is_valid(): - establishment_serializer.save() - return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_201_CREATED) - return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class EstablishmentDetailView(APIView): - def get(self, request, id=None): - try: - establishment = Establishment.objects.get(id=id) - establishment_serializer = EstablishmentSerializer(establishment) - return JsonResponse(establishment_serializer.data, safe=False) - except Establishment.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) - - def put(self, request, id): - establishment_data = JSONParser().parse(request) - try: - establishment = Establishment.objects.get(id=id) - except Establishment.DoesNotExist: - return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) - establishment_serializer = EstablishmentSerializer(establishment, data=establishment_data, partial=True) - if establishment_serializer.is_valid(): - establishment_serializer.save() - return JsonResponse(establishment_serializer.data, safe=False) - return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, id): - return delete_object(Establishment, id) \ No newline at end of file diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index d9fdc60..4f70800 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -4,6 +4,8 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from School.models import SchoolClass, Fee, Discount +from Auth.models import ProfileRole +from Establishment.models import Establishment from datetime import datetime @@ -25,10 +27,9 @@ class Guardian(models.Model): first_name = models.CharField(max_length=200, default="") birth_date = models.CharField(max_length=200, default="", blank=True) address = models.CharField(max_length=200, default="", blank=True) - email = models.CharField(max_length=200, default="", blank=True) phone = models.CharField(max_length=200, default="", blank=True) profession = models.CharField(max_length=200, default="", blank=True) - associated_profile = models.ForeignKey('Auth.Profile', on_delete=models.CASCADE) + profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', null=True, blank=True) def __str__(self): return self.last_name + "_" + self.first_name @@ -163,6 +164,7 @@ class Student(models.Model): class RegistrationFileGroup(models.Model): name = models.CharField(max_length=255, default="") description = models.TextField(blank=True, null=True) + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='file_group', null=True, blank=True) def __str__(self): return self.name @@ -214,7 +216,7 @@ class RegistrationForm(models.Model): null=True, blank=True) - establishment = models.ForeignKey('School.Establishment', on_delete=models.CASCADE, related_name='register_forms') + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE, related_name='register_forms') def __str__(self): return "RF_" + self.student.last_name + "_" + self.student.first_name diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index bec6584..bcd60a7 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationTemplateMaster, RegistrationTemplate from School.models import SchoolClass, Fee, Discount, FeeType from School.serializers import FeeSerializer, DiscountSerializer -from Auth.models import Profile +from Auth.models import ProfileRole from Auth.serializers import ProfileSerializer from GestionMessagerie.models import Messagerie from GestionNotification.models import Notification @@ -68,7 +68,7 @@ class SiblingSerializer(serializers.ModelSerializer): class GuardianSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) - associated_profile = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), required=True) + profile_role = serializers.PrimaryKeyRelatedField(queryset=ProfileRole.objects.all(), required=True) associated_profile_email = serializers.SerializerMethodField() class Meta: @@ -76,7 +76,7 @@ class GuardianSerializer(serializers.ModelSerializer): fields = '__all__' def get_associated_profile_email(self, obj): - return obj.associated_profile.email + return obj.profile_role.profile.email class StudentSerializer(serializers.ModelSerializer): @@ -248,7 +248,7 @@ class GuardianByDICreationSerializer(serializers.ModelSerializer): class Meta: model = Guardian - fields = ['id', 'last_name', 'first_name', 'email', 'associated_profile'] + fields = ['id', 'last_name', 'first_name', 'email', 'profile'] class StudentByRFCreationSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py index d20c671..02cfed4 100644 --- a/Back-End/Subscriptions/views/student_views.py +++ b/Back-End/Subscriptions/views/student_views.py @@ -90,6 +90,12 @@ class ChildrenListView(APIView): # Récupération des élèves d'un parent # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves def get(self, request, id): - students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=id) + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + + students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__profile_role__profile__id', _value=id) + if students: + students = students.filter(establishment=establishment_id).distinct() students_serializer = RegistrationFormByParentSerializer(students, many=True) return JsonResponse(students_serializer.data, safe=False) diff --git a/Back-End/start.py b/Back-End/start.py index c11e339..4e08847 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -15,6 +15,7 @@ test_mode = os.getenv('TEST_MODE', 'False') == 'True' commands = [ ["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "flush", "--noinput"], + ["python", "manage.py", "makemigrations", "Establishment", "--noinput"], ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"], ["python", "manage.py", "makemigrations", "Planning", "--noinput"], ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"], diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index 5539218..0764980 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import Sidebar from '@/components/Sidebar'; import { usePathname } from 'next/navigation'; -import {useTranslations} from 'next-intl'; +import { useTranslations } from 'next-intl'; import Image from 'next/image'; import { Users, @@ -35,6 +35,9 @@ import ProtectedRoute from '@/components/ProtectedRoute'; import { getGravatarUrl } from '@/utils/gravatar'; import Footer from '@/components/Footer'; import { getRightStr, RIGHTS } from '@/utils/rights'; +import { getSession } from 'next-auth/react'; +import logger from '@/utils/logger'; +import { useEstablishment } from '@/context/EstablishmentContext'; export default function Layout({ children, @@ -51,11 +54,12 @@ export default function Layout({ "settings": { "id": "settings", "name": t('settings'), "url": FE_ADMIN_SETTINGS_URL, "icon": Settings } }; - const [establishment, setEstablishment] = useState(null); + const [establishments, setEstablishments] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false); const [user, setUser] = useState(null); const { data: session } = useSession(); + const { selectedEstablishmentId, setSelectedEstablishmentId, profileRole, setProfileRole } = useEstablishment(); const pathname = usePathname(); const currentPage = pathname.split('/').pop(); @@ -80,7 +84,7 @@ export default function Layout({ content: (
{user?.email || 'Utilisateur'}
-
{getRightStr(user?.role) || ''}
+
{getRightStr(profileRole) || ''}
) }, @@ -106,17 +110,34 @@ export default function Layout({ }, [pathname]); useEffect(() => { - setIsLoading(true); - fetchEstablishment() - .then(data => { - setEstablishment(data); + getSession() + .then(session => { + if (session && session.user) { + setUser(session.user); + setEstablishments(session.user.roles.map(role => ({ + id: role.establishment__id, + name: role.establishment__name, + role_type: role.role_type + }))); + // Sélectionner l'établissement depuis le localStorage ou le premier établissement par défaut + const storedEstablishmentId = localStorage.getItem('selectedEstablishmentId'); + if (storedEstablishmentId) { + setSelectedEstablishmentId(storedEstablishmentId); + const storedProfileRole = session.user.roles.find(role => role.establishment__id === parseInt(storedEstablishmentId))?.role_type; + setProfileRole(storedProfileRole); + } else if (session.user.roles.length > 0) { + setSelectedEstablishmentId(session.user.roles[0].establishment__id); + setProfileRole(session.user.roles[0].role_type); + } + } }) - .catch(error => console.error('Error fetching establishment : ', error)) - .finally(() => setIsLoading(false)); + .catch(err => { + logger.error('Error fetching session:', err); + }); }, []); useEffect(() => { - const fetchUser = async () => { + const fetchUser = async () => { if (session) { // Vérifier que la session existe const userData = await getUser(); setUser(userData); @@ -126,8 +147,6 @@ export default function Layout({ fetchUser(); }, [session]); - - return ( {!isLoading && ( @@ -138,12 +157,16 @@ export default function Layout({ style={{ height: '100vh' }} // Force la hauteur à 100% de la hauteur de la vue > { + const parsedEstablishmentId = parseInt(establishmentId, 10); + setSelectedEstablishmentId(parsedEstablishmentId); + const role = session.user.roles.find(role => role.establishment__id === parsedEstablishmentId)?.role_type; + setProfileRole(role); + }} /> diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 331e7ba..aa08d38 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -39,47 +39,61 @@ export default function DashboardPage() { const [classes, setClasses] = useState([]); - + const [establishmentId, setEstablishmentId] = useState(null); useEffect(() => { - // Fetch data for classes - fetchClasses().then(data => { - setClasses(data); - logger.info('Classes fetched:', data); - const nbMaxStudents = data.reduce((acc, classe) => acc + classe.number_of_students, 0); - const nbStudents = data.reduce((acc, classe) => acc + classe.students.length, 0); - setStructureCapacity(nbMaxStudents); - setTotalStudents(nbStudents); - + getSession() + .then(session => { + if (session && session.user) { + setEstablishmentId(session.user.establishment); + } }) - .catch(error => { - logger.error('Error fetching classes:', error); + .catch(err => { + logger.error('Error fetching session:', err); }); + }, []); - fetchRegisterForms().then(data => { - logger.info('Pending registrations fetched:', data); - setPendingRegistration(data.count); - }) - .catch(error => { - logger.error('Error fetching pending registrations:', error); - }); + useEffect(() => { + if (establishmentId) { + // Fetch data for classes + fetchClasses(establishmentId).then(data => { + setClasses(data); + logger.info('Classes fetched:', data); + const nbMaxStudents = data.reduce((acc, classe) => acc + classe.number_of_students, 0); + const nbStudents = data.reduce((acc, classe) => acc + classe.students.length, 0); + setStructureCapacity(nbMaxStudents); + setTotalStudents(nbStudents); - fetchUpcomingEvents().then(data => { - setUpcomingEvents(data); - }).catch(error => { - logger.error('Error fetching upcoming events:', error); - }); + }) + .catch(error => { + logger.error('Error fetching classes:', error); + }); - // Simulation de chargement des données - setTimeout(() => { - setMonthlyStats({ - inscriptions: [150, 180, 210, 245], - completionRate: 78 - }); - setIsLoading(false); - }, 1000); - } - , []); + fetchRegisterForms().then(data => { + logger.info('Pending registrations fetched:', data); + setPendingRegistration(data.count); + }) + .catch(error => { + logger.error('Error fetching pending registrations:', error); + }); + + fetchUpcomingEvents().then(data => { + setUpcomingEvents(data); + }).catch(error => { + logger.error('Error fetching upcoming events:', error); + }); + + // Simulation de chargement des données + setTimeout(() => { + setMonthlyStats({ + inscriptions: [150, 180, 210, 245], + completionRate: 78 + }); + setIsLoading(false); + }, 1000); + } + } + , [establishmentId]); if (isLoading) return ; diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index ac4f08a..5b2d25a 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -27,13 +27,13 @@ import { fetchRegistrationTemplateMaster } from "@/app/actions/registerFileGroupAction"; import logger from '@/utils/logger'; - +import { useEstablishment } from '@/context/EstablishmentContext'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); - const [schedules, setSchedules] = useState([]); // Add this line + const [schedules, setSchedules] = useState([]); const [registrationDiscounts, setRegistrationDiscounts] = useState([]); const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); @@ -45,54 +45,57 @@ export default function Page() { const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]); const csrfToken = useCsrfToken(); + const { selectedEstablishmentId } = useEstablishment(); useEffect(() => { - // Fetch data for specialities - handleSpecialities(); + if (selectedEstablishmentId) { + // Fetch data for specialities + handleSpecialities(); - // Fetch data for teachers - handleTeachers(); + // Fetch data for teachers + handleTeachers(); - // Fetch data for classes - handleClasses(); + // Fetch data for classes + handleClasses(); - // Fetch data for schedules - handleSchedules(); + // Fetch data for schedules + handleSchedules(); - // Fetch data for registration discounts - handleRegistrationDiscounts(); + // Fetch data for registration discounts + handleRegistrationDiscounts(); - // Fetch data for tuition discounts - handleTuitionDiscounts(); + // Fetch data for tuition discounts + handleTuitionDiscounts(); - // Fetch data for registration fees - handleRegistrationFees(); + // Fetch data for registration fees + handleRegistrationFees(); - // Fetch data for tuition fees - handleTuitionFees(); + // Fetch data for tuition fees + handleTuitionFees(); - // Fetch data for registration file templates - fetchRegistrationTemplateMaster() - .then((data)=> { - setFichiers(data) - }) - .catch(error => logger.error('Error fetching files:', error)); + // Fetch data for registration file templates + fetchRegistrationTemplateMaster() + .then((data)=> { + setFichiers(data) + }) + .catch(error => logger.error('Error fetching files:', error)); - // Fetch data for registration payment plans - handleRegistrationPaymentPlans(); + // Fetch data for registration payment plans + handleRegistrationPaymentPlans(); - // Fetch data for tuition payment plans - handleTuitionPaymentPlans(); + // Fetch data for tuition payment plans + handleTuitionPaymentPlans(); - // Fetch data for registration payment modes - handleRegistrationPaymentModes(); + // Fetch data for registration payment modes + handleRegistrationPaymentModes(); - // Fetch data for tuition payment modes - handleTuitionPaymentModes(); - }, []); + // Fetch data for tuition payment modes + handleTuitionPaymentModes(); + } + }, [selectedEstablishmentId]); const handleSpecialities = () => { - fetchSpecialities() + fetchSpecialities(selectedEstablishmentId) .then(data => { setSpecialities(data); }) @@ -100,7 +103,7 @@ export default function Page() { }; const handleTeachers = () => { - fetchTeachers() + fetchTeachers(selectedEstablishmentId) .then(data => { setTeachers(data); }) @@ -108,7 +111,7 @@ export default function Page() { }; const handleClasses = () => { - fetchClasses() + fetchClasses(selectedEstablishmentId) .then(data => { setClasses(data); }) @@ -124,7 +127,7 @@ export default function Page() { }; const handleRegistrationDiscounts = () => { - fetchRegistrationDiscounts() + fetchRegistrationDiscounts(selectedEstablishmentId) .then(data => { setRegistrationDiscounts(data); }) @@ -132,7 +135,7 @@ export default function Page() { }; const handleTuitionDiscounts = () => { - fetchTuitionDiscounts() + fetchTuitionDiscounts(selectedEstablishmentId) .then(data => { setTuitionDiscounts(data); }) @@ -140,7 +143,7 @@ export default function Page() { }; const handleRegistrationFees = () => { - fetchRegistrationFees() + fetchRegistrationFees(selectedEstablishmentId) .then(data => { setRegistrationFees(data); }) @@ -148,7 +151,7 @@ export default function Page() { }; const handleTuitionFees = () => { - fetchTuitionFees() + fetchTuitionFees(selectedEstablishmentId) .then(data => { setTuitionFees(data); }) @@ -156,7 +159,7 @@ export default function Page() { }; const handleRegistrationPaymentPlans = () => { - fetchRegistrationPaymentPlans() + fetchRegistrationPaymentPlans(selectedEstablishmentId) .then(data => { setRegistrationPaymentPlans(data); }) @@ -164,7 +167,7 @@ export default function Page() { }; const handleTuitionPaymentPlans = () => { - fetchTuitionPaymentPlans() + fetchTuitionPaymentPlans(selectedEstablishmentId) .then(data => { setTuitionPaymentPlans(data); }) @@ -172,7 +175,7 @@ export default function Page() { }; const handleRegistrationPaymentModes = () => { - fetchRegistrationPaymentModes() + fetchRegistrationPaymentModes(selectedEstablishmentId) .then(data => { setRegistrationPaymentModes(data); }) @@ -180,7 +183,7 @@ export default function Page() { }; const handleTuitionPaymentModes = () => { - fetchTuitionPaymentModes() + fetchTuitionPaymentModes(selectedEstablishmentId) .then(data => { setTuitionPaymentModes(data); }) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index f87377b..2a1d4e1 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -16,6 +16,7 @@ import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' import { getSession } from 'next-auth/react'; +import { useEstablishment } from '@/context/EstablishmentContext'; import { PENDING, @@ -50,7 +51,6 @@ import { import DjangoCSRFToken from '@/components/DjangoCSRFToken' import { useCsrfToken } from '@/context/CsrfContext'; -import { ESTABLISHMENT_ID } from '@/utils/Url'; import logger from '@/utils/logger'; export default function Page({ params: { locale } }) { @@ -87,9 +87,8 @@ export default function Page({ params: { locale } }) { const [tuitionFees, setTuitionFees] = useState([]); const [groups, setGroups] = useState([]); - const [establishmentId, setEstablishmentId] = useState(null); - const csrfToken = useCsrfToken(); + const { selectedEstablishmentId } = useEstablishment(); const openModal = () => { setIsOpen(true); @@ -167,23 +166,11 @@ const registerFormArchivedDataHandler = (data) => { } useEffect(() => { - getSession() - .then(session => { - if (session && session.user) { - setEstablishmentId(session.user.establishment); - } - }) - .catch(err => { - logger.error('Error fetching session:', err); - }); -}, []); - -useEffect(() => { - if (establishmentId) { + if (selectedEstablishmentId) { const fetchInitialData = () => { Promise.all([ - fetchClasses(), - fetchStudents(establishmentId) // Utiliser l'ID de l'établissement ici + fetchClasses(selectedEstablishmentId), + fetchStudents(selectedEstablishmentId) // Utiliser l'ID de l'établissement ici ]) .then(([classesData, studentsData]) => { setClasses(classesData); @@ -198,21 +185,21 @@ useEffect(() => { fetchInitialData(); } -}, [establishmentId]); +}, [selectedEstablishmentId]); useEffect(() => { - if (establishmentId) { + if (selectedEstablishmentId) { const fetchDataAndSetState = () => { setIsLoading(true); Promise.all([ - fetchRegisterForms(establishmentId, PENDING, currentPage, itemsPerPage, searchTerm) + fetchRegisterForms(selectedEstablishmentId, PENDING, currentPage, itemsPerPage, searchTerm) .then(registerFormPendingDataHandler) .catch(requestErrorHandler), - fetchRegisterForms(establishmentId, SUBSCRIBED) + fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED) .then(registerFormSubscribedDataHandler) .catch(requestErrorHandler), - fetchRegisterForms(establishmentId, ARCHIVED) + fetchRegisterForms(selectedEstablishmentId, ARCHIVED) .then(registerFormArchivedDataHandler) .catch(requestErrorHandler), fetchRegistrationTemplateMaster() @@ -222,27 +209,27 @@ useEffect(() => { .catch(err => { logger.debug(err.message); }), - fetchRegistrationDiscounts() + fetchRegistrationDiscounts(selectedEstablishmentId) .then(data => { setRegistrationDiscounts(data); }) .catch(requestErrorHandler), - fetchTuitionDiscounts() + fetchTuitionDiscounts(selectedEstablishmentId) .then(data => { setTuitionDiscounts(data); }) .catch(requestErrorHandler), - fetchRegistrationFees() + fetchRegistrationFees(selectedEstablishmentId) .then(data => { setRegistrationFees(data); }) .catch(requestErrorHandler), - fetchTuitionFees() + fetchTuitionFees(selectedEstablishmentId) .then(data => { setTuitionFees(data); }) .catch(requestErrorHandler), - fetchRegistrationFileGroups() + fetchRegistrationFileGroups(selectedEstablishmentId) .then(data => { setGroups(data); }) @@ -263,20 +250,20 @@ useEffect(() => { fetchDataAndSetState(); } -}, [establishmentId, reloadFetch, currentPage, searchTerm]); +}, [selectedEstablishmentId, reloadFetch, currentPage, searchTerm]); useEffect(() => { - if (establishmentId) { + if (selectedEstablishmentId) { const fetchDataAndSetState = () => { setIsLoading(true); - fetchRegisterForms(establishmentId, PENDING, currentPage, itemsPerPage, searchTerm) + fetchRegisterForms(selectedEstablishmentId, PENDING, currentPage, itemsPerPage, searchTerm) .then(registerFormPendingDataHandler) .catch(requestErrorHandler) - fetchRegisterForms(establishmentId, SUBSCRIBED) + fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED) .then(registerFormSubscribedDataHandler) .catch(requestErrorHandler) - fetchRegisterForms(establishmentId, ARCHIVED) + fetchRegisterForms(selectedEstablishmentId, ARCHIVED) .then(registerFormArchivedDataHandler) .catch(requestErrorHandler) fetchRegistrationTemplateMaster() @@ -545,7 +532,7 @@ useEffect(()=>{ const columns = [ { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, - { name: t('mainContactMail'), transform: (row) => row.student.guardians[0].email }, + { name: t('mainContactMail'), transform: (row) => row.student.guardians[0].associated_profile_email }, { name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0].phone) }, { name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update}, { name: t('registrationFileStatus'), transform: (row) => ( diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index c982afe..ddf78c3 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -37,8 +37,9 @@ export default function Layout({ setIsPopupVisible(false); disconnect(); }; - useEffect(() => { - const fetchUser = async () => { + + useEffect(() => { + const fetchUser = async () => { if (session) { // Vérifier que la session existe const userData = await getUser(); setUser(userData); @@ -47,6 +48,7 @@ export default function Layout({ fetchUser(); }, [session]); + // useEffect(() => { // if (status === 'loading') return; @@ -81,7 +83,7 @@ const dropdownItems = [ content: (
{user?.email || 'Utilisateur'}
-
{getRightStr(user?.role) || ''}
+
{getRightStr(user?.roles[0]?.role_type) || ''}
) }, diff --git a/Front-End/src/app/[locale]/parents/page.js b/Front-End/src/app/[locale]/parents/page.js index ca17147..b0f3490 100644 --- a/Front-End/src/app/[locale]/parents/page.js +++ b/Front-End/src/app/[locale]/parents/page.js @@ -9,13 +9,15 @@ import { fetchChildren } from '@/app/actions/subscriptionAction'; import logger from '@/utils/logger'; import { useSession } from 'next-auth/react'; import { FE_USERS_LOGIN_URL } from '@/utils/Url'; +import { useEstablishment } from '@/context/EstablishmentContext'; export default function ParentHomePage() { - const [children, setChildren] = useState([]); const { data: session, status } = useSession(); const [userId, setUserId] = useState(null); const [currentPage, setCurrentPage] = useState(1); + const [establishments, setEstablishments] = useState([]); + const { selectedEstablishmentId, setSelectedEstablishmentId } = useEstablishment(); const router = useRouter(); @@ -24,21 +26,42 @@ export default function ParentHomePage() { if (!session) { router.push(`${FE_USERS_LOGIN_URL}`); - } - console.log(session); - const userIdFromSession = session.user.user_id; - setUserId(userIdFromSession); + } else { + const userIdFromSession = session.user.user_id; + setUserId(userIdFromSession); - fetchChildren(userIdFromSession).then(data => { - setChildren(data); - }); - }, [userId]); + const userEstablishments = session.user.roles.map(role => ({ + id: role.establishment__id, + name: role.establishment__name, + role_type: role.role_type + })); + setEstablishments(userEstablishments); + + const storedEstablishmentId = localStorage.getItem('selectedEstablishmentId'); + if (storedEstablishmentId) { + setSelectedEstablishmentId(storedEstablishmentId); + } else if (userEstablishments.length > 0) { + setSelectedEstablishmentId(userEstablishments[0].id); + } + + fetchChildren(userIdFromSession, storedEstablishmentId).then(data => { + setChildren(data); + }); + } + }, [status, session, selectedEstablishmentId]); + + const handleEstablishmentChange = (e) => { + const establishmentId = parseInt(e.target.value, 10); + setSelectedEstablishmentId(establishmentId); + localStorage.setItem('selectedEstablishmentId', establishmentId); + }; function handleEdit(eleveId) { // Logique pour éditer le dossier de l'élève logger.debug(`Edit dossier for student id: ${eleveId}`); router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`); } + const actionColumns = [ { name: 'Action', transform: (row) => row.action }, ]; @@ -106,6 +129,22 @@ export default function ParentHomePage() { Enfants + {establishments.length > 1 && ( +
+ + +
+ )}
{ - logger.debug('Sign In Result', result); - setIsLoading(false); - if (result.error) { - setErrorMessage(result.error); - } else { - getSession().then(session => { - if (!session || !session.user) { - throw new Error('Session not found'); - } - const user = session.user; - logger.debug('User Session:', user); + login({ + email: formData.get('login'), + password: formData.get('password'), + role_type: selectedProfile // Utilisez le profil sélectionné + }).then(result => { + logger.debug('Sign In Result', result); + setIsLoading(false); + if (result.error) { + setErrorMessage(result.error); + } else { + getSession().then(session => { + if (!session || !session.user) { + throw new Error('Session not found'); + } + const user = session.user; + logger.debug('User Session:', user); - if (user.establishment_id) { - localStorage.setItem('establishment_id', user.establishment_id); - } - - if (user.droit === 0) { - // Vue ECOLE - } else if (user.droit === 1) { - // Vue ADMIN + const roles = user.roles.filter(role => role.role_type === selectedProfile); + if (roles.length > 0) { + // const establishment = roles[0].establishment; + // localStorage.setItem('establishment_id', establishment); + + // Redirection en fonction du rôle + if (roles[0].role_type === 1) { router.push(FE_ADMIN_SUBSCRIPTIONS_URL); - } else if (user.droit === 2) { - // Vue PARENT + } else if (roles[0].role_type === 2) { router.push(FE_PARENTS_HOME_URL); } else { // Cas anormal } - }).catch(error => { - logger.error('Error during session retrieval:', error); - setIsLoading(false); - setErrorMessage('An error occurred during session retrieval.'); - }); - } - }).catch(error => { - logger.error('Error during sign in:', error); - setIsLoading(false); - setErrorMessage('An error occurred during sign in.'); - }); - } + } else { + setErrorMessage('No roles found for the specified role type.'); + } + }).catch(error => { + logger.error('Error during session retrieval:', error); + setIsLoading(false); + setErrorMessage('An error occurred during session retrieval.'); + }); + } + }).catch(error => { + logger.error('Error during sign in:', error); + setIsLoading(false); + setErrorMessage('An error occurred during sign in.'); + }); + } if (isLoading === true) { return // Affichez le composant Loader @@ -94,7 +98,8 @@ export default function Page() {
{ e.preventDefault(); handleFormLogin(new FormData(e.target)); }}> - + +
diff --git a/Front-End/src/app/actions/authAction.js b/Front-End/src/app/actions/authAction.js index 357151a..54e5031 100644 --- a/Front-End/src/app/actions/authAction.js +++ b/Front-End/src/app/actions/authAction.js @@ -30,6 +30,7 @@ export const login = (data) => { redirect: false, email: data.email, password: data.password, + role_type: data.role_type, }) }; @@ -194,7 +195,11 @@ export const getUser = async () => { return { id: session.user.user_id, email: session.user.email, - role: session.user.droit + roles: session.user.roles.map(role => ({ + role_type: role.role_type, + establishment_id: role.establishment__id, + establishment_name: role.establishment__name + })) }; } catch (error) { diff --git a/Front-End/src/app/actions/schoolAction.js b/Front-End/src/app/actions/schoolAction.js index f3def56..a890ba1 100644 --- a/Front-End/src/app/actions/schoolAction.js +++ b/Front-End/src/app/actions/schoolAction.js @@ -7,8 +7,7 @@ import { BE_SCHOOL_DISCOUNTS_URL, BE_SCHOOL_PAYMENT_PLANS_URL, BE_SCHOOL_PAYMENT_MODES_URL, - BE_SCHOOL_ESTABLISHMENT_URL, - ESTABLISHMENT_ID + BE_SCHOOL_ESTABLISHMENT_URL } from '@/utils/Url'; const requestResponseHandler = async (response) => { @@ -24,18 +23,18 @@ const requestResponseHandler = async (response) => { } -export const fetchSpecialities = () => { - return fetch(`${BE_SCHOOL_SPECIALITIES_URL}`) +export const fetchSpecialities = (establishment) => { + return fetch(`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchTeachers = () => { - return fetch(`${BE_SCHOOL_TEACHERS_URL}`) +export const fetchTeachers = (establishment) => { + return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchClasses = () => { - return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}`) +export const fetchClasses = (establishment) => { + return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`) .then(requestResponseHandler) }; @@ -44,48 +43,48 @@ export const fetchSchedules = () => { .then(requestResponseHandler) }; -export const fetchRegistrationDiscounts = () => { - return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration`) +export const fetchRegistrationDiscounts = (establishment) => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchTuitionDiscounts = () => { - return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition`) +export const fetchTuitionDiscounts = (establishment) => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchRegistrationFees = () => { - return fetch(`${BE_SCHOOL_FEES_URL}?filter=registration`) +export const fetchRegistrationFees = (establishment) => { + return fetch(`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchTuitionFees = () => { - return fetch(`${BE_SCHOOL_FEES_URL}?filter=tuition`) +export const fetchTuitionFees = (establishment) => { + return fetch(`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`) .then(requestResponseHandler) }; -export const fetchRegistrationPaymentPlans = () => { - return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration`) +export const fetchRegistrationPaymentPlans = (establishment) => { + return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`) .then(requestResponseHandler) } -export const fetchTuitionPaymentPlans = () => { - return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition`) +export const fetchTuitionPaymentPlans = (establishment) => { + return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`) .then(requestResponseHandler) } -export const fetchRegistrationPaymentModes = () => { - return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration`) +export const fetchRegistrationPaymentModes = (establishment) => { + return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`) .then(requestResponseHandler) } -export const fetchTuitionPaymentModes = () => { - return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition`) +export const fetchTuitionPaymentModes = (establishment) => { + return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`) .then(requestResponseHandler) } -export const fetchEstablishment = () => { - return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${ESTABLISHMENT_ID}`) +export const fetchEstablishment = (establishment) => { + return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`) .then(requestResponseHandler) } diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index aaa328a..b821fb7 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -100,7 +100,7 @@ export const archiveRegisterForm = (id) => { } export const fetchStudents = (id=null, establishment) => { - const url = (id)?`${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`:`${BE_SUBSCRIPTION_STUDENTS_URL}?establishment_id=${establishment}`; + const url = (id)?`${BE_SUBSCRIPTION_STUDENTS_URL}/${id}?establishment_id=${establishment}`:`${BE_SUBSCRIPTION_STUDENTS_URL}?establishment_id=${establishment}`; const request = new Request( url, { @@ -114,9 +114,9 @@ export const fetchStudents = (id=null, establishment) => { }; -export const fetchChildren = (id) =>{ +export const fetchChildren = (id, establishment) =>{ const request = new Request( - `${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}`, + `${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`, { method:'GET', headers: { diff --git a/Front-End/src/components/ProfileSelector.js b/Front-End/src/components/ProfileSelector.js new file mode 100644 index 0000000..5f86e1d --- /dev/null +++ b/Front-End/src/components/ProfileSelector.js @@ -0,0 +1,31 @@ +import React from 'react'; + +const ProfileSelector = ({ selectedProfile, setSelectedProfile }) => { + return ( +
+ + + +
+ ); +}; + +export default ProfileSelector; \ No newline at end of file diff --git a/Front-End/src/components/ProtectedRoute.js b/Front-End/src/components/ProtectedRoute.js index badfa7f..fd13f76 100644 --- a/Front-End/src/components/ProtectedRoute.js +++ b/Front-End/src/components/ProtectedRoute.js @@ -18,8 +18,10 @@ const ProtectedRoute = ({ children, requiredRight }) => { return ; } - // Vérifier le rôle de l'utilisateur - if (session && requiredRight && session.user.droit !== requiredRight) { + // Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight + const hasRequiredRight = session?.user?.roles?.some(role => role.role_type === requiredRight); + + if (session && requiredRight && !hasRequiredRight) { router.push(`${FE_USERS_LOGIN_URL}`); return null; } diff --git a/Front-End/src/components/Providers.js b/Front-End/src/components/Providers.js index ef8a3de..8af8f17 100644 --- a/Front-End/src/components/Providers.js +++ b/Front-End/src/components/Providers.js @@ -3,6 +3,8 @@ import { SessionProvider } from "next-auth/react" import { CsrfProvider } from '@/context/CsrfContext' import { NextIntlClientProvider } from 'next-intl' +import { EstablishmentProvider } from '@/context/EstablishmentContext'; + export default function Providers({ children, messages, locale, session }) { if (!locale) { @@ -12,9 +14,11 @@ export default function Providers({ children, messages, locale, session }) { return ( - - {children} - + + + {children} + + ) diff --git a/Front-End/src/components/Sidebar.js b/Front-End/src/components/Sidebar.js index 528fdba..cfd51b1 100644 --- a/Front-End/src/components/Sidebar.js +++ b/Front-End/src/components/Sidebar.js @@ -1,6 +1,7 @@ 'use client' import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { useEstablishment } from '@/context/EstablishmentContext'; const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
(
); -function Sidebar({ establishment, currentPage, items }) { +function Sidebar({ establishments, currentPage, items, onCloseMobile, onEstablishmentChange }) { const router = useRouter(); + const { selectedEstablishmentId, setSelectedEstablishmentId, setProfileRole } = useEstablishment(); const [selectedItem, setSelectedItem] = useState(currentPage); useEffect(() => { @@ -25,31 +27,49 @@ function Sidebar({ establishment, currentPage, items }) { const handleItemClick = (url) => { setSelectedItem(url); router.push(url); + if (onCloseMobile) { + onCloseMobile(); + } }; - return <> - {/* Sidebar */} + const handleEstablishmentChange = (e) => { + const establishmentId = parseInt(e.target.value, 10); + setSelectedEstablishmentId(establishmentId); + const role = establishments.find(est => est.id === establishmentId)?.role_type; + setProfileRole(role); + onEstablishmentChange(establishmentId); + }; + + return (
-
{establishment?.name}
+
- + ); } export default Sidebar; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index f70a4d0..f982282 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -329,7 +329,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha ); case 'EMAIL': - return teacher.email; + return teacher.associated_profile_email; case 'SPECIALITES': return (
diff --git a/Front-End/src/components/Structure/Files/FilesManagement.js b/Front-End/src/components/Structure/Files/FilesManagement.js new file mode 100644 index 0000000..2db2fe3 --- /dev/null +++ b/Front-End/src/components/Structure/Files/FilesManagement.js @@ -0,0 +1,341 @@ +import React, { useState, useEffect } from 'react'; +import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react'; +import Modal from '@/components/Modal'; +import Table from '@/components/Table'; +import FileUpload from '@/components/FileUpload'; +import { formatDate } from '@/utils/Date'; +import { BASE_URL } from '@/utils/Url'; +import { + fetchRegisterFormFileTemplate, + createRegistrationFormFileTemplate, + editRegistrationFormFileTemplate, + deleteRegisterFormFileTemplate, + getRegisterFormFileTemplate +} from '@/app/actions/subscriptionAction'; +import { + fetchRegistrationFileGroups, + createRegistrationFileGroup, + deleteRegistrationFileGroup, + editRegistrationFileGroup +} from '@/app/actions/registerFileGroupAction'; +import RegistrationFileGroupForm from '@/components/RegistrationFileGroupForm'; + +export default function FilesManagement({ csrfToken }) { + const [fichiers, setFichiers] = useState([]); + const [groups, setGroups] = useState([]); + const [selectedGroup, setSelectedGroup] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [fileToEdit, setFileToEdit] = useState(null); + const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); + const [groupToEdit, setGroupToEdit] = useState(null); + const [token] + + // Fonction pour transformer les données des fichiers avec les informations complètes du groupe + const transformFileData = (file, groups) => { + if (!file.group) return file; + + const groupInfo = groups.find(g => g.id === file.group); + return { + ...file, + group: groupInfo || { id: file.group, name: 'Groupe inconnu' } + }; + }; + + useEffect(() => { + Promise.all([ + fetchRegisterFormFileTemplate(), + fetchRegistrationFileGroups() + ]).then(([filesData, groupsData]) => { + setGroups(groupsData); + // Sélectionner automatiquement le premier groupe s'il existe + if (groupsData.length > 0) { + setSelectedGroup(groupsData[0].id.toString()); + } + // Transformer chaque fichier pour inclure les informations complètes du groupe + const transformedFiles = filesData.map(file => transformFileData(file, groupsData)); + setFichiers(transformedFiles); + }).catch(err => { + console.log(err.message); + }); + }, []); + + const handleFileDelete = (fileId) => { + deleteRegisterFormFileTemplate(fileId, csrfToken) + .then(response => { + if (response.ok) { + setFichiers(fichiers.filter(fichier => fichier.id !== fileId)); + alert('Fichier supprimé avec succès.'); + } else { + alert('Erreur lors de la suppression du fichier.'); + } + }) + .catch(error => { + console.error('Error deleting file:', error); + alert('Erreur lors de la suppression du fichier.'); + }); + }; + + const handleFileEdit = (file) => { + setIsEditing(true); + setFileToEdit(file); + setIsModalOpen(true); + }; + + const handleFileUpload = ({file, name, is_required, order, groupId}) => { + if (!name) { + alert('Veuillez entrer un nom de fichier.'); + return; + } + + const formData = new FormData(); + if(file) { + formData.append('file', file); + } + formData.append('name', name); + formData.append('is_required', is_required); + formData.append('order', order); + + // Modification ici : vérifier si groupId existe et n'est pas vide + if (groupId && groupId !== '') { + formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id' + } + + if (isEditing && fileToEdit) { + editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) + .then(data => { + // Transformer le fichier mis à jour avec les informations du groupe + const transformedFile = transformFileData(data, groups); + setFichiers(prevFichiers => + prevFichiers.map(f => f.id === fileToEdit.id ? transformedFile : f) + ); + setIsModalOpen(false); + setFileToEdit(null); + setIsEditing(false); + }) + .catch(error => { + console.error('Error editing file:', error); + alert('Erreur lors de la modification du fichier'); + }); + } else { + createRegistrationFormFileTemplate(formData, csrfToken) + .then(data => { + // Transformer le nouveau fichier avec les informations du groupe + const transformedFile = transformFileData(data, groups); + setFichiers(prevFiles => [...prevFiles, transformedFile]); + setIsModalOpen(false); + }) + .catch(error => { + console.error('Error uploading file:', error); + }); + } + }; + + const handleGroupSubmit = (groupData) => { + if (groupToEdit) { + editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken) + .then(updatedGroup => { + setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group)); + setGroupToEdit(null); + setIsGroupModalOpen(false); + }) + .catch(error => { + console.error('Error handling group:', error); + alert('Erreur lors de l\'opération sur le groupe'); + }); + } else { + createRegistrationFileGroup(groupData, csrfToken) + .then(newGroup => { + setGroups([...groups, newGroup]); + setIsGroupModalOpen(false); + }) + .catch(error => { + console.error('Error handling group:', error); + alert('Erreur lors de l\'opération sur le groupe'); + }); + } + }; + + const handleGroupEdit = (group) => { + setGroupToEdit(group); + setIsGroupModalOpen(true); + }; + + const handleGroupDelete = (groupId) => { + // Vérifier si des fichiers utilisent ce groupe + const filesInGroup = fichiers.filter(file => file.group && file.group.id === groupId); + if (filesInGroup.length > 0) { + alert('Impossible de supprimer ce groupe car il contient des fichiers. Veuillez d\'abord retirer tous les fichiers de ce groupe.'); + return; + } + + if (window.confirm('Êtes-vous sûr de vouloir supprimer ce groupe ?')) { + deleteRegistrationFileGroup(groupId, csrfToken) + .then((response) => { + if (response.status === 409) { + throw new Error('Ce groupe est lié à des inscriptions existantes.'); + } + if (!response.ok) { + throw new Error('Erreur lors de la suppression du groupe.'); + } + setGroups(groups.filter(group => group.id !== groupId)); + alert('Groupe supprimé avec succès.'); + }) + .catch(error => { + console.error('Error deleting group:', error); + alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.'); + }); + } + }; + + // Ajouter cette fonction de filtrage + const filteredFiles = fichiers.filter(file => { + if (!selectedGroup) return true; + return file.group && file.group.id === parseInt(selectedGroup); + }); + + const columnsFiles = [ + { name: 'Nom du fichier', transform: (row) => row.name }, + { name: 'Groupe', transform: (row) => row.group ? row.group.name : 'Aucun' }, + { name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") }, + { name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' }, + { name: 'Ordre de fusion', transform: (row) => row.order }, + { name: 'Actions', transform: (row) => ( +
+ {row.file && ( + + + + )} + + + +
+ )} + ]; + + const columnsGroups = [ + { name: 'Nom du groupe', transform: (row) => row.name }, + { name: 'Description', transform: (row) => row.description }, + { name: 'Actions', transform: (row) => ( +
+ + +
+ )} + ]; + + // Fonction pour gérer la demande de signature + const handleSignatureRequest = (file) => { + const formData = new FormData(); + formData.append('file', file); + console.log('Demande de signature pour le fichier :', file); + + fetch('http://localhost:8080:/DocuSeal/generateToken', { + method: 'POST', + headers: { + 'Authorization': 'Bearer NFPZy6BBGvYs1BwTuXMQ3XAu5N1kLFiXWftGQhkiz2A', + }, + body: formData, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Erreur lors du téléversement du document : ' + response.statusText); + } + return response.json(); + }) + .then((data) => { + const documentId = data.documentId; + console.log('Document téléversé avec succès, ID :', documentId); + onUpload(documentId); + }); + .catch((error) => console.error(error)); + }; + + return ( +
+ ( + + )} + /> + ( + + )} + /> +
+
+

Groupes de fichiers

+ +
+
+ + {groups.length > 0 && ( +
+
+

Fichiers

+
+ + +
+
+
+ + )} + + ); +} diff --git a/Front-End/src/context/EstablishmentContext.js b/Front-End/src/context/EstablishmentContext.js new file mode 100644 index 0000000..bf17647 --- /dev/null +++ b/Front-End/src/context/EstablishmentContext.js @@ -0,0 +1,45 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const EstablishmentContext = createContext(); + +export const EstablishmentProvider = ({ children }) => { + const [selectedEstablishmentId, setSelectedEstablishmentId] = useState(() => { + // Récupérer l'ID de l'établissement depuis le localStorage + if (typeof window !== 'undefined') { + return localStorage.getItem('selectedEstablishmentId') || null; + } + return null; + }); + + const [profileRole, setProfileRole] = useState(() => { + // Récupérer le rôle du profil depuis le localStorage + if (typeof window !== 'undefined') { + return localStorage.getItem('profileRole') || null; + } + return null; + }); + + useEffect(() => { + // Sauvegarder l'ID de l'établissement dans le localStorage + if (selectedEstablishmentId) { + localStorage.setItem('selectedEstablishmentId', selectedEstablishmentId); + } + }, [selectedEstablishmentId]); + + useEffect(() => { + // Sauvegarder le rôle du profil dans le localStorage + if (profileRole) { + localStorage.setItem('profileRole', profileRole); + } + }, [profileRole]); + + return ( + + {children} + + ); +}; + +export const useEstablishment = () => { + return useContext(EstablishmentContext); +}; \ No newline at end of file diff --git a/Front-End/src/pages/api/auth/[...nextauth].js b/Front-End/src/pages/api/auth/[...nextauth].js index 5ae8993..73d4a01 100644 --- a/Front-End/src/pages/api/auth/[...nextauth].js +++ b/Front-End/src/pages/api/auth/[...nextauth].js @@ -11,24 +11,23 @@ const options = { name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, - password: { label: 'Password', type: 'password' } + password: { label: 'Password', type: 'password' }, + role_type: { label: 'Role Type', type: 'text' } }, authorize: async (credentials, req) => { try { const data = { email: credentials.email, - password: credentials.password + password: credentials.password, + role_type: credentials.role_type }; const user = await getJWT(data); if (user) { - logger.debug("API response:", user); return user; } - logger.error('Invalid credentials') } catch (error) { - logger.error('Invalid credentials') throw new Error(error.message || 'Invalid credentials'); } } @@ -88,16 +87,15 @@ const options = { }, async session({ session, token }) { if (token && token?.token) { - const {user_id, droit, email, establishment} = jwt_decode.decode(token.token); + const { user_id, email, roles } = jwt_decode.decode(token.token); session.user = { ...session.user, token: token.token, - refresh: token.refresh + refresh: token.refresh, + user_id: user_id, + email: email, + roles: roles }; - session.user.user_id = user_id; - session.user.droit = droit; - session.user.email = email; - session.user.establishment = establishment; } return session; } diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 9c1fc4b..29e0002 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -40,7 +40,9 @@ export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`; export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`; export const BE_SCHOOL_PAYMENT_PLANS_URL = `${BASE_URL}/School/paymentPlans`; export const BE_SCHOOL_PAYMENT_MODES_URL = `${BASE_URL}/School/paymentModes`; -export const BE_SCHOOL_ESTABLISHMENT_URL = `${BASE_URL}/School/establishments`; + +// ESTABLISHMENT +export const BE_SCHOOL_ESTABLISHMENT_URL = `${BASE_URL}/Establishment/establishments`; // GESTION PLANNING