feat: Ajout de la configuration des tarifs de l'école [#18]

This commit is contained in:
N3WT DE COMPET
2025-01-19 21:00:58 +01:00
committed by Luc SORIGNET
parent 147a70135d
commit 5a0e65bb75
45 changed files with 2089 additions and 376 deletions

View File

@ -7,6 +7,7 @@ FROM python:3.12.7
# Allows docker to cache installed dependencies between builds # Allows docker to cache installed dependencies between builds
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
RUN pip install pymupdf
# Mounts the application code to the image # Mounts the application code to the image
COPY . . COPY . .

View File

@ -92,6 +92,7 @@ def searchObjects(_objectName, _searchTerm=None, _excludeStates=None):
def delete_object(model_class, object_id, related_field=None): def delete_object(model_class, object_id, related_field=None):
try: try:
obj = model_class.objects.get(id=object_id) obj = model_class.objects.get(id=object_id)
if related_field and hasattr(obj, related_field): if related_field and hasattr(obj, related_field):
related_obj = getattr(obj, related_field) related_obj = getattr(obj, related_field)
if related_obj: 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) return JsonResponse({'error': f'L\'objet {model_class.__name__} n\'existe pas avec cet ID'}, status=404, safe=False)
except Exception as e: except Exception as e:
return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False) return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False)

View File

@ -3,6 +3,9 @@ from Auth.models import Profile
from django.db.models import JSONField from django.db.models import JSONField
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
LEVEL_CHOICES = [ LEVEL_CHOICES = [
(1, 'Très Petite Section (TPS)'), (1, 'Très Petite Section (TPS)'),
@ -47,7 +50,7 @@ class SchoolClass(models.Model):
number_of_students = models.PositiveIntegerField(blank=True) number_of_students = models.PositiveIntegerField(blank=True)
teaching_language = models.CharField(max_length=255, blank=True) teaching_language = models.CharField(max_length=255, blank=True)
school_year = models.CharField(max_length=9, 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) teachers = models.ManyToManyField(Teacher, blank=True)
levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list) levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list)
type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1)
@ -64,3 +67,56 @@ class Planning(models.Model):
def __str__(self): def __str__(self):
return f'Planning for {self.level} of {self.school_class.atmosphere_name}' 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

View File

@ -1,5 +1,5 @@
from rest_framework import serializers 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.models import RegistrationForm
from Subscriptions.serializers import StudentSerializer from Subscriptions.serializers import StudentSerializer
from Auth.serializers import ProfileSerializer from Auth.serializers import ProfileSerializer
@ -173,3 +173,56 @@ class SchoolClassSerializer(serializers.ModelSerializer):
local_tz = pytz.timezone(settings.TZ_APPLI) local_tz = pytz.timezone(settings.TZ_APPLI)
local_time = utc_time.astimezone(local_tz) local_time = utc_time.astimezone(local_tz)
return local_time.strftime("%d-%m-%Y %H:%M") return local_time.strftime("%d-%m-%Y %H:%M")
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

View File

@ -1,6 +1,21 @@
from django.urls import path, re_path 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 = [ urlpatterns = [
re_path(r'^specialities$', SpecialitiesView.as_view(), name="specialities"), 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'^plannings$', PlanningsView.as_view(), name="plannings"),
re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning$', PlanningView.as_view(), name="planning"),
re_path(r'^planning/([0-9]+)$', 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"),
] ]

View File

@ -5,17 +5,17 @@ from rest_framework.parsers import JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from .models import Teacher, Speciality, SchoolClass, Planning from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee
from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer
from N3wtSchool import bdd from N3wtSchool import bdd
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class SpecialitiesView(APIView): class SpecialitiesView(APIView):
def get(self, request): def get(self, request):
specialitiesList=bdd.getAllObjects(Speciality) specialitiesList = getAllObjects(Speciality)
specialities_serializer = SpecialitySerializer(specialitiesList, many=True) specialities_serializer = SpecialitySerializer(specialitiesList, many=True)
return JsonResponse(specialities_serializer.data, safe=False) return JsonResponse(specialities_serializer.data, safe=False)
def post(self, request): def post(self, request):
@ -23,28 +23,23 @@ class SpecialitiesView(APIView):
all_valid = True all_valid = True
for speciality_data in specialities_data: for speciality_data in specialities_data:
speciality_serializer = SpecialitySerializer(data=speciality_data) speciality_serializer = SpecialitySerializer(data=speciality_data)
if speciality_serializer.is_valid(): if speciality_serializer.is_valid():
speciality_serializer.save() speciality_serializer.save()
else: else:
all_valid = False all_valid = False
break break
if all_valid: if all_valid:
specialitiesList = bdd.getAllObjects(Speciality) specialitiesList = getAllObjects(Speciality)
specialities_serializer = SpecialitySerializer(specialitiesList, many=True) specialities_serializer = SpecialitySerializer(specialitiesList, many=True)
return JsonResponse(specialities_serializer.data, safe=False) return JsonResponse(specialities_serializer.data, safe=False)
return JsonResponse(speciality_serializer.errors, safe=False) return JsonResponse(speciality_serializer.errors, safe=False)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class SpecialityView(APIView): class SpecialityView(APIView):
def get(self, request, _id): def get(self, request, _id):
speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id)
speciality_serializer = SpecialitySerializer(speciality) speciality_serializer = SpecialitySerializer(speciality)
return JsonResponse(speciality_serializer.data, safe=False) return JsonResponse(speciality_serializer.data, safe=False)
def post(self, request): def post(self, request):
@ -59,7 +54,7 @@ class SpecialityView(APIView):
def put(self, request, _id): def put(self, request, _id):
speciality_data=JSONParser().parse(request) 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) speciality_serializer = SpecialitySerializer(speciality, data=speciality_data)
if speciality_serializer.is_valid(): if speciality_serializer.is_valid():
speciality_serializer.save() speciality_serializer.save()
@ -68,11 +63,62 @@ class SpecialityView(APIView):
return JsonResponse(speciality_serializer.errors, safe=False) return JsonResponse(speciality_serializer.errors, safe=False)
def delete(self, request, _id): 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): class TeachersView(APIView):
def get(self, request): def get(self, request):
teachersList=bdd.getAllObjects(Teacher) teachersList=getAllObjects(Teacher)
teachers_serializer=TeacherSerializer(teachersList, many=True) teachers_serializer=TeacherSerializer(teachersList, many=True)
return JsonResponse(teachers_serializer.data, safe=False) return JsonResponse(teachers_serializer.data, safe=False)
@ -81,7 +127,7 @@ class TeachersView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class TeacherView(APIView): class TeacherView(APIView):
def get (self, request, _id): 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) teacher_serializer=TeacherSerializer(teacher)
return JsonResponse(teacher_serializer.data, safe=False) return JsonResponse(teacher_serializer.data, safe=False)
@ -99,7 +145,7 @@ class TeacherView(APIView):
def put(self, request, _id): def put(self, request, _id):
teacher_data=JSONParser().parse(request) 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) teacher_serializer = TeacherSerializer(teacher, data=teacher_data)
if teacher_serializer.is_valid(): if teacher_serializer.is_valid():
teacher_serializer.save() teacher_serializer.save()
@ -108,13 +154,13 @@ class TeacherView(APIView):
return JsonResponse(teacher_serializer.errors, safe=False) return JsonResponse(teacher_serializer.errors, safe=False)
def delete(self, request, _id): 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(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class ClassesView(APIView): class ClassesView(APIView):
def get(self, request): def get(self, request):
classesList=bdd.getAllObjects(SchoolClass) classesList=getAllObjects(SchoolClass)
classes_serializer=SchoolClassSerializer(classesList, many=True) classes_serializer=SchoolClassSerializer(classesList, many=True)
return JsonResponse(classes_serializer.data, safe=False) return JsonResponse(classes_serializer.data, safe=False)
@ -131,7 +177,7 @@ class ClassesView(APIView):
break break
if all_valid: if all_valid:
classesList = bdd.getAllObjects(SchoolClass) classesList = getAllObjects(SchoolClass)
classes_serializer = SchoolClassSerializer(classesList, many=True) classes_serializer = SchoolClassSerializer(classesList, many=True)
return JsonResponse(classes_serializer.data, safe=False) return JsonResponse(classes_serializer.data, safe=False)
@ -142,7 +188,7 @@ class ClassesView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class ClasseView(APIView): class ClasseView(APIView):
def get (self, request, _id): 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) classe_serializer=SchoolClassSerializer(schoolClass)
return JsonResponse(classe_serializer.data, safe=False) return JsonResponse(classe_serializer.data, safe=False)
@ -159,7 +205,7 @@ class ClasseView(APIView):
def put(self, request, _id): def put(self, request, _id):
classe_data=JSONParser().parse(request) 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) classe_serializer = SchoolClassSerializer(schoolClass, data=classe_data)
if classe_serializer.is_valid(): if classe_serializer.is_valid():
classe_serializer.save() classe_serializer.save()
@ -168,14 +214,14 @@ class ClasseView(APIView):
return JsonResponse(classe_serializer.errors, safe=False) return JsonResponse(classe_serializer.errors, safe=False)
def delete(self, request, _id): def delete(self, request, _id):
return bdd.delete_object(SchoolClass, _id) return delete_object(SchoolClass, _id)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class PlanningsView(APIView): class PlanningsView(APIView):
def get(self, request): def get(self, request):
schedulesList=bdd.getAllObjects(Planning) schedulesList=getAllObjects(Planning)
schedules_serializer=PlanningSerializer(schedulesList, many=True) schedules_serializer=PlanningSerializer(schedulesList, many=True)
return JsonResponse(schedules_serializer.data, safe=False) return JsonResponse(schedules_serializer.data, safe=False)
@ -183,7 +229,7 @@ class PlanningsView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class PlanningView(APIView): class PlanningView(APIView):
def get (self, request, _id): 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) planning_serializer=PlanningSerializer(planning)
return JsonResponse(planning_serializer.data, safe=False) 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.data, safe=False)
return JsonResponse(planning_serializer.errors, 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)

View File

@ -9,6 +9,9 @@ from School.models import SchoolClass
from datetime import datetime from datetime import datetime
class RegistrationFee(models.Model): class RegistrationFee(models.Model):
"""
Représente un tarif ou frais dinscription avec différentes options de paiement.
"""
class PaymentOptions(models.IntegerChoices): class PaymentOptions(models.IntegerChoices):
SINGLE_PAYMENT = 0, _('Paiement en une seule fois') SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
MONTHLY_PAYMENT = 1, _('Paiement mensuel') MONTHLY_PAYMENT = 1, _('Paiement mensuel')
@ -27,6 +30,9 @@ class RegistrationFee(models.Model):
return self.name return self.name
class Language(models.Model): class Language(models.Model):
"""
Représente une langue parlée par lélève.
"""
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
label = models.CharField(max_length=200, default="") label = models.CharField(max_length=200, default="")
@ -34,6 +40,9 @@ class Language(models.Model):
return "LANGUAGE" return "LANGUAGE"
class Guardian(models.Model): class Guardian(models.Model):
"""
Représente un responsable légal (parent/tuteur) dun élève.
"""
last_name = models.CharField(max_length=200, default="") last_name = models.CharField(max_length=200, default="")
first_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) 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 return self.last_name + "_" + self.first_name
class Sibling(models.Model): class Sibling(models.Model):
"""
Représente un frère ou une sœur dun élève.
"""
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
last_name = models.CharField(max_length=200, default="") last_name = models.CharField(max_length=200, default="")
first_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" return "SIBLING"
class Student(models.Model): class Student(models.Model):
"""
Représente lélève inscrit ou en cours dinscription.
"""
class StudentGender(models.IntegerChoices): class StudentGender(models.IntegerChoices):
NONE = 0, _('Sélection du genre') NONE = 0, _('Sélection du genre')
MALE = 1, _('Garçon') MALE = 1, _('Garçon')
@ -95,6 +109,9 @@ class Student(models.Model):
# Many-to-Many Relationship # Many-to-Many Relationship
siblings = models.ManyToManyField(Sibling, blank=True) siblings = models.ManyToManyField(Sibling, blank=True)
# Many-to-Many Relationship
registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students')
# Many-to-Many Relationship # Many-to-Many Relationship
spoken_languages = models.ManyToManyField(Language, blank=True) spoken_languages = models.ManyToManyField(Language, blank=True)
@ -105,21 +122,39 @@ class Student(models.Model):
return self.last_name + "_" + self.first_name return self.last_name + "_" + self.first_name
def getSpokenLanguages(self): def getSpokenLanguages(self):
"""
Retourne la liste des langues parlées par lélève.
"""
return self.spoken_languages.all() return self.spoken_languages.all()
def getMainGuardian(self): def getMainGuardian(self):
"""
Retourne le responsable légal principal de lélève.
"""
return self.guardians.all()[0] return self.guardians.all()[0]
def getGuardians(self): def getGuardians(self):
"""
Retourne tous les responsables légaux de lélève.
"""
return self.guardians.all() return self.guardians.all()
def getProfiles(self): def getProfiles(self):
"""
Retourne les profils utilisateurs liés à lélève.
"""
return self.profiles.all() return self.profiles.all()
def getSiblings(self): def getSiblings(self):
"""
Retourne les frères et sœurs de lélève.
"""
return self.siblings.all() return self.siblings.all()
def getNumberOfSiblings(self): def getNumberOfSiblings(self):
"""
Retourne le nombre de frères et sœurs.
"""
return self.siblings.count() return self.siblings.count()
@property @property
@ -148,7 +183,9 @@ class Student(models.Model):
return None return None
class RegistrationForm(models.Model): class RegistrationForm(models.Model):
"""
Gère le dossier dinscription lié à un élève donné.
"""
class RegistrationFormStatus(models.IntegerChoices): class RegistrationFormStatus(models.IntegerChoices):
RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_ABSENT = 0, _('Pas de dossier d\'inscription')
RF_CREATED = 1, _('Dossier d\'inscription créé') 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 return "RF_" + self.student.last_name + "_" + self.student.first_name
class RegistrationFileTemplate(models.Model): class RegistrationFileTemplate(models.Model):
"""
Modèle pour stocker les fichiers "templates" dinscription.
"""
name = models.CharField(max_length=255) 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) 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): def __str__(self):
return self.name 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 dinscription 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 dinscription 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

View File

@ -1,5 +1,5 @@
from rest_framework import serializers 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 School.models import SchoolClass
from Auth.models import Profile from Auth.models import Profile
from Auth.serializers import ProfileSerializer from Auth.serializers import ProfileSerializer
@ -10,17 +10,22 @@ from django.utils import timezone
import pytz import pytz
from datetime import datetime from datetime import datetime
class RegistrationFileTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationFileTemplate
fields = '__all__'
class RegistrationFeeSerializer(serializers.ModelSerializer): class RegistrationFeeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
class Meta: class Meta:
model = RegistrationFee model = RegistrationFee
fields = '__all__' 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): class LanguageSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
class Meta: class Meta:
@ -47,6 +52,7 @@ class GuardianSerializer(serializers.ModelSerializer):
class StudentSerializer(serializers.ModelSerializer): class StudentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
guardians = GuardianSerializer(many=True, required=False) guardians = GuardianSerializer(many=True, required=False)
siblings = SiblingSerializer(many=True, required=False) siblings = SiblingSerializer(many=True, required=False)
@ -126,7 +132,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
registration_file = serializers.FileField(required=False) registration_file = serializers.FileField(required=False)
status_label = serializers.SerializerMethodField() status_label = serializers.SerializerMethodField()
formatted_last_update = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField()
registration_files = RegistrationFileSerializer(many=True, required=False)
class Meta: class Meta:
model = RegistrationForm model = RegistrationForm
fields = '__all__' fields = '__all__'

View File

@ -1,36 +1,44 @@
from django.urls import path, re_path from django.urls import path, re_path
from . import views 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 = [ 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$', 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 # 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 # Page de formulaire d'inscription - RESPONSABLE
re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"), re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"),
# Envoi d'un dossier d'inscription # 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 # 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 # 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 # 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 # Page INSCRIPTION - Liste des élèves
re_path(r'^students$', StudentListView.as_view(), name="students"), re_path(r'^students$', StudentListView.as_view(), name="students"),
# Frais d'inscription # Frais d'inscription
re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"), 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$', 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"),
] ]

View File

@ -16,52 +16,95 @@ from enum import Enum
import random import random
import string import string
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
import pymupdf
def recupereListeFichesInscription(): def recupereListeFichesInscription():
"""
Retourne la liste complète des fiches dinscription.
"""
context = { context = {
"ficheInscriptions_list": bdd.getAllObjects(RegistrationForm), "ficheInscriptions_list": bdd.getAllObjects(RegistrationForm),
} }
return context return context
def recupereListeFichesInscriptionEnAttenteSEPA(): def recupereListeFichesInscriptionEnAttenteSEPA():
"""
Retourne les fiches dinscription avec paiement SEPA en attente.
"""
ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE']) ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE'])
return ficheInscriptionsSEPA_list return ficheInscriptionsSEPA_list
def _now(): def _now():
"""
Retourne la date et lheure en cours, avec fuseau.
"""
return datetime.now(ZoneInfo(settings.TZ_APPLI)) return datetime.now(ZoneInfo(settings.TZ_APPLI))
def convertToStr(dateValue, dateFormat): def convertToStr(dateValue, dateFormat):
"""
Convertit un objet datetime en chaîne selon un format donné.
"""
return dateValue.strftime(dateFormat) return dateValue.strftime(dateFormat)
def convertToDate(date_time): 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' format = '%d-%m-%Y %H:%M'
datetime_str = datetime.strptime(date_time, format) datetime_str = datetime.strptime(date_time, format)
return datetime_str return datetime_str
def convertTelephone(telephoneValue, separator='-'): 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]}" return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}"
def genereRandomCode(length): 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)) return ''.join(random.choice(string.ascii_letters) for i in range(length))
def calculeDatePeremption(_start, nbDays): def calculeDatePeremption(_start, nbDays):
"""
Calcule la date de fin à partir dun point de départ et dun nombre de jours.
"""
return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT) return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT)
# Fonction permettant de retourner la valeur du QueryDict # Fonction permettant de retourner la valeur du QueryDict
# QueryDict [ index ] -> Dernière valeur d'une liste # QueryDict [ index ] -> Dernière valeur d'une liste
# dict (QueryDict [ index ]) -> Toutes les valeurs de la liste # dict (QueryDict [ index ]) -> Toutes les valeurs de la liste
def _(liste): def _(liste):
"""
Retourne la première valeur dune liste extraite dun QueryDict.
"""
return liste[0] return liste[0]
def getArgFromRequest(_argument, _request): def getArgFromRequest(_argument, _request):
"""
Extrait la valeur dun argument depuis la requête (JSON).
"""
resultat = None resultat = None
data=JSONParser().parse(_request) data=JSONParser().parse(_request)
resultat = data[_argument] resultat = data[_argument]
return resultat 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 dun dossier dinscription et lassocie au RegistrationForm.
"""
# Ajout du fichier d'inscriptions # Ajout du fichier d'inscriptions
data = { data = {
'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name, 'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name,
@ -69,14 +112,12 @@ def rfToPDF(registerForm):
'signatureTime': convertToStr(_now(), '%H:%M'), 'signatureTime': convertToStr(_now(), '%H:%M'),
'student':registerForm.student, 'student':registerForm.student,
} }
PDFFileName = filename
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
pathFichier = Path(filename)
PDFFileName = "Dossier_Inscription_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name)
pathFichier = Path(settings.DOCUMENT_DIR + "/" + PDFFileName)
if os.path.exists(str(pathFichier)): if os.path.exists(str(pathFichier)):
print(f'File exists : {str(pathFichier)}') print(f'File exists : {str(pathFichier)}')
os.remove(str(pathFichier)) os.remove(str(pathFichier))
receipt_file = BytesIO(pdf.content) receipt_file = BytesIO(pdf.content)
registerForm.fichierInscription = File(receipt_file, PDFFileName) registerForm.fichierInscription = File(receipt_file, PDFFileName)
registerForm.fichierInscription.save()

View File

@ -10,6 +10,8 @@ from rest_framework.parsers import JSONParser,MultiPartParser, FormParser
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
import json import json
from pathlib import Path from pathlib import Path
@ -18,18 +20,22 @@ from io import BytesIO
import Subscriptions.mailManager as mailer import Subscriptions.mailManager as mailer
import Subscriptions.util as util 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 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 Auth.models import Profile
from N3wtSchool import settings, renderers, bdd from N3wtSchool import settings, renderers, bdd
class RegisterFormListView(APIView): class RegisterFormListView(APIView):
"""
Gère la liste des dossiers dinscription, lecture et création.
"""
pagination_class = CustomPagination pagination_class = CustomPagination
def get_register_form(self, _filter, search=None): 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 bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED)
return None 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): def get(self, request, _filter):
"""
Récupère les fiches d'inscriptions en fonction du filtre passé.
"""
# Récupération des paramètres # Récupération des paramètres
search = request.GET.get('search', '').strip() search = request.GET.get('search', '').strip()
page_size = request.GET.get('page_size', None) 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) 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): def post(self, request):
studentFormList_serializer=JSONParser().parse(request) studentFormList_serializer=JSONParser().parse(request)
for studentForm_data in studentFormList_serializer: for studentForm_data in studentFormList_serializer:
@ -104,14 +125,23 @@ class RegisterFormListView(APIView):
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class RegisterFormView(APIView): class RegisterFormView(APIView):
"""
Gère la lecture, création, modification et suppression dun dossier dinscription.
"""
pagination_class = CustomPagination pagination_class = CustomPagination
def get(self, request, _id): def get(self, request, _id):
"""
Récupère un dossier d'inscription donné.
"""
registerForm=bdd.getObject(RegistrationForm, "student__id", _id) registerForm=bdd.getObject(RegistrationForm, "student__id", _id)
registerForm_serializer=RegistrationFormSerializer(registerForm) registerForm_serializer=RegistrationFormSerializer(registerForm)
return JsonResponse(registerForm_serializer.data, safe=False) return JsonResponse(registerForm_serializer.data, safe=False)
def post(self, request): def post(self, request):
"""
Crée un dossier d'inscription.
"""
studentForm_data=JSONParser().parse(request) studentForm_data=JSONParser().parse(request)
# Ajout de la date de mise à jour # Ajout de la date de mise à jour
studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') 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.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) 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')) 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 # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école
json.dumps(studentForm_data) 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 # Mise à jour de l'automate
updateStateMachine(registerForm, 'saisiDI') updateStateMachine(registerForm, 'saisiDI')
elif status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED:
# L'école a validé le dossier d'inscription # L'école a validé le dossier d'inscription
# Mise à jour de l'automate # Mise à jour de l'automate
updateStateMachine(registerForm, 'valideDI') updateStateMachine(registerForm, 'valideDI')
@ -162,34 +204,49 @@ class RegisterFormView(APIView):
studentForm_serializer.save() studentForm_serializer.save()
return JsonResponse(studentForm_serializer.data, safe=False) 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): def delete(self, request, id):
"""
Supprime un dossier d'inscription donné.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
if register_form != None: if register_form != None:
student = register_form.student student = register_form.student
student.guardians.clear() student.guardians.clear()
student.profiles.clear() student.profiles.clear()
student.registration_files.clear()
student.delete() student.delete()
clear_cache() clear_cache()
return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) 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): class StudentView(APIView):
"""
Gère la lecture dun élève donné.
"""
def get(self, request, _id): def get(self, request, _id):
student = bdd.getObject(_objectName=Student, _columnName='id', _value=_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) student_serializer = StudentSerializer(student)
return JsonResponse(student_serializer.data, safe=False) return JsonResponse(student_serializer.data, safe=False)
class GuardianView(APIView): class GuardianView(APIView):
"""
Récupère le dernier ID de responsable légal créé.
"""
def get(self, request): def get(self, request):
lastGuardian = bdd.getLastId(Guardian) lastGuardian = bdd.getLastId(Guardian)
return JsonResponse({"lastid":lastGuardian}, safe=False) return JsonResponse({"lastid":lastGuardian}, safe=False)
def send(request, id): def send(request, _id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) """
Envoie le dossier dinscription par e-mail.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None: if register_form != None:
student = register_form.student student = register_form.student
guardian = student.getMainGuardian() guardian = student.getMainGuardian()
@ -199,24 +256,31 @@ def send(request, id):
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
# Mise à jour de l'automate # Mise à jour de l'automate
updateStateMachine(register_form, 'envoiDI') 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): def archive(request, _id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) """
Archive le dossier dinscription visé.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None: if register_form != None:
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
# Mise à jour de l'automate # Mise à jour de l'automate
updateStateMachine(register_form, 'archiveDI') 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): def relance(request, _id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) """
Relance un dossier dinscription par e-mail.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None: if register_form != None:
student = register_form.student student = register_form.student
guardian = student.getMainGuardian() 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.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
register_form.save() 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 # API utilisée pour la vue parent
class ChildrenListView(APIView): 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 # Récupération des élèves d'un parent
# idProfile : identifiant du profil connecté rattaché aux fiches d'élèves # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves
def get(self, request, _idProfile): def get(self, request, _idProfile):
@ -242,6 +309,9 @@ class ChildrenListView(APIView):
# API utilisée pour la vue de création d'un DI # API utilisée pour la vue de création d'un DI
class StudentListView(APIView): class StudentListView(APIView):
"""
Pour la vue de création dun dossier dinscription : liste les élèves disponibles.
"""
# Récupération de la liste des élèves inscrits ou en cours d'inscriptions # Récupération de la liste des élèves inscrits ou en cours d'inscriptions
def get(self, request): def get(self, request):
students = bdd.getAllObjects(_objectName=Student) 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 # API utilisée pour la vue de personnalisation des frais d'inscription pour la structure
class RegistrationFeeView(APIView): class RegistrationFeeView(APIView):
"""
Liste les frais dinscription.
"""
def get(self, request): def get(self, request):
tarifs = bdd.getAllObjects(RegistrationFee) tarifs = bdd.getAllObjects(RegistrationFee)
tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True) tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True)
return JsonResponse(tarifs_serializer.data, safe=False) return JsonResponse(tarifs_serializer.data, safe=False)
class RegistrationFileTemplateView(APIView): class RegistrationFileTemplateView(APIView):
"""
Gère les fichiers templates pour les dossiers dinscription.
"""
parser_classes = (MultiPartParser, FormParser) parser_classes = (MultiPartParser, FormParser)
def get(self, request): def get(self, request, _id=None):
fichiers = RegistrationFileTemplate.objects.all() """
serializer = RegistrationFileTemplateSerializer(fichiers, many=True) Récupère les fichiers templates pour les dossiers dinscription.
"""
if _id is None:
files = RegistrationFileTemplate.objects.all()
serializer = RegistrationFileTemplateSerializer(files, many=True)
return Response(serializer.data) 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): def post(self, request):
"""
Crée un fichier template pour les dossiers dinscription.
"""
serializer = RegistrationFileTemplateSerializer(data=request.data) serializer = RegistrationFileTemplateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -271,10 +373,71 @@ class RegistrationFileTemplateView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, _id): def delete(self, request, _id):
"""
Supprime un fichier template existant.
"""
registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id)
if registrationFileTemplate is not None: if registrationFileTemplate is not None:
registrationFileTemplate.file.delete() # Supprimer le fichier uploadé registrationFileTemplate.file.delete() # Supprimer le fichier uploadé
registrationFileTemplate.delete() 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: 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 dinscription.
"""
parser_classes = (MultiPartParser, FormParser)
def get(self, request, _id=None):
"""
Récupère les fichiers liés à un dossier dinscription 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)

View File

@ -13,12 +13,12 @@ def run_command(command):
commands = [ commands = [
["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "collectstatic", "--noinput"],
["python", "manage.py", "flush", "--noinput"], ["python", "manage.py", "flush", "--noinput"],
["python", "manage.py", "makemigrations", "Subscriptions"], ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
["python", "manage.py", "makemigrations", "GestionNotification"], ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
["python", "manage.py", "makemigrations", "GestionMessagerie"], ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
["python", "manage.py", "makemigrations", "Auth"], ["python", "manage.py", "makemigrations", "Auth", "--noinput"],
["python", "manage.py", "makemigrations", "School"], ["python", "manage.py", "makemigrations", "School", "--noinput"],
["python", "manage.py", "migrate"] ["python", "manage.py", "migrate", "--noinput"]
] ]
for command in commands: for command in commands:

View File

@ -1,28 +1,22 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { School, Calendar } from 'lucide-react'; import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign
import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
import StructureManagement from '@/components/Structure/Configuration/StructureManagement' import FeesManagement from '@/components/Structure/Configuration/FeesManagement';
import { BE_SCHOOL_SPECIALITIES_URL, import DjangoCSRFToken from '@/components/DjangoCSRFToken';
BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken'; import useCsrfToken from '@/hooks/useCsrfToken';
import { ClassesProvider } from '@/context/ClassesContext'; 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() { export default function Page() {
const [specialities, setSpecialities] = useState([]); const [specialities, setSpecialities] = useState([]);
const [classes, setClasses] = useState([]); const [classes, setClasses] = useState([]);
const [teachers, setTeachers] = useState([]); const [teachers, setTeachers] = useState([]);
const [schedules, setSchedules] = useState([]); const [fees, setFees] = useState([]);
const [activeTab, setActiveTab] = useState('Configuration'); const [discounts, setDiscounts] = useState([]);
const tabs = [ const [tuitionFees, setTuitionFees] = useState([]);
{ id: 'Configuration', title: "Configuration de l'école", icon: School },
{ id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar },
];
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
@ -38,6 +32,15 @@ export default function Page() {
// Fetch data for schedules // Fetch data for schedules
handleSchedules(); handleSchedules();
// Fetch data for fees
handleFees();
// Fetch data for discounts
handleDiscounts();
// Fetch data for TuitionFee
handleTuitionFees();
}, []); }, []);
const handleSpecialities = () => { const handleSpecialities = () => {
@ -45,9 +48,7 @@ export default function Page() {
.then(data => { .then(data => {
setSpecialities(data); setSpecialities(data);
}) })
.catch(error => { .catch(error => console.error('Error fetching specialities:', error));
console.error('Error fetching specialities:', error);
});
}; };
const handleTeachers = () => { const handleTeachers = () => {
@ -55,9 +56,7 @@ export default function Page() {
.then(data => { .then(data => {
setTeachers(data); setTeachers(data);
}) })
.catch(error => { .catch(error => console.error('Error fetching teachers:', error));
console.error('Error fetching teachers:', error);
});
}; };
const handleClasses = () => { const handleClasses = () => {
@ -65,9 +64,7 @@ export default function Page() {
.then(data => { .then(data => {
setClasses(data); setClasses(data);
}) })
.catch(error => { .catch(error => console.error('Error fetching classes:', error));
console.error('Error fetching classes:', error);
});
}; };
const handleSchedules = () => { const handleSchedules = () => {
@ -75,13 +72,35 @@ export default function Page() {
.then(data => { .then(data => {
setSchedules(data); setSchedules(data);
}) })
.catch(error => { .catch(error => console.error('Error fetching schedules:', error));
console.error('Error fetching classes:', error);
});
}; };
const handleCreate = (url, newData, setDatas) => { const handleFees = () => {
fetch(url, { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -90,18 +109,28 @@ export default function Page() {
body: JSON.stringify(newData), body: JSON.stringify(newData),
credentials: 'include' credentials: 'include'
}) })
.then(response => response.json()) .then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw errorData;
});
}
return response.json();
})
.then(data => { .then(data => {
console.log('Succes :', data);
setDatas(prevState => [...prevState, data]); setDatas(prevState => [...prevState, data]);
setErrors({});
return data;
}) })
.catch(error => { .catch(error => {
console.error('Erreur :', error); setErrors(error);
console.error('Error creating data:', error);
throw error;
}); });
}; };
const handleEdit = (url, id, updatedData, setDatas) => { const handleEdit = (url, id, updatedData, setDatas, setErrors) => {
fetch(`${url}/${id}`, { return fetch(`${url}/${id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -110,15 +139,41 @@ export default function Page() {
body: JSON.stringify(updatedData), body: JSON.stringify(updatedData),
credentials: 'include' credentials: 'include'
}) })
.then(response => response.json()) .then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw errorData;
});
}
return response.json();
})
.then(data => { .then(data => {
setDatas(prevState => prevState.map(item => item.id === id ? data : item)); setDatas(prevState => prevState.map(item => item.id === id ? data : item));
setErrors({});
return data;
}) })
.catch(error => { .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) => { const handleUpdatePlanning = (url, planningId, updatedData) => {
fetch(`${url}/${planningId}`, { fetch(`${url}/${planningId}`, {
method: 'PUT', method: 'PUT',
@ -139,35 +194,11 @@ export default function Page() {
}); });
}; };
const handleDelete = (url, id, setDatas) => { const tabs = [
fetch(`${url}/${id}`, { {
method:'DELETE', id: 'Configuration',
headers: { label: "Configuration de l'école",
'Content-Type':'application/json', content: (
'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 (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<TabsStructure activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} />
{activeTab === 'Configuration' && (
<>
<StructureManagement <StructureManagement
specialities={specialities} specialities={specialities}
setSpecialities={setSpecialities} setSpecialities={setSpecialities}
@ -177,18 +208,49 @@ export default function Page() {
setClasses={setClasses} setClasses={setClasses}
handleCreate={handleCreate} handleCreate={handleCreate}
handleEdit={handleEdit} handleEdit={handleEdit}
handleDelete={handleDelete} /> handleDelete={handleDelete}
</> />
)} )
},
{activeTab === 'Schedule' && ( {
id: 'Schedule',
label: "Gestion de l'emploi du temps",
content: (
<ClassesProvider> <ClassesProvider>
<ScheduleManagement <ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning} handleUpdatePlanning={handleUpdatePlanning}
classes={classes} classes={classes}
/> />
</ClassesProvider> </ClassesProvider>
)} )
},
{
id: 'Fees',
label: 'Tarifications',
content: (
<FeesManagement
fees={fees}
setFees={setFees}
discounts={discounts}
setDiscounts={setDiscounts}
tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
)
}
];
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<SidebarTabs tabs={tabs} />
</div>
</div> </div>
); );
}; }

View File

@ -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 (
<div>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
</div>
);
}

View File

@ -1,61 +1,47 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Upload } from 'lucide-react'; import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import DraggableFileUpload from './DraggableFileUpload';
export default function FileUpload({ onFileUpload }) { export default function FileUpload({ onFileUpload, fileToEdit = null }) {
const [dragActive, setDragActive] = useState(false);
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const handleDragOver = (event) => { useEffect(() => {
event.preventDefault(); if (fileToEdit) {
setDragActive(true); setFileName(fileToEdit.name || '');
}; setIsRequired(fileToEdit.is_required || false);
setOrder(fileToEdit.fusion_order || 0);
const handleDragLeave = () => { }
setDragActive(false); }, [fileToEdit]);
};
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(/\.[^/.]+$/, ""));
};
const handleFileNameChange = (event) => { const handleFileNameChange = (event) => {
setFileName(event.target.value); setFileName(event.target.value);
}; };
const handleUpload = () => { const handleUpload = () => {
onFileUpload({
onFileUpload(file, fileName); file,
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
});
setFile(null); setFile(null);
setFileName(''); setFileName('');
setIsRequired(false);
setOrder(0);
}; };
return ( return (
<div> <div>
<div <DraggableFileUpload
onDragOver={handleDragOver} fileName={fileName}
onDragLeave={handleDragLeave} onFileSelect={(selectedFile) => {
onDrop={handleDrop} setFile(selectedFile);
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`} setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
style={{ height: '200px' }} }}
> />
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
<div className="flex mt-2"> <div className="flex mt-2">
<input <input
type="text" type="text"
@ -64,14 +50,28 @@ export default function FileUpload({ onFileUpload }) {
onChange={handleFileNameChange} onChange={handleFileNameChange}
className="flex-grow p-2 border border-gray-200 rounded-md" className="flex-grow p-2 border border-gray-200 rounded-md"
/> />
<input
type="number"
value={order}
onChange={(e) => setOrder(e.target.value)}
placeholder="Ordre de fusion"
className="p-2 border border-gray-200 rounded-md ml-2 w-20"
/>
<button <button
onClick={handleUpload} onClick={handleUpload}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName!="" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`} className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== "" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName==""} disabled={fileName === ""}
> >
Ajouter Ajouter
</button> </button>
</div> </div>
<div className="flex items-center mt-4">
<ToggleSwitch
label="Fichier à remplir obligatoirement"
checked={isRequired}
onChange={() => setIsRequired(!isRequired)}
/>
</div>
</div> </div>
); );
} }

View File

@ -17,6 +17,7 @@ export default function Page() {
const [initialData, setInitialData] = useState(null); const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
useEffect(() => { useEffect(() => {
@ -55,9 +56,8 @@ export default function Page() {
console.error('Error:', error.message); console.error('Error:', error.message);
if (error.details) { if (error.details) {
console.error('Form errors:', 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} onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
isLoading={isLoading} isLoading={isLoading}
errors={formErrors}
/> />
); );
} }

View File

@ -28,6 +28,7 @@ import {
fetchRegisterFormFileTemplate, fetchRegisterFormFileTemplate,
deleteRegisterFormFileTemplate, deleteRegisterFormFileTemplate,
createRegistrationFormFileTemplate, createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
fetchStudents, fetchStudents,
editRegisterForm } from "@/app/lib/subscriptionAction" editRegisterForm } from "@/app/lib/subscriptionAction"
@ -40,6 +41,7 @@ import {
import DjangoCSRFToken from '@/components/DjangoCSRFToken' import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken'; import useCsrfToken from '@/hooks/useCsrfToken';
import { formatDate } from '@/utils/Date';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; 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 [classes, setClasses] = useState([]);
const [students, setEleves] = useState([]); const [students, setEleves] = useState([]);
const [reloadFetch, setReloadFetch] = useState(false); const [reloadFetch, setReloadFetch] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
@ -185,7 +190,11 @@ const registerFormArchivedDataHandler = (data) => {
.then(registerFormArchivedDataHandler) .then(registerFormArchivedDataHandler)
.catch(requestErrorHandler) .catch(requestErrorHandler)
fetchRegisterFormFileTemplate() fetchRegisterFormFileTemplate()
.then((data)=> {setFichiers(data)}) .then((data)=> {
console.log(data);
setFichiers(data)
})
.catch((err)=>{ err = err.message; console.log(err);}); .catch((err)=>{ err = err.message; console.log(err);});
} else { } else {
setTimeout(() => { setTimeout(() => {
@ -548,9 +557,17 @@ const handleFileDelete = (fileId) => {
}); });
}; };
const handleFileEdit = (file) => {
setIsEditing(true);
setFileToEdit(file);
setIsModalOpen(true);
};
const columnsFiles = [ const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name }, { 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) => ( { name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
{ {
@ -559,6 +576,9 @@ const columnsFiles = [
<Download size={16} /> <Download size={16} />
</a>) </a>)
} }
<button onClick={() => handleFileEdit(row)} className="text-blue-500 hover:text-blue-700">
<Edit size={16} />
</button>
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700"> <button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
@ -566,27 +586,43 @@ const columnsFiles = [
) }, ) },
]; ];
const handleFileUpload = (file, fileName) => { const handleFileUpload = ({file, name, is_required, order}) => {
if ( !fileName) { if (!name) {
alert('Veuillez entrer un nom de fichier.'); alert('Veuillez entrer un nom de fichier.');
return; return;
} }
const formData = new FormData(); const formData = new FormData();
if(file){ if(file){
formData.append('file', file); formData.append('file', file);
} }
formData.append('name', fileName); 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) createRegistrationFormFileTemplate(formData, csrfToken)
.then(data => { .then(data => {
console.log('Success:', data);
setFichiers([...fichiers, data]); setFichiers([...fichiers, data]);
closeUploadModal(); setIsModalOpen(false);
}) })
.catch(error => { .catch(error => {
console.error('Error uploading file:', error); console.error('Error uploading file:', error);
}); });
}
}; };
if (isLoading) { if (isLoading) {
@ -699,7 +735,23 @@ const handleFileUpload = (file, fileName) => {
{/*SI STATE == subscribeFiles */} {/*SI STATE == subscribeFiles */}
{activeTab === 'subscribeFiles' && ( {activeTab === 'subscribeFiles' && (
<div> <div>
<FileUpload onFileUpload={handleFileUpload} className="mb-4" /> <button
onClick={() => { setIsModalOpen(true); setIsEditing(false); }}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200 ml-4"
>
<Plus className="w-5 h-5" />
</button>
<Modal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'}
ContentComponent={() => (
<FileUpload
onFileUpload={handleFileUpload}
fileToEdit={fileToEdit}
/>
)}
/>
<div className="mt-8"> <div className="mt-8">
<Table <Table
data={fichiers} data={fichiers}

View File

@ -3,10 +3,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
import { useRouter } from 'next/navigation'; // Ajout de l'importation import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { Bell, User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL, BE_GESTIONMESSAGERIE_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { fetchMessages } from '@/app/lib/messagerieAction';
import ProtectedRoute from '@/components/ProtectedRoute';
export default function Layout({ export default function Layout({
children, children,
@ -18,12 +20,8 @@ export default function Layout({
useEffect(() => { useEffect(() => {
setUserId(userId); setUserId(userId)
fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${userId}`, { fetchMessages(userId)
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => { .then(data => {
if (data) { if (data) {
setMessages(data); setMessages(data);
@ -33,11 +31,10 @@ export default function Layout({
.catch(error => { .catch(error => {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
}); });
}, []); }, []);
return ( return (
<> <ProtectedRoute>
<div className="flex flex-col min-h-screen bg-gray-50"> <div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */} {/* Entête */}
<header className="bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10"> <header className="bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10">
@ -85,7 +82,7 @@ export default function Layout({
{children} {children}
</div> </div>
</div> </div>
</> </ProtectedRoute>
); );
} }

View File

@ -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)
}

View File

@ -2,7 +2,10 @@ import {
BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_SPECIALITIES_URL,
BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_SCHOOLCLASSES_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'; } from '@/utils/Url';
const requestResponseHandler = async (response) => { const requestResponseHandler = async (response) => {
@ -37,3 +40,18 @@ export const fetchSchedules = () => {
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`) return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
.then(requestResponseHandler) .then(requestResponseHandler)
}; };
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)
};

View File

@ -7,7 +7,8 @@ import {
BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORM_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_URL BE_SUBSCRIPTION_LAST_GUARDIAN_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL
} from '@/utils/Url'; } from '@/utils/Url';
export const PENDING = 'pending'; export const PENDING = 'pending';
@ -110,6 +111,32 @@ export const fetchRegisterFormFileTemplate = () => {
return fetch(request).then(requestResponseHandler) 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) => { export const createRegistrationFormFileTemplate = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, {
@ -132,6 +159,19 @@ export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => {
credentials: 'include', 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 = () => { export const fetchStudents = () => {
const request = new Request( const request = new Request(
`${BE_SUBSCRIPTION_STUDENTS_URL}`, `${BE_SUBSCRIPTION_STUDENTS_URL}`,

View File

@ -6,8 +6,8 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
<> <>
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8 w-1/3`}> <div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full"> <span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
<Palette className="w-5 h-5" /> <Palette className="w-5 h-5" />
</span> </span>
<input <input
@ -16,7 +16,7 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
name={name} name={name}
value={value} value={value}
onChange={onChange} onChange={onChange}
className="flex-1 block rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full p-0 w-8 cursor-pointer" className="flex-1 h-8 w-full sm:text-sm border-none focus:ring-0 outline-none rounded-r-md cursor-pointer"
/> />
</div> </div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { isValidPhoneNumber } from 'react-phone-number-input';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) { export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) {
const inputRef = useRef(null); const inputRef = useRef(null);
@ -19,12 +19,12 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla
<> <>
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}> <div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<input <input
type="tel" type="tel"
name={name} name={name}
ref={inputRef} ref={inputRef}
className="flex-1 pl-2 block w-full sm:text-sm focus:ring-0 h-full rounded-md border-none outline-none" className="flex-1 px-3 py-2 block w-full sm:text-sm focus:ring-0 rounded-md border-none outline-none"
value={typeof value === 'string' ? value : ''} value={typeof value === 'string' ? value : ''}
onChange={handleChange} onChange={handleChange}
placeholder={placeholder} placeholder={placeholder}

View File

@ -1,8 +1,11 @@
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder,className}) { export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) {
return ( return (
<> <>
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}> <div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<input <input
type={type} type={type}
@ -12,6 +15,7 @@ export default function InputText({name, type, label, value, onChange, errorMsg,
value={value} value={value}
onChange={onChange} onChange={onChange}
className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md" className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md"
required={required}
/> />
</div> </div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,11 +1,10 @@
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) { export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
return ( return (
<> <>
<div className={`mb-4 ${className}`}> <div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}> <div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full"> <span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
{IconItem && <IconItem />} {IconItem && <IconItem />}
</span> </span>
<input <input
@ -15,7 +14,7 @@ export default function InputTextIcon({name, type, IconItem, label, value, onCha
name={name} name={name}
value={value} value={value}
onChange={onChange} onChange={onChange}
className="flex-1 pl-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none h-full" className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none"
/> />
</div> </div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -5,8 +5,12 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import Button from '@/components/Button'; import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import FileUpload from '@/app/[locale]/admin/subscriptions/components/FileUpload';
import Table from '@/components/Table'; 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 = [ const levels = [
{ value:'1', label: 'TPS - Très Petite Section'}, { value:'1', label: 'TPS - Très Petite Section'},
@ -20,7 +24,8 @@ export default function InscriptionFormShared({
csrfToken, csrfToken,
onSubmit, onSubmit,
cancelUrl, cancelUrl,
isLoading = false isLoading = false,
errors = {} // Nouvelle prop pour les erreurs
}) { }) {
const [formData, setFormData] = useState(() => ({ const [formData, setFormData] = useState(() => ({
@ -41,6 +46,11 @@ export default function InscriptionFormShared({
); );
const [uploadedFiles, setUploadedFiles] = useState([]); 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 // Mettre à jour les données quand initialData change
useEffect(() => { useEffect(() => {
@ -58,6 +68,9 @@ export default function InscriptionFormShared({
level: initialData.level || '' level: initialData.level || ''
}); });
setGuardians(initialData.guardians || []); setGuardians(initialData.guardians || []);
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
} }
}, [initialData]); }, [initialData]);
@ -65,8 +78,22 @@ export default function InscriptionFormShared({
setFormData(prev => ({...prev, [field]: value})); setFormData(prev => ({...prev, [field]: value}));
}; };
const handleFileUpload = (file, fileName) => { const handleFileUpload = async (file, fileName) => {
setUploadedFiles([...uploadedFiles, { 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) => { const handleSubmit = (e) => {
@ -80,12 +107,31 @@ export default function InscriptionFormShared({
onSubmit(data); onSubmit(data);
}; };
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
const getGuardianError = (index, field) => {
return errors?.student?.guardians?.[index]?.[field]?.[0];
};
const columns = [ 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 && <div className="flex items-center justify-center gap-2"> <a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
<Download size={16} />
</a> </div>},
{ name: 'Actions', transform: (row) => ( { name: 'Actions', transform: (row) => (
<a href={URL.createObjectURL(row.fichier)} target='_blank' className="text-blue-500 hover:text-blue-700"> <div className="flex items-center justify-center gap-2">
Télécharger {row.is_required &&
</a> <button className="text-emerald-500 hover:text-emerald-700" type="button" onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}>
<Upload size={16} />
</button>
}
</div>
) }, ) },
]; ];
@ -105,12 +151,14 @@ export default function InscriptionFormShared({
value={formData.last_name} value={formData.last_name}
onChange={(e) => updateFormField('last_name', e.target.value)} onChange={(e) => updateFormField('last_name', e.target.value)}
required required
errorMsg={getError('last_name')}
/> />
<InputText <InputText
name="first_name" name="first_name"
label="Prénom" label="Prénom"
value={formData.first_name} value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)} onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required required
/> />
<InputText <InputText
@ -126,18 +174,22 @@ export default function InscriptionFormShared({
value={formData.birth_date} value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)} onChange={(e) => updateFormField('birth_date', e.target.value)}
required required
errorMsg={getError('birth_date')}
/> />
<InputText <InputText
name="birth_place" name="birth_place"
label="Lieu de Naissance" label="Lieu de Naissance"
value={formData.birth_place} value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)} onChange={(e) => updateFormField('birth_place', e.target.value)}
errorMsg={getError('birth_place')}
/> />
<InputText <InputText
name="birth_postal_code" name="birth_postal_code"
label="Code Postal de Naissance" label="Code Postal de Naissance"
value={formData.birth_postal_code} value={formData.birth_postal_code}
onChange={(e) => updateFormField('birth_postal_code', e.target.value)} onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
required
errorMsg={getError('birth_postal_code')}
/> />
<div className="md:col-span-2"> <div className="md:col-span-2">
<InputText <InputText
@ -145,6 +197,7 @@ export default function InscriptionFormShared({
label="Adresse" label="Adresse"
value={formData.address} value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)} onChange={(e) => updateFormField('address', e.target.value)}
errorMsg={getError('address')}
/> />
</div> </div>
<InputText <InputText
@ -152,14 +205,17 @@ export default function InscriptionFormShared({
label="Médecin Traitant" label="Médecin Traitant"
value={formData.attending_physician} value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)} onChange={(e) => updateFormField('attending_physician', e.target.value)}
errorMsg={getError('attending_physician')}
/> />
<SelectChoice <SelectChoice
name="level" name="level"
label="Niveau" label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level} selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)} callback={(e) => updateFormField('level', e.target.value)}
choices={levels} choices={levels}
required required
errorMsg={getError('level')}
/> />
</div> </div>
</div> </div>
@ -184,6 +240,7 @@ export default function InscriptionFormShared({
newArray.splice(index, 1); newArray.splice(index, 1);
setGuardians(newArray); setGuardians(newArray);
}} }}
errors={errors?.student?.guardians || []}
/> />
</div> </div>
@ -191,14 +248,13 @@ export default function InscriptionFormShared({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à remplir</h2> <h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à remplir</h2>
<Table <Table
data={uploadedFiles} data={fileTemplates}
columns={columns} columns={columns}
itemsPerPage={5} itemsPerPage={5}
currentPage={1} currentPage={1}
totalPages={1} totalPages={1}
onPageChange={() => {}} onPageChange={() => {}}
/> />
<FileUpload onFileUpload={handleFileUpload} />
</div> </div>
{/* Boutons de contrôle */} {/* Boutons de contrôle */}
@ -207,6 +263,44 @@ export default function InscriptionFormShared({
<Button type="submit" text="Valider" primary /> <Button type="submit" text="Valider" primary />
</div> </div>
</form> </form>
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name);
}}
>
<input type="hidden" name="template" value={currentTemplateId} />
<input type="hidden" name="register_form" value={formData.id} />
</DraggableFileUpload>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
}}
/>
<Button
text="Valider"
onClick={() => {
setShowUploadModal(false);
handleFileUpload(file, fileName);
setCurrentTemplateId(null);
}}
primary={true}
/>
</div>
</>
)}
/>
</div> </div>
); );
} }

View File

@ -5,9 +5,13 @@ import React from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import 'react-phone-number-input/style.css' import 'react-phone-number-input/style.css'
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian}) { export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) {
const t = useTranslations('ResponsableInputFields'); const t = useTranslations('ResponsableInputFields');
const getError = (index, field) => {
return errors[index]?.[field]?.[0];
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{guardians.map((item, index) => ( {guardians.map((item, index) => (
@ -33,6 +37,8 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('lastname')} label={t('lastname')}
value={item.last_name} value={item.last_name}
onChange={(event) => {onGuardiansChange(item.id, "last_name", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "last_name", event.target.value)}}
errorMsg={getError(index, 'last_name')}
required
/> />
<InputText <InputText
name="prenomResponsable" name="prenomResponsable"
@ -40,6 +46,8 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('firstname')} label={t('firstname')}
value={item.first_name} value={item.first_name}
onChange={(event) => {onGuardiansChange(item.id, "first_name", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "first_name", event.target.value)}}
errorMsg={getError(index, 'first_name')}
required
/> />
</div> </div>
@ -50,12 +58,14 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('email')} label={t('email')}
value={item.email} value={item.email}
onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "email", event.target.value)}}
errorMsg={getError(index, 'email')}
/> />
<InputPhone <InputPhone
name="telephoneResponsable" name="telephoneResponsable"
label={t('phone')} label={t('phone')}
value={item.phone} value={item.phone}
onChange={(event) => {onGuardiansChange(item.id, "phone", event)}} onChange={(event) => {onGuardiansChange(item.id, "phone", event)}}
errorMsg={getError(index, 'phone')}
/> />
</div> </div>
@ -66,6 +76,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('birthdate')} label={t('birthdate')}
value={item.birth_date} value={item.birth_date}
onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}}
errorMsg={getError(index, 'birth_date')}
/> />
<InputText <InputText
name="professionResponsable" name="professionResponsable"
@ -73,6 +84,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('profession')} label={t('profession')}
value={item.profession} value={item.profession}
onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}}
errorMsg={getError(index, 'profession')}
/> />
</div> </div>
@ -83,6 +95,7 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
label={t('address')} label={t('address')}
value={item.address} value={item.address}
onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}} onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}}
errorMsg={getError(index, 'address')}
/> />
</div> </div>
</div> </div>

View File

@ -1,16 +1,20 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel }) => { const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
if (!visible) return null; if (!visible) return null;
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-md shadow-md"> <div className="bg-white p-6 rounded-md shadow-md">
<p className="mb-4">{message}</p> <p className="mb-4">{message}</p>
<div className="flex justify-end gap-4"> <div className={`flex ${uniqueConfirmButton ? 'justify-center' : 'justify-end'} gap-4`}>
{!uniqueConfirmButton && (
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button> <button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>Confirmer</button> )}
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>
{uniqueConfirmButton ? 'Compris !' : 'Confirmer'}
</button>
</div> </div>
</div> </div>
</div>, </div>,

View File

@ -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;

View File

@ -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 ( return (
<> <>
<div className="mb-4"> <div className="mb-4">
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-700">
<div {label}
className={`flex items-center border-2 rounded-md ${disabled ? 'border-gray-200' : 'border-gray-200 hover:border-gray-400 focus-within:border-gray-500'} h-8 mt-2`} {required && <span className="text-red-500 ml-1">*</span>}
> </label>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full"> <div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
{IconItem && <IconItem />} {IconItem &&
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
{<IconItem />}
</span> </span>
}
<select <select
className={`mt-1 block w-full px-2 py-0 text-base rounded-r-md sm:text-sm border-none focus:ring-0 outline-none cursor-pointer ${disabled ? 'bg-gray-100' : ''}`} className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${disabled ? 'bg-gray-100' : ''} ${selected === "" ? 'italic' : ''}`}
type={type} type={type}
id={name} id={name}
name={name} name={name}
value={selected} value={selected}
onChange={callback} onChange={callback}
disabled={disabled} // Ajout de l'attribut disabled avec une valeur par défaut de false disabled={disabled}
> >
<option value="" className="italic">{placeHolder?.toLowerCase()}</option>
{choices.map(({ value, label }, index) => ( {choices.map(({ value, label }, index) => (
<option key={value} value={value} className={value === '' ? 'italic' : ''}> <option key={value} value={value}>
{label} {label}
</option> </option>
))} ))}
</select> </select>
</div> </div>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>} {errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div> </div>
</> </>
); );
} }

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
const SidebarTabs = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return (
<div className="w-full">
<div className="flex border-b-2 border-gray-200">
{tabs.map(tab => (
<button
key={tab.id}
className={`flex-1 p-4 ${activeTab === tab.id ? 'border-b-2 border-emerald-500 text-emerald-500' : 'text-gray-500 hover:text-emerald-500'}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
<div className="p-4">
{tabs.map(tab => (
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
{tab.content}
</div>
))}
</div>
</div>
);
};
export default SidebarTabs;

View File

@ -203,7 +203,7 @@ const ClassForm = ({ onSubmit, isNew, teachers }) => {
<div className="space-y-4"> <div className="space-y-4">
<SelectChoice <SelectChoice
name="school_year" name="school_year"
placeholder="Sélectionner l'année scolaire" placeHolder="Sélectionner l'année scolaire"
selected={formData.school_year} selected={formData.school_year}
callback={handleChange} callback={handleChange}
choices={schoolYears} choices={schoolYears}

View File

@ -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 { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
@ -50,10 +50,7 @@ const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDel
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0"> <div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center"> <h2 className="text-xl font-bold mb-4">Gestion des classes</h2>
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
<button <button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200" className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"

View File

@ -0,0 +1,188 @@
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 DiscountsSection = ({ discounts, handleCreate, handleEdit, handleDelete, errors }) => {
const [editingDiscount, setEditingDiscount] = useState(null);
const [newDiscount, setNewDiscount] = useState(null);
const [formData, setFormData] = useState({});
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const handleAddDiscount = () => {
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '' });
};
const handleRemoveDiscount = (id) => {
handleDelete(id);
};
const handleSaveNewDiscount = () => {
if (newDiscount.name && newDiscount.amount) {
handleCreate(newDiscount)
.then(() => {
setNewDiscount(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 handleUpdateDiscount = (id, updatedDiscount) => {
if (updatedDiscount.name && updatedDiscount.amount) {
handleEdit(id, updatedDiscount)
.then(() => {
setEditingDiscount(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 (editingDiscount) {
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
} else if (newDiscount) {
setNewDiscount((prevData) => ({
...prevData,
[name]: value,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div>
<InputTextIcon
name={field}
type={field === 'amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderDiscountCell = (discount, column) => {
const isEditing = editingDiscount === discount.id;
const isCreating = newDiscount && newDiscount.id === discount.id;
const currentData = isEditing ? formData : newDiscount;
if (isEditing || isCreating) {
switch (column) {
case 'LIBELLE':
return renderInputField('name', currentData.name, handleChange, 'Libellé de la réduction');
case 'MONTANT':
return renderInputField('amount', currentData.amount, handleChange, 'Montant');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'ACTIONS':
return (
<div className="flex space-x-2">
<button
type="button"
onClick={() => (isEditing ? handleUpdateDiscount(editingDiscount, formData) : handleSaveNewDiscount())}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => (isEditing ? setEditingDiscount(null) : setNewDiscount(null))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'LIBELLE':
return discount.name;
case 'MONTANT':
return discount.amount + ' €';
case 'DESCRIPTION':
return discount.description;
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingDiscount(discount.id) || setFormData(discount)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveDiscount(discount.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Réductions</h2>
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={[
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderDiscountCell}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</div>
);
};
export default DiscountsSection;

View File

@ -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 (
<TuitionFeesProvider>
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
<div className="p-4 bg-white rounded-lg shadow-md">
<FeesSection
fees={fees}
handleCreate={(newData) => 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
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<DiscountsSection
discounts={discounts}
handleCreate={(newData) => 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)}
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<TuitionFeesSection
handleCreate={(newData) => 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)}
/>
</div>
</div>
</TuitionFeesProvider>
);
};
export default FeesManagement;

View File

@ -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) => (
<div>
<InputTextIcon
name={field}
type={field === 'amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
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 (
<div className="flex space-x-2">
<button
type="button"
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => (isEditing ? setEditingFee(null) : setNewFee(null))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'LIBELLE':
return fee.name;
case 'MONTANT':
return fee.amount + ' €';
case 'DESCRIPTION':
return fee.description;
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveFee(fee.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Frais d'inscription</h2>
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newFee ? [newFee, ...fees] : fees}
columns={[
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderFeeCell}
/>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</>
);
};
export default FeesSection;

View File

@ -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 { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
@ -33,10 +33,7 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0"> <div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center"> <h2 className="text-xl font-bold mb-4">Gestion des spécialités</h2>
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
<button <button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200" className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"

View File

@ -3,15 +3,13 @@ import SpecialitiesSection from '@/components/Structure/Configuration/Specialiti
import TeachersSection from '@/components/Structure/Configuration/TeachersSection'; import TeachersSection from '@/components/Structure/Configuration/TeachersSection';
import ClassesSection from '@/components/Structure/Configuration/ClassesSection'; import ClassesSection from '@/components/Structure/Configuration/ClassesSection';
import { ClassesProvider } from '@/context/ClassesContext'; import { ClassesProvider } from '@/context/ClassesContext';
import { BE_SCHOOL_SPECIALITY_URL, BE_SCHOOL_TEACHER_URL, BE_SCHOOL_SCHOOLCLASS_URL } from '@/utils/Url';
import { BE_SCHOOL_SPECIALITY_URL,
BE_SCHOOL_TEACHER_URL,
BE_SCHOOL_SCHOOLCLASS_URL } from '@/utils/Url';
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => { const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, handleCreate, handleEdit, handleDelete }) => {
return ( return (
<div className='p-8'> <div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
<ClassesProvider> <ClassesProvider>
<div className="p-4 bg-white rounded-lg shadow-md">
<SpecialitiesSection <SpecialitiesSection
specialities={specialities} specialities={specialities}
setSpecialities={setSpecialities} setSpecialities={setSpecialities}
@ -19,7 +17,8 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SPECIALITY_URL}`, id, updatedData, setSpecialities)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SPECIALITY_URL}`, id, updatedData, setSpecialities)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITY_URL}`, id, setSpecialities)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITY_URL}`, id, setSpecialities)}
/> />
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<TeachersSection <TeachersSection
teachers={teachers} teachers={teachers}
specialities={specialities} specialities={specialities}
@ -27,7 +26,8 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHER_URL}`, id, updatedData, setTeachers)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TEACHER_URL}`, id, updatedData, setTeachers)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHER_URL}`, id, setTeachers)}
/> />
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<ClassesSection <ClassesSection
classes={classes} classes={classes}
teachers={teachers} teachers={teachers}
@ -35,10 +35,9 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)} handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, updatedData, setClasses)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, setClasses)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_SCHOOLCLASS_URL}`, id, setClasses)}
/> />
</div>
</ClassesProvider> </ClassesProvider>
</div> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
import { GraduationCap, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react'; import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
@ -77,10 +77,7 @@ const TeachersSection = ({ teachers, specialities , handleCreate, handleEdit, ha
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0"> <div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center"> <h2 className="text-xl font-bold mb-4">Gestion des enseignants</h2>
<GraduationCap className="w-8 h-8 mr-2" />
Enseignants
</h2>
<button <button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200" className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"

View File

@ -0,0 +1,286 @@
import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X, Calendar } from 'lucide-react';
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
import SelectChoice from '@/components/SelectChoice';
import { useTuitionFees } from '@/context/TuitionFeesContext';
const TuitionFeesSection = ({ handleCreate, handleEdit, handleDelete, errors }) => {
const { fees, tuitionFees, setTuitionFees, discounts } = useTuitionFees();
const [editingTuitionFee, setEditingTuitionFee] = useState(null);
const [newTuitionFee, setNewTuitionFee] = useState(null);
const [formData, setFormData] = useState({});
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const paymentOptions = [
{ value: 0, label: '1 fois' },
{ value: 1, label: '4 fois' },
{ value: 2, label: '10 fois' }
];
const handleAddTuitionFee = () => {
setNewTuitionFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', payment_option: '', discounts: [] });
};
const handleRemoveTuitionFee = (id) => {
handleDelete(id);
setTuitionFees(tuitionFees.filter(fee => fee.id !== id));
};
const handleSaveNewTuitionFee = () => {
if (
newTuitionFee.name &&
newTuitionFee.base_amount &&
newTuitionFee.payment_option >= 0 &&
newTuitionFee.validity_start_date &&
newTuitionFee.validity_end_date &&
new Date(newTuitionFee.validity_start_date) <= new Date(newTuitionFee.validity_end_date)
) {
handleCreate(newTuitionFee)
.then((createdTuitionFee) => {
setTuitionFees([createdTuitionFee, ...tuitionFees]);
setNewTuitionFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis et valides");
setPopupVisible(true);
}
};
const handleUpdateTuitionFee = (id, updatedTuitionFee) => {
if (
updatedTuitionFee.name &&
updatedTuitionFee.base_amount &&
updatedTuitionFee.payment_option >= 0 &&
updatedTuitionFee.validity_start_date &&
updatedTuitionFee.validity_end_date &&
new Date(updatedTuitionFee.validity_start_date) <= new Date(updatedTuitionFee.validity_end_date)
) {
handleEdit(id, updatedTuitionFee)
.then((updatedFee) => {
setTuitionFees(tuitionFees.map(fee => fee.id === id ? updatedFee : fee));
setEditingTuitionFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis et valides");
setPopupVisible(true);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
let parsedValue = value;
if (name === 'payment_option') {
parsedValue = parseInt(value, 10);
} else if (name === 'discounts') {
parsedValue = value.split(',').map(v => parseInt(v, 10));
}
if (editingTuitionFee) {
setFormData((prevData) => ({
...prevData,
[name]: parsedValue,
}));
} else if (newTuitionFee) {
setNewTuitionFee((prevData) => ({
...prevData,
[name]: parsedValue,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div className="flex justify-center items-center h-full">
<InputTextIcon
name={field}
type={field === 'base_amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderDateField = (field, value, onChange) => (
<div className="relative flex items-center justify-center h-full">
<Calendar className="w-5 h-5 text-emerald-500 absolute left-3" />
<input
type="date"
name={field}
value={value}
onChange={onChange}
className="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
);
const renderSelectField = (field, value, options, callback, label) => (
<div className="flex justify-center items-center h-full">
<SelectChoice
name={field}
value={value}
options={options}
callback={callback}
placeHolder={label}
choices={options}
/>
</div>
);
const calculateFinalAmount = (baseAmount, discountIds) => {
const totalFees = fees.reduce((sum, fee) => sum + parseFloat(fee.amount), 0);
const totalDiscounts = discountIds.reduce((sum, discountId) => {
const discount = discounts.find(d => d.id === discountId);
return discount ? sum + parseFloat(discount.amount) : sum;
}, 0);
const finalAmount = parseFloat(baseAmount) + totalFees - totalDiscounts;
return finalAmount.toFixed(2);
};
const renderTuitionFeeCell = (tuitionFee, column) => {
const isEditing = editingTuitionFee === tuitionFee.id;
const isCreating = newTuitionFee && newTuitionFee.id === tuitionFee.id;
const currentData = isEditing ? formData : newTuitionFee;
if (isEditing || isCreating) {
switch (column) {
case 'NOM':
return renderInputField('name', currentData.name, handleChange, 'Nom des frais de scolarité');
case 'MONTANT DE BASE':
return renderInputField('base_amount', currentData.base_amount, handleChange, 'Montant de base');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'DATE DE DEBUT':
return renderDateField('validity_start_date', currentData.validity_start_date, handleChange);
case 'DATE DE FIN':
return renderDateField('validity_end_date', currentData.validity_end_date, handleChange);
case 'OPTIONS DE PAIEMENT':
return renderSelectField('payment_option', currentData.payment_option, paymentOptions, handleChange, 'Options de paiement');
case 'REMISES':
return renderSelectField('discounts', currentData.discounts, discounts.map(discount => ({ value: discount.id, label: discount.name })), handleChange, 'Remises');
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => (isEditing ? handleUpdateTuitionFee(editingTuitionFee, formData) : handleSaveNewTuitionFee())}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => (isEditing ? setEditingTuitionFee(null) : setNewTuitionFee(null))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
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 (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingTuitionFee(tuitionFee.id) || setFormData(tuitionFee)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveTuitionFee(tuitionFee.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Frais de scolarité</h2>
<button type="button" onClick={handleAddTuitionFee} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newTuitionFee ? [newTuitionFee, ...tuitionFees] : tuitionFees}
columns={[
{ name: 'NOM', label: 'Nom' },
{ name: 'MONTANT DE BASE', label: 'Montant de base' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'DATE DE DEBUT', label: 'Date de début' },
{ name: 'DATE DE FIN', label: 'Date de fin' },
{ name: 'OPTIONS DE PAIEMENT', label: 'Options de paiement' },
{ name: 'REMISES', label: 'Remises' },
{ name: 'MONTANT FINAL', label: 'Montant final' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderTuitionFeeCell}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</div>
);
};
export default TuitionFeesSection;

View File

@ -160,7 +160,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
<div> <div>
<SelectChoice <SelectChoice
name="specialites" name="specialites"
label="Spécialités" placeHolder="Spécialités"
selected={selectedSpeciality} selected={selectedSpeciality}
choices={[ choices={[
{ value: '', label: 'Sélectionner une spécialité' }, { value: '', label: 'Sélectionner une spécialité' },
@ -178,7 +178,7 @@ const SpecialityEventModal = ({ isOpen, onClose, selectedCell, existingEvent, ha
<div> <div>
<SelectChoice <SelectChoice
name="teachers" name="teachers"
label="Enseignants" placeHolder="Enseignants"
selected={selectedTeacher} selected={selectedTeacher}
choices={[ choices={[
{ value: '', label: 'Sélectionner un enseignant'}, { value: '', label: 'Sélectionner un enseignant'},

View File

@ -9,7 +9,6 @@ export const ClassesProvider = ({ children }) => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const schoolYears = [ const schoolYears = [
{ value: '', label: 'Sélectionner une période' },
{ value: `${currentYear - 1}-${currentYear}`, label: `${currentYear - 1}-${currentYear}` }, { value: `${currentYear - 1}-${currentYear}`, label: `${currentYear - 1}-${currentYear}` },
{ value: `${currentYear}-${currentYear + 1}`, label: `${currentYear}-${currentYear + 1}` }, { value: `${currentYear}-${currentYear + 1}`, label: `${currentYear}-${currentYear + 1}` },
{ value: `${currentYear + 1}-${currentYear + 2}`, label: `${currentYear + 1}-${currentYear + 2}` }, { value: `${currentYear + 1}-${currentYear + 2}`, label: `${currentYear + 1}-${currentYear + 2}` },

View File

@ -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 (
<TuitionFeesContext.Provider value={{ tuitionFees, setTuitionFees, fees, setFees, discounts, setDiscounts }}>
{children}
</TuitionFeesContext.Provider>
);
};

View File

@ -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_REGISTERFORM_URL = `${BASE_URL}/Subscriptions/registerForm`
export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms` 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_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` export const BE_SUBSCRIPTION_LAST_GUARDIAN_URL = `${BASE_URL}/Subscriptions/lastGuardian`
//GESTION ENSEIGNANT //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_TEACHERS_URL = `${BASE_URL}/School/teachers`
export const BE_SCHOOL_PLANNING_URL = `${BASE_URL}/School/planning` export const BE_SCHOOL_PLANNING_URL = `${BASE_URL}/School/planning`
export const BE_SCHOOL_PLANNINGS_URL = `${BASE_URL}/School/plannings` 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 // GESTION MESSAGERIE
export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie` export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie`
@ -47,7 +54,6 @@ export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe`
export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset` export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset`
export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new` export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new`
// ADMIN // ADMIN
export const FE_ADMIN_HOME_URL = `/admin` export const FE_ADMIN_HOME_URL = `/admin`