mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Ajout de la configuration des tarifs de l'école [#18]
This commit is contained in:
committed by
Luc SORIGNET
parent
147a70135d
commit
5a0e65bb75
@ -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 . .
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -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"),
|
||||||
]
|
]
|
||||||
@ -5,46 +5,41 @@ 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):
|
||||||
specialities_data=JSONParser().parse(request)
|
specialities_data = JSONParser().parse(request)
|
||||||
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)
|
||||||
@ -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 d’inscription 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) d’un é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 d’un é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 d’inscription.
|
||||||
|
"""
|
||||||
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 d’inscription 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" d’inscription.
|
||||||
|
"""
|
||||||
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 d’inscription particulier.
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
file = models.FileField(upload_to=registration_file_upload_to)
|
||||||
|
date_added = models.DateTimeField(auto_now_add=True)
|
||||||
|
template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE)
|
||||||
|
register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatted_date_added(self):
|
||||||
|
if self.date_added:
|
||||||
|
return self.date_added.strftime('%d-%m-%Y')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_files_from_rf(register_form_id):
|
||||||
|
"""
|
||||||
|
Récupère tous les fichiers liés à un dossier d’inscription donné.
|
||||||
|
"""
|
||||||
|
registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order')
|
||||||
|
filenames = []
|
||||||
|
for reg_file in registration_files:
|
||||||
|
filenames.append(reg_file.file.path)
|
||||||
|
return filenames
|
||||||
|
|||||||
@ -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__'
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|
||||||
]
|
]
|
||||||
@ -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 d’inscription.
|
||||||
|
"""
|
||||||
context = {
|
context = {
|
||||||
"ficheInscriptions_list": bdd.getAllObjects(RegistrationForm),
|
"ficheInscriptions_list": bdd.getAllObjects(RegistrationForm),
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def recupereListeFichesInscriptionEnAttenteSEPA():
|
def recupereListeFichesInscriptionEnAttenteSEPA():
|
||||||
|
"""
|
||||||
|
Retourne les fiches d’inscription avec paiement SEPA en attente.
|
||||||
|
"""
|
||||||
ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE'])
|
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 l’heure 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 d’un point de départ et d’un 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 d’une liste extraite d’un QueryDict.
|
||||||
|
"""
|
||||||
return liste[0]
|
return liste[0]
|
||||||
|
|
||||||
def getArgFromRequest(_argument, _request):
|
def getArgFromRequest(_argument, _request):
|
||||||
|
"""
|
||||||
|
Extrait la valeur d’un 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 d’un dossier d’inscription et l’associe 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()
|
||||||
@ -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 d’inscription, 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 d’un dossier d’inscription.
|
||||||
|
"""
|
||||||
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 d’un é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 d’inscription 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 d’inscription 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 d’inscription 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 d’un dossier d’inscription : liste les élèves disponibles.
|
||||||
|
"""
|
||||||
# Récupération de la liste des élèves inscrits ou en cours d'inscriptions
|
# 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 d’inscription.
|
||||||
|
"""
|
||||||
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 d’inscription.
|
||||||
|
"""
|
||||||
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 d’inscription.
|
||||||
|
"""
|
||||||
|
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 d’inscription.
|
||||||
|
"""
|
||||||
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 d’inscription.
|
||||||
|
"""
|
||||||
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
|
def get(self, request, _id=None):
|
||||||
|
"""
|
||||||
|
Récupère les fichiers liés à un dossier d’inscription donné.
|
||||||
|
"""
|
||||||
|
if (_id is None):
|
||||||
|
files = RegistrationFile.objects.all()
|
||||||
|
serializer = RegistrationFileSerializer(files, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
else:
|
||||||
|
registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id)
|
||||||
|
if registationFile is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationFileSerializer(registationFile)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Crée un RegistrationFile pour le RegistrationForm associé.
|
||||||
|
"""
|
||||||
|
serializer = RegistrationFileSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
def put(self, request, fileId):
|
||||||
|
"""
|
||||||
|
Met à jour un RegistrationFile existant.
|
||||||
|
"""
|
||||||
|
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId)
|
||||||
|
if registrationFile is None:
|
||||||
|
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationFileSerializer(registrationFile, data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, _id):
|
||||||
|
"""
|
||||||
|
Supprime un RegistrationFile existant.
|
||||||
|
"""
|
||||||
|
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id)
|
||||||
|
if registrationFile is not None:
|
||||||
|
registrationFile.file.delete() # Supprimer le fichier uploadé
|
||||||
|
registrationFile.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
createRegistrationFormFileTemplate(formData,csrfToken)
|
formData.append('is_required', is_required);
|
||||||
|
formData.append('order', order);
|
||||||
|
|
||||||
|
if (isEditing && fileToEdit) {
|
||||||
|
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
|
||||||
|
.then(data => {
|
||||||
|
setFichiers(prevFichiers =>
|
||||||
|
prevFichiers.map(f => f.id === fileToEdit.id ? data : f)
|
||||||
|
);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setFileToEdit(null);
|
||||||
|
setIsEditing(false);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error editing file:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createRegistrationFormFileTemplate(formData, csrfToken)
|
||||||
.then(data => {
|
.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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
24
Front-End/src/app/lib/messagerieAction.js
Normal file
24
Front-End/src/app/lib/messagerieAction.js
Normal 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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
};
|
||||||
|
|||||||
@ -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}`,
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
21
Front-End/src/components/ProtectedRoute.js
Normal file
21
Front-End/src/components/ProtectedRoute.js
Normal 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;
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
30
Front-End/src/components/SidebarTabs.js
Normal file
30
Front-End/src/components/SidebarTabs.js
Normal 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;
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
190
Front-End/src/components/Structure/Configuration/FeesSection.js
Normal file
190
Front-End/src/components/Structure/Configuration/FeesSection.js
Normal 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;
|
||||||
@ -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"
|
||||||
@ -52,8 +49,8 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
|
|||||||
transform: (row) => (
|
transform: (row) => (
|
||||||
<div
|
<div
|
||||||
className="inline-block px-3 py-1 rounded-full font-bold text-white"
|
className="inline-block px-3 py-1 rounded-full font-bold text-white"
|
||||||
style={{ backgroundColor: row. color_code }}
|
style={{ backgroundColor: row.color_code }}
|
||||||
title={row. color_code}
|
title={row.color_code}
|
||||||
>
|
>
|
||||||
<span className="font-bold text-white">{row.name.toUpperCase()}</span>
|
<span className="font-bold text-white">{row.name.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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'},
|
||||||
|
|||||||
@ -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}` },
|
||||||
|
|||||||
24
Front-End/src/context/TuitionFeesContext.js
Normal file
24
Front-End/src/context/TuitionFeesContext.js
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,8 +54,7 @@ export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe`
|
|||||||
export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset`
|
export const FE_USERS_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`
|
||||||
|
|
||||||
// ADMIN/SUBSCRIPTIONS URL
|
// ADMIN/SUBSCRIPTIONS URL
|
||||||
|
|||||||
Reference in New Issue
Block a user