diff --git a/Back-End/Dockerfile b/Back-End/Dockerfile index 7f46014..afa5421 100644 --- a/Back-End/Dockerfile +++ b/Back-End/Dockerfile @@ -7,6 +7,7 @@ FROM python:3.12.7 # Allows docker to cache installed dependencies between builds COPY requirements.txt requirements.txt RUN pip install -r requirements.txt +RUN pip install pymupdf # Mounts the application code to the image COPY . . diff --git a/Back-End/N3wtSchool/bdd.py b/Back-End/N3wtSchool/bdd.py index 1fe62ef..6c4b58b 100644 --- a/Back-End/N3wtSchool/bdd.py +++ b/Back-End/N3wtSchool/bdd.py @@ -92,6 +92,7 @@ def searchObjects(_objectName, _searchTerm=None, _excludeStates=None): def delete_object(model_class, object_id, related_field=None): try: obj = model_class.objects.get(id=object_id) + if related_field and hasattr(obj, related_field): related_obj = getattr(obj, related_field) if related_obj: @@ -103,5 +104,3 @@ def delete_object(model_class, object_id, related_field=None): return JsonResponse({'error': f'L\'objet {model_class.__name__} n\'existe pas avec cet ID'}, status=404, safe=False) except Exception as e: return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False) - - diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 31d2a80..a992eac 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -3,6 +3,9 @@ from Auth.models import Profile from django.db.models import JSONField from django.dispatch import receiver from django.contrib.postgres.fields import ArrayField +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + LEVEL_CHOICES = [ (1, 'Très Petite Section (TPS)'), @@ -47,7 +50,7 @@ class SchoolClass(models.Model): number_of_students = models.PositiveIntegerField(blank=True) teaching_language = models.CharField(max_length=255, blank=True) school_year = models.CharField(max_length=9, blank=True) - updated_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) teachers = models.ManyToManyField(Teacher, blank=True) levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list) type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) @@ -64,3 +67,56 @@ class Planning(models.Model): def __str__(self): return f'Planning for {self.level} of {self.school_class.atmosphere_name}' + +class Discount(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class Fee(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class TuitionFee(models.Model): + class PaymentOptions(models.IntegerChoices): + SINGLE_PAYMENT = 0, _('Paiement en une seule fois') + FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') + TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') + + name = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True) + base_amount = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=3, default='EUR') + discounts = models.ManyToManyField('Discount', blank=True) + validity_start_date = models.DateField() + validity_end_date = models.DateField() + payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) + is_active = models.BooleanField(default=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def clean(self): + if self.validity_end_date <= self.validity_start_date: + raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.')) + + def calculate_final_amount(self): + amount = self.base_amount + + # Apply fees (supplements and taxes) + # for fee in self.fees.all(): + # amount += fee.amount + + # Apply discounts + for discount in self.discounts.all(): + amount -= discount.amount + + return amount diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index d3bae13..2248861 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee from Subscriptions.models import RegistrationForm from Subscriptions.serializers import StudentSerializer from Auth.serializers import ProfileSerializer @@ -172,4 +172,57 @@ class SchoolClassSerializer(serializers.ModelSerializer): utc_time = timezone.localtime(obj.updated_date) local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file + return local_time.strftime("%d-%m-%Y %H:%M") + +class DiscountSerializer(serializers.ModelSerializer): + class Meta: + model = Discount + fields = '__all__' + +class FeeSerializer(serializers.ModelSerializer): + class Meta: + model = Fee + fields = '__all__' + +class TuitionFeeSerializer(serializers.ModelSerializer): + discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + final_amount = serializers.SerializerMethodField() + + class Meta: + model = TuitionFee + fields = '__all__' + + def get_final_amount(self, obj): + return obj.calculate_final_amount() + + def create(self, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Create the TuitionFee instance + tuition_fee = TuitionFee.objects.create(**validated_data) + + # Add discounts if provided + for discount in discounts_data: + tuition_fee.discounts.add(discount) + + return tuition_fee + + def update(self, instance, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Update the TuitionFee instance + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.base_amount = validated_data.get('base_amount', instance.base_amount) + instance.currency = validated_data.get('currency', instance.currency) + instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date) + instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date) + instance.payment_option = validated_data.get('payment_option', instance.payment_option) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.save() + + # Update discounts if provided + if discounts_data: + instance.discounts.set(discounts_data) + + return instance \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 77897c3..4ccae46 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -1,6 +1,21 @@ from django.urls import path, re_path -from School.views import TeachersView, TeacherView, SpecialitiesView, SpecialityView, ClassesView, ClasseView, PlanningsView, PlanningView +from School.views import ( + TeachersView, + TeacherView, + SpecialitiesView, + SpecialityView, + ClassesView, + ClasseView, + PlanningsView, + PlanningView, + FeesView, + FeeView, + TuitionFeesView, + TuitionFeeView, + DiscountsView, + DiscountView, +) urlpatterns = [ re_path(r'^specialities$', SpecialitiesView.as_view(), name="specialities"), @@ -18,4 +33,16 @@ urlpatterns = [ re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"), re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), + + re_path(r'^fees$', FeesView.as_view(), name="fees"), + re_path(r'^fee$', FeeView.as_view(), name="fee"), + re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), + + re_path(r'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"), + re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"), + re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"), + + re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), + re_path(r'^discount$', DiscountView.as_view(), name="discount"), + re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 7ba4984..e06e944 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -5,46 +5,41 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from .models import Teacher, Speciality, SchoolClass, Planning -from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer +from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee +from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer from N3wtSchool import bdd +from N3wtSchool.bdd import delete_object, getAllObjects, getObject @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialitiesView(APIView): def get(self, request): - specialitiesList=bdd.getAllObjects(Speciality) - specialities_serializer=SpecialitySerializer(specialitiesList, many=True) - + specialitiesList = getAllObjects(Speciality) + specialities_serializer = SpecialitySerializer(specialitiesList, many=True) return JsonResponse(specialities_serializer.data, safe=False) def post(self, request): - specialities_data=JSONParser().parse(request) + specialities_data = JSONParser().parse(request) all_valid = True for speciality_data in specialities_data: speciality_serializer = SpecialitySerializer(data=speciality_data) - if speciality_serializer.is_valid(): speciality_serializer.save() else: all_valid = False break if all_valid: - specialitiesList = bdd.getAllObjects(Speciality) + specialitiesList = getAllObjects(Speciality) specialities_serializer = SpecialitySerializer(specialitiesList, many=True) - return JsonResponse(specialities_serializer.data, safe=False) - return JsonResponse(speciality_serializer.errors, safe=False) - @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialityView(APIView): - def get (self, request, _id): - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) - speciality_serializer=SpecialitySerializer(speciality) - + def get(self, request, _id): + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality_serializer = SpecialitySerializer(speciality) return JsonResponse(speciality_serializer.data, safe=False) def post(self, request): @@ -59,7 +54,7 @@ class SpecialityView(APIView): def put(self, request, _id): speciality_data=JSONParser().parse(request) - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) speciality_serializer = SpecialitySerializer(speciality, data=speciality_data) if speciality_serializer.is_valid(): speciality_serializer.save() @@ -68,11 +63,62 @@ class SpecialityView(APIView): return JsonResponse(speciality_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Speciality, _id) + return delete_object(Speciality, _id) + +# Vues pour les réductions (Discount) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountsView(APIView): + def get(self, request): + discountsList = Discount.objects.all() + discounts_serializer = DiscountSerializer(discountsList, many=True) + return JsonResponse(discounts_serializer.data, safe=False) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountView(APIView): + def get(self, request, _id): + try: + discount = Discount.objects.get(id=_id) + discount_serializer = DiscountSerializer(discount) + return JsonResponse(discount_serializer.data, safe=False) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + discount_data = JSONParser().parse(request) + try: + discount = Discount.objects.get(id=_id) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Discount, _id) class TeachersView(APIView): def get(self, request): - teachersList=bdd.getAllObjects(Teacher) + teachersList=getAllObjects(Teacher) teachers_serializer=TeacherSerializer(teachersList, many=True) return JsonResponse(teachers_serializer.data, safe=False) @@ -81,7 +127,7 @@ class TeachersView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class TeacherView(APIView): def get (self, request, _id): - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer=TeacherSerializer(teacher) return JsonResponse(teacher_serializer.data, safe=False) @@ -99,7 +145,7 @@ class TeacherView(APIView): def put(self, request, _id): teacher_data=JSONParser().parse(request) - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer = TeacherSerializer(teacher, data=teacher_data) if teacher_serializer.is_valid(): teacher_serializer.save() @@ -108,13 +154,13 @@ class TeacherView(APIView): return JsonResponse(teacher_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Teacher, _id, related_field='associated_profile') + return delete_object(Teacher, _id, related_field='associated_profile') @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class ClassesView(APIView): def get(self, request): - classesList=bdd.getAllObjects(SchoolClass) + classesList=getAllObjects(SchoolClass) classes_serializer=SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -131,7 +177,7 @@ class ClassesView(APIView): break if all_valid: - classesList = bdd.getAllObjects(SchoolClass) + classesList = getAllObjects(SchoolClass) classes_serializer = SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -142,7 +188,7 @@ class ClassesView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class ClasseView(APIView): def get (self, request, _id): - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer=SchoolClassSerializer(schoolClass) return JsonResponse(classe_serializer.data, safe=False) @@ -159,7 +205,7 @@ class ClasseView(APIView): def put(self, request, _id): classe_data=JSONParser().parse(request) - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer = SchoolClassSerializer(schoolClass, data=classe_data) if classe_serializer.is_valid(): classe_serializer.save() @@ -168,14 +214,14 @@ class ClasseView(APIView): return JsonResponse(classe_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(SchoolClass, _id) + return delete_object(SchoolClass, _id) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningsView(APIView): def get(self, request): - schedulesList=bdd.getAllObjects(Planning) + schedulesList=getAllObjects(Planning) schedules_serializer=PlanningSerializer(schedulesList, many=True) return JsonResponse(schedules_serializer.data, safe=False) @@ -183,7 +229,7 @@ class PlanningsView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningView(APIView): def get (self, request, _id): - planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id) + planning = getObject(_objectName=Planning, _columnName='classe__id', _value=_id) planning_serializer=PlanningSerializer(planning) return JsonResponse(planning_serializer.data, safe=False) @@ -215,3 +261,98 @@ class PlanningView(APIView): return JsonResponse(planning_serializer.data, safe=False) return JsonResponse(planning_serializer.errors, safe=False) + + +# Vues pour les frais (Fee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeesView(APIView): + def get(self, request): + feesList = Fee.objects.all() + fees_serializer = FeeSerializer(feesList, many=True) + return JsonResponse(fees_serializer.data, safe=False) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeeView(APIView): + def get(self, request, _id): + try: + fee = Fee.objects.get(id=_id) + fee_serializer = FeeSerializer(fee) + return JsonResponse(fee_serializer.data, safe=False) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + fee_data = JSONParser().parse(request) + try: + fee = Fee.objects.get(id=_id) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) # Utilisation de partial=True + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Fee, _id) + +# Vues pour les frais de scolarité (TuitionFee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeesView(APIView): + def get(self, request): + tuitionFeesList = TuitionFee.objects.all() + tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True) + return JsonResponse(tuitionFees_serializer.data, safe=False) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeeView(APIView): + def get(self, request, _id): + try: + tuitionFee = TuitionFee.objects.get(id=_id) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee) + return JsonResponse(tuitionFee_serializer.data, safe=False) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + tuitionFee_data = JSONParser().parse(request) + tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data) + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False, status=201) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + tuitionFee_data = JSONParser().parse(request) + try: + tuitionFee = TuitionFee.objects.get(id=_id) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(TuitionFee, _id) \ No newline at end of file diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 766577d..b183107 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -9,6 +9,9 @@ from School.models import SchoolClass from datetime import datetime class RegistrationFee(models.Model): + """ + Représente un tarif ou frais d’inscription avec différentes options de paiement. + """ class PaymentOptions(models.IntegerChoices): SINGLE_PAYMENT = 0, _('Paiement en une seule fois') MONTHLY_PAYMENT = 1, _('Paiement mensuel') @@ -27,6 +30,9 @@ class RegistrationFee(models.Model): return self.name class Language(models.Model): + """ + Représente une langue parlée par l’élève. + """ id = models.AutoField(primary_key=True) label = models.CharField(max_length=200, default="") @@ -34,6 +40,9 @@ class Language(models.Model): return "LANGUAGE" class Guardian(models.Model): + """ + Représente un responsable légal (parent/tuteur) d’un élève. + """ last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") birth_date = models.CharField(max_length=200, default="", blank=True) @@ -47,6 +56,9 @@ class Guardian(models.Model): return self.last_name + "_" + self.first_name class Sibling(models.Model): + """ + Représente un frère ou une sœur d’un élève. + """ id = models.AutoField(primary_key=True) last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") @@ -56,7 +68,9 @@ class Sibling(models.Model): return "SIBLING" class Student(models.Model): - + """ + Représente l’élève inscrit ou en cours d’inscription. + """ class StudentGender(models.IntegerChoices): NONE = 0, _('Sélection du genre') MALE = 1, _('Garçon') @@ -95,6 +109,9 @@ class Student(models.Model): # Many-to-Many Relationship siblings = models.ManyToManyField(Sibling, blank=True) + # Many-to-Many Relationship + registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students') + # Many-to-Many Relationship spoken_languages = models.ManyToManyField(Language, blank=True) @@ -105,21 +122,39 @@ class Student(models.Model): return self.last_name + "_" + self.first_name def getSpokenLanguages(self): + """ + Retourne la liste des langues parlées par l’élève. + """ return self.spoken_languages.all() def getMainGuardian(self): + """ + Retourne le responsable légal principal de l’élève. + """ return self.guardians.all()[0] def getGuardians(self): + """ + Retourne tous les responsables légaux de l’élève. + """ return self.guardians.all() def getProfiles(self): + """ + Retourne les profils utilisateurs liés à l’élève. + """ return self.profiles.all() def getSiblings(self): + """ + Retourne les frères et sœurs de l’élève. + """ return self.siblings.all() def getNumberOfSiblings(self): + """ + Retourne le nombre de frères et sœurs. + """ return self.siblings.count() @property @@ -148,7 +183,9 @@ class Student(models.Model): return None class RegistrationForm(models.Model): - + """ + Gère le dossier d’inscription lié à un élève donné. + """ class RegistrationFormStatus(models.IntegerChoices): RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_CREATED = 1, _('Dossier d\'inscription créé') @@ -171,9 +208,53 @@ class RegistrationForm(models.Model): return "RF_" + self.student.last_name + "_" + self.student.first_name class RegistrationFileTemplate(models.Model): + """ + Modèle pour stocker les fichiers "templates" d’inscription. + """ name = models.CharField(max_length=255) - file = models.FileField(upload_to='registration_files/') + file = models.FileField(upload_to='templates_files/', blank=True, null=True) + order = models.PositiveIntegerField(default=0) # Ajout du champ order date_added = models.DateTimeField(auto_now_add=True) + is_required = models.BooleanField(default=False) + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None def __str__(self): return self.name + +def registration_file_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.register_form.pk}/{filename}" + +class RegistrationFile(models.Model): + """ + Fichier lié à un dossier d’inscription particulier. + """ + name = models.CharField(max_length=255) + file = models.FileField(upload_to=registration_file_upload_to) + date_added = models.DateTimeField(auto_now_add=True) + template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE) + register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files') + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None + + def __str__(self): + return self.name + + @staticmethod + def get_files_from_rf(register_form_id): + """ + Récupère tous les fichiers liés à un dossier d’inscription donné. + """ + registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order') + filenames = [] + for reg_file in registration_files: + filenames.append(reg_file.file.path) + return filenames diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 99538d7..b4a996c 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import RegistrationFileTemplate, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee from School.models import SchoolClass from Auth.models import Profile from Auth.serializers import ProfileSerializer @@ -10,17 +10,22 @@ from django.utils import timezone import pytz from datetime import datetime -class RegistrationFileTemplateSerializer(serializers.ModelSerializer): - class Meta: - model = RegistrationFileTemplate - fields = '__all__' - class RegistrationFeeSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: model = RegistrationFee fields = '__all__' +class RegistrationFileSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFile + fields = '__all__' + +class RegistrationFileTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFileTemplate + fields = '__all__' + class LanguageSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: @@ -47,6 +52,7 @@ class GuardianSerializer(serializers.ModelSerializer): class StudentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) guardians = GuardianSerializer(many=True, required=False) siblings = SiblingSerializer(many=True, required=False) @@ -126,7 +132,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer): registration_file = serializers.FileField(required=False) status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() - + registration_files = RegistrationFileSerializer(many=True, required=False) class Meta: model = RegistrationForm fields = '__all__' diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 53647fa..0e7fbf8 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,36 +1,44 @@ from django.urls import path, re_path from . import views -from Subscriptions.views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView +from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView urlpatterns = [ - re_path(r'^registerForms/([a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), + re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), + re_path(r'^registerForm$', RegisterFormView.as_view(), name="registerForm"), - re_path(r'^registerForm/([0-9]+)$', RegisterFormView.as_view(), name="registerForm"), + re_path(r'^registerForm/(?P<_id>[0-9]+)$', RegisterFormView.as_view(), name="registerForm"), # Page de formulaire d'inscription - ELEVE - re_path(r'^student/([0-9]+)$', StudentView.as_view(), name="students"), + re_path(r'^student/(?P<_id>[0-9]+)$', StudentView.as_view(), name="students"), # Page de formulaire d'inscription - RESPONSABLE re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"), # Envoi d'un dossier d'inscription - re_path(r'^send/([0-9]+)$', views.send, name="send"), + re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"), # Archivage d'un dossier d'inscription - re_path(r'^archive/([0-9]+)$', views.archive, name="archive"), + re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"), # Envoi d'une relance de dossier d'inscription - re_path(r'^sendRelance/([0-9]+)$', views.relance, name="sendRelance"), + re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"), # Page PARENT - Liste des children - re_path(r'^children/([0-9]+)$', ChildrenListView.as_view(), name="children"), + re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"), # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), # Frais d'inscription re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"), + + # modèles de fichiers d'inscription re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), - re_path(r'^registrationFileTemplates/([0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), + re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), + + # fichiers d'inscription + re_path(r'^registrationFiles/(?P<_id>[0-9]+)$', RegistrationFileView.as_view(), name='registrationFiles'), + re_path(r'^registrationFiles', RegistrationFileView.as_view(), name="registrationFiles"), + ] \ No newline at end of file diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 9ae17a9..e76d06b 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -16,52 +16,95 @@ from enum import Enum import random import string from rest_framework.parsers import JSONParser +import pymupdf def recupereListeFichesInscription(): + """ + Retourne la liste complète des fiches d’inscription. + """ context = { "ficheInscriptions_list": bdd.getAllObjects(RegistrationForm), } return context def recupereListeFichesInscriptionEnAttenteSEPA(): - + """ + Retourne les fiches d’inscription avec paiement SEPA en attente. + """ ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE']) return ficheInscriptionsSEPA_list def _now(): + """ + Retourne la date et l’heure en cours, avec fuseau. + """ return datetime.now(ZoneInfo(settings.TZ_APPLI)) def convertToStr(dateValue, dateFormat): + """ + Convertit un objet datetime en chaîne selon un format donné. + """ return dateValue.strftime(dateFormat) def convertToDate(date_time): + """ + Convertit une chaîne en objet datetime selon le format '%d-%m-%Y %H:%M'. + """ format = '%d-%m-%Y %H:%M' datetime_str = datetime.strptime(date_time, format) return datetime_str def convertTelephone(telephoneValue, separator='-'): + """ + Reformate un numéro de téléphone en y insérant un séparateur donné. + """ return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}" def genereRandomCode(length): + """ + Génère un code aléatoire de longueur spécifiée. + """ return ''.join(random.choice(string.ascii_letters) for i in range(length)) def calculeDatePeremption(_start, nbDays): + """ + Calcule la date de fin à partir d’un point de départ et d’un nombre de jours. + """ return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT) # Fonction permettant de retourner la valeur du QueryDict # QueryDict [ index ] -> Dernière valeur d'une liste # dict (QueryDict [ index ]) -> Toutes les valeurs de la liste def _(liste): + """ + Retourne la première valeur d’une liste extraite d’un QueryDict. + """ return liste[0] def getArgFromRequest(_argument, _request): + """ + Extrait la valeur d’un argument depuis la requête (JSON). + """ resultat = None data=JSONParser().parse(_request) resultat = data[_argument] return resultat -def rfToPDF(registerForm): +def merge_files_pdf(filenames, output_filename): + """ + Insère plusieurs fichiers PDF dans un seul document de sortie. + """ + merger = pymupdf.open() + for filename in filenames: + merger.insert_file(filename) + merger.save(output_filename) + merger.close() + +def rfToPDF(registerForm,filename): + """ + Génère le PDF d’un dossier d’inscription et l’associe au RegistrationForm. + """ # Ajout du fichier d'inscriptions data = { 'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name, @@ -69,14 +112,12 @@ def rfToPDF(registerForm): 'signatureTime': convertToStr(_now(), '%H:%M'), 'student':registerForm.student, } - + PDFFileName = filename pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) - - PDFFileName = "Dossier_Inscription_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) - pathFichier = Path(settings.DOCUMENT_DIR + "/" + PDFFileName) + pathFichier = Path(filename) if os.path.exists(str(pathFichier)): print(f'File exists : {str(pathFichier)}') os.remove(str(pathFichier)) - receipt_file = BytesIO(pdf.content) - registerForm.fichierInscription = File(receipt_file, PDFFileName) \ No newline at end of file + registerForm.fichierInscription = File(receipt_file, PDFFileName) + registerForm.fichierInscription.save() \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index 47a085d..5a29bd8 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -10,6 +10,8 @@ from rest_framework.parsers import JSONParser,MultiPartParser, FormParser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi import json from pathlib import Path @@ -18,18 +20,22 @@ from io import BytesIO import Subscriptions.mailManager as mailer import Subscriptions.util as util -from Subscriptions.serializers import RegistrationFormSerializer, RegistrationFileTemplateSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer -from Subscriptions.pagination import CustomPagination -from Subscriptions.signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine +from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer +from .pagination import CustomPagination +from .signals import clear_cache +from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate, RegistrationFile +from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine from Auth.models import Profile from N3wtSchool import settings, renderers, bdd class RegisterFormListView(APIView): + """ + Gère la liste des dossiers d’inscription, lecture et création. + """ pagination_class = CustomPagination def get_register_form(self, _filter, search=None): @@ -47,8 +53,18 @@ class RegisterFormListView(APIView): return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) return None + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter('_filter', openapi.IN_PATH, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), + openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), + openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), + ], + responses={200: RegistrationFormSerializer(many=True)} + ) def get(self, request, _filter): - + """ + Récupère les fiches d'inscriptions en fonction du filtre passé. + """ # Récupération des paramètres search = request.GET.get('search', '').strip() page_size = request.GET.get('page_size', None) @@ -84,6 +100,11 @@ class RegisterFormListView(APIView): return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) + @swagger_auto_schema( + manual_parameters=[ + ], + responses={200: RegistrationFormSerializer(many=True)} + ) def post(self, request): studentFormList_serializer=JSONParser().parse(request) for studentForm_data in studentFormList_serializer: @@ -104,14 +125,23 @@ class RegisterFormListView(APIView): @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class RegisterFormView(APIView): + """ + Gère la lecture, création, modification et suppression d’un dossier d’inscription. + """ pagination_class = CustomPagination def get(self, request, _id): + """ + Récupère un dossier d'inscription donné. + """ registerForm=bdd.getObject(RegistrationForm, "student__id", _id) registerForm_serializer=RegistrationFormSerializer(registerForm) return JsonResponse(registerForm_serializer.data, safe=False) def post(self, request): + """ + Crée un dossier d'inscription. + """ studentForm_data=JSONParser().parse(request) # Ajout de la date de mise à jour studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') @@ -137,21 +167,33 @@ class RegisterFormView(APIView): return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - def put(self, request, id): + def put(self, request, _id): + """ + Modifie un dossier d'inscription donné. + """ studentForm_data=JSONParser().parse(request) - status = studentForm_data.pop('status', 0) + _status = studentForm_data.pop('status', 0) studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) - registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école json.dumps(studentForm_data) - util.rfToPDF(registerForm) + #Génération de la fiche d'inscription au format PDF + PDFFileName = "rf_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) + path = Path(f"registration_files/dossier_rf_{registerForm.pk}/{PDFFileName}") + registerForm.fichierInscription = util.rfToPDF(registerForm, path) + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + fileNames.insert(0,path) + # Création du fichier PDF Fusionné avec le dossier complet + output_path = f"registration_files/dossier_rf_{registerForm.pk}/dossier_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, output_path) # Mise à jour de l'automate updateStateMachine(registerForm, 'saisiDI') - elif status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: # L'école a validé le dossier d'inscription # Mise à jour de l'automate updateStateMachine(registerForm, 'valideDI') @@ -162,34 +204,49 @@ class RegisterFormView(APIView): studentForm_serializer.save() return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, id): + """ + Supprime un dossier d'inscription donné. + """ register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) if register_form != None: student = register_form.student student.guardians.clear() student.profiles.clear() + student.registration_files.clear() student.delete() clear_cache() return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) class StudentView(APIView): + """ + Gère la lecture d’un élève donné. + """ def get(self, request, _id): student = bdd.getObject(_objectName=Student, _columnName='id', _value=_id) + if student is None: + return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) student_serializer = StudentSerializer(student) return JsonResponse(student_serializer.data, safe=False) class GuardianView(APIView): + """ + Récupère le dernier ID de responsable légal créé. + """ def get(self, request): lastGuardian = bdd.getLastId(Guardian) return JsonResponse({"lastid":lastGuardian}, safe=False) -def send(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def send(request, _id): + """ + Envoie le dossier d’inscription par e-mail. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: student = register_form.student guardian = student.getMainGuardian() @@ -199,24 +256,31 @@ def send(request, id): register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') # Mise à jour de l'automate updateStateMachine(register_form, 'envoiDI') + return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def archive(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def archive(request, _id): + """ + Archive le dossier d’inscription visé. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') # Mise à jour de l'automate updateStateMachine(register_form, 'archiveDI') - return JsonResponse({"errorMessage":''}, safe=False, status=400) + return JsonResponse({"errorMessage":''}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def relance(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +def relance(request, _id): + """ + Relance un dossier d’inscription par e-mail. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) if register_form != None: student = register_form.student guardian = student.getMainGuardian() @@ -227,12 +291,15 @@ def relance(request, id): register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') register_form.save() - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) # API utilisée pour la vue parent class ChildrenListView(APIView): + """ + Pour la vue parent : liste les élèves rattachés à un profil donné. + """ # Récupération des élèves d'un parent # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves def get(self, request, _idProfile): @@ -242,6 +309,9 @@ class ChildrenListView(APIView): # API utilisée pour la vue de création d'un DI class StudentListView(APIView): + """ + Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. + """ # Récupération de la liste des élèves inscrits ou en cours d'inscriptions def get(self, request): students = bdd.getAllObjects(_objectName=Student) @@ -250,20 +320,52 @@ class StudentListView(APIView): # API utilisée pour la vue de personnalisation des frais d'inscription pour la structure class RegistrationFeeView(APIView): + """ + Liste les frais d’inscription. + """ def get(self, request): tarifs = bdd.getAllObjects(RegistrationFee) tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True) return JsonResponse(tarifs_serializer.data, safe=False) class RegistrationFileTemplateView(APIView): + """ + Gère les fichiers templates pour les dossiers d’inscription. + """ parser_classes = (MultiPartParser, FormParser) - def get(self, request): - fichiers = RegistrationFileTemplate.objects.all() - serializer = RegistrationFileTemplateSerializer(fichiers, many=True) - return Response(serializer.data) + def get(self, request, _id=None): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + if _id is None: + files = RegistrationFileTemplate.objects.all() + serializer = RegistrationFileTemplateSerializer(files, many=True) + return Response(serializer.data) + else : + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) + if registationFileTemplate is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate) + return JsonResponse(serializer.data, safe=False) + + def put(self, request, _id): + """ + Met à jour un fichier template existant. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) + if registationFileTemplate is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def post(self, request): + """ + Crée un fichier template pour les dossiers d’inscription. + """ serializer = RegistrationFileTemplateSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -271,10 +373,71 @@ class RegistrationFileTemplateView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, _id): + """ + Supprime un fichier template existant. + """ registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) if registrationFileTemplate is not None: registrationFileTemplate.file.delete() # Supprimer le fichier uploadé registrationFileTemplate.delete() - return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False) + return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) else: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=400) + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationFileView(APIView): + """ + Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + def get(self, request, _id=None): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + if (_id is None): + files = RegistrationFile.objects.all() + serializer = RegistrationFileSerializer(files, many=True) + return Response(serializer.data) + else: + registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) + if registationFile is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registationFile) + return JsonResponse(serializer.data, safe=False) + + def post(self, request): + """ + Crée un RegistrationFile pour le RegistrationForm associé. + """ + serializer = RegistrationFileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def put(self, request, fileId): + """ + Met à jour un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId) + if registrationFile is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registrationFile, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, _id): + """ + Supprime un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) + if registrationFile is not None: + registrationFile.file.delete() # Supprimer le fichier uploadé + registrationFile.delete() + return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) + else: + return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + diff --git a/Back-End/start.py b/Back-End/start.py index 0b0aff0..be242c4 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -13,12 +13,12 @@ def run_command(command): commands = [ ["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "flush", "--noinput"], - ["python", "manage.py", "makemigrations", "Subscriptions"], - ["python", "manage.py", "makemigrations", "GestionNotification"], - ["python", "manage.py", "makemigrations", "GestionMessagerie"], - ["python", "manage.py", "makemigrations", "Auth"], - ["python", "manage.py", "makemigrations", "School"], - ["python", "manage.py", "migrate"] + ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"], + ["python", "manage.py", "makemigrations", "Auth", "--noinput"], + ["python", "manage.py", "makemigrations", "School", "--noinput"], + ["python", "manage.py", "migrate", "--noinput"] ] for command in commands: diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 365a30f..2f111a2 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,28 +1,22 @@ 'use client' import React, { useState, useEffect } from 'react'; -import { School, Calendar } from 'lucide-react'; -import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; -import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' -import StructureManagement from '@/components/Structure/Configuration/StructureManagement' -import { BE_SCHOOL_SPECIALITIES_URL, - BE_SCHOOL_SCHOOLCLASSES_URL, - BE_SCHOOL_TEACHERS_URL, - BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url'; -import DjangoCSRFToken from '@/components/DjangoCSRFToken' +import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign +import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; +import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; +import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); - const [schedules, setSchedules] = useState([]); - const [activeTab, setActiveTab] = useState('Configuration'); - const tabs = [ - { id: 'Configuration', title: "Configuration de l'école", icon: School }, - { id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar }, - ]; + const [fees, setFees] = useState([]); + const [discounts, setDiscounts] = useState([]); + const [tuitionFees, setTuitionFees] = useState([]); const csrfToken = useCsrfToken(); @@ -38,6 +32,15 @@ export default function Page() { // Fetch data for schedules handleSchedules(); + + // Fetch data for fees + handleFees(); + + // Fetch data for discounts + handleDiscounts(); + + // Fetch data for TuitionFee + handleTuitionFees(); }, []); const handleSpecialities = () => { @@ -45,9 +48,7 @@ export default function Page() { .then(data => { setSpecialities(data); }) - .catch(error => { - console.error('Error fetching specialities:', error); - }); + .catch(error => console.error('Error fetching specialities:', error)); }; const handleTeachers = () => { @@ -55,9 +56,7 @@ export default function Page() { .then(data => { setTeachers(data); }) - .catch(error => { - console.error('Error fetching teachers:', error); - }); + .catch(error => console.error('Error fetching teachers:', error)); }; const handleClasses = () => { @@ -65,9 +64,7 @@ export default function Page() { .then(data => { setClasses(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching classes:', error)); }; const handleSchedules = () => { @@ -75,13 +72,35 @@ export default function Page() { .then(data => { setSchedules(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching schedules:', error)); }; - const handleCreate = (url, newData, setDatas) => { - fetch(url, { + const handleFees = () => { + fetchFees() + .then(data => { + setFees(data); + }) + .catch(error => console.error('Error fetching fees:', error)); + }; + + const handleDiscounts = () => { + fetchDiscounts() + .then(data => { + setDiscounts(data); + }) + .catch(error => console.error('Error fetching discounts:', error)); + }; + + const handleTuitionFees = () => { + fetchTuitionFees() + .then(data => { + setTuitionFees(data); + }) + .catch(error => console.error('Error fetching tuition fees', error)); + }; + + const handleCreate = (url, newData, setDatas, setErrors) => { + return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -90,18 +109,28 @@ export default function Page() { body: JSON.stringify(newData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { - console.log('Succes :', data); setDatas(prevState => [...prevState, data]); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error creating data:', error); + throw error; }); }; - const handleEdit = (url, id, updatedData, setDatas) => { - fetch(`${url}/${id}`, { + const handleEdit = (url, id, updatedData, setDatas, setErrors) => { + return fetch(`${url}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -110,15 +139,41 @@ export default function Page() { body: JSON.stringify(updatedData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { setDatas(prevState => prevState.map(item => item.id === id ? data : item)); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error editing data:', error); + throw error; }); }; + const handleDelete = (url, id, setDatas) => { + fetch(`${url}/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' + }) + .then(response => response.json()) + .then(data => { + setDatas(prevState => prevState.filter(item => item.id !== id)); + }) + .catch(error => console.error('Error deleting data:', error)); + }; const handleUpdatePlanning = (url, planningId, updatedData) => { fetch(`${url}/${planningId}`, { method: 'PUT', @@ -139,35 +194,11 @@ export default function Page() { }); }; - const handleDelete = (url, id, setDatas) => { - fetch(`${url}/${id}`, { - method:'DELETE', - headers: { - 'Content-Type':'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include' - }) - .then(response => response.json()) - .then(data => { - console.log('Success:', data); - setDatas(prevState => prevState.filter(item => item.id !== id)); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); - }); - }; - - return ( -
- - - - - {activeTab === 'Configuration' && ( - <> + const tabs = [ + { + id: 'Configuration', + label: "Configuration de l'école", + content: ( - - )} - - {activeTab === 'Schedule' && ( + handleDelete={handleDelete} + /> + ) + }, + { + id: 'Schedule', + label: "Gestion de l'emploi du temps", + content: ( - )} + ) + }, + { + id: 'Fees', + label: 'Tarifications', + content: ( + + ) + } + ]; + + return ( +
+ + +
+ +
+
); -}; +} diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js b/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js new file mode 100644 index 0000000..38a4bbb --- /dev/null +++ b/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { Upload } from 'lucide-react'; + +export default function DraggableFileUpload({ fileName, onFileSelect }) { + const [dragActive, setDragActive] = useState(false); + + + const handleDragOver = (event) => { + event.preventDefault(); + setDragActive(true); + }; + + const handleDragLeave = () => { + setDragActive(false); + }; + + const handleFileChosen = (selectedFile) => { + onFileSelect && onFileSelect(selectedFile); + }; + + const handleDrop = (event) => { + event.preventDefault(); + setDragActive(false); + const droppedFile = event.dataTransfer.files[0]; + handleFileChosen(droppedFile); + }; + + const handleFileChange = (event) => { + const selectedFile = event.target.files[0]; + handleFileChosen(selectedFile); + }; + + return ( +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js index db1c60b..92cd485 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js @@ -1,61 +1,47 @@ -import React, { useState } from 'react'; -import { Upload } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch +import DraggableFileUpload from './DraggableFileUpload'; -export default function FileUpload({ onFileUpload }) { - const [dragActive, setDragActive] = useState(false); +export default function FileUpload({ onFileUpload, fileToEdit = null }) { const [fileName, setFileName] = useState(''); const [file, setFile] = useState(null); + const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired + const [order, setOrder] = useState(0); - const handleDragOver = (event) => { - event.preventDefault(); - setDragActive(true); - }; - - const handleDragLeave = () => { - setDragActive(false); - }; - - const handleDrop = (event) => { - event.preventDefault(); - setDragActive(false); - const droppedFile = event.dataTransfer.files[0]; - setFile(droppedFile); - setFileName(droppedFile.name.replace(/\.[^/.]+$/, "")); - }; - - const handleFileChange = (event) => { - const selectedFile = event.target.files[0]; - setFile(selectedFile); - setFileName(selectedFile.name.replace(/\.[^/.]+$/, "")); - }; + useEffect(() => { + if (fileToEdit) { + setFileName(fileToEdit.name || ''); + setIsRequired(fileToEdit.is_required || false); + setOrder(fileToEdit.fusion_order || 0); + } + }, [fileToEdit]); const handleFileNameChange = (event) => { setFileName(event.target.value); }; const handleUpload = () => { - - onFileUpload(file, fileName); - setFile(null); - setFileName(''); - + onFileUpload({ + file, + name: fileName, + is_required: isRequired, + order: parseInt(order, 10), + }); + setFile(null); + setFileName(''); + setIsRequired(false); + setOrder(0); }; return (
-
- - -
+ { + setFile(selectedFile); + setFileName(selectedFile.name.replace(/\.[^/.]+$/, "")); + }} + />
+ setOrder(e.target.value)} + placeholder="Ordre de fusion" + className="p-2 border border-gray-200 rounded-md ml-2 w-20" + />
+
+ setIsRequired(!isRequired)} + /> +
); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js index 9f7cab6..35d0520 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editInscription/page.js @@ -17,6 +17,7 @@ export default function Page() { const [initialData, setInitialData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); useEffect(() => { @@ -55,9 +56,8 @@ export default function Page() { console.error('Error:', error.message); if (error.details) { console.error('Form errors:', error.details); - // Handle form errors (e.g., display them to the user) + setFormErrors(error.details); } - alert('Une erreur est survenue lors de la mise à jour des données'); }); }; @@ -69,6 +69,7 @@ export default function Page() { onSubmit={handleSubmit} cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} isLoading={isLoading} + errors={formErrors} /> ); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 10a865c..5ddb2da 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -28,6 +28,7 @@ import { fetchRegisterFormFileTemplate, deleteRegisterFormFileTemplate, createRegistrationFormFileTemplate, + editRegistrationFormFileTemplate, fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" @@ -40,6 +41,7 @@ import { import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; +import { formatDate } from '@/utils/Date'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -69,6 +71,9 @@ export default function Page({ params: { locale } }) { const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [fileToEdit, setFileToEdit] = useState(null); const csrfToken = useCsrfToken(); @@ -185,7 +190,11 @@ const registerFormArchivedDataHandler = (data) => { .then(registerFormArchivedDataHandler) .catch(requestErrorHandler) fetchRegisterFormFileTemplate() - .then((data)=> {setFichiers(data)}) + .then((data)=> { + console.log(data); + + setFichiers(data) + }) .catch((err)=>{ err = err.message; console.log(err);}); } else { setTimeout(() => { @@ -548,9 +557,17 @@ const handleFileDelete = (fileId) => { }); }; +const handleFileEdit = (file) => { + setIsEditing(true); + setFileToEdit(file); + setIsModalOpen(true); +}; + const columnsFiles = [ { name: 'Nom du fichier', transform: (row) => row.name }, - { name: 'Date de création', transform: (row) => row.last_update }, + { 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) => (
{ @@ -559,6 +576,9 @@ const columnsFiles = [ ) } + @@ -566,27 +586,43 @@ const columnsFiles = [ ) }, ]; -const handleFileUpload = (file, fileName) => { - if ( !fileName) { +const handleFileUpload = ({file, name, is_required, order}) => { + if (!name) { alert('Veuillez entrer un nom de fichier.'); return; } - const formData = new FormData(); if(file){ formData.append('file', file); } - formData.append('name', fileName); - createRegistrationFormFileTemplate(formData,csrfToken) - .then(data => { - console.log('Success:', data); - setFichiers([...fichiers, data]); - closeUploadModal(); - }) - .catch(error => { - console.error('Error uploading file:', error); - }); + formData.append('name', name); + formData.append('is_required', is_required); + formData.append('order', order); + + if (isEditing && fileToEdit) { + editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) + .then(data => { + setFichiers(prevFichiers => + prevFichiers.map(f => f.id === fileToEdit.id ? data : f) + ); + setIsModalOpen(false); + setFileToEdit(null); + setIsEditing(false); + }) + .catch(error => { + console.error('Error editing file:', error); + }); + } else { + createRegistrationFormFileTemplate(formData, csrfToken) + .then(data => { + setFichiers([...fichiers, data]); + setIsModalOpen(false); + }) + .catch(error => { + console.error('Error uploading file:', error); + }); + } }; if (isLoading) { @@ -699,7 +735,23 @@ const handleFileUpload = (file, fileName) => { {/*SI STATE == subscribeFiles */} {activeTab === 'subscribeFiles' && (
- + + ( + + )} + />
{ - setUserId(userId); - fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${userId}`, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(response => response.json()) + setUserId(userId) + fetchMessages(userId) .then(data => { if (data) { setMessages(data); @@ -33,11 +31,10 @@ export default function Layout({ .catch(error => { console.error('Error fetching data:', error); }); - }, []); return ( - <> +
{/* Entête */}
@@ -85,7 +82,7 @@ export default function Layout({ {children}
- +
); } diff --git a/Front-End/src/app/lib/messagerieAction.js b/Front-End/src/app/lib/messagerieAction.js new file mode 100644 index 0000000..d7623fc --- /dev/null +++ b/Front-End/src/app/lib/messagerieAction.js @@ -0,0 +1,24 @@ +import { +BE_GESTIONMESSAGERIE_MESSAGES_URL +} from '@/utils/Url'; + +const requestResponseHandler = async (response) => { + + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error('Form submission error'); + error.details = body; + throw error; +} + + +export const fetchMessages = (id) =>{ + return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) +} \ No newline at end of file diff --git a/Front-End/src/app/lib/schoolAction.js b/Front-End/src/app/lib/schoolAction.js index 6ee39ba..9675da9 100644 --- a/Front-End/src/app/lib/schoolAction.js +++ b/Front-End/src/app/lib/schoolAction.js @@ -2,7 +2,10 @@ import { BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_SCHOOLCLASSES_URL, - BE_SCHOOL_PLANNINGS_URL + BE_SCHOOL_PLANNINGS_URL, + BE_SCHOOL_FEES_URL, + BE_SCHOOL_DISCOUNTS_URL, + BE_SCHOOL_TUITION_FEES_URL } from '@/utils/Url'; const requestResponseHandler = async (response) => { @@ -36,4 +39,19 @@ export const fetchClasses = () => { export const fetchSchedules = () => { return fetch(`${BE_SCHOOL_PLANNINGS_URL}`) .then(requestResponseHandler) -}; \ No newline at end of file +}; + +export const fetchDiscounts = () => { + return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`) + .then(requestResponseHandler) +}; + +export const fetchFees = () => { + return fetch(`${BE_SCHOOL_FEES_URL}`) + .then(requestResponseHandler) +}; + +export const fetchTuitionFees = () => { + return fetch(`${BE_SCHOOL_TUITION_FEES_URL}`) + .then(requestResponseHandler) +}; diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js index 7adabb5..39ff411 100644 --- a/Front-End/src/app/lib/subscriptionAction.js +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -7,7 +7,8 @@ import { BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, - BE_SUBSCRIPTION_LAST_GUARDIAN_URL + BE_SUBSCRIPTION_LAST_GUARDIAN_URL, + BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL } from '@/utils/Url'; export const PENDING = 'pending'; @@ -110,6 +111,32 @@ export const fetchRegisterFormFileTemplate = () => { return fetch(request).then(requestResponseHandler) }; +export const fetchRegisterFormFile = (id) => { + const request = new Request( + `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`, + { + method:'GET', + headers: { + 'Content-Type':'application/json' + }, + } + ); + return fetch(request).then(requestResponseHandler) +}; + +export const createRegistrationFormFile = (data,csrfToken) => { + + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}`, { + method: 'POST', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(requestResponseHandler) +} + export const createRegistrationFormFileTemplate = (data,csrfToken) => { return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { @@ -132,6 +159,19 @@ export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => { credentials: 'include', }) } + +export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => { + return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, { + method: 'PUT', + body: data, + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include', + }) + .then(requestResponseHandler) +} + export const fetchStudents = () => { const request = new Request( `${BE_SUBSCRIPTION_STUDENTS_URL}`, diff --git a/Front-End/src/components/InputColorIcon.js b/Front-End/src/components/InputColorIcon.js index 1aa8243..7120dec 100644 --- a/Front-End/src/components/InputColorIcon.js +++ b/Front-End/src/components/InputColorIcon.js @@ -6,8 +6,8 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) = <>
-
- +
+
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/InputPhone.js b/Front-End/src/components/InputPhone.js index 26e4dfb..4f11a1a 100644 --- a/Front-End/src/components/InputPhone.js +++ b/Front-End/src/components/InputPhone.js @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { isValidPhoneNumber } from 'react-phone-number-input'; + export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) { const inputRef = useRef(null); @@ -19,12 +19,12 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla <>
-
+
- +
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/InputTextIcon.js b/Front-End/src/components/InputTextIcon.js index 1f792b8..8b60ba2 100644 --- a/Front-End/src/components/InputTextIcon.js +++ b/Front-End/src/components/InputTextIcon.js @@ -1,11 +1,10 @@ export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) { - return ( <>
-
- +
+ {IconItem && }
{errorMsg &&

{errorMsg}

} diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index 3d2a0f8..051f749 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -5,8 +5,12 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie import Loader from '@/components/Loader'; import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; -import FileUpload from '@/app/[locale]/admin/subscriptions/components/FileUpload'; import Table from '@/components/Table'; +import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction'; +import { Download, Upload } from 'lucide-react'; +import { BASE_URL } from '@/utils/Url'; +import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload'; +import Modal from '@/components/Modal'; const levels = [ { value:'1', label: 'TPS - Très Petite Section'}, @@ -20,7 +24,8 @@ export default function InscriptionFormShared({ csrfToken, onSubmit, cancelUrl, - isLoading = false + isLoading = false, + errors = {} // Nouvelle prop pour les erreurs }) { const [formData, setFormData] = useState(() => ({ @@ -41,6 +46,11 @@ export default function InscriptionFormShared({ ); const [uploadedFiles, setUploadedFiles] = useState([]); + const [fileTemplates, setFileTemplates] = useState([]); + const [fileName, setFileName] = useState(""); + const [file, setFile] = useState(""); + const [showUploadModal, setShowUploadModal] = useState(false); + const [currentTemplateId, setCurrentTemplateId] = useState(null); // Mettre à jour les données quand initialData change useEffect(() => { @@ -58,6 +68,9 @@ export default function InscriptionFormShared({ level: initialData.level || '' }); setGuardians(initialData.guardians || []); + fetchRegisterFormFileTemplate().then((data) => { + setFileTemplates(data); + }); } }, [initialData]); @@ -65,8 +78,22 @@ export default function InscriptionFormShared({ setFormData(prev => ({...prev, [field]: value})); }; - const handleFileUpload = (file, fileName) => { - setUploadedFiles([...uploadedFiles, { file, fileName }]); + const handleFileUpload = async (file, fileName) => { + const data = new FormData(); + data.append('file', file); + data.append('name',fileName); + data.append('template', currentTemplateId); + data.append('register_form', formData.id); + + try { + await createRegistrationFormFile(data, csrfToken); + // Optionnellement, rafraîchir la liste des fichiers + fetchRegisterFormFileTemplate().then((data) => { + setFileTemplates(data); + }); + } catch (error) { + console.error('Error uploading file:', error); + } }; const handleSubmit = (e) => { @@ -80,12 +107,31 @@ export default function InscriptionFormShared({ onSubmit(data); }; + const getError = (field) => { + return errors?.student?.[field]?.[0]; + }; + + const getGuardianError = (index, field) => { + return errors?.student?.guardians?.[index]?.[field]?.[0]; + }; + const columns = [ - { name: 'Nom du fichier', transform: (row) => row.last_name }, + { name: 'Nom du fichier', transform: (row) => row.name }, + { name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' }, + { name: 'Fichier de référence', transform: (row) => row.file && }, { name: 'Actions', transform: (row) => ( - - Télécharger - +
+ {row.is_required && + + } +
) }, ]; @@ -105,12 +151,14 @@ export default function InscriptionFormShared({ value={formData.last_name} onChange={(e) => updateFormField('last_name', e.target.value)} required + errorMsg={getError('last_name')} /> updateFormField('first_name', e.target.value)} + errorMsg={getError('first_name')} required /> updateFormField('birth_date', e.target.value)} required + errorMsg={getError('birth_date')} /> updateFormField('birth_place', e.target.value)} + errorMsg={getError('birth_place')} /> updateFormField('birth_postal_code', e.target.value)} + required + errorMsg={getError('birth_postal_code')} />
updateFormField('address', e.target.value)} + errorMsg={getError('address')} />
updateFormField('attending_physician', e.target.value)} + errorMsg={getError('attending_physician')} /> updateFormField('level', e.target.value)} choices={levels} required + errorMsg={getError('level')} />
@@ -184,6 +240,7 @@ export default function InscriptionFormShared({ newArray.splice(index, 1); setGuardians(newArray); }} + errors={errors?.student?.guardians || []} />
@@ -191,14 +248,13 @@ export default function InscriptionFormShared({

Fichiers à remplir

{}} /> - {/* Boutons de contrôle */} @@ -207,6 +263,44 @@ export default function InscriptionFormShared({ - +
+ {!uniqueConfirmButton && ( + + )} +
, diff --git a/Front-End/src/components/ProtectedRoute.js b/Front-End/src/components/ProtectedRoute.js new file mode 100644 index 0000000..61c7671 --- /dev/null +++ b/Front-End/src/components/ProtectedRoute.js @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import useLocalStorage from '@/hooks/useLocalStorage'; +import { FE_USERS_LOGIN_URL } from '@/utils/Url'; + +const ProtectedRoute = ({ children }) => { + const router = useRouter(); + const [userId] = useLocalStorage("userId", ''); + + useEffect(() => { + if (!userId) { + // Rediriger vers la page de login si l'utilisateur n'est pas connecté + router.push(FE_USERS_LOGIN_URL); + } + }, [userId, router]); + + // Afficher les enfants seulement si l'utilisateur est connecté + return userId ? children : null; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/Front-End/src/components/SelectChoice.js b/Front-End/src/components/SelectChoice.js index 4040ea1..6e3fbd7 100644 --- a/Front-End/src/components/SelectChoice.js +++ b/Front-End/src/components/SelectChoice.js @@ -1,34 +1,36 @@ -export default function SelectChoice({ type, name, label, choices, callback, selected, error, IconItem, disabled = false }) { +export default function SelectChoice({ type, name, label,required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) { return ( <>
- -
- - {IconItem && } - + +
+ {IconItem && + + {} + + }
- {error &&

{error}

} + {errorMsg &&

{errorMsg}

}
); -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/Front-End/src/components/SidebarTabs.js b/Front-End/src/components/SidebarTabs.js new file mode 100644 index 0000000..ada4f20 --- /dev/null +++ b/Front-End/src/components/SidebarTabs.js @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; + +const SidebarTabs = ({ tabs }) => { + const [activeTab, setActiveTab] = useState(tabs[0].id); + + return ( +
+
+ {tabs.map(tab => ( + + ))} +
+
+ {tabs.map(tab => ( +
+ {tab.content} +
+ ))} +
+
+ ); +}; + +export default SidebarTabs; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/ClassForm.js b/Front-End/src/components/Structure/Configuration/ClassForm.js index 1d333fe..01cf73e 100644 --- a/Front-End/src/components/Structure/Configuration/ClassForm.js +++ b/Front-End/src/components/Structure/Configuration/ClassForm.js @@ -34,15 +34,15 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { setFormData(prevState => { const updatedTimes = [...prevState.time_range]; updatedTimes[index] = value; - + const updatedFormData = { ...prevState, time_range: updatedTimes, }; - + const existingPlannings = prevState.plannings || []; updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings); - + return updatedFormData; }); }; @@ -50,24 +50,24 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { const handleJoursChange = (e) => { const { value, checked } = e.target; const dayId = parseInt(value, 10); - + setFormData((prevState) => { const updatedJoursOuverture = checked ? [...prevState.opening_days, dayId] : prevState.opening_days.filter((id) => id !== dayId); - + const updatedFormData = { ...prevState, opening_days: updatedJoursOuverture, }; - + const existingPlannings = prevState.plannings || []; updatedFormData.plannings = updatePlannings(updatedFormData, existingPlannings); - + return updatedFormData; }); }; - + const handleChange = (e) => { e.preventDefault(); const { name, value, type, checked } = e.target; @@ -78,8 +78,8 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { let newState = { ...prevState }; if (type === 'checkbox') { - const newValues = checked - ? [...(prevState[name] || []), parseInt(value)] + const newValues = checked + ? [...(prevState[name] || []), parseInt(value)] : (prevState[name] || []).filter(v => v !== parseInt(value)); newState[name] = newValues; } else if (name === 'age_range') { @@ -117,14 +117,14 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => { return (
- +
{/* Section Ambiance */}
- { />
- {
- {
- {
{/* Section Enseignants */} - {/* Section Emploi du temps */} -
diff --git a/Front-End/src/components/Structure/Configuration/ClassesSection.js b/Front-End/src/components/Structure/Configuration/ClassesSection.js index 8b4cba3..d2f8d07 100644 --- a/Front-End/src/components/Structure/Configuration/ClassesSection.js +++ b/Front-End/src/components/Structure/Configuration/ClassesSection.js @@ -1,4 +1,4 @@ -import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react'; +import { Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react'; import { useState } from 'react'; import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; @@ -49,11 +49,8 @@ const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDel return (
-
-

- - Classes -

+
+

Gestion des classes

+ +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'LIBELLE': + return discount.name; + case 'MONTANT': + return discount.amount + ' €'; + case 'DESCRIPTION': + return discount.description; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( +
+
+

Réductions

+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default DiscountsSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/FeesManagement.js b/Front-End/src/components/Structure/Configuration/FeesManagement.js new file mode 100644 index 0000000..ef4ef98 --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/FeesManagement.js @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import FeesSection from './FeesSection'; +import DiscountsSection from './DiscountsSection'; +import TuitionFeesSection from './TuitionFeesSection'; +import { TuitionFeesProvider } from '@/context/TuitionFeesContext'; +import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url'; + +const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => { + const [errors, setErrors] = useState({}); + + return ( + +
+
+ handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)} + errors + /> +
+
+ handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)} + /> +
+
+ handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)} + handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)} + handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)} + /> +
+
+
+ ); +}; + +export default FeesManagement; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/FeesSection.js b/Front-End/src/components/Structure/Configuration/FeesSection.js new file mode 100644 index 0000000..60f494c --- /dev/null +++ b/Front-End/src/components/Structure/Configuration/FeesSection.js @@ -0,0 +1,190 @@ +import React, { useState } from 'react'; +import { Plus, Trash, Edit3, Check, X } from 'lucide-react'; +import Table from '@/components/Table'; +import InputTextIcon from '@/components/InputTextIcon'; +import Popup from '@/components/Popup'; + +const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => { + const [editingFee, setEditingFee] = useState(null); + const [newFee, setNewFee] = useState(null); + const [formData, setFormData] = useState({}); + const [localErrors, setLocalErrors] = useState({}); + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + + const handleAddFee = () => { + setNewFee({ id: Date.now(), name: '', amount: '', description: '' }); + }; + + const handleRemoveFee = (id) => { + handleDelete(id); + }; + + const handleSaveNewFee = () => { + if (newFee.name && newFee.amount) { + handleCreate(newFee) + .then(() => { + setNewFee(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis"); + setPopupVisible(true); + } + }; + + const handleUpdateFee = (id, updatedFee) => { + if (updatedFee.name && updatedFee.amount) { + handleEdit(id, updatedFee) + .then(() => { + setEditingFee(null); + setLocalErrors({}); + }) + .catch(error => { + if (error && typeof error === 'object') { + setLocalErrors(error); + } else { + console.error(error); + } + }); + } else { + setPopupMessage("Tous les champs doivent être remplis"); + setPopupVisible(true); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + if (editingFee) { + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + } else if (newFee) { + setNewFee((prevData) => ({ + ...prevData, + [name]: value, + })); + } + }; + + const renderInputField = (field, value, onChange, placeholder) => ( +
+ +
+ ); + + const renderFeeCell = (fee, column) => { + const isEditing = editingFee === fee.id; + const isCreating = newFee && newFee.id === fee.id; + const currentData = isEditing ? formData : newFee; + + if (isEditing || isCreating) { + switch (column) { + case 'LIBELLE': + return renderInputField('name', currentData.name, handleChange, 'Libellé du frais'); + case 'MONTANT': + return renderInputField('amount', currentData.amount, handleChange, 'Montant'); + case 'DESCRIPTION': + return renderInputField('description', currentData.description, handleChange, 'Description'); + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'LIBELLE': + return fee.name; + case 'MONTANT': + return fee.amount + ' €'; + case 'DESCRIPTION': + return fee.description; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( + <> +
+
+

Frais d'inscription

+ +
+
+ + setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default FeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js index 8ec7b45..b4149bc 100644 --- a/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js +++ b/Front-End/src/components/Structure/Configuration/SpecialitiesSection.js @@ -1,4 +1,4 @@ -import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; +import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; import { useState } from 'react'; import Table from '@/components/Table'; import DropdownMenu from '@/components/DropdownMenu'; @@ -33,10 +33,7 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel return (
-

- - Spécialités -

+

Gestion des spécialités

+ +
+ ); + default: + return null; + } + } else { + switch (column) { + case 'NOM': + return tuitionFee.name; + case 'MONTANT DE BASE': + return tuitionFee.base_amount + ' €'; + case 'DESCRIPTION': + return tuitionFee.description; + case 'DATE DE DEBUT': + return tuitionFee.validity_start_date; + case 'DATE DE FIN': + return tuitionFee.validity_end_date; + case 'OPTIONS DE PAIEMENT': + return paymentOptions.find(option => option.value === tuitionFee.payment_option)?.label || ''; + case 'REMISES': + const discountNames = tuitionFee.discounts + .map(discountId => discounts.find(discount => discount.id === discountId)?.name) + .filter(name => name) + .join(', '); + return discountNames; + case 'MONTANT FINAL': + return calculateFinalAmount(tuitionFee.base_amount, tuitionFee.discounts) + ' €'; + case 'ACTIONS': + return ( +
+ + +
+ ); + default: + return null; + } + } + }; + + return ( +
+
+

Frais de scolarité

+ +
+
+ setPopupVisible(false)} + onCancel={() => setPopupVisible(false)} + uniqueConfirmButton={true} + /> + + ); +}; + +export default TuitionFeesSection; \ No newline at end of file diff --git a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js index bd4b74e..3c93d1b 100644 --- a/Front-End/src/components/Structure/Planning/SpecialityEventModal.js +++ b/Front-End/src/components/Structure/Planning/SpecialityEventModal.js @@ -160,7 +160,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
{
- -
diff --git a/Front-End/src/context/ClassesContext.js b/Front-End/src/context/ClassesContext.js index 7cc291c..21b86b0 100644 --- a/Front-End/src/context/ClassesContext.js +++ b/Front-End/src/context/ClassesContext.js @@ -9,7 +9,6 @@ export const ClassesProvider = ({ children }) => { const currentYear = new Date().getFullYear(); const schoolYears = [ - { value: '', label: 'Sélectionner une période' }, { value: `${currentYear - 1}-${currentYear}`, label: `${currentYear - 1}-${currentYear}` }, { value: `${currentYear}-${currentYear + 1}`, label: `${currentYear}-${currentYear + 1}` }, { value: `${currentYear + 1}-${currentYear + 2}`, label: `${currentYear + 1}-${currentYear + 2}` }, diff --git a/Front-End/src/context/TuitionFeesContext.js b/Front-End/src/context/TuitionFeesContext.js new file mode 100644 index 0000000..dd356e3 --- /dev/null +++ b/Front-End/src/context/TuitionFeesContext.js @@ -0,0 +1,24 @@ +import React, { createContext, useState, useEffect, useContext } from 'react'; +import { fetchTuitionFees, fetchFees, fetchDiscounts } from '@/app/lib/schoolAction'; + +const TuitionFeesContext = createContext(); + +export const useTuitionFees = () => useContext(TuitionFeesContext); + +export const TuitionFeesProvider = ({ children }) => { + const [tuitionFees, setTuitionFees] = useState([]); + const [fees, setFees] = useState([]); + const [discounts, setDiscounts] = useState([]); + + useEffect(() => { + fetchTuitionFees().then(data => setTuitionFees(data)); + fetchFees().then(data => setFees(data)); + fetchDiscounts().then(data => setDiscounts(data)); + }, []); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index b99268e..82c3e20 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -23,6 +23,7 @@ export const BE_SUBSCRIPTION_ARCHIVE_URL = `${BASE_URL}/Subscriptions/archive` export const BE_SUBSCRIPTION_REGISTERFORM_URL = `${BASE_URL}/Subscriptions/registerForm` export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms` export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registrationFileTemplates` +export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL = `${BASE_URL}/Subscriptions/registrationFiles` export const BE_SUBSCRIPTION_LAST_GUARDIAN_URL = `${BASE_URL}/Subscriptions/lastGuardian` //GESTION ENSEIGNANT @@ -34,6 +35,12 @@ export const BE_SCHOOL_TEACHER_URL = `${BASE_URL}/School/teacher` export const BE_SCHOOL_TEACHERS_URL = `${BASE_URL}/School/teachers` export const BE_SCHOOL_PLANNING_URL = `${BASE_URL}/School/planning` export const BE_SCHOOL_PLANNINGS_URL = `${BASE_URL}/School/plannings` +export const BE_SCHOOL_FEE_URL = `${BASE_URL}/School/fee`; +export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`; +export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`; +export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`; +export const BE_SCHOOL_TUITION_FEE_URL = `${BASE_URL}/School/tuitionFee`; +export const BE_SCHOOL_TUITION_FEES_URL = `${BASE_URL}/School/tuitionFees`; // GESTION MESSAGERIE export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` @@ -47,8 +54,7 @@ export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe` export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset` export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new` - -//ADMIN +// ADMIN export const FE_ADMIN_HOME_URL = `/admin` // ADMIN/SUBSCRIPTIONS URL