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

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

View File

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

View File

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

View File

@ -3,6 +3,9 @@ from Auth.models import Profile
from django.db.models import JSONField
from django.dispatch import receiver
from django.contrib.postgres.fields import ArrayField
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
LEVEL_CHOICES = [
(1, 'Très Petite Section (TPS)'),
@ -47,7 +50,7 @@ class SchoolClass(models.Model):
number_of_students = models.PositiveIntegerField(blank=True)
teaching_language = models.CharField(max_length=255, blank=True)
school_year = models.CharField(max_length=9, blank=True)
updated_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
teachers = models.ManyToManyField(Teacher, blank=True)
levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list)
type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1)
@ -64,3 +67,56 @@ class Planning(models.Model):
def __str__(self):
return f'Planning for {self.level} of {self.school_class.atmosphere_name}'
class Discount(models.Model):
name = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField(blank=True)
def __str__(self):
return self.name
class Fee(models.Model):
name = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField(blank=True)
def __str__(self):
return self.name
class TuitionFee(models.Model):
class PaymentOptions(models.IntegerChoices):
SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois')
TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois')
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
base_amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='EUR')
discounts = models.ManyToManyField('Discount', blank=True)
validity_start_date = models.DateField()
validity_end_date = models.DateField()
payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
def clean(self):
if self.validity_end_date <= self.validity_start_date:
raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.'))
def calculate_final_amount(self):
amount = self.base_amount
# Apply fees (supplements and taxes)
# for fee in self.fees.all():
# amount += fee.amount
# Apply discounts
for discount in self.discounts.all():
amount -= discount.amount
return amount

View File

@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee
from Subscriptions.models import RegistrationForm
from Subscriptions.serializers import StudentSerializer
from Auth.serializers import ProfileSerializer
@ -173,3 +173,56 @@ class SchoolClassSerializer(serializers.ModelSerializer):
local_tz = pytz.timezone(settings.TZ_APPLI)
local_time = utc_time.astimezone(local_tz)
return local_time.strftime("%d-%m-%Y %H:%M")
class DiscountSerializer(serializers.ModelSerializer):
class Meta:
model = Discount
fields = '__all__'
class FeeSerializer(serializers.ModelSerializer):
class Meta:
model = Fee
fields = '__all__'
class TuitionFeeSerializer(serializers.ModelSerializer):
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True)
final_amount = serializers.SerializerMethodField()
class Meta:
model = TuitionFee
fields = '__all__'
def get_final_amount(self, obj):
return obj.calculate_final_amount()
def create(self, validated_data):
discounts_data = validated_data.pop('discounts', [])
# Create the TuitionFee instance
tuition_fee = TuitionFee.objects.create(**validated_data)
# Add discounts if provided
for discount in discounts_data:
tuition_fee.discounts.add(discount)
return tuition_fee
def update(self, instance, validated_data):
discounts_data = validated_data.pop('discounts', [])
# Update the TuitionFee instance
instance.name = validated_data.get('name', instance.name)
instance.description = validated_data.get('description', instance.description)
instance.base_amount = validated_data.get('base_amount', instance.base_amount)
instance.currency = validated_data.get('currency', instance.currency)
instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date)
instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date)
instance.payment_option = validated_data.get('payment_option', instance.payment_option)
instance.is_active = validated_data.get('is_active', instance.is_active)
instance.save()
# Update discounts if provided
if discounts_data:
instance.discounts.set(discounts_data)
return instance

View File

@ -1,6 +1,21 @@
from django.urls import path, re_path
from School.views import TeachersView, TeacherView, SpecialitiesView, SpecialityView, ClassesView, ClasseView, PlanningsView, PlanningView
from School.views import (
TeachersView,
TeacherView,
SpecialitiesView,
SpecialityView,
ClassesView,
ClasseView,
PlanningsView,
PlanningView,
FeesView,
FeeView,
TuitionFeesView,
TuitionFeeView,
DiscountsView,
DiscountView,
)
urlpatterns = [
re_path(r'^specialities$', SpecialitiesView.as_view(), name="specialities"),
@ -18,4 +33,16 @@ urlpatterns = [
re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"),
re_path(r'^planning$', PlanningView.as_view(), name="planning"),
re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"),
re_path(r'^fees$', FeesView.as_view(), name="fees"),
re_path(r'^fee$', FeeView.as_view(), name="fee"),
re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"),
re_path(r'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"),
re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"),
re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"),
re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"),
re_path(r'^discount$', DiscountView.as_view(), name="discount"),
re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"),
]

View File

@ -5,46 +5,41 @@ from rest_framework.parsers import JSONParser
from rest_framework.views import APIView
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from .models import Teacher, Speciality, SchoolClass, Planning
from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer
from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee
from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer
from N3wtSchool import bdd
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class SpecialitiesView(APIView):
def get(self, request):
specialitiesList=bdd.getAllObjects(Speciality)
specialities_serializer=SpecialitySerializer(specialitiesList, many=True)
specialitiesList = getAllObjects(Speciality)
specialities_serializer = SpecialitySerializer(specialitiesList, many=True)
return JsonResponse(specialities_serializer.data, safe=False)
def post(self, request):
specialities_data=JSONParser().parse(request)
specialities_data = JSONParser().parse(request)
all_valid = True
for speciality_data in specialities_data:
speciality_serializer = SpecialitySerializer(data=speciality_data)
if speciality_serializer.is_valid():
speciality_serializer.save()
else:
all_valid = False
break
if all_valid:
specialitiesList = bdd.getAllObjects(Speciality)
specialitiesList = getAllObjects(Speciality)
specialities_serializer = SpecialitySerializer(specialitiesList, many=True)
return JsonResponse(specialities_serializer.data, safe=False)
return JsonResponse(speciality_serializer.errors, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class SpecialityView(APIView):
def get (self, request, _id):
speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id)
speciality_serializer=SpecialitySerializer(speciality)
def get(self, request, _id):
speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id)
speciality_serializer = SpecialitySerializer(speciality)
return JsonResponse(speciality_serializer.data, safe=False)
def post(self, request):
@ -59,7 +54,7 @@ class SpecialityView(APIView):
def put(self, request, _id):
speciality_data=JSONParser().parse(request)
speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id)
speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id)
speciality_serializer = SpecialitySerializer(speciality, data=speciality_data)
if speciality_serializer.is_valid():
speciality_serializer.save()
@ -68,11 +63,62 @@ class SpecialityView(APIView):
return JsonResponse(speciality_serializer.errors, safe=False)
def delete(self, request, _id):
return bdd.delete_object(Speciality, _id)
return delete_object(Speciality, _id)
# Vues pour les réductions (Discount)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class DiscountsView(APIView):
def get(self, request):
discountsList = Discount.objects.all()
discounts_serializer = DiscountSerializer(discountsList, many=True)
return JsonResponse(discounts_serializer.data, safe=False)
def post(self, request):
discount_data = JSONParser().parse(request)
discount_serializer = DiscountSerializer(data=discount_data)
if discount_serializer.is_valid():
discount_serializer.save()
return JsonResponse(discount_serializer.data, safe=False, status=201)
return JsonResponse(discount_serializer.errors, safe=False, status=400)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class DiscountView(APIView):
def get(self, request, _id):
try:
discount = Discount.objects.get(id=_id)
discount_serializer = DiscountSerializer(discount)
return JsonResponse(discount_serializer.data, safe=False)
except Discount.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
def post(self, request):
discount_data = JSONParser().parse(request)
discount_serializer = DiscountSerializer(data=discount_data)
if discount_serializer.is_valid():
discount_serializer.save()
return JsonResponse(discount_serializer.data, safe=False, status=201)
return JsonResponse(discount_serializer.errors, safe=False, status=400)
def put(self, request, _id):
discount_data = JSONParser().parse(request)
try:
discount = Discount.objects.get(id=_id)
except Discount.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True
if discount_serializer.is_valid():
discount_serializer.save()
return JsonResponse(discount_serializer.data, safe=False)
return JsonResponse(discount_serializer.errors, safe=False, status=400)
def delete(self, request, _id):
return delete_object(Discount, _id)
class TeachersView(APIView):
def get(self, request):
teachersList=bdd.getAllObjects(Teacher)
teachersList=getAllObjects(Teacher)
teachers_serializer=TeacherSerializer(teachersList, many=True)
return JsonResponse(teachers_serializer.data, safe=False)
@ -81,7 +127,7 @@ class TeachersView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch')
class TeacherView(APIView):
def get (self, request, _id):
teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id)
teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id)
teacher_serializer=TeacherSerializer(teacher)
return JsonResponse(teacher_serializer.data, safe=False)
@ -99,7 +145,7 @@ class TeacherView(APIView):
def put(self, request, _id):
teacher_data=JSONParser().parse(request)
teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id)
teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id)
teacher_serializer = TeacherSerializer(teacher, data=teacher_data)
if teacher_serializer.is_valid():
teacher_serializer.save()
@ -108,13 +154,13 @@ class TeacherView(APIView):
return JsonResponse(teacher_serializer.errors, safe=False)
def delete(self, request, _id):
return bdd.delete_object(Teacher, _id, related_field='associated_profile')
return delete_object(Teacher, _id, related_field='associated_profile')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ClassesView(APIView):
def get(self, request):
classesList=bdd.getAllObjects(SchoolClass)
classesList=getAllObjects(SchoolClass)
classes_serializer=SchoolClassSerializer(classesList, many=True)
return JsonResponse(classes_serializer.data, safe=False)
@ -131,7 +177,7 @@ class ClassesView(APIView):
break
if all_valid:
classesList = bdd.getAllObjects(SchoolClass)
classesList = getAllObjects(SchoolClass)
classes_serializer = SchoolClassSerializer(classesList, many=True)
return JsonResponse(classes_serializer.data, safe=False)
@ -142,7 +188,7 @@ class ClassesView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ClasseView(APIView):
def get (self, request, _id):
schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id)
schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id)
classe_serializer=SchoolClassSerializer(schoolClass)
return JsonResponse(classe_serializer.data, safe=False)
@ -159,7 +205,7 @@ class ClasseView(APIView):
def put(self, request, _id):
classe_data=JSONParser().parse(request)
schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id)
schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id)
classe_serializer = SchoolClassSerializer(schoolClass, data=classe_data)
if classe_serializer.is_valid():
classe_serializer.save()
@ -168,14 +214,14 @@ class ClasseView(APIView):
return JsonResponse(classe_serializer.errors, safe=False)
def delete(self, request, _id):
return bdd.delete_object(SchoolClass, _id)
return delete_object(SchoolClass, _id)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class PlanningsView(APIView):
def get(self, request):
schedulesList=bdd.getAllObjects(Planning)
schedulesList=getAllObjects(Planning)
schedules_serializer=PlanningSerializer(schedulesList, many=True)
return JsonResponse(schedules_serializer.data, safe=False)
@ -183,7 +229,7 @@ class PlanningsView(APIView):
@method_decorator(ensure_csrf_cookie, name='dispatch')
class PlanningView(APIView):
def get (self, request, _id):
planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id)
planning = getObject(_objectName=Planning, _columnName='classe__id', _value=_id)
planning_serializer=PlanningSerializer(planning)
return JsonResponse(planning_serializer.data, safe=False)
@ -215,3 +261,98 @@ class PlanningView(APIView):
return JsonResponse(planning_serializer.data, safe=False)
return JsonResponse(planning_serializer.errors, safe=False)
# Vues pour les frais (Fee)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class FeesView(APIView):
def get(self, request):
feesList = Fee.objects.all()
fees_serializer = FeeSerializer(feesList, many=True)
return JsonResponse(fees_serializer.data, safe=False)
def post(self, request):
fee_data = JSONParser().parse(request)
fee_serializer = FeeSerializer(data=fee_data)
if fee_serializer.is_valid():
fee_serializer.save()
return JsonResponse(fee_serializer.data, safe=False, status=201)
return JsonResponse(fee_serializer.errors, safe=False, status=400)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class FeeView(APIView):
def get(self, request, _id):
try:
fee = Fee.objects.get(id=_id)
fee_serializer = FeeSerializer(fee)
return JsonResponse(fee_serializer.data, safe=False)
except Fee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
def post(self, request):
fee_data = JSONParser().parse(request)
fee_serializer = FeeSerializer(data=fee_data)
if fee_serializer.is_valid():
fee_serializer.save()
return JsonResponse(fee_serializer.data, safe=False, status=201)
return JsonResponse(fee_serializer.errors, safe=False, status=400)
def put(self, request, _id):
fee_data = JSONParser().parse(request)
try:
fee = Fee.objects.get(id=_id)
except Fee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) # Utilisation de partial=True
if fee_serializer.is_valid():
fee_serializer.save()
return JsonResponse(fee_serializer.data, safe=False)
return JsonResponse(fee_serializer.errors, safe=False, status=400)
def delete(self, request, _id):
return delete_object(Fee, _id)
# Vues pour les frais de scolarité (TuitionFee)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class TuitionFeesView(APIView):
def get(self, request):
tuitionFeesList = TuitionFee.objects.all()
tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True)
return JsonResponse(tuitionFees_serializer.data, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class TuitionFeeView(APIView):
def get(self, request, _id):
try:
tuitionFee = TuitionFee.objects.get(id=_id)
tuitionFee_serializer = TuitionFeeSerializer(tuitionFee)
return JsonResponse(tuitionFee_serializer.data, safe=False)
except TuitionFee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
def post(self, request):
tuitionFee_data = JSONParser().parse(request)
tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data)
if tuitionFee_serializer.is_valid():
tuitionFee_serializer.save()
return JsonResponse(tuitionFee_serializer.data, safe=False, status=201)
return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400)
def put(self, request, _id):
tuitionFee_data = JSONParser().parse(request)
try:
tuitionFee = TuitionFee.objects.get(id=_id)
except TuitionFee.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=404)
tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True
if tuitionFee_serializer.is_valid():
tuitionFee_serializer.save()
return JsonResponse(tuitionFee_serializer.data, safe=False)
return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400)
def delete(self, request, _id):
return delete_object(TuitionFee, _id)

View File

@ -9,6 +9,9 @@ from School.models import SchoolClass
from datetime import datetime
class RegistrationFee(models.Model):
"""
Représente un tarif ou frais dinscription avec différentes options de paiement.
"""
class PaymentOptions(models.IntegerChoices):
SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
MONTHLY_PAYMENT = 1, _('Paiement mensuel')
@ -27,6 +30,9 @@ class RegistrationFee(models.Model):
return self.name
class Language(models.Model):
"""
Représente une langue parlée par lélève.
"""
id = models.AutoField(primary_key=True)
label = models.CharField(max_length=200, default="")
@ -34,6 +40,9 @@ class Language(models.Model):
return "LANGUAGE"
class Guardian(models.Model):
"""
Représente un responsable légal (parent/tuteur) dun élève.
"""
last_name = models.CharField(max_length=200, default="")
first_name = models.CharField(max_length=200, default="")
birth_date = models.CharField(max_length=200, default="", blank=True)
@ -47,6 +56,9 @@ class Guardian(models.Model):
return self.last_name + "_" + self.first_name
class Sibling(models.Model):
"""
Représente un frère ou une sœur dun élève.
"""
id = models.AutoField(primary_key=True)
last_name = models.CharField(max_length=200, default="")
first_name = models.CharField(max_length=200, default="")
@ -56,7 +68,9 @@ class Sibling(models.Model):
return "SIBLING"
class Student(models.Model):
"""
Représente lélève inscrit ou en cours dinscription.
"""
class StudentGender(models.IntegerChoices):
NONE = 0, _('Sélection du genre')
MALE = 1, _('Garçon')
@ -95,6 +109,9 @@ class Student(models.Model):
# Many-to-Many Relationship
siblings = models.ManyToManyField(Sibling, blank=True)
# Many-to-Many Relationship
registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students')
# Many-to-Many Relationship
spoken_languages = models.ManyToManyField(Language, blank=True)
@ -105,21 +122,39 @@ class Student(models.Model):
return self.last_name + "_" + self.first_name
def getSpokenLanguages(self):
"""
Retourne la liste des langues parlées par lélève.
"""
return self.spoken_languages.all()
def getMainGuardian(self):
"""
Retourne le responsable légal principal de lélève.
"""
return self.guardians.all()[0]
def getGuardians(self):
"""
Retourne tous les responsables légaux de lélève.
"""
return self.guardians.all()
def getProfiles(self):
"""
Retourne les profils utilisateurs liés à lélève.
"""
return self.profiles.all()
def getSiblings(self):
"""
Retourne les frères et sœurs de lélève.
"""
return self.siblings.all()
def getNumberOfSiblings(self):
"""
Retourne le nombre de frères et sœurs.
"""
return self.siblings.count()
@property
@ -148,7 +183,9 @@ class Student(models.Model):
return None
class RegistrationForm(models.Model):
"""
Gère le dossier dinscription lié à un élève donné.
"""
class RegistrationFormStatus(models.IntegerChoices):
RF_ABSENT = 0, _('Pas de dossier d\'inscription')
RF_CREATED = 1, _('Dossier d\'inscription créé')
@ -171,9 +208,53 @@ class RegistrationForm(models.Model):
return "RF_" + self.student.last_name + "_" + self.student.first_name
class RegistrationFileTemplate(models.Model):
"""
Modèle pour stocker les fichiers "templates" dinscription.
"""
name = models.CharField(max_length=255)
file = models.FileField(upload_to='registration_files/')
file = models.FileField(upload_to='templates_files/', blank=True, null=True)
order = models.PositiveIntegerField(default=0) # Ajout du champ order
date_added = models.DateTimeField(auto_now_add=True)
is_required = models.BooleanField(default=False)
@property
def formatted_date_added(self):
if self.date_added:
return self.date_added.strftime('%d-%m-%Y')
return None
def __str__(self):
return self.name
def registration_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.register_form.pk}/{filename}"
class RegistrationFile(models.Model):
"""
Fichier lié à un dossier dinscription particulier.
"""
name = models.CharField(max_length=255)
file = models.FileField(upload_to=registration_file_upload_to)
date_added = models.DateTimeField(auto_now_add=True)
template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE)
register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files')
@property
def formatted_date_added(self):
if self.date_added:
return self.date_added.strftime('%d-%m-%Y')
return None
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order')
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames

View File

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

View File

@ -1,36 +1,44 @@
from django.urls import path, re_path
from . import views
from Subscriptions.views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView
from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView
urlpatterns = [
re_path(r'^registerForms/([a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"),
re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"),
re_path(r'^registerForm$', RegisterFormView.as_view(), name="registerForm"),
re_path(r'^registerForm/([0-9]+)$', RegisterFormView.as_view(), name="registerForm"),
re_path(r'^registerForm/(?P<_id>[0-9]+)$', RegisterFormView.as_view(), name="registerForm"),
# Page de formulaire d'inscription - ELEVE
re_path(r'^student/([0-9]+)$', StudentView.as_view(), name="students"),
re_path(r'^student/(?P<_id>[0-9]+)$', StudentView.as_view(), name="students"),
# Page de formulaire d'inscription - RESPONSABLE
re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"),
# Envoi d'un dossier d'inscription
re_path(r'^send/([0-9]+)$', views.send, name="send"),
re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"),
# Archivage d'un dossier d'inscription
re_path(r'^archive/([0-9]+)$', views.archive, name="archive"),
re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"),
# Envoi d'une relance de dossier d'inscription
re_path(r'^sendRelance/([0-9]+)$', views.relance, name="sendRelance"),
re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"),
# Page PARENT - Liste des children
re_path(r'^children/([0-9]+)$', ChildrenListView.as_view(), name="children"),
re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"),
# Page INSCRIPTION - Liste des élèves
re_path(r'^students$', StudentListView.as_view(), name="students"),
# Frais d'inscription
re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"),
# modèles de fichiers d'inscription
re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'),
re_path(r'^registrationFileTemplates/([0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"),
re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"),
# fichiers d'inscription
re_path(r'^registrationFiles/(?P<_id>[0-9]+)$', RegistrationFileView.as_view(), name='registrationFiles'),
re_path(r'^registrationFiles', RegistrationFileView.as_view(), name="registrationFiles"),
]

View File

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

View File

@ -10,6 +10,8 @@ from rest_framework.parsers import JSONParser,MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
import json
from pathlib import Path
@ -18,18 +20,22 @@ from io import BytesIO
import Subscriptions.mailManager as mailer
import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationFileTemplateSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer
from Subscriptions.pagination import CustomPagination
from Subscriptions.signals import clear_cache
from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate
from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine
from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer
from .pagination import CustomPagination
from .signals import clear_cache
from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate, RegistrationFile
from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine
from Auth.models import Profile
from N3wtSchool import settings, renderers, bdd
class RegisterFormListView(APIView):
"""
Gère la liste des dossiers dinscription, lecture et création.
"""
pagination_class = CustomPagination
def get_register_form(self, _filter, search=None):
@ -47,8 +53,18 @@ class RegisterFormListView(APIView):
return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED)
return None
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter('_filter', openapi.IN_PATH, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True),
openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False),
openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False),
],
responses={200: RegistrationFormSerializer(many=True)}
)
def get(self, request, _filter):
"""
Récupère les fiches d'inscriptions en fonction du filtre passé.
"""
# Récupération des paramètres
search = request.GET.get('search', '').strip()
page_size = request.GET.get('page_size', None)
@ -84,6 +100,11 @@ class RegisterFormListView(APIView):
return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False)
@swagger_auto_schema(
manual_parameters=[
],
responses={200: RegistrationFormSerializer(many=True)}
)
def post(self, request):
studentFormList_serializer=JSONParser().parse(request)
for studentForm_data in studentFormList_serializer:
@ -104,14 +125,23 @@ class RegisterFormListView(APIView):
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class RegisterFormView(APIView):
"""
Gère la lecture, création, modification et suppression dun dossier dinscription.
"""
pagination_class = CustomPagination
def get(self, request, _id):
"""
Récupère un dossier d'inscription donné.
"""
registerForm=bdd.getObject(RegistrationForm, "student__id", _id)
registerForm_serializer=RegistrationFormSerializer(registerForm)
return JsonResponse(registerForm_serializer.data, safe=False)
def post(self, request):
"""
Crée un dossier d'inscription.
"""
studentForm_data=JSONParser().parse(request)
# Ajout de la date de mise à jour
studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
@ -137,21 +167,33 @@ class RegisterFormView(APIView):
return JsonResponse(studentForm_serializer.data, safe=False)
return JsonResponse(studentForm_serializer.errors, safe=False, status=400)
return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def put(self, request, id):
def put(self, request, _id):
"""
Modifie un dossier d'inscription donné.
"""
studentForm_data=JSONParser().parse(request)
status = studentForm_data.pop('status', 0)
_status = studentForm_data.pop('status', 0)
studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'))
registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
# Le parent a complété le dossier d'inscription, il est soumis à validation par l'école
json.dumps(studentForm_data)
util.rfToPDF(registerForm)
#Génération de la fiche d'inscription au format PDF
PDFFileName = "rf_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name)
path = Path(f"registration_files/dossier_rf_{registerForm.pk}/{PDFFileName}")
registerForm.fichierInscription = util.rfToPDF(registerForm, path)
# Récupération des fichiers d'inscription
fileNames = RegistrationFile.get_files_from_rf(registerForm.pk)
fileNames.insert(0,path)
# Création du fichier PDF Fusionné avec le dossier complet
output_path = f"registration_files/dossier_rf_{registerForm.pk}/dossier_{registerForm.pk}.pdf"
util.merge_files_pdf(fileNames, output_path)
# Mise à jour de l'automate
updateStateMachine(registerForm, 'saisiDI')
elif status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED:
elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED:
# L'école a validé le dossier d'inscription
# Mise à jour de l'automate
updateStateMachine(registerForm, 'valideDI')
@ -162,34 +204,49 @@ class RegisterFormView(APIView):
studentForm_serializer.save()
return JsonResponse(studentForm_serializer.data, safe=False)
return JsonResponse(studentForm_serializer.errors, safe=False, status=400)
return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
"""
Supprime un dossier d'inscription donné.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
if register_form != None:
student = register_form.student
student.guardians.clear()
student.profiles.clear()
student.registration_files.clear()
student.delete()
clear_cache()
return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class StudentView(APIView):
"""
Gère la lecture dun élève donné.
"""
def get(self, request, _id):
student = bdd.getObject(_objectName=Student, _columnName='id', _value=_id)
if student is None:
return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
student_serializer = StudentSerializer(student)
return JsonResponse(student_serializer.data, safe=False)
class GuardianView(APIView):
"""
Récupère le dernier ID de responsable légal créé.
"""
def get(self, request):
lastGuardian = bdd.getLastId(Guardian)
return JsonResponse({"lastid":lastGuardian}, safe=False)
def send(request, id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
def send(request, _id):
"""
Envoie le dossier dinscription par e-mail.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None:
student = register_form.student
guardian = student.getMainGuardian()
@ -199,24 +256,31 @@ def send(request, id):
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
# Mise à jour de l'automate
updateStateMachine(register_form, 'envoiDI')
return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False)
return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400)
return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND)
def archive(request, id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
def archive(request, _id):
"""
Archive le dossier dinscription visé.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None:
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
# Mise à jour de l'automate
updateStateMachine(register_form, 'archiveDI')
return JsonResponse({"errorMessage":''}, safe=False, status=400)
return JsonResponse({"errorMessage":''}, safe=False, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND)
def relance(request, id):
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
def relance(request, _id):
"""
Relance un dossier dinscription par e-mail.
"""
register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id)
if register_form != None:
student = register_form.student
guardian = student.getMainGuardian()
@ -227,12 +291,15 @@ def relance(request, id):
register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
register_form.save()
return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400)
return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=400)
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND)
# API utilisée pour la vue parent
class ChildrenListView(APIView):
"""
Pour la vue parent : liste les élèves rattachés à un profil donné.
"""
# Récupération des élèves d'un parent
# idProfile : identifiant du profil connecté rattaché aux fiches d'élèves
def get(self, request, _idProfile):
@ -242,6 +309,9 @@ class ChildrenListView(APIView):
# API utilisée pour la vue de création d'un DI
class StudentListView(APIView):
"""
Pour la vue de création dun dossier dinscription : liste les élèves disponibles.
"""
# Récupération de la liste des élèves inscrits ou en cours d'inscriptions
def get(self, request):
students = bdd.getAllObjects(_objectName=Student)
@ -250,20 +320,52 @@ class StudentListView(APIView):
# API utilisée pour la vue de personnalisation des frais d'inscription pour la structure
class RegistrationFeeView(APIView):
"""
Liste les frais dinscription.
"""
def get(self, request):
tarifs = bdd.getAllObjects(RegistrationFee)
tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True)
return JsonResponse(tarifs_serializer.data, safe=False)
class RegistrationFileTemplateView(APIView):
"""
Gère les fichiers templates pour les dossiers dinscription.
"""
parser_classes = (MultiPartParser, FormParser)
def get(self, request):
fichiers = RegistrationFileTemplate.objects.all()
serializer = RegistrationFileTemplateSerializer(fichiers, many=True)
def get(self, request, _id=None):
"""
Récupère les fichiers templates pour les dossiers dinscription.
"""
if _id is None:
files = RegistrationFileTemplate.objects.all()
serializer = RegistrationFileTemplateSerializer(files, many=True)
return Response(serializer.data)
else :
registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id)
if registationFileTemplate is None:
return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileTemplateSerializer(registationFileTemplate)
return JsonResponse(serializer.data, safe=False)
def put(self, request, _id):
"""
Met à jour un fichier template existant.
"""
registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id)
if registationFileTemplate is None:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def post(self, request):
"""
Crée un fichier template pour les dossiers dinscription.
"""
serializer = RegistrationFileTemplateSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
@ -271,10 +373,71 @@ class RegistrationFileTemplateView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, _id):
"""
Supprime un fichier template existant.
"""
registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id)
if registrationFileTemplate is not None:
registrationFileTemplate.file.delete() # Supprimer le fichier uploadé
registrationFileTemplate.delete()
return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False)
return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=400)
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationFileView(APIView):
"""
Gère la création, mise à jour et suppression de fichiers liés à un dossier dinscription.
"""
parser_classes = (MultiPartParser, FormParser)
def get(self, request, _id=None):
"""
Récupère les fichiers liés à un dossier dinscription donné.
"""
if (_id is None):
files = RegistrationFile.objects.all()
serializer = RegistrationFileSerializer(files, many=True)
return Response(serializer.data)
else:
registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id)
if registationFile is None:
return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileSerializer(registationFile)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
"""
Crée un RegistrationFile pour le RegistrationForm associé.
"""
serializer = RegistrationFileSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def put(self, request, fileId):
"""
Met à jour un RegistrationFile existant.
"""
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId)
if registrationFile is None:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileSerializer(registrationFile, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, _id):
"""
Supprime un RegistrationFile existant.
"""
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id)
if registrationFile is not None:
registrationFile.file.delete() # Supprimer le fichier uploadé
registrationFile.delete()
return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False)
else:
return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)

View File

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

View File

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

View File

@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Upload } from 'lucide-react';
export default function DraggableFileUpload({ fileName, onFileSelect }) {
const [dragActive, setDragActive] = useState(false);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
};
const handleDragLeave = () => {
setDragActive(false);
};
const handleFileChosen = (selectedFile) => {
onFileSelect && onFileSelect(selectedFile);
};
const handleDrop = (event) => {
event.preventDefault();
setDragActive(false);
const droppedFile = event.dataTransfer.files[0];
handleFileChosen(droppedFile);
};
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
handleFileChosen(selectedFile);
};
return (
<div>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
</div>
);
}

View File

@ -1,61 +1,47 @@
import React, { useState } from 'react';
import { Upload } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import DraggableFileUpload from './DraggableFileUpload';
export default function FileUpload({ onFileUpload }) {
const [dragActive, setDragActive] = useState(false);
export default function FileUpload({ onFileUpload, fileToEdit = null }) {
const [fileName, setFileName] = useState('');
const [file, setFile] = useState(null);
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
};
const handleDragLeave = () => {
setDragActive(false);
};
const handleDrop = (event) => {
event.preventDefault();
setDragActive(false);
const droppedFile = event.dataTransfer.files[0];
setFile(droppedFile);
setFileName(droppedFile.name.replace(/\.[^/.]+$/, ""));
};
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
};
useEffect(() => {
if (fileToEdit) {
setFileName(fileToEdit.name || '');
setIsRequired(fileToEdit.is_required || false);
setOrder(fileToEdit.fusion_order || 0);
}
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setFileName(event.target.value);
};
const handleUpload = () => {
onFileUpload(file, fileName);
onFileUpload({
file,
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
});
setFile(null);
setFileName('');
setIsRequired(false);
setOrder(0);
};
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>
<DraggableFileUpload
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
}}
/>
<div className="flex mt-2">
<input
type="text"
@ -64,14 +50,28 @@ export default function FileUpload({ onFileUpload }) {
onChange={handleFileNameChange}
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
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'}`}
disabled={fileName==""}
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 === ""}
>
Ajouter
</button>
</div>
<div className="flex items-center mt-4">
<ToggleSwitch
label="Fichier à remplir obligatoirement"
checked={isRequired}
onChange={() => setIsRequired(!isRequired)}
/>
</div>
</div>
);
}

View File

@ -17,6 +17,7 @@ export default function Page() {
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
useEffect(() => {
@ -55,9 +56,8 @@ export default function Page() {
console.error('Error:', error.message);
if (error.details) {
console.error('Form errors:', error.details);
// Handle form errors (e.g., display them to the user)
setFormErrors(error.details);
}
alert('Une erreur est survenue lors de la mise à jour des données');
});
};
@ -69,6 +69,7 @@ export default function Page() {
onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
isLoading={isLoading}
errors={formErrors}
/>
);
}

View File

@ -28,6 +28,7 @@ import {
fetchRegisterFormFileTemplate,
deleteRegisterFormFileTemplate,
createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
fetchStudents,
editRegisterForm } from "@/app/lib/subscriptionAction"
@ -40,6 +41,7 @@ import {
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken';
import { formatDate } from '@/utils/Date';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
@ -69,6 +71,9 @@ export default function Page({ params: { locale } }) {
const [classes, setClasses] = useState([]);
const [students, setEleves] = useState([]);
const [reloadFetch, setReloadFetch] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const csrfToken = useCsrfToken();
@ -185,7 +190,11 @@ const registerFormArchivedDataHandler = (data) => {
.then(registerFormArchivedDataHandler)
.catch(requestErrorHandler)
fetchRegisterFormFileTemplate()
.then((data)=> {setFichiers(data)})
.then((data)=> {
console.log(data);
setFichiers(data)
})
.catch((err)=>{ err = err.message; console.log(err);});
} else {
setTimeout(() => {
@ -548,9 +557,17 @@ const handleFileDelete = (fileId) => {
});
};
const handleFileEdit = (file) => {
setIsEditing(true);
setFileToEdit(file);
setIsModalOpen(true);
};
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Date de création', transform: (row) => row.last_update },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{
@ -559,6 +576,9 @@ const columnsFiles = [
<Download size={16} />
</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">
<Trash2 size={16} />
</button>
@ -566,27 +586,43 @@ const columnsFiles = [
) },
];
const handleFileUpload = (file, fileName) => {
if ( !fileName) {
const handleFileUpload = ({file, name, is_required, order}) => {
if (!name) {
alert('Veuillez entrer un nom de fichier.');
return;
}
const formData = new FormData();
if(file){
formData.append('file', file);
}
formData.append('name', fileName);
createRegistrationFormFileTemplate(formData,csrfToken)
formData.append('name', name);
formData.append('is_required', is_required);
formData.append('order', order);
if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
.then(data => {
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? data : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
});
} else {
createRegistrationFormFileTemplate(formData, csrfToken)
.then(data => {
console.log('Success:', data);
setFichiers([...fichiers, data]);
closeUploadModal();
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
if (isLoading) {
@ -699,7 +735,23 @@ const handleFileUpload = (file, fileName) => {
{/*SI STATE == subscribeFiles */}
{activeTab === 'subscribeFiles' && (
<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">
<Table
data={fichiers}

View File

@ -3,10 +3,12 @@
import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu';
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 { 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 { fetchMessages } from '@/app/lib/messagerieAction';
import ProtectedRoute from '@/components/ProtectedRoute';
export default function Layout({
children,
@ -18,12 +20,8 @@ export default function Layout({
useEffect(() => {
setUserId(userId);
fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${userId}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
setUserId(userId)
fetchMessages(userId)
.then(data => {
if (data) {
setMessages(data);
@ -33,11 +31,10 @@ export default function Layout({
.catch(error => {
console.error('Error fetching data:', error);
});
}, []);
return (
<>
<ProtectedRoute>
<div className="flex flex-col min-h-screen bg-gray-50">
{/* 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">
@ -85,7 +82,7 @@ export default function Layout({
{children}
</div>
</div>
</>
</ProtectedRoute>
);
}

View File

@ -0,0 +1,24 @@
import {
BE_GESTIONMESSAGERIE_MESSAGES_URL
} from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error('Form submission error');
error.details = body;
throw error;
}
export const fetchMessages = (id) =>{
return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
}

View File

@ -2,7 +2,10 @@ import {
BE_SCHOOL_SPECIALITIES_URL,
BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_PLANNINGS_URL
BE_SCHOOL_PLANNINGS_URL,
BE_SCHOOL_FEES_URL,
BE_SCHOOL_DISCOUNTS_URL,
BE_SCHOOL_TUITION_FEES_URL
} from '@/utils/Url';
const requestResponseHandler = async (response) => {
@ -37,3 +40,18 @@ export const fetchSchedules = () => {
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
.then(requestResponseHandler)
};
export const fetchDiscounts = () => {
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}`)
.then(requestResponseHandler)
};
export const fetchFees = () => {
return fetch(`${BE_SCHOOL_FEES_URL}`)
.then(requestResponseHandler)
};
export const fetchTuitionFees = () => {
return fetch(`${BE_SCHOOL_TUITION_FEES_URL}`)
.then(requestResponseHandler)
};

View File

@ -7,7 +7,8 @@ import {
BE_SUBSCRIPTION_REGISTERFORM_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_URL
BE_SUBSCRIPTION_LAST_GUARDIAN_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL
} from '@/utils/Url';
export const PENDING = 'pending';
@ -110,6 +111,32 @@ export const fetchRegisterFormFileTemplate = () => {
return fetch(request).then(requestResponseHandler)
};
export const fetchRegisterFormFile = (id) => {
const request = new Request(
`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
};
export const createRegistrationFormFile = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}`, {
method: 'POST',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
}
export const createRegistrationFormFileTemplate = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, {
@ -132,6 +159,19 @@ export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => {
credentials: 'include',
})
}
export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, {
method: 'PUT',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
}
export const fetchStudents = () => {
const request = new Request(
`${BE_SUBSCRIPTION_STUDENTS_URL}`,

View File

@ -6,8 +6,8 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
<>
<div className={`mb-4 ${className}`}>
<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`}>
<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-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">
<Palette className="w-5 h-5" />
</span>
<input
@ -16,7 +16,7 @@ const InputColorIcon = ({ name, label, value, onChange, errorMsg, className }) =
name={name}
value={value}
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>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { isValidPhoneNumber } from 'react-phone-number-input';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) {
const inputRef = useRef(null);
@ -19,12 +19,12 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, pla
<>
<div className={`mb-4 ${className}`}>
<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
type="tel"
name={name}
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 : ''}
onChange={handleChange}
placeholder={placeholder}

View File

@ -1,8 +1,11 @@
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder,className}) {
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) {
return (
<>
<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`}>
<input
type={type}
@ -12,6 +15,7 @@ export default function InputText({name, type, label, value, onChange, errorMsg,
value={value}
onChange={onChange}
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>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -1,11 +1,10 @@
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
return (
<>
<div className={`mb-4 ${className}`}>
<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`}>
<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-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">
{IconItem && <IconItem />}
</span>
<input
@ -15,7 +14,7 @@ export default function InputTextIcon({name, type, IconItem, label, value, onCha
name={name}
value={value}
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>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}

View File

@ -5,8 +5,12 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
import Loader from '@/components/Loader';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import FileUpload from '@/app/[locale]/admin/subscriptions/components/FileUpload';
import Table from '@/components/Table';
import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction';
import { Download, Upload } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload';
import Modal from '@/components/Modal';
const levels = [
{ value:'1', label: 'TPS - Très Petite Section'},
@ -20,7 +24,8 @@ export default function InscriptionFormShared({
csrfToken,
onSubmit,
cancelUrl,
isLoading = false
isLoading = false,
errors = {} // Nouvelle prop pour les erreurs
}) {
const [formData, setFormData] = useState(() => ({
@ -41,6 +46,11 @@ export default function InscriptionFormShared({
);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileName, setFileName] = useState("");
const [file, setFile] = useState("");
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
// Mettre à jour les données quand initialData change
useEffect(() => {
@ -58,6 +68,9 @@ export default function InscriptionFormShared({
level: initialData.level || ''
});
setGuardians(initialData.guardians || []);
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
}
}, [initialData]);
@ -65,8 +78,22 @@ export default function InscriptionFormShared({
setFormData(prev => ({...prev, [field]: value}));
};
const handleFileUpload = (file, fileName) => {
setUploadedFiles([...uploadedFiles, { file, fileName }]);
const handleFileUpload = async (file, fileName) => {
const data = new FormData();
data.append('file', file);
data.append('name',fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try {
await createRegistrationFormFile(data, csrfToken);
// Optionnellement, rafraîchir la liste des fichiers
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
} catch (error) {
console.error('Error uploading file:', error);
}
};
const handleSubmit = (e) => {
@ -80,12 +107,31 @@ export default function InscriptionFormShared({
onSubmit(data);
};
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
const getGuardianError = (index, field) => {
return errors?.student?.guardians?.[index]?.[field]?.[0];
};
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.last_name },
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Fichier de référence', transform: (row) => row.file && <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) => (
<a href={URL.createObjectURL(row.fichier)} target='_blank' className="text-blue-500 hover:text-blue-700">
Télécharger
</a>
<div className="flex items-center justify-center gap-2">
{row.is_required &&
<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}
onChange={(e) => updateFormField('last_name', e.target.value)}
required
errorMsg={getError('last_name')}
/>
<InputText
name="first_name"
label="Prénom"
value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required
/>
<InputText
@ -126,18 +174,22 @@ export default function InscriptionFormShared({
value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)}
required
errorMsg={getError('birth_date')}
/>
<InputText
name="birth_place"
label="Lieu de Naissance"
value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)}
errorMsg={getError('birth_place')}
/>
<InputText
name="birth_postal_code"
label="Code Postal de Naissance"
value={formData.birth_postal_code}
onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
required
errorMsg={getError('birth_postal_code')}
/>
<div className="md:col-span-2">
<InputText
@ -145,6 +197,7 @@ export default function InscriptionFormShared({
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
errorMsg={getError('address')}
/>
</div>
<InputText
@ -152,14 +205,17 @@ export default function InscriptionFormShared({
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)}
errorMsg={getError('attending_physician')}
/>
<SelectChoice
name="level"
label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)}
choices={levels}
required
errorMsg={getError('level')}
/>
</div>
</div>
@ -184,6 +240,7 @@ export default function InscriptionFormShared({
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/>
</div>
@ -191,14 +248,13 @@ export default function InscriptionFormShared({
<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>
<Table
data={uploadedFiles}
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
<FileUpload onFileUpload={handleFileUpload} />
</div>
{/* Boutons de contrôle */}
@ -207,6 +263,44 @@ export default function InscriptionFormShared({
<Button type="submit" text="Valider" primary />
</div>
</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>
);
}

View File

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

View File

@ -1,16 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel }) => {
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
if (!visible) return null;
return ReactDOM.createPortal(
<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">
<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-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>,

View File

@ -0,0 +1,21 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import useLocalStorage from '@/hooks/useLocalStorage';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
const ProtectedRoute = ({ children }) => {
const router = useRouter();
const [userId] = useLocalStorage("userId", '');
useEffect(() => {
if (!userId) {
// Rediriger vers la page de login si l'utilisateur n'est pas connecté
router.push(FE_USERS_LOGIN_URL);
}
}, [userId, router]);
// Afficher les enfants seulement si l'utilisateur est connecté
return userId ? children : null;
};
export default ProtectedRoute;

View File

@ -1,34 +1,36 @@
export default function SelectChoice({ type, name, label, choices, callback, selected, error, IconItem, disabled = false }) {
export default function SelectChoice({ type, name, label,required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
return (
<>
<div className="mb-4">
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div
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`}
>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm h-full">
{IconItem && <IconItem />}
<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' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
{IconItem &&
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
{<IconItem />}
</span>
}
<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}
id={name}
name={name}
value={selected}
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) => (
<option key={value} value={value} className={value === '' ? 'italic' : ''}>
<option key={value} value={value}>
{label}
</option>
))}
</select>
</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>
</>
);
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Users, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -50,10 +50,7 @@ const ClassesSection = ({ classes, teachers, handleCreate, handleEdit, handleDel
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<Users className="w-8 h-8 mr-2" />
Classes
</h2>
<h2 className="text-xl font-bold mb-4">Gestion des classes</h2>
<button
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"

View File

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

View File

@ -0,0 +1,43 @@
import React, { useState } from 'react';
import FeesSection from './FeesSection';
import DiscountsSection from './DiscountsSection';
import TuitionFeesSection from './TuitionFeesSection';
import { TuitionFeesProvider } from '@/context/TuitionFeesContext';
import { BE_SCHOOL_FEE_URL, BE_SCHOOL_DISCOUNT_URL, BE_SCHOOL_TUITION_FEE_URL } from '@/utils/Url';
const FeesManagement = ({ fees, setFees, discounts, setDiscounts, setTuitionFees, handleCreate, handleEdit, handleDelete }) => {
const [errors, setErrors] = useState({});
return (
<TuitionFeesProvider>
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-6">
<div className="p-4 bg-white rounded-lg shadow-md">
<FeesSection
fees={fees}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEE_URL}`, newData, setFees, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEE_URL}`, id, updatedData, setFees, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEE_URL}`, id, setFees)}
errors
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<DiscountsSection
discounts={discounts}
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNT_URL}`, newData, setDiscounts, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNT_URL}`, id, updatedData, setDiscounts, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNT_URL}`, id, setDiscounts)}
/>
</div>
<div className="p-4 bg-white rounded-lg shadow-md">
<TuitionFeesSection
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_TUITION_FEE_URL}`, newData, setTuitionFees, setErrors)}
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_TUITION_FEE_URL}`, id, updatedData, setTuitionFees, setErrors)}
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TUITION_FEE_URL}`, id, setTuitionFees)}
/>
</div>
</div>
</TuitionFeesProvider>
);
};
export default FeesManagement;

View File

@ -0,0 +1,190 @@
import React, { useState } from 'react';
import { Plus, Trash, Edit3, Check, X } from 'lucide-react';
import Table from '@/components/Table';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
const FeesSection = ({ fees, handleCreate, handleEdit, handleDelete, errors }) => {
const [editingFee, setEditingFee] = useState(null);
const [newFee, setNewFee] = useState(null);
const [formData, setFormData] = useState({});
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const handleAddFee = () => {
setNewFee({ id: Date.now(), name: '', amount: '', description: '' });
};
const handleRemoveFee = (id) => {
handleDelete(id);
};
const handleSaveNewFee = () => {
if (newFee.name && newFee.amount) {
handleCreate(newFee)
.then(() => {
setNewFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleUpdateFee = (id, updatedFee) => {
if (updatedFee.name && updatedFee.amount) {
handleEdit(id, updatedFee)
.then(() => {
setEditingFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
}
});
} else {
setPopupMessage("Tous les champs doivent être remplis");
setPopupVisible(true);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
if (editingFee) {
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
} else if (newFee) {
setNewFee((prevData) => ({
...prevData,
[name]: value,
}));
}
};
const renderInputField = (field, value, onChange, placeholder) => (
<div>
<InputTextIcon
name={field}
type={field === 'amount' ? 'number' : 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
/>
</div>
);
const renderFeeCell = (fee, column) => {
const isEditing = editingFee === fee.id;
const isCreating = newFee && newFee.id === fee.id;
const currentData = isEditing ? formData : newFee;
if (isEditing || isCreating) {
switch (column) {
case 'LIBELLE':
return renderInputField('name', currentData.name, handleChange, 'Libellé du frais');
case 'MONTANT':
return renderInputField('amount', currentData.amount, handleChange, 'Montant');
case 'DESCRIPTION':
return renderInputField('description', currentData.description, handleChange, 'Description');
case 'ACTIONS':
return (
<div className="flex space-x-2">
<button
type="button"
onClick={() => (isEditing ? handleUpdateFee(editingFee, formData) : handleSaveNewFee())}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => (isEditing ? setEditingFee(null) : setNewFee(null))}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'LIBELLE':
return fee.name;
case 'MONTANT':
return fee.amount + ' €';
case 'DESCRIPTION':
return fee.description;
case 'ACTIONS':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => setEditingFee(fee.id) || setFormData(fee)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => handleRemoveFee(fee.id)}
className="text-red-500 hover:text-red-700"
>
<Trash className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
return (
<>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Frais d'inscription</h2>
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<Table
data={newFee ? [newFee, ...fees] : fees}
columns={[
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'MONTANT', label: 'Montant' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'ACTIONS', label: 'Actions' }
]}
renderCell={renderFeeCell}
/>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
</>
);
};
export default FeesSection;

View File

@ -1,4 +1,4 @@
import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -33,10 +33,7 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
<h2 className="text-xl font-bold mb-4">Gestion des spécialités</h2>
<button
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"
@ -52,8 +49,8 @@ const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDel
transform: (row) => (
<div
className="inline-block px-3 py-1 rounded-full font-bold text-white"
style={{ backgroundColor: row. color_code }}
title={row. color_code}
style={{ backgroundColor: row.color_code }}
title={row.color_code}
>
<span className="font-bold text-white">{row.name.toUpperCase()}</span>
</div>

View File

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

View File

@ -1,4 +1,4 @@
import { GraduationCap, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
@ -77,10 +77,7 @@ const TeachersSection = ({ teachers, specialities , handleCreate, handleEdit, ha
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<GraduationCap className="w-8 h-8 mr-2" />
Enseignants
</h2>
<h2 className="text-xl font-bold mb-4">Gestion des enseignants</h2>
<button
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"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
import React, { createContext, useState, useEffect, useContext } from 'react';
import { fetchTuitionFees, fetchFees, fetchDiscounts } from '@/app/lib/schoolAction';
const TuitionFeesContext = createContext();
export const useTuitionFees = () => useContext(TuitionFeesContext);
export const TuitionFeesProvider = ({ children }) => {
const [tuitionFees, setTuitionFees] = useState([]);
const [fees, setFees] = useState([]);
const [discounts, setDiscounts] = useState([]);
useEffect(() => {
fetchTuitionFees().then(data => setTuitionFees(data));
fetchFees().then(data => setFees(data));
fetchDiscounts().then(data => setDiscounts(data));
}, []);
return (
<TuitionFeesContext.Provider value={{ tuitionFees, setTuitionFees, fees, setFees, discounts, setDiscounts }}>
{children}
</TuitionFeesContext.Provider>
);
};

View File

@ -23,6 +23,7 @@ export const BE_SUBSCRIPTION_ARCHIVE_URL = `${BASE_URL}/Subscriptions/archive`
export const BE_SUBSCRIPTION_REGISTERFORM_URL = `${BASE_URL}/Subscriptions/registerForm`
export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`
export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registrationFileTemplates`
export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL = `${BASE_URL}/Subscriptions/registrationFiles`
export const BE_SUBSCRIPTION_LAST_GUARDIAN_URL = `${BASE_URL}/Subscriptions/lastGuardian`
//GESTION ENSEIGNANT
@ -34,6 +35,12 @@ export const BE_SCHOOL_TEACHER_URL = `${BASE_URL}/School/teacher`
export const BE_SCHOOL_TEACHERS_URL = `${BASE_URL}/School/teachers`
export const BE_SCHOOL_PLANNING_URL = `${BASE_URL}/School/planning`
export const BE_SCHOOL_PLANNINGS_URL = `${BASE_URL}/School/plannings`
export const BE_SCHOOL_FEE_URL = `${BASE_URL}/School/fee`;
export const BE_SCHOOL_FEES_URL = `${BASE_URL}/School/fees`;
export const BE_SCHOOL_DISCOUNT_URL = `${BASE_URL}/School/discount`;
export const BE_SCHOOL_DISCOUNTS_URL = `${BASE_URL}/School/discounts`;
export const BE_SCHOOL_TUITION_FEE_URL = `${BASE_URL}/School/tuitionFee`;
export const BE_SCHOOL_TUITION_FEES_URL = `${BASE_URL}/School/tuitionFees`;
// GESTION MESSAGERIE
export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messagerie`
@ -47,8 +54,7 @@ export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe`
export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset`
export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new`
//ADMIN
// ADMIN
export const FE_ADMIN_HOME_URL = `/admin`
// ADMIN/SUBSCRIPTIONS URL