Merge remote-tracking branch 'origin/WIP_Inscriptions' into develop

This commit is contained in:
Luc SORIGNET
2025-04-25 10:55:54 +02:00
39 changed files with 2400 additions and 1610 deletions

View File

@ -4,11 +4,22 @@ from django.template.loader import get_template
from xhtml2pdf import pisa
class PDFResult:
def __init__(self, content):
self.content = content
def render_to_pdf(template_src, context_dict={}):
"""
Génère un PDF à partir d'un template HTML et retourne le contenu en mémoire.
"""
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
return HttpResponse(result.getvalue(), content_type='application/pdf')
# Lever une exception ou retourner None en cas d'erreur
raise ValueError("Erreur lors de la génération du PDF.")
# Retourner le contenu du PDF en mémoire
return PDFResult(result.getvalue())

View File

@ -6,8 +6,8 @@ from Subscriptions.models import (
Fee,
Discount,
RegistrationFileGroup,
RegistrationTemplateMaster,
RegistrationTemplate
RegistrationSchoolFileMaster,
RegistrationSchoolFileTemplate
)
from Auth.models import Profile, ProfileRole
from School.models import (

View File

@ -9,6 +9,8 @@ from Establishment.models import Establishment
from datetime import datetime
import os
class Language(models.Model):
"""
Représente une langue parlée par lélève.
@ -83,7 +85,7 @@ class Student(models.Model):
siblings = models.ManyToManyField(Sibling, blank=True)
# Many-to-Many Relationship
registration_files = models.ManyToManyField('RegistrationTemplate', blank=True, related_name='students')
registration_files = models.ManyToManyField('RegistrationSchoolFileTemplate', blank=True, related_name='students')
# Many-to-Many Relationship
spoken_languages = models.ManyToManyField(Language, blank=True)
@ -163,19 +165,13 @@ class RegistrationFileGroup(models.Model):
def __str__(self):
return self.name
def __str__(self):
return f'{self.group.name} - {self.id}'
def registration_file_path(instance, filename):
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename
return f'registration_files/dossier_rf_{instance.student_id}/{filename}'
class RegistrationTemplateMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='template_masters', blank=True)
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
is_required = models.BooleanField(default=False)
def __str__(self):
return f'{self.group.name} - {self.id}'
class RegistrationForm(models.Model):
class RegistrationFormStatus(models.IntegerChoices):
RF_IDLE = 0, _('Pas de dossier d\'inscription')
@ -211,7 +207,7 @@ class RegistrationForm(models.Model):
# Many-to-Many Relationship
discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms')
fileGroup = models.ForeignKey(RegistrationFileGroup,
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
related_name='register_forms',
null=True,
blank=True)
@ -223,16 +219,58 @@ class RegistrationForm(models.Model):
def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name
def registration_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/{filename}"
def save(self, *args, **kwargs):
# Vérifier si un fichier existant doit être remplacé
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationForm.objects.get(pk=self.pk)
if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file:
# Supprimer l'ancien fichier
old_instance.sepa_file.delete(save=False)
except RegistrationForm.DoesNotExist:
pass # L'objet n'existe pas encore, rien à supprimer
class RegistrationTemplate(models.Model):
master = models.ForeignKey(RegistrationTemplateMaster, on_delete=models.CASCADE, related_name='templates', blank=True)
# Appeler la méthode save originale
super().save(*args, **kwargs)
def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
#############################################################
####################### MASTER FILES ########################
#############################################################
####### DocuSeal masters (documents école, à signer ou pas) #######
class RegistrationSchoolFileMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
is_required = models.BooleanField(default=False)
def __str__(self):
return f'{self.group.name} - {self.id}'
####### Parent files masters (documents à fournir par les parents) #######
class RegistrationParentFileMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_file_masters', blank=True)
name = models.CharField(max_length=255, default="")
description = models.CharField(blank=True, null=True)
############################################################
####################### CLONE FILES ########################
############################################################
####### DocuSeal templates (par dossier d'inscription) #######
class RegistrationSchoolFileTemplate(models.Model):
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
id = models.IntegerField(primary_key=True)
slug = models.CharField(max_length=255, default="")
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
def __str__(self):
return self.name
@ -242,7 +280,41 @@ class RegistrationTemplate(models.Model):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationTemplate.objects.filter(registration_form=register_form_id)
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
####### Parent files templates (par dossier d'inscription) #######
class RegistrationParentFileTemplate(models.Model):
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
if old_instance.file and (not self.file or self.file.name == ''):
if os.path.exists(old_instance.file.path):
old_instance.file.delete(save=False)
self.file = None
else:
print(f"Le fichier {old_instance.file.path} n'existe pas.")
except RegistrationParentFileTemplate.DoesNotExist:
print("Ancienne instance introuvable.")
super().save(*args, **kwargs)
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)

View File

@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationTemplateMaster, RegistrationTemplate
from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
from School.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import ProfileRole, Profile
@ -12,18 +12,43 @@ import pytz
from datetime import datetime
import Subscriptions.util as util
class RegistrationTemplateMasterSerializer(serializers.ModelSerializer):
class RegistrationSchoolFileMasterSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = RegistrationTemplateMaster
model = RegistrationSchoolFileMaster
fields = '__all__'
class RegistrationTemplateSerializer(serializers.ModelSerializer):
class RegistrationParentFileMasterSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = RegistrationTemplate
model = RegistrationParentFileMaster
fields = '__all__'
class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
file_url = serializers.SerializerMethodField()
class Meta:
model = RegistrationSchoolFileTemplate
fields = '__all__'
def get_file_url(self, obj):
# Retourne l'URL complète du fichier si disponible
return obj.file.url if obj.file else None
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
file_url = serializers.SerializerMethodField()
master_name = serializers.CharField(source='master.name', read_only=True)
master_description = serializers.CharField(source='master.description', read_only=True)
class Meta:
model = RegistrationParentFileTemplate
fields = '__all__'
def get_file_url(self, obj):
# Retourne l'URL complète du fichier si disponible
return obj.file.url if obj.file else None
class GuardianSimpleSerializer(serializers.ModelSerializer):
associated_profile_email = serializers.SerializerMethodField()
@ -199,7 +224,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
sepa_file = serializers.FileField(required=False)
status_label = serializers.SerializerMethodField()
formatted_last_update = serializers.SerializerMethodField()
registration_files = RegistrationTemplateSerializer(many=True, required=False)
registration_files = RegistrationSchoolFileTemplateSerializer(many=True, required=False)
fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False)
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False)
totalRegistrationFees = serializers.SerializerMethodField()
@ -280,7 +305,7 @@ class RegistrationFormByParentSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationForm
fields = ['student', 'status']
fields = ['student', 'status', 'sepa_file']
def __init__(self, *args, **kwargs):
super(RegistrationFormByParentSerializer, self).__init__(*args, **kwargs)

View File

@ -7,16 +7,27 @@ from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archi
# SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView
# Files
from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .views import (
RegistrationSchoolFileMasterView,
RegistrationSchoolFileMasterSimpleView,
RegistrationSchoolFileTemplateView,
RegistrationSchoolFileTemplateSimpleView,
RegistrationParentFileMasterSimpleView,
RegistrationParentFileMasterView,
RegistrationParentFileTemplateSimpleView,
RegistrationParentFileTemplateView
)
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .views import registration_file_views, get_templates_by_rf
from .views import registration_file_views, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
urlpatterns = [
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"),
re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"),
re_path(r'^registerForms/(?P<id>[0-9]+)$', RegisterFormWithIdView.as_view(), name="registerForm"),
re_path(r'^registerForms/(?P<id>[0-9]+)/templates$', get_templates_by_rf, name="get_templates_by_rf"),
re_path(r'^registerForms/(?P<id>[0-9]+)/school_file_templates$', get_school_file_templates_by_rf, name="get_school_file_templates_by_rf"),
re_path(r'^registerForms/(?P<id>[0-9]+)/parent_file_templates$', get_parent_file_templates_by_rf, name="get_parent_file_templates_by_rf"),
re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"),
# Page INSCRIPTION - Liste des élèves
@ -33,11 +44,17 @@ urlpatterns = [
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)/templates$', get_registration_files_by_group, name="get_registration_files_by_group"),
re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'),
re_path(r'^registrationTemplateMasters/(?P<id>[0-9]+)$', RegistrationTemplateMasterSimpleView.as_view(), name='registrationTemplateMasters'),
re_path(r'^registrationTemplateMasters$', RegistrationTemplateMasterView.as_view(), name='registrationTemplateMasters'),
re_path(r'^registrationSchoolFileMasters/(?P<id>[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'),
re_path(r'^registrationSchoolFileMasters$', RegistrationSchoolFileMasterView.as_view(), name='registrationSchoolFileMasters'),
re_path(r'^registrationTemplates/(?P<id>[0-9]+)$', RegistrationTemplateSimpleView.as_view(), name='registrationTemplates'),
re_path(r'^registrationTemplates$', RegistrationTemplateView.as_view(), name="registrationTemplates"),
re_path(r'^registrationParentFileMasters/(?P<id>[0-9]+)$', RegistrationParentFileMasterSimpleView.as_view(), name='registrationParentFileMasters'),
re_path(r'^registrationParentFileMasters$', RegistrationParentFileMasterView.as_view(), name="registrationParentFileMasters"),
re_path(r'^registrationSchoolFileTemplates/(?P<id>[0-9]+)$', RegistrationSchoolFileTemplateSimpleView.as_view(), name='registrationSchoolFileTemplates'),
re_path(r'^registrationSchoolFileTemplates$', RegistrationSchoolFileTemplateView.as_view(), name="registrationSchoolFileTemplates"),
re_path(r'^registrationParentFileTemplates/(?P<id>[0-9]+)$', RegistrationParentFileTemplateSimpleView.as_view(), name='registrationParentFileTemplates'),
re_path(r'^registrationParentFileTemplates$', RegistrationParentFileTemplateView.as_view(), name="registrationSchoolFileTregistrationParentFileTemplatesemplates"),
re_path(r'^students/(?P<student_id>[0-9]+)/guardians/(?P<guardian_id>[0-9]+)/dissociate', DissociateGuardianView.as_view(), name='dissociate-guardian'),

View File

@ -19,6 +19,9 @@ from rest_framework.parsers import JSONParser
from PyPDF2 import PdfMerger
import shutil
import logging
logger = logging.getLogger(__name__)
def recupereListeFichesInscription():
"""
@ -121,7 +124,6 @@ def rfToPDF(registerForm, filename):
Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
"""
filename = filename.replace(" ", "_")
data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
@ -131,20 +133,37 @@ def rfToPDF(registerForm, filename):
# Générer le PDF
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
if not pdf:
raise ValueError("Erreur lors de la génération du PDF.")
# Vérifier si un fichier avec le même nom existe déjà et le supprimer
if registerForm.registration_file and os.path.exists(registerForm.registration_file.path):
os.remove(registerForm.registration_file.path)
if registerForm.registration_file and registerForm.registration_file.name:
# Vérifiez si le chemin est déjà absolu ou relatif
if os.path.isabs(registerForm.registration_file.name):
existing_file_path = registerForm.registration_file.name
else:
existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/'))
# Vérifier si le fichier existe et le supprimer
if os.path.exists(existing_file_path):
print(f'exist ! REMOVE')
os.remove(existing_file_path)
registerForm.registration_file.delete(save=False)
else:
print(f'File does not exist: {existing_file_path}')
# Enregistrer directement le fichier dans le champ registration_file
try:
registerForm.registration_file.save(
os.path.basename(filename),
File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque
os.path.basename(filename), # Utiliser uniquement le nom de fichier
File(BytesIO(pdf.content)),
save=True
)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
raise
return registerForm.registration_file.path
return registerForm.registration_file
def delete_registration_files(registerForm):
"""

View File

@ -1,5 +1,14 @@
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_templates_by_rf
from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
from .registration_file_views import (
RegistrationSchoolFileMasterView,
RegistrationSchoolFileMasterSimpleView,
RegistrationSchoolFileTemplateView,
RegistrationSchoolFileTemplateSimpleView,
RegistrationParentFileMasterView,
RegistrationParentFileMasterSimpleView,
RegistrationParentFileTemplateSimpleView,
RegistrationParentFileTemplateView
)
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView, DissociateGuardianView
@ -10,14 +19,19 @@ __all__ = [
'send',
'resend',
'archive',
'RegistrationTemplateView',
'RegistrationTemplateSimpleView',
'RegistrationTemplateMasterView',
'RegistrationTemplateMasterSimpleView',
'RegistrationSchoolFileTemplateView',
'RegistrationSchoolFileTemplateSimpleView',
'RegistrationParentFileMasterSimpleView',
'RegistrationParentFileMasterView',
'RegistrationSchoolFileMasterView',
'RegistrationSchoolFileMasterSimpleView',
'RegistrationParentFileTemplateSimpleView',
'RegistrationParentFileTemplateView',
'RegistrationFileGroupView',
'RegistrationFileGroupSimpleView',
'get_registration_files_by_group',
'get_templates_by_rf',
'get_school_file_templates_by_rf',
'get_parent_file_templates_by_rf'
'StudentView',
'StudentListView',
'ChildrenListView',

View File

@ -14,9 +14,9 @@ from django.core.files import File
import Subscriptions.mailManager as mailer
import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.pagination import CustomPagination
from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationTemplate, RegistrationFileGroup
from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationSchoolFileTemplate, RegistrationFileGroup, RegistrationParentFileTemplate
from Subscriptions.automate import updateStateMachine
from N3wtSchool import settings, bdd
@ -254,29 +254,36 @@ class RegisterFormWithIdView(APIView):
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
try:
# Génération de la fiche d'inscription au format PDF
base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}"
base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}")
os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial
initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save()
# Récupération des fichiers d'inscription
fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path)
# fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
# if registerForm.registration_file:
# fileNames.insert(0, registerForm.registration_file.path)
# Création du fichier PDF Fusionné
merged_pdf_content = util.merge_files_pdf(fileNames)
# # Création du fichier PDF Fusionné
# merged_pdf_content = util.merge_files_pdf(fileNames)
# # Mise à jour du champ registration_file avec le fichier fusionné
# registerForm.registration_file.save(
# f"dossier_complet.pdf",
# File(merged_pdf_content),
# save=True
# )
# Mise à jour du champ registration_file avec le fichier fusionné
registerForm.registration_file.save(
f"dossier_complet_{registerForm.pk}.pdf",
File(merged_pdf_content),
save=True
)
# Mise à jour de l'automate
# Vérification de la présence du fichier SEPA
if registerForm.sepa_file:
# Mise à jour de l'automate pour SEPA
updateStateMachine(registerForm, 'EVENT_SIGNATURE_SEPA')
else:
# Mise à jour de l'automate pour une signature classique
updateStateMachine(registerForm, 'EVENT_SIGNATURE')
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@ -413,10 +420,40 @@ def resend(request,id):
operation_summary="Récupérer les fichiers à signer d'un dossier d'inscription donné"
)
@api_view(['GET'])
def get_templates_by_rf(request, id):
def get_school_file_templates_by_rf(request, id):
try:
templates = RegistrationTemplate.objects.filter(registration_form=id)
templates_data = list(templates.values())
return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist:
return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404)
# Récupérer les templates associés au RegistrationForm donné
templates = RegistrationSchoolFileTemplate.objects.filter(registration_form=id)
# Sérialiser les données
serializer = RegistrationSchoolFileTemplateSerializer(templates, many=True)
# Retourner les données sérialisées
return JsonResponse(serializer.data, safe=False)
except RegistrationSchoolFileTemplate.DoesNotExist:
return JsonResponse({'error': 'Aucun template trouvé pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(
method='get',
responses={200: openapi.Response('Success', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING)
}
))},
operation_description="Récupère les pièces à fournir d'un dossier d'inscription donné",
operation_summary="Récupérer les pièces à fournir d'un dossier d'inscription donné"
)
@api_view(['GET'])
def get_parent_file_templates_by_rf(request, id):
try:
# Récupérer les pièces à fournir associés au RegistrationForm donné
parent_files = RegistrationParentFileTemplate.objects.filter(registration_form=id)
# Sérialiser les données
serializer = RegistrationParentFileTemplateSerializer(parent_files, many=True)
# Retourner les données sérialisées
return JsonResponse(serializer.data, safe=False)
except RegistrationParentFileTemplate.DoesNotExist:
return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND)

View File

@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from Subscriptions.serializers import RegistrationFileGroupSerializer
from Subscriptions.models import RegistrationFileGroup, RegistrationTemplateMaster
from Subscriptions.models import RegistrationFileGroup, RegistrationSchoolFileMaster
from N3wtSchool import bdd
class RegistrationFileGroupView(APIView):
@ -124,7 +124,7 @@ class RegistrationFileGroupSimpleView(APIView):
def get_registration_files_by_group(request, id):
try:
group = RegistrationFileGroup.objects.get(id=id)
templateMasters = RegistrationTemplateMaster.objects.filter(groups=group)
templateMasters = RegistrationSchoolFileMaster.objects.filter(groups=group)
templates_data = list(templateMasters.values())
return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist:

View File

@ -6,64 +6,64 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from Subscriptions.serializers import RegistrationTemplateMasterSerializer, RegistrationTemplateSerializer
from Subscriptions.models import RegistrationTemplateMaster, RegistrationTemplate
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
from N3wtSchool import bdd
class RegistrationTemplateMasterView(APIView):
class RegistrationSchoolFileMasterView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les masters de templates d'inscription",
responses={200: RegistrationTemplateMasterSerializer(many=True)}
responses={200: RegistrationSchoolFileMasterSerializer(many=True)}
)
def get(self, request):
masters = RegistrationTemplateMaster.objects.all()
serializer = RegistrationTemplateMasterSerializer(masters, many=True)
masters = RegistrationSchoolFileMaster.objects.all()
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau master de template d'inscription",
request_body=RegistrationTemplateMasterSerializer,
request_body=RegistrationSchoolFileMasterSerializer,
responses={
201: RegistrationTemplateMasterSerializer,
201: RegistrationSchoolFileMasterSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationTemplateMasterSerializer(data=request.data)
serializer = RegistrationSchoolFileMasterSerializer(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)
class RegistrationTemplateMasterSimpleView(APIView):
class RegistrationSchoolFileMasterSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un master de template d'inscription spécifique",
responses={
200: RegistrationTemplateMasterSerializer,
200: RegistrationSchoolFileMasterSerializer,
404: "Master non trouvé"
}
)
def get(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
if master is None:
return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateMasterSerializer(master)
serializer = RegistrationSchoolFileMasterSerializer(master)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un master de template d'inscription existant",
request_body=RegistrationTemplateMasterSerializer,
request_body=RegistrationSchoolFileMasterSerializer,
responses={
200: RegistrationTemplateMasterSerializer,
200: RegistrationSchoolFileMasterSerializer,
400: "Données invalides",
404: "Master non trouvé"
}
)
def put(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
if master is None:
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateMasterSerializer(master, data=request.data)
serializer = RegistrationSchoolFileMasterSerializer(master, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@ -77,67 +77,67 @@ class RegistrationTemplateMasterSimpleView(APIView):
}
)
def delete(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
if master is not None:
master.delete()
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationTemplateView(APIView):
class RegistrationSchoolFileTemplateView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les templates d'inscription",
responses={200: RegistrationTemplateSerializer(many=True)}
responses={200: RegistrationSchoolFileTemplateSerializer(many=True)}
)
def get(self, request):
templates = RegistrationTemplate.objects.all()
serializer = RegistrationTemplateSerializer(templates, many=True)
templates = RegistrationSchoolFileTemplate.objects.all()
serializer = RegistrationSchoolFileTemplateSerializer(templates, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau template d'inscription",
request_body=RegistrationTemplateSerializer,
request_body=RegistrationSchoolFileTemplateSerializer,
responses={
201: RegistrationTemplateSerializer,
201: RegistrationSchoolFileTemplateSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationTemplateSerializer(data=request.data)
serializer = RegistrationSchoolFileTemplateSerializer(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)
class RegistrationTemplateSimpleView(APIView):
class RegistrationSchoolFileTemplateSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un template d'inscription spécifique",
responses={
200: RegistrationTemplateSerializer,
200: RegistrationSchoolFileTemplateSerializer,
404: "Template non trouvé"
}
)
def get(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateSerializer(template)
serializer = RegistrationSchoolFileTemplateSerializer(template)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un template d'inscription existant",
request_body=RegistrationTemplateSerializer,
request_body=RegistrationSchoolFileTemplateSerializer,
responses={
200: RegistrationTemplateSerializer,
200: RegistrationSchoolFileTemplateSerializer,
400: "Données invalides",
404: "Template non trouvé"
}
)
def put(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateSerializer(template, data=request.data)
serializer = RegistrationSchoolFileTemplateSerializer(template, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
@ -151,7 +151,156 @@ class RegistrationTemplateSimpleView(APIView):
}
)
def delete(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
if template is not None:
template.delete()
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationParentFileMasterView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les fichiers parents",
responses={200: RegistrationParentFileMasterSerializer(many=True)}
)
def get(self, request):
templates = RegistrationParentFileMaster.objects.all()
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau fichier parent",
request_body=RegistrationParentFileMasterSerializer,
responses={
201: RegistrationParentFileMasterSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationParentFileMasterSerializer(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)
class RegistrationParentFileMasterSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un fichier parent spécifique",
responses={
200: RegistrationParentFileMasterSerializer,
404: "Fichier parent non trouvé"
}
)
def get(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
if template is None:
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileMasterSerializer(template)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un fichier parent existant",
request_body=RegistrationParentFileMasterSerializer,
responses={
200: RegistrationParentFileMasterSerializer,
400: "Données invalides",
404: "Fichier parent non trouvé"
}
)
def put(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
if template is None:
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileMasterSerializer(template, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_description="Supprime un fichier parent",
responses={
204: "Suppression réussie",
404: "Fichier parent non trouvé"
}
)
def delete(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
if template is not None:
template.delete()
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationParentFileTemplateView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les templates d'inscription",
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
)
def get(self, request):
templates = RegistrationParentFileTemplate.objects.all()
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau template d'inscription",
request_body=RegistrationParentFileTemplateSerializer,
responses={
201: RegistrationParentFileTemplateSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationParentFileTemplateSerializer(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)
class RegistrationParentFileTemplateSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un template d'inscription spécifique",
responses={
200: RegistrationParentFileTemplateSerializer,
404: "Template non trouvé"
}
)
def get(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileTemplateSerializer(template)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un template d'inscription existant",
request_body=RegistrationParentFileTemplateSerializer,
responses={
200: RegistrationParentFileTemplateSerializer,
400: "Données invalides",
404: "Template non trouvé"
}
)
def put(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_description="Supprime un template d'inscription",
responses={
204: "Suppression réussie",
404: "Template non trouvé"
}
)
def delete(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
if template is not None:
template.delete()
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)

View File

@ -96,6 +96,13 @@ class ChildrenListView(APIView):
students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__profile_role__profile__id', _value=id)
if students:
students = students.filter(establishment=establishment_id).distinct()
students = students.filter(
establishment=establishment_id,
status__in=[
RegistrationForm.RegistrationFormStatus.RF_SENT,
RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW,
RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT
]
).distinct()
students_serializer = RegistrationFormByParentSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False)

View File

@ -23,10 +23,10 @@ import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction';
import { fetchProfileRoles, fetchProfiles } from '@/app/actions/authAction';
import { fetchProfiles } from '@/app/actions/authAction';
import SidebarTabs from '@/components/SidebarTabs';
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
import { fetchRegistrationTemplateMaster } from '@/app/actions/registerFileGroupAction';
import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
@ -75,8 +75,8 @@ export default function Page() {
// Fetch data for tuition fees
handleTuitionFees();
// Fetch data for registration file templates
fetchRegistrationTemplateMaster()
// Fetch data for registration file schoolFileTemplates
fetchRegistrationSchoolFileMasters()
.then((data) => {
setFichiers(data);
})
@ -275,7 +275,7 @@ export default function Page() {
const tabs = [
{
id: 'Configuration',
label: "Configuration de l'école",
label: 'Classes',
content: (
<StructureManagement
specialities={specialities}
@ -293,7 +293,7 @@ export default function Page() {
},
{
id: 'Schedule',
label: "Gestion de l'emploi du temps",
label: 'Emploi du temps',
content: (
<ClassesProvider>
<ScheduleManagement
@ -305,7 +305,7 @@ export default function Page() {
},
{
id: 'Fees',
label: 'Tarifications',
label: 'Tarifs',
content: (
<FeesManagement
registrationDiscounts={registrationDiscounts}
@ -332,7 +332,7 @@ export default function Page() {
},
{
id: 'Files',
label: "Documents d'inscription",
label: 'Documents',
content: (
<FilesGroupsManagement
csrfToken={csrfToken}
@ -343,7 +343,7 @@ export default function Page() {
];
return (
<div className="p-8">
<div className="p-4">
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">

View File

@ -16,7 +16,7 @@ import {
Edit,
Archive,
FileText,
CircleCheck,
CheckCircle,
Plus,
XCircle,
} from 'lucide-react';
@ -39,8 +39,10 @@ import {
} from '@/app/actions/subscriptionAction';
import {
fetchRegistrationTemplateMaster,
createRegistrationTemplates,
fetchRegistrationSchoolFileMasters,
fetchRegistrationParentFileMasters,
createRegistrationSchoolFileTemplate,
createRegistrationParentFileTemplate,
fetchRegistrationFileGroups,
cloneTemplate,
} from '@/app/actions/registerFileGroupAction';
@ -96,7 +98,8 @@ export default function Page({ params: { locale } }) {
const [totalArchives, setTotalArchives] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(10); // Définir le nombre d'éléments par page
const [templateMasters, setTemplateMasters] = useState([]);
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [parentFileMasters, setParentFileMasters] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false);
const [student, setStudent] = useState('');
@ -239,9 +242,16 @@ export default function Page({ params: { locale } }) {
fetchRegisterForms(selectedEstablishmentId, ARCHIVED)
.then(registerFormArchivedDataHandler)
.catch(requestErrorHandler),
fetchRegistrationTemplateMaster()
fetchRegistrationSchoolFileMasters()
.then((data) => {
setTemplateMasters(data);
setSchoolFileMasters(data);
})
.catch((err) => {
logger.debug(err.message);
}),
fetchRegistrationParentFileMasters()
.then((data) => {
setParentFileMasters(data);
})
.catch((err) => {
logger.debug(err.message);
@ -315,9 +325,9 @@ export default function Page({ params: { locale } }) {
fetchRegisterForms(selectedEstablishmentId, ARCHIVED)
.then(registerFormArchivedDataHandler)
.catch(requestErrorHandler);
fetchRegistrationTemplateMaster()
fetchRegistrationSchoolFileMasters()
.then((data) => {
setTemplateMasters(data);
setSchoolFileMasters(data);
})
.catch((err) => {
err = err.message;
@ -521,18 +531,22 @@ export default function Page({ params: { locale } }) {
createRegisterForm(data, csrfToken)
.then((data) => {
// Cloner les templates pour chaque templateMaster du fileGroup
const masters = templateMasters.filter((file) =>
// Cloner les schoolFileTemplates pour chaque templateMaster du fileGroup
const masters = schoolFileMasters.filter((file) =>
file.groups.includes(selectedFileGroup)
);
const clonePromises = masters.map((templateMaster, index) => {
const parent_masters = parentFileMasters.filter((file) =>
file.groups.includes(selectedFileGroup)
);
const clonePromises = masters
.map((templateMaster, index) => {
return cloneTemplate(
templateMaster.id,
updatedData.guardianEmail,
templateMaster.is_required
)
.then((clonedDocument) => {
// Sauvegarde des templates clonés dans la base de données
// Sauvegarde des schoolFileTemplates clonés dans la base de données
const cloneData = {
name: `${templateMaster.name}_${updatedData.guardianFirstName}_${updatedData.guardianLastName}`,
slug: clonedDocument.slug,
@ -541,7 +555,10 @@ export default function Page({ params: { locale } }) {
registration_form: data.student.id,
};
return createRegistrationTemplates(cloneData, csrfToken)
return createRegistrationSchoolFileTemplate(
cloneData,
csrfToken
)
.then((response) => {
logger.debug('Template enregistré avec succès:', response);
})
@ -555,8 +572,38 @@ export default function Page({ params: { locale } }) {
.catch((error) => {
logger.error('Error during cloning or sending:', error);
});
})
.catch((error) => {
logger.error('Error:', error);
});
// Créer les parentFileTemplates pour chaque parentMaster
const parentClonePromises = parent_masters.map(
(parentMaster, index) => {
const parentTemplateData = {
master: parentMaster.id,
registration_form: data.student.id,
};
return createRegistrationParentFileTemplate(
parentTemplateData,
csrfToken
)
.then((response) => {
logger.debug(
'Parent template enregistré avec succès:',
response
);
})
.catch((error) => {
logger.error(
"Erreur lors de l'enregistrement du parent template:",
error
);
});
}
);
// Attendre que tous les clones soient créés
Promise.all(clonePromises)
.then(() => {
@ -670,7 +717,11 @@ export default function Page({ params: { locale } }) {
const actions = {
1: [
{
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
icon: (
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
),
onClick: () =>
router.push(
`${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}`
@ -678,7 +729,9 @@ export default function Page({ params: { locale } }) {
},
{
icon: (
<span title="Envoyer le dossier">
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () =>
sendConfirmRegisterForm(
@ -690,7 +743,11 @@ export default function Page({ params: { locale } }) {
],
2: [
{
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
icon: (
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
),
onClick: () =>
router.push(
`${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}`
@ -700,18 +757,26 @@ export default function Page({ params: { locale } }) {
3: [
{
icon: (
<CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />
),
onClick: () =>
router.push(
`${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentMode=${row.registration_payment}&file=${row.registration_file}`
<span title="Valider le dossier">
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () => {
const paymentSepa =
row.registration_payment === 1 || row.tuition_payment === 1
? 1
: 0;
const url = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentSepa=${paymentSepa}&file=${row.registration_file}`;
router.push(`${url}`);
},
},
],
5: [
{
icon: (
<CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />
<span title="Valider le dossier">
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () => openModalAssociationEleve(row.student),
},
@ -719,7 +784,9 @@ export default function Page({ params: { locale } }) {
default: [
{
icon: (
<span title="Archiver le dossier">
<Archive className="w-5 h-5 text-gray-500 hover:text-gray-700" />
</span>
),
onClick: () =>
archiveFicheInscription(
@ -785,15 +852,32 @@ export default function Page({ params: { locale } }) {
},
{
name: t('files'),
transform: (row) =>
row.registration_file != null && (
transform: (row) => (
<ul>
{row.registration_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a href={`${BASE_URL}${row.registration_file}`} target="_blank">
<a
href={`${BASE_URL}${row.registration_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.registration_file?.split('/').pop()}
</a>
</li>
)}
{row.sepa_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a
href={`${BASE_URL}${row.sepa_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.sepa_file?.split('/').pop()}
</a>
</li>
)}
</ul>
),
},
@ -865,7 +949,7 @@ export default function Page({ params: { locale } }) {
{
label: (
<>
<CircleCheck size={16} className="mr-2" /> Rattacher
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.student),

View File

@ -15,7 +15,7 @@ export default function Page() {
const studentId = searchParams.get('studentId');
const firstName = searchParams.get('firstName');
const lastName = searchParams.get('lastName');
const paymentMode = searchParams.get('paymentMode');
const paymentSepa = searchParams.get('paymentSepa') === '1';
const file = searchParams.get('file');
const csrfToken = useCsrfToken();
@ -45,7 +45,7 @@ export default function Page() {
studentId={studentId}
firstName={firstName}
lastName={lastName}
paymentMode={paymentMode}
paymentSepa={paymentSepa}
file={file}
onAccept={handleAcceptRF}
/>

View File

@ -2,21 +2,28 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import { Edit, Users } from 'lucide-react';
import { Edit3, Users, Download, Eye, Upload } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
import { fetchChildren } from '@/app/actions/subscriptionAction';
import {
fetchChildren,
sendSEPARegisterForm,
} from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { BASE_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext';
import ProfileSelector from '@/components/ProfileSelector';
import { useCsrfToken } from '@/context/CsrfContext';
export default function ParentHomePage() {
const [children, setChildren] = useState([]);
const [userId, setUserId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const { user, selectedEstablishmentId } = useEstablishment();
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
const userIdFromSession = user.user_id;
@ -27,17 +34,62 @@ export default function ParentHomePage() {
});
}, [selectedEstablishmentId]);
function handleView(eleveId) {
logger.debug(`View dossier for student id: ${eleveId}`);
router.push(
`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}&view=true`
);
}
function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`);
router.push(
`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`
);
}
const actionColumns = [{ name: 'Action', transform: (row) => row.action }];
const handleFileUpload = (file) => {
if (!file) {
logger.error("Aucun fichier sélectionné pour l'upload.");
return;
}
setUploadedFile(file); // Conserve le fichier en mémoire
logger.debug('Fichier sélectionné :', file.name);
};
const handleSubmit = () => {
if (!uploadedFile || !uploadingStudentId) {
logger.error('Aucun fichier ou étudiant sélectionné.');
return;
}
const formData = new FormData();
formData.append('sepa_file', uploadedFile); // Ajoute le fichier SEPA
formData.append('status', 3); // Statut à envoyer
sendSEPARegisterForm(uploadingStudentId, formData, csrfToken)
.then((response) => {
logger.debug('RF mis à jour avec succès:', response);
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du RF:', error);
});
};
const toggleUpload = (studentId) => {
if (uploadingStudentId === studentId && uploadState === 'on') {
// Si le composant est déjà affiché pour cet étudiant, on le masque
setUploadState('off');
setUploadingStudentId(null);
setUploadedFile(null); // Réinitialise le fichier
} else {
// Sinon, on l'affiche pour cet étudiant
setUploadState('on');
setUploadingStudentId(studentId);
}
};
// Définir les colonnes du tableau
const childrenColumns = [
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
@ -52,29 +104,76 @@ export default function ParentHomePage() {
{
name: 'Actions',
transform: (row) => (
<div className="flex justify-center">
<div className="flex justify-center items-center gap-2">
{row.status === 2 && (
<button
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
className="text-blue-500 hover:text-blue-700"
onClick={(e) => {
e.stopPropagation();
handleEdit(row.student.id);
}}
aria-label="Modifier"
aria-label="Remplir le dossier"
>
<Edit className="h-5 w-5" />
<Edit3 className="h-5 w-5" />
</button>
)}
{row.status === 3 && (
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
<Eye className="h-5 w-5" />
</button>
)}
{row.status === 7 && (
<>
<button
className="flex items-center justify-center w-8 h-8 rounded-full text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
<Eye className="h-5 w-5" />
</button>
<a
href={`${BASE_URL}${row.sepa_file}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
aria-label="Télécharger le mandat SEPA"
>
<Download className="h-5 w-5" />
</a>
{/* Nouvelle action Upload */}
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
uploadingStudentId === row.student.id && uploadState === 'on'
? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
: 'text-blue-500 hover:text-blue-700'
}`}
onClick={(e) => {
e.stopPropagation();
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
}}
aria-label="Uploader un fichier"
>
<Upload className="h-5 w-5" />
</button>
</>
)}
</div>
),
},
];
const itemsPerPage = 5;
const totalPages = Math.ceil(children.length / itemsPerPage) || 1;
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
return (
<div className="px-2 py-4 md:px-4 max-w-full">
<div>
@ -86,13 +185,29 @@ export default function ParentHomePage() {
<Table
data={children}
columns={childrenColumns}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
defaultTheme="bg-gray-50"
/>
</div>
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
{uploadState === 'on' && uploadingStudentId && (
<div className="mt-4">
<FileUpload
selectionMessage="Sélectionnez un fichier à uploader"
onFileSelect={handleFileUpload}
/>
<button
className={`mt-4 px-6 py-2 rounded-md ${
uploadedFile
? 'bg-emerald-500 text-white hover:bg-emerald-600'
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
}`}
onClick={handleSubmit}
disabled={!uploadedFile}
>
Valider
</button>
</div>
)}
</div>
</div>
);

View File

@ -1,7 +1,9 @@
import {
BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL,
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL,
FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
@ -18,6 +20,8 @@ const requestResponseHandler = async (response) => {
throw error;
};
// FETCH requests
export async function fetchRegistrationFileGroups(establishment) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
@ -34,6 +38,68 @@ export async function fetchRegistrationFileGroups(establishment) {
return response.json();
}
export const fetchRegistrationFileFromGroup = async (groupId) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const fetchRegistrationSchoolFileMasters = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${id}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const fetchRegistrationParentFileMasters = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const fetchRegistrationSchoolFileTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${id}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
// CREATE requests
export async function createRegistrationFileGroup(groupData, csrfToken) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
@ -55,20 +121,55 @@ export async function createRegistrationFileGroup(groupData, csrfToken) {
return response.json();
}
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'DELETE',
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}
);
}).then(requestResponseHandler);
};
return response;
}
export const createRegistrationParentFileMaster = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
};
// EDIT requests
export const editRegistrationFileGroup = async (
groupId,
@ -94,113 +195,9 @@ export const editRegistrationFileGroup = async (
return response.json();
};
export const fetchRegistrationFileFromGroup = async (groupId) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const fetchRegistrationTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${id}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const editRegistrationTemplates = (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${fileId}`, {
method: 'PUT',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const createRegistrationTemplates = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const deleteRegistrationTemplates = (fileId, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${fileId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
});
};
export const fetchRegistrationTemplateMaster = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${id}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const createRegistrationTemplateMaster = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const deleteRegistrationTemplateMaster = (fileId, csrfToken) => {
export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
export const editRegistrationTemplateMaster = (fileId, data, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`,
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
{
method: 'PUT',
body: JSON.stringify(data),
@ -213,6 +210,128 @@ export const editRegistrationTemplateMaster = (fileId, data, csrfToken) => {
).then(requestResponseHandler);
};
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
{
method: 'PUT',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}
).then(requestResponseHandler);
};
export const editRegistrationSchoolFileTemplates = (
fileId,
data,
csrfToken
) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'PUT',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
).then(requestResponseHandler);
};
export const editRegistrationParentFileTemplates = (
fileId,
data,
csrfToken
) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'PUT',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
).then(requestResponseHandler);
};
// DELETE requests
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
return response;
}
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
// API requests
export const cloneTemplate = (templateId, email, is_required) => {
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
method: 'POST',

View File

@ -153,9 +153,27 @@ export async function getRegisterFormFileTemplate(fileId) {
return response.json();
}
export const fetchTemplatesFromRegistrationFiles = async (id) => {
export const fetchSchoolFileTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/templates`,
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const fetchParentFileTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`,
{
credentials: 'include',
headers: {

View File

@ -1,105 +0,0 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import DraggableFileUpload from './DraggableFileUpload';
import { fetchRegistrationFileGroups } from '@/app/actions/registerFileGroupAction';
import { useEstablishment } from '@/context/EstablishmentContext';
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 [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState('');
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => {
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
setGroups(data)
);
if (fileToEdit) {
setFileName(fileToEdit.name || '');
setIsRequired(fileToEdit.is_required || false);
setOrder(fileToEdit.fusion_order || 0);
setSelectedGroup(fileToEdit.group_id || '');
}
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setFileName(event.target.value);
};
const handleUpload = () => {
onFileUpload({
file,
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
groupId: selectedGroup || null,
});
setFile(null);
setFileName('');
setIsRequired(false);
setOrder(0);
setSelectedGroup('');
};
return (
<div>
<DraggableFileUpload
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ''));
}}
/>
<div className="flex mt-2">
<input
type="text"
placeholder="Nom du fichier"
value={fileName}
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 === ''}
>
Ajouter
</button>
</div>
<div className="flex items-center mt-4">
<ToggleSwitch
label="Fichier à remplir obligatoirement"
checked={isRequired}
onChange={() => setIsRequired(!isRequired)}
/>
</div>
<div className="mt-4">
<label className="block mb-2">Groupe</label>
<select
value={selectedGroup}
onChange={(e) => setSelectedGroup(e.target.value)}
className="w-full border rounded p-2"
>
<option value="">Aucun groupe</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</div>
</div>
);
}

View File

@ -0,0 +1,60 @@
import React, { useState, useRef } from 'react';
import { CloudUpload } from 'lucide-react';
import logger from '@/utils/logger';
export default function FileUpload({ selectionMessage, onFileSelect, uploadedFileName }) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier sélectionné:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
const handleFileDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier déposé:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
return (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => fileInputRef.current.click()} // Utilisation de la référence pour ouvrir l'explorateur
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
ref={fileInputRef} // Attachement de la référence
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
</label>
</div>
{localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{localFileName}</span>
</p>
</div>
)}
</div>
);
}

View File

@ -11,6 +11,11 @@ export default function InputPhone({
className,
required,
}) {
const handlePhoneChange = (phone) => {
// Appeler onChange avec un objet personnalisé
onChange({ target: { name, value: phone } });
};
return (
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
@ -21,7 +26,7 @@ export default function InputPhone({
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
onChange={handlePhoneChange}
inputProps={{
name: name,
required: required,

View File

@ -1,19 +1,244 @@
import React from 'react';
import React, { useState } from 'react';
import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload';
import { Upload, Eye, Trash2, FileText } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
export default function FilesToUpload({ fileTemplates, columns }) {
export default function FilesToUpload({
parentFileTemplates,
uploadedFiles,
onFileUpload,
onFileDelete,
}) {
const [selectedFile, setSelectedFile] = useState(null); // État pour le fichier sélectionné
const [actionType, setActionType] = useState(null);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (file) => {
return file && file.fileName; // Si `fileName` est défini, le fichier est considéré comme téléversé
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName
);
};
const handleUpload = (file, selectedFile) => {
if (!file || !selectedFile) {
logger.error('Données manquantes pour le téléversement.');
return;
}
// Appeler la fonction de téléversement passée en prop
onFileUpload(file, selectedFile)
.then((response) => {
// Mettre à jour uploadedFiles avec les nouvelles données
const updatedFiles = uploadedFiles.map((f) =>
f.id === selectedFile.id
? {
...f,
fileName: response.data.fileName,
file: response.data.file_url,
}
: f
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((f) => f.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
fileName: response.data.fileName,
file: response.data.file_url,
});
}
})
.catch((error) => {
logger.error('Erreur lors du téléversement du fichier :', error);
});
// Mettre à jour l'état local
setSelectedFile(null);
setActionType(null); // Réinitialiser l'action après l'upload
};
// Définition des colonnes
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.master_name },
{
name: 'Description du fichier',
transform: (row) => row.master_description,
},
{
name: 'Statut',
transform: (row) => {
const uploadedFile = getUploadedFile(row.id);
return (
<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 à uploader
<span
className={`px-2 py-1 rounded-md text-sm font-medium ${
isFileUploaded(uploadedFile)
? 'bg-green-50 text-green-600'
: 'bg-orange-50 text-orange-600'
}`}
>
{isFileUploaded(uploadedFile) ? 'Chargé' : 'A ajouter'}
</span>
);
},
},
{
name: 'Actions',
transform: (row) => {
const uploadedFile = getUploadedFile(row.id);
return (
<div className="flex items-center justify-center gap-4">
{uploadedFile && (
<>
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
actionType === 'view' && selectedFile?.id === row.id
? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
: 'text-blue-500 hover:text-blue-700'
}`}
onClick={() => {
if (actionType === 'view' && selectedFile?.id === row.id) {
setSelectedFile(null);
setActionType(null);
} else {
const uploadedFile = getUploadedFile(row.id);
setSelectedFile(uploadedFile || row); // Utiliser les données mises à jour
setActionType('view');
}
}}
type="button"
>
<Eye className="w-5 h-5" />
</button>
<button
className="flex items-center justify-center w-8 h-8 rounded-full text-red-500 hover:text-red-700"
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Êtes-vous sûr(e) de vouloir supprimer le fichier "${row.master_name}" ?`
);
setRemovePopupOnConfirm(() => () => {
onFileDelete(row.id)
.then(() => {
setPopupMessage(
`Le fichier "${row.master_name}" a été supprimé avec succès.`
);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier :',
error
);
setPopupMessage(
`Erreur lors de la suppression du fichier "${row.master_name}".`
);
setPopupVisible(true);
setRemovePopupVisible(false);
});
setActionType(null);
setSelectedFile(null);
});
}}
type="button"
>
<Trash2 className="w-5 h-5" />
</button>
</>
)}
{!uploadedFile && (
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
actionType === 'upload' && selectedFile?.id === row.id
? 'bg-emerald-100 text-emerald-600 ring-3 ring-emerald-500'
: 'text-emerald-500 hover:text-emerald-700'
}`}
onClick={() => {
if (actionType === 'upload' && selectedFile?.id === row.id) {
setSelectedFile(null);
setActionType(null);
} else {
setSelectedFile(row);
setActionType('upload');
}
}}
type="button"
>
<Upload className="w-5 h-5" />
</button>
)}
</div>
);
},
},
];
return (
<div className="mt-8 mb-4 w-3/5">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
<FileText className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">
Pièces à fournir
</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
<p className="text-sm text-gray-500 italic">
Ajoutez les documents pour compléter votre inscription
</p>
</div>
</div>
</div>
<Table data={parentFileTemplates} columns={columns} />
{selectedFile && (
<div className="mt-4">
{actionType === 'view' && selectedFile.fileName ? (
<iframe
src={`${BASE_URL}/${selectedFile.fileName}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh',
border: 'none',
}}
/>
) : actionType === 'upload' ? (
<FileUpload
selectionMessage={`Téléversez le fichier ${selectedFile.master_name}`}
onFileSelect={(file) => handleUpload(file, selectedFile)}
uploadedFileName={selectedFile.fileName || ''}
/>
) : null}
</div>
)}
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);

View File

@ -1,12 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import {
User,
Mail,
Phone,
UserCheck,
DollarSign,
Percent,
} from 'lucide-react';
import { User, Mail } from 'lucide-react';
import InputTextIcon from '@/components/InputTextIcon';
import ToggleSwitch from '@/components/ToggleSwitch';
import Button from '@/components/Button';
@ -150,7 +143,7 @@ const InscriptionForm = ({
};
const handleChange = (e) => {
const { name, value, type } = e.target;
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value,
@ -444,7 +437,7 @@ const InscriptionForm = ({
/>
<InputPhone
name="guardianPhone"
label={t('Numéro de téléphone (optionnel)')}
label="Numéro de téléphone (optionnel)"
value={formData.guardianPhone}
onChange={handleChange}
className="w-full mt-4"

View File

@ -5,23 +5,19 @@ import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import {
fetchRegisterForm,
fetchTemplatesFromRegistrationFiles,
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import {
downloadTemplate,
createRegistrationTemplates,
editRegistrationTemplates,
deleteRegistrationTemplates,
editRegistrationSchoolFileTemplates,
editRegistrationParentFileTemplates,
} from '@/app/actions/registerFileGroupAction';
import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/components/DraggableFileUpload';
import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel';
import logger from '@/utils/logger';
import StudentInfoForm, {
validateStudentInfo,
@ -42,7 +38,6 @@ export default function InscriptionFormShared({
csrfToken,
selectedEstablishmentId,
onSubmit,
cancelUrl,
errors = {}, // Nouvelle prop pour les erreurs
}) {
// États pour gérer les données du formulaire
@ -69,13 +64,8 @@ export default function InscriptionFormShared({
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileGroup, setFileGroup] = useState(null);
const [fileName, setFileName] = useState('');
const [file, setFile] = useState('');
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
@ -110,7 +100,6 @@ export default function InscriptionFormShared({
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setIsLoading(false);
@ -118,12 +107,23 @@ export default function InscriptionFormShared({
}, [studentId]);
useEffect(() => {
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
setFileTemplates(data);
fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setSchoolFileTemplates(data);
});
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setParentFileTemplates(data);
// Initialiser uploadedFiles avec uniquement les fichiers dont `file` n'est pas null
const filteredFiles = data
.filter((item) => item.file !== null)
.map((item) => ({
id: item.id,
fileName: item.file,
}));
setUploadedFiles(filteredFiles);
});
}, []);
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
@ -164,71 +164,89 @@ export default function InscriptionFormShared({
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Gestion du téléversement de fichiers
const handleFileUpload = async (file, fileName) => {
if (!file || !currentTemplateId || !formData.id) {
logger.error('Missing required data for upload');
const handleFileUpload = (file, selectedFile) => {
if (!file || !selectedFile) {
logger.error('Données manquantes pour le téléversement.');
return Promise.reject(
new Error('Données manquantes pour le téléversement.')
);
}
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationParentFileTemplates(
selectedFile.id,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Template mis à jour avec succès :', response);
setUploadedFiles((prev) => {
const updatedFiles = prev.map((uploadedFile) =>
uploadedFile.id === selectedFile.id
? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé
: uploadedFile
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((file) => file.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
fileName: response.data.file,
});
}
return updatedFiles;
});
return response; // Retourner la réponse pour signaler le succès
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du fichier :', error);
throw error; // Relancer l'erreur pour que l'appelant puisse la capturer
});
};
const handleDeleteFile = (templateId) => {
const fileToDelete = uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName
);
if (!fileToDelete) {
logger.error('Aucun fichier trouvé pour suppression.');
return;
}
const data = new FormData();
data.append('file', file);
data.append('name', fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
// Créer un FormData avec un champ vide pour "file"
const updateData = new FormData();
updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
try {
const response = await createRegistrationTemplates(data, csrfToken);
if (response) {
setUploadedFiles((prev) => {
const newFiles = prev.filter(
(f) => parseInt(f.template) !== currentTemplateId
);
return [
...newFiles,
{
name: fileName,
template: currentTemplateId,
file: response.file,
},
];
});
return editRegistrationParentFileTemplates(
templateId,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Fichier supprimé avec succès dans la base :', response);
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
setUploadedFiles(data.registration_files || []);
});
}
}
} catch (error) {
logger.error('Error uploading file:', error);
}
};
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (templateId) => {
return uploadedFiles.find((template) => template.template === templateId);
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find((file) => parseInt(file.template) === templateId);
};
// Suppression d'un fichier
const handleDeleteFile = async (templateId) => {
const fileToDelete = getUploadedFile(templateId);
if (!fileToDelete) return;
try {
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
// Mettre à jour l'état local pour refléter la suppression
setUploadedFiles((prev) =>
prev.filter((f) => parseInt(f.template) !== templateId)
prev.map((uploadedFile) =>
uploadedFile.id === templateId
? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
: uploadedFile
)
);
} catch (error) {
logger.error('Error deleting file:', error);
}
return response;
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier dans la base :',
error
);
throw error;
});
};
// Soumission du formulaire
@ -268,90 +286,12 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1);
};
const requiredFileTemplates = fileTemplates;
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ 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: 'Statut',
transform: (row) =>
row.is_required && (
<FileStatusLabel
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
/>
),
},
{
name: 'Actions',
transform: (row) => {
if (!row.is_required) return null;
const uploadedFile = getUploadedFile(row.id);
if (uploadedFile) {
return (
<div className="flex items-center justify-center gap-2">
<a
href={`${BASE_URL}${uploadedFile.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Eye size={16} />
</a>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDeleteFile(row.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
);
}
return (
<button
className="text-emerald-500 hover:text-emerald-700"
type="button"
onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}
>
<Upload size={16} />
</button>
);
},
},
];
// Affichage du loader pendant le chargement
if (isLoading) return <Loader />;
// Rendu du composant
return (
<div className="max-w-4xl mx-auto p-6">
<div className="mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken} />
{/* Page 1 : Informations de l'élève et Responsables */}
@ -368,44 +308,45 @@ export default function InscriptionFormShared({
)}
{/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
{currentPage > 1 && currentPage <= schoolFileTemplates.length + 1 && (
<div className="mt-8 mb-4 w-3/5">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name ||
{schoolFileTemplates[currentPage - 2].name ||
'Document sans nom'}
</h2>
<p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description ||
{schoolFileTemplates[currentPage - 2].description ||
'Aucune description disponible pour ce document.'}
</p>
</div>
{/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === '' ? (
{schoolFileTemplates[currentPage - 2].file === null ? (
<DocusealForm
id="docusealForm"
src={
'https://docuseal.com/s/' +
requiredFileTemplates[currentPage - 2].slug
schoolFileTemplates[currentPage - 2].slug
}
withDownloadButton={false}
onComplete={() => {
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
downloadTemplate(schoolFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data))
.then((response) => response.blob())
.then((blob) => {
const file = new File(
[blob],
`${requiredFileTemplates[currentPage - 2].name}.pdf`,
`${schoolFileTemplates[currentPage - 2].name}.pdf`,
{ type: blob.type }
);
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationTemplates(
requiredFileTemplates[currentPage - 2].id,
return editRegistrationSchoolFileTemplates(
schoolFileTemplates[currentPage - 2].id,
updateData,
csrfToken
);
@ -420,7 +361,7 @@ export default function InscriptionFormShared({
/>
) : (
<iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
src={`${BASE_URL}/${schoolFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
@ -430,22 +371,21 @@ export default function InscriptionFormShared({
/>
)}
</div>
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && (
<>
{currentPage === schoolFileTemplates.length + 2 && (
<FilesToUpload
fileTemplates={fileTemplates.filter(
(template) => !template.is_required
)}
columns={columns}
parentFileTemplates={parentFileTemplates}
uploadedFiles={uploadedFiles}
onFileUpload={handleFileUpload}
onFileDelete={handleDeleteFile}
/>
</>
)}
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
<div className="flex justify-center space-x-4">
<Button
text="Sauvegarder"
onClick={handleSave}
@ -462,7 +402,7 @@ export default function InscriptionFormShared({
}}
/>
)}
{currentPage < requiredFileTemplates.length + 2 && (
{currentPage < schoolFileTemplates.length + 2 && (
<Button
text="Suivant"
onClick={(e) => {
@ -479,57 +419,11 @@ export default function InscriptionFormShared({
name="Next"
/>
)}
{currentPage === requiredFileTemplates.length + 2 && (
{currentPage === schoolFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary />
)}
</div>
</form>
{fileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
)}
</div>
);
}

View File

@ -1,25 +1,35 @@
'use client';
import React, { useState, useEffect } from 'react';
import { DocusealBuilder } from '@docuseal/react';
import Button from '@/components/Button';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import { BASE_URL } from '@/utils/Url';
import { generateToken } from '@/app/actions/registerFileGroupAction';
import {
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { GraduationCap, CloudUpload } from 'lucide-react';
import { GraduationCap } from 'lucide-react';
import FileUpload from '@/components/FileUpload';
import SectionHeader from '@/components/SectionHeader';
export default function ValidateSubscription({
studentId,
firstName,
lastName,
paymentMode,
paymentSepa,
file,
onAccept,
}) {
const [token, setToken] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedFile, setSelectedFile] = useState(null); // Nouvel état pour le fichier sélectionné
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des pages
const [isSepa, setIsSepa] = useState(paymentSepa); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des étapes
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); // Stocke les fichiers schoolFileTemplates
const [parentFileTemplates, setParentFileTemplates] = useState([]); // Stocke les fichiers parentFileTemplates
const [mergeDocuments, setMergeDocuments] = useState(false); // État pour activer/désactiver la fusion des documents
useEffect(() => {
if (isSepa) {
@ -33,38 +43,67 @@ export default function ValidateSubscription({
}
}, [isSepa]);
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
useEffect(() => {
// Récupérer les fichiers schoolFileTemplates pour l'étudiant
fetchSchoolFileTemplatesFromRegistrationFiles(studentId)
.then((data) => {
setSchoolFileTemplates(data);
logger.debug('Fichiers schoolFileTemplates récupérés:', data);
})
.catch((error) =>
logger.error(
'Erreur lors de la récupération des schoolFileTemplates:',
error
)
);
// Récupérer les fichiers parentFileTemplates pour l'étudiant
fetchParentFileTemplatesFromRegistrationFiles(studentId)
.then((data) => {
setParentFileTemplates(data);
logger.debug('Fichiers parentFileTemplates récupérés:', data);
})
.catch((error) =>
logger.error(
'Erreur lors de la récupération des parentFileTemplates:',
error
)
);
}, [studentId]);
const handleAccept = () => {
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
if (!file) {
if (!selectedFile && isSepa) {
logger.error('Aucun fichier sélectionné pour le champ SEPA.');
return;
}
// Ajouter le paramètre fusion dans l'URL
const fusionParam = mergeDocuments ? 'true' : 'false';
const data = {
status: 7,
sepa_file: file,
sepa_file: selectedFile, // Utilise le fichier sélectionné depuis l'état
fusionParam: fusionParam,
};
// Appeler la fonction passée par le parent pour mettre à jour le RF
onAccept(data);
};
const handleRefuse = () => {
logger.debug("Dossier refusé pour l'étudiant:", studentId);
// Logique pour refuser l'inscription
const handleToggleMergeDocuments = () => {
// Inverser l'état de mergeDocuments
setMergeDocuments((prevState) => !prevState);
};
const isValidateButtonDisabled = isSepa && !uploadedFileName;
const goToNextPage = () => {
if (currentPage < (isSepa ? 2 : 1)) {
const totalPages =
1 +
schoolFileTemplates.length +
parentFileTemplates.length +
(isSepa ? 1 : 0);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
@ -75,30 +114,16 @@ export default function ValidateSubscription({
}
};
return (
<div className="p-8 space-y-6 bg-gray-50 rounded-lg shadow-lg">
{/* Titre */}
<div className="flex items-center space-x-4">
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
<GraduationCap className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-800">
Dossier scolaire de{' '}
<span className="text-emerald-600">
{firstName} {lastName}
</span>
</h1>
<p className="text-sm text-gray-500 italic">
Année scolaire {new Date().getFullYear()}-
{new Date().getFullYear() + 1}
</p>
</div>
</div>
const totalPages =
1 +
schoolFileTemplates.length +
parentFileTemplates.length +
(isSepa ? 1 : 0);
{/* Contenu principal */}
{currentPage === 1 && (
<div className="border p-6 rounded-lg shadow-md bg-white flex justify-center items-center">
const renderContent = () => {
if (currentPage === 1) {
// Page 1 : Afficher le PDF principal
return (
<iframe
src={pdfUrl}
title="Aperçu du PDF"
@ -109,59 +134,82 @@ export default function ValidateSubscription({
border: 'none',
}}
/>
</div>
)}
{currentPage === 2 && isSepa && (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">
Sélection du mandat de pélèvement SEPA
</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
);
} else if (
currentPage > 1 &&
currentPage <= 1 + schoolFileTemplates.length
) {
// Pages des schoolFileTemplates
const index = currentPage - 2; // Décalage pour correspondre à l'index du tableau
return (
<iframe
src={`${BASE_URL}/${schoolFileTemplates[index]?.file}`}
title={`Document ${index + 1}`}
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" />{' '}
{/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}
}}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">
Déposez votre fichier ici
</p>
<p className="text-sm text-gray-500 mt-2">
ou cliquez pour sélectionner un fichier PDF
</p>
</label>
</div>
{uploadedFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{uploadedFileName}</span>
</p>
</div>
)}
);
} else if (
currentPage > 1 + schoolFileTemplates.length &&
currentPage <= 1 + schoolFileTemplates.length + parentFileTemplates.length
) {
// Pages des parentFileTemplates
const index = currentPage - 2 - schoolFileTemplates.length; // Décalage pour correspondre à l'index du tableau
return (
<iframe
src={`${BASE_URL}/${parentFileTemplates[index]?.file}`}
title={`Document Parent ${index + 1}`}
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
/>
);
} else if (currentPage === totalPages && isSepa) {
// Dernière page : Mandat SEPA
return (
<FileUpload
selectionMessage="Sélectionnez un mandat de prélèvement SEPA"
onFileSelect={(file) => {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
setSelectedFile(file); // Stocke le fichier dans l'état
logger.debug('Fichier sélectionné:', file.name);
}}
uploadedFileName={uploadedFileName}
/>
);
}
return null;
};
return (
<div className="space-y-6 p-6">
<SectionHeader
icon={GraduationCap}
title={`Dossier scolaire de ${firstName} ${lastName}`}
description={`Année scolaire ${new Date().getFullYear()}-${new Date().getFullYear() + 1}`}
/>
{/* Contenu principal */}
<div className="p-6 items-center">{renderContent()}</div>
{/* Option de fusion des documents (affichée uniquement sur la dernière page) */}
{currentPage === totalPages && (
<div className="flex items-center justify-between mt-6">
<span className="text-gray-700">
Fusionner les documents en un seul fichier PDF
</span>
<ToggleSwitch
label="Fusionner"
checked={mergeDocuments}
onChange={handleToggleMergeDocuments} // Appeler la fonction pour inverser l'état
/>
</div>
)}
@ -174,7 +222,7 @@ export default function ValidateSubscription({
className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
/>
)}
{currentPage < (isSepa ? 2 : 1) && (
{currentPage < totalPages && (
<Button
text="Suivant"
onClick={goToNextPage}
@ -182,7 +230,7 @@ export default function ValidateSubscription({
className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-2"
/>
)}
{currentPage === (isSepa ? 2 : 1) && (
{currentPage === totalPages && (
<Button
text="Valider"
onClick={handleAccept}

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Plus } from 'lucide-react';
const SectionHeader = ({
icon: Icon,
discountStyle = false,
title,
description,
button = false,
buttonOpeningModal = false,
onClick = null
}) => {
return (
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className={`${discountStyle ? "bg-yellow-100" : "bg-emerald-100"} p-3 rounded-full shadow-md`}>
<Icon
className={discountStyle ?
"w-8 h-8 text-yellow-600" :
"w-8 h-8 text-emerald-600"
}
/>
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">{title}</h2>
<p className="text-sm text-gray-500 italic">{description}</p>
</div>
</div>
{button && onClick && (
<button
onClick={onClick}
className={buttonOpeningModal ?
"flex items-center bg-emerald-200 text-emerald-700 p-2 rounded-full shadow-sm hover:bg-emerald-300" :
"text-emerald-500 hover:bg-emerald-200 rounded-full p-2"
}
>
<Plus className="w-6 h-6" />
</button>
)}
</div>
);
};
export default SectionHeader;

View File

@ -9,7 +9,11 @@ const SidebarTabs = ({ tabs }) => {
{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'}`}
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}

View File

@ -1,13 +1,4 @@
import {
Trash2,
Edit3,
Plus,
ZoomIn,
Users,
Check,
X,
Hand,
} from 'lucide-react';
import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
@ -21,6 +12,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger';
import ClasseDetails from '@/components/ClasseDetails';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = {
TEACHER: 'teacher',
@ -553,19 +545,13 @@ const ClassesSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Users className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Classes</h2>
</div>
<button
type="button"
<SectionHeader
icon={Users}
title="Liste des classes"
description="Gérez les classes de votre école"
button={true}
onClick={handleAddClass}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
/>
<Table
data={newClass ? [newClass, ...classes] : classes}
columns={columns}

View File

@ -1,4 +1,4 @@
import { Plus, Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
import { Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
@ -6,7 +6,9 @@ import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
const SpecialitiesSection = ({
specialities,
@ -26,6 +28,8 @@ const SpecialitiesSection = ({
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId } = useEstablishment();
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
@ -49,7 +53,13 @@ const SpecialitiesSection = ({
const handleSaveNewSpeciality = () => {
if (newSpeciality.name) {
handleCreate(newSpeciality)
// Ajouter l'ID de l'établissement à la nouvelle spécialité
const specialityData = {
...newSpeciality,
establishment: selectedEstablishmentId, // Inclure l'ID de l'établissement
};
handleCreate(specialityData)
.then((createdSpeciality) => {
setSpecialities([createdSpeciality, ...specialities]);
setNewSpeciality(null);
@ -234,19 +244,13 @@ const SpecialitiesSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Spécialités</h2>
</div>
<button
type="button"
<SectionHeader
icon={BookOpen}
title="Liste des spécialités"
description="Gérez les spécialités de votre école"
button={true}
onClick={handleAddSpeciality}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
/>
<Table
data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns}

View File

@ -22,9 +22,9 @@ const StructureManagement = ({
handleDelete,
}) => {
return (
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8">
<div className="w-full mx-auto mt-6">
<ClassesProvider>
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md">
<div className="mt-8 w-2/5">
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
@ -48,7 +48,7 @@ const StructureManagement = ({
}
/>
</div>
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md">
<div className="w-4/5 mt-12">
<TeachersSection
teachers={teachers}
setTeachers={setTeachers}
@ -70,7 +70,7 @@ const StructureManagement = ({
}
/>
</div>
<div className="w-full p-4 bg-white rounded-lg shadow-md">
<div className="w-full mt-12">
<ClassesSection
classes={classes}
setClasses={setClasses}

View File

@ -1,18 +1,8 @@
import React, { useState, useEffect } from 'react';
import {
Plus,
Edit3,
Trash2,
GraduationCap,
Check,
X,
Hand,
Search,
} from 'lucide-react';
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/ToggleSwitch';
import { createProfile, updateProfile } from '@/app/actions/authAction';
import { useCsrfToken } from '@/context/CsrfContext';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -20,8 +10,8 @@ import InputText from '@/components/InputText';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import TeacherItem from './TeacherItem';
import logger from '@/utils/logger';
import { fetchProfiles } from '@/app/actions/authAction';
import { useEstablishment } from '@/context/EstablishmentContext';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = {
SPECIALITY: 'speciality',
@ -578,19 +568,13 @@ const TeachersSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Enseignants</h2>
</div>
<button
type="button"
<SectionHeader
icon={GraduationCap}
title="Liste des enseignants.es"
description="Gérez les enseignants.es de votre école"
button={true}
onClick={handleAddTeacher}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
/>
<Table
data={newTeacher ? [newTeacher, ...teachers] : teachers}
columns={columns}

View File

@ -1,31 +1,22 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import {
fetchRegistrationFileGroups,
createRegistrationTemplates,
createRegistrationSchoolFileTemplate,
cloneTemplate,
generateToken,
} from '@/app/actions/registerFileGroupAction';
import { DocusealBuilder } from '@docuseal/react';
import logger from '@/utils/logger';
import {
BE_DOCUSEAL_GET_JWT,
BASE_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
} from '@/utils/Url';
import Button from '@/components/Button'; // Import du composant Button
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FileUpload({
export default function FileUploadDocuSeal({
handleCreateTemplateMaster,
handleEditTemplateMaster,
fileToEdit = null,
onSuccess,
}) {
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);
const [token, setToken] = useState(null);
const [templateMaster, setTemplateMaster] = useState(null);
@ -61,10 +52,6 @@ export default function FileUpload({
);
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setUploadedFileName(event.target.value);
};
const handleGroupChange = (selectedGroups) => {
setSelectedGroups(selectedGroups);
@ -120,7 +107,7 @@ export default function FileUpload({
logger.debug('creation du clone avec required : ', is_required);
cloneTemplate(templateMaster?.id, guardian.email, is_required)
.then((clonedDocument) => {
// Sauvegarde des templates clonés dans la base de données
// Sauvegarde des schoolFileTemplates clonés dans la base de données
const data = {
name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`,
slug: clonedDocument.slug,
@ -128,7 +115,7 @@ export default function FileUpload({
master: templateMaster?.id,
registration_form: guardian.registration_form,
};
createRegistrationTemplates(data, csrfToken)
createRegistrationSchoolFileTemplate(data, csrfToken)
.then((response) => {
logger.debug('Template enregistré avec succès:', response);
onSuccess();

View File

@ -1,36 +1,40 @@
import React, { useState, useEffect } from 'react';
import {
Plus,
Download,
Edit3,
Trash2,
FolderPlus,
Signature,
} from 'lucide-react';
import { Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/Structure/Files/FileUpload';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import { BASE_URL } from '@/utils/Url';
import {
// GET
fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
fetchRegistrationSchoolFileTemplates,
fetchRegistrationParentFileMasters,
// POST
createRegistrationFileGroup,
deleteRegistrationFileGroup,
createRegistrationSchoolFileMaster,
createRegistrationParentFileMaster,
// PUT
editRegistrationFileGroup,
fetchRegistrationTemplateMaster,
createRegistrationTemplateMaster,
editRegistrationTemplateMaster,
deleteRegistrationTemplateMaster,
fetchRegistrationTemplates,
editRegistrationSchoolFileMaster,
editRegistrationParentFileMaster,
// DELETE
deleteRegistrationFileGroup,
deleteRegistrationSchoolFileMaster,
deleteRegistrationParentFileMaster,
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
import SectionHeader from '@/components/SectionHeader';
export default function FilesGroupsManagement({
csrfToken,
selectedEstablishmentId,
}) {
const [templateMasters, setTemplateMasters] = useState([]);
const [templates, setTemplates] = useState([]);
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -39,6 +43,10 @@ export default function FilesGroupsManagement({
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const [reloadTemplates, setReloadTemplates] = useState(false);
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({});
const [uploadedFileName, setUploadedFileName] = useState('');
const handleReloadTemplates = () => {
setReloadTemplates(true);
@ -61,19 +69,28 @@ export default function FilesGroupsManagement({
useEffect(() => {
if (selectedEstablishmentId) {
Promise.all([
fetchRegistrationTemplateMaster(),
fetchRegistrationSchoolFileMasters(),
fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationTemplates(),
fetchRegistrationSchoolFileTemplates(),
fetchRegistrationParentFileMasters(),
])
.then(([filesTemplateMasters, groupsData, filesTemplates]) => {
.then(
([
dataSchoolFileMasters,
groupsData,
dataSchoolFileTemplates,
dataParentFileMasters,
]) => {
setGroups(groupsData);
setTemplates(filesTemplates);
setSchoolFileTemplates(dataSchoolFileTemplates);
setParentFileMasters(dataParentFileMasters);
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesTemplateMasters.map((file) =>
const transformedFiles = dataSchoolFileMasters.map((file) =>
transformFileData(file, groupsData)
);
setTemplateMasters(transformedFiles);
})
setSchoolFileMasters(transformedFiles);
}
)
.catch((err) => {
console.log(err.message);
})
@ -85,7 +102,7 @@ export default function FilesGroupsManagement({
const deleteTemplateMaster = (templateMaster) => {
// Supprimer les clones associés via l'API DocuSeal
const removeClonesPromises = templates
const removeClonesPromises = schoolFileTemplates
.filter((template) => template.master === templateMaster.id)
.map((template) => removeTemplate(template.id));
@ -100,11 +117,11 @@ export default function FilesGroupsManagement({
logger.debug('Master et clones supprimés avec succès de DocuSeal.');
// Supprimer le template master de la base de données
deleteRegistrationTemplateMaster(templateMaster.id, csrfToken)
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => {
if (response.ok) {
setTemplateMasters(
templateMasters.filter(
setSchoolFileMasters(
schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id
)
);
@ -175,11 +192,11 @@ export default function FilesGroupsManagement({
};
logger.debug(data);
createRegistrationTemplateMaster(data, csrfToken)
createRegistrationSchoolFileMaster(data, csrfToken)
.then((data) => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFiles) => [...prevFiles, transformedFile]);
setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch((error) => {
@ -196,11 +213,11 @@ export default function FilesGroupsManagement({
};
logger.debug(data);
editRegistrationTemplateMaster(id, data, csrfToken)
editRegistrationSchoolFileMaster(id, data, csrfToken)
.then((data) => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFichiers) =>
setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
);
setIsModalOpen(false);
@ -228,7 +245,13 @@ export default function FilesGroupsManagement({
alert("Erreur lors de l'opération sur le groupe");
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
// Ajouter l'établissement sélectionné lors de la création d'un nouveau groupe
const newGroupData = {
...groupData,
establishment: selectedEstablishmentId,
};
createRegistrationFileGroup(newGroupData, csrfToken)
.then((newGroup) => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
@ -246,13 +269,13 @@ export default function FilesGroupsManagement({
};
const handleGroupDelete = (groupId) => {
// Vérifier si des templateMasters utilisent ce groupe
const filesInGroup = templateMasters.filter(
// Vérifier si des schoolFileMasters utilisent ce groupe
const filesInGroup = schoolFileMasters.filter(
(file) => file.group && file.group.id === groupId
);
if (filesInGroup.length > 0) {
alert(
"Impossible de supprimer ce groupe car il contient des templateMasters. Veuillez d'abord retirer tous les templateMasters de ce groupe."
"Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe."
);
return;
}
@ -279,7 +302,62 @@ export default function FilesGroupsManagement({
}
};
const filteredFiles = templateMasters.filter((file) => {
const handleCreate = (newParentFile) => {
return createRegistrationParentFileMaster(newParentFile, csrfToken)
.then((response) => {
const createdFile = response;
// Ajouter le nouveau fichier parent à la liste existante
setParentFileMasters((prevFiles) => [...prevFiles, createdFile]);
logger.debug('Document parent créé avec succès:', createdFile);
return createdFile;
})
.catch((error) => {
logger.error('Erreur lors de la création du document parent:', error);
alert(
'Une erreur est survenue lors de la création du document parent.'
);
throw error;
});
};
const handleEdit = (id, updatedFile) => {
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
.then((response) => {
const modifiedFile = response.data; // Extraire les données mises à jour
// Mettre à jour la liste des fichiers parents
setParentFileMasters((prevFiles) =>
prevFiles.map((file) => (file.id === id ? modifiedFile : file))
);
logger.debug('Document parent mis à jour avec succès:', modifiedFile);
return modifiedFile; // Retourner le fichier mis à jour
})
.catch((error) => {
logger.error(
'Erreur lors de la modification du document parent:',
error
);
alert(
'Une erreur est survenue lors de la modification du document parent.'
);
throw error;
});
};
const handleDelete = (id) => {
return deleteRegistrationParentFileMaster(id, csrfToken)
.then(() => {
// Mettre à jour la liste des fichiers parents en supprimant l'élément correspondant
setParentFileMasters((prevFiles) =>
prevFiles.filter((file) => file.id !== id)
);
logger.debug('Document parent supprimé avec succès:', id);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du fichier parent:', error);
});
};
const filteredFiles = schoolFileMasters.filter((file) => {
if (!selectedGroup) return true;
return (
file.groups &&
@ -288,9 +366,9 @@ export default function FilesGroupsManagement({
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Nom du formulaire', transform: (row) => row.name },
{
name: 'Groupes',
name: "Dossiers d'inscription",
transform: (row) =>
row.groups && row.groups.length > 0
? row.groups.map((group) => group.name).join(', ')
@ -327,7 +405,7 @@ export default function FilesGroupsManagement({
];
const columnsGroups = [
{ name: 'Nom du groupe', transform: (row) => row.name },
{ name: 'Nom du dossier', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{
name: 'Actions',
@ -351,7 +429,8 @@ export default function FilesGroupsManagement({
];
return (
<div>
<div className="w-full mx-auto mt-6">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}
setIsOpen={(isOpen) => {
@ -362,7 +441,7 @@ export default function FilesGroupsManagement({
}}
title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => (
<FileUpload
<FileUploadDocuSeal
handleCreateTemplateMaster={handleCreateTemplateMaster}
handleEditTemplateMaster={handleEditTemplateMaster}
fileToEdit={fileToEdit}
@ -371,13 +450,15 @@ export default function FilesGroupsManagement({
)}
modalClassName="w-4/5 h-4/5"
/>
{/* Modal pour les groupes */}
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
title={
groupToEdit
? 'Modifier le groupe'
: 'Ajouter un groupe de templateMasters'
: 'Ajouter un groupe de schoolFileMasters'
}
ContentComponent={() => (
<RegistrationFileGroupForm
@ -386,61 +467,45 @@ export default function FilesGroupsManagement({
/>
)}
/>
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Groupes de fichiers</h2>
<button
{/* Section Groupes de fichiers */}
<div className="mt-8 w-3/5">
<SectionHeader
icon={FolderPlus}
title="Dossiers d'inscriptions"
description="Gérez les dossiers d'inscription pour organiser vos documents."
button={true}
buttonOpeningModal={true}
onClick={() => setIsGroupModalOpen(true)}
className="flex items-center bg-blue-600 text-white p-2 rounded-full shadow hover:bg-blue-900 transition duration-200"
>
<FolderPlus className="w-5 h-5" />
</button>
</div>
<Table
data={groups}
columns={columnsGroups}
itemsPerPage={5}
currentPage={1}
totalPages={Math.ceil(groups.length / 5)}
/>
<Table data={groups} columns={columnsGroups} />
</div>
{groups.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Fichiers</h2>
<div className="flex items-center gap-4">
<select
className="border rounded p-2"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
>
<option value="">Tous les groupes</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
<button
{/* Section Fichiers */}
<div className="mt-12 mb-4 w-3/5">
<SectionHeader
icon={Signature}
title="Formulaires à remplir"
description="Gérez les formulaires nécessitant une signature électronique."
button={true}
buttonOpeningModal={true}
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"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<Table
data={filteredFiles}
columns={columnsFiles}
itemsPerPage={10}
currentPage={1}
totalPages={Math.ceil(filteredFiles.length / 10)}
/>
<Table data={filteredFiles} columns={columnsFiles} />
</div>
)}
{/* Section Pièces à fournir */}
<ParentFilesSection
parentFiles={parentFiles}
setParentFileMasters={setParentFileMasters}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div>
);
}

View File

@ -1,343 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload';
import { formatDate } from '@/utils/Date';
import { BASE_URL } from '@/utils/Url';
import {
fetchRegisterFormFileTemplate,
createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
deleteRegisterFormFileTemplate,
getRegisterFormFileTemplate
} from '@/app/actions/subscriptionAction';
import {
fetchRegistrationFileGroups,
createRegistrationFileGroup,
deleteRegistrationFileGroup,
editRegistrationFileGroup
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/RegistrationFileGroupForm';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FilesManagement({ csrfToken }) {
const [fichiers, setFichiers] = useState([]);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const { selectedEstablishmentId } = useEstablishment();
// Fonction pour transformer les données des fichiers avec les informations complètes du groupe
const transformFileData = (file, groups) => {
if (!file.group) return file;
const groupInfo = groups.find(g => g.id === file.group);
return {
...file,
group: groupInfo || { id: file.group, name: 'Groupe inconnu' }
};
};
useEffect(() => {
Promise.all([
fetchRegisterFormFileTemplate(),
fetchRegistrationFileGroups(selectedEstablishmentId)
]).then(([filesData, groupsData]) => {
setGroups(groupsData);
// Sélectionner automatiquement le premier groupe s'il existe
if (groupsData.length > 0) {
setSelectedGroup(groupsData[0].id.toString());
}
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesData.map(file => transformFileData(file, groupsData));
setFichiers(transformedFiles);
}).catch(err => {
console.log(err.message);
});
}, []);
const handleFileDelete = (fileId) => {
deleteRegisterFormFileTemplate(fileId, csrfToken)
.then(response => {
if (response.ok) {
setFichiers(fichiers.filter(fichier => fichier.id !== fileId));
alert('Fichier supprimé avec succès.');
} else {
alert('Erreur lors de la suppression du fichier.');
}
})
.catch(error => {
console.error('Error deleting file:', error);
alert('Erreur lors de la suppression du fichier.');
});
};
const handleFileEdit = (file) => {
setIsEditing(true);
setFileToEdit(file);
setIsModalOpen(true);
};
const handleFileUpload = ({file, name, is_required, order, groupId}) => {
if (!name) {
alert('Veuillez entrer un nom de fichier.');
return;
}
const formData = new FormData();
if(file) {
formData.append('file', file);
}
formData.append('name', name);
formData.append('is_required', is_required);
formData.append('order', order);
// Modification ici : vérifier si groupId existe et n'est pas vide
if (groupId && groupId !== '') {
formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id'
}
if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
.then(data => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? transformedFile : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
});
} else {
createRegistrationFormFileTemplate(formData, csrfToken)
.then(data => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFiles => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
const handleGroupSubmit = (groupData) => {
if (groupToEdit) {
editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken)
.then(updatedGroup => {
setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group));
setGroupToEdit(null);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
.then(newGroup => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
}
};
const handleGroupEdit = (group) => {
setGroupToEdit(group);
setIsGroupModalOpen(true);
};
const handleGroupDelete = (groupId) => {
// Vérifier si des fichiers utilisent ce groupe
const filesInGroup = fichiers.filter(file => file.group && file.group.id === groupId);
if (filesInGroup.length > 0) {
alert('Impossible de supprimer ce groupe car il contient des fichiers. Veuillez d\'abord retirer tous les fichiers de ce groupe.');
return;
}
if (window.confirm('Êtes-vous sûr de vouloir supprimer ce groupe ?')) {
deleteRegistrationFileGroup(groupId, csrfToken)
.then((response) => {
if (response.status === 409) {
throw new Error('Ce groupe est lié à des inscriptions existantes.');
}
if (!response.ok) {
throw new Error('Erreur lors de la suppression du groupe.');
}
setGroups(groups.filter(group => group.id !== groupId));
alert('Groupe supprimé avec succès.');
})
.catch(error => {
console.error('Error deleting group:', error);
alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.');
});
}
};
// Ajouter cette fonction de filtrage
const filteredFiles = fichiers.filter(file => {
if (!selectedGroup) return true;
return file.group && file.group.id === parseInt(selectedGroup);
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Groupe', transform: (row) => row.group ? row.group.name : 'Aucun' },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{row.file && (
<a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
<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>
<button onClick={() => handleSignatureRequest(row)} className="text-green-500 hover:text-green-700">
<Signature size={16} />
</button>
</div>
)}
];
const columnsGroups = [
{ name: 'Nom du groupe', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
<button onClick={() => handleGroupEdit(row)} className="text-blue-500 hover:text-blue-700">
<Edit size={16} />
</button>
<button onClick={() => handleGroupDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
</div>
)}
];
// Fonction pour gérer la demande de signature
const handleSignatureRequest = (file) => {
const formData = new FormData();
formData.append('file', file);
console.log('Demande de signature pour le fichier :', file);
fetch('http://localhost:8080:/DocuSeal/generateToken', {
method: 'POST',
headers: {
'Authorization': 'Bearer NFPZy6BBGvYs1BwTuXMQ3XAu5N1kLFiXWftGQhkiz2A',
},
body: formData,
})
.then((response) => {
if (!response.ok) {
throw new Error('Erreur lors du téléversement du document : ' + response.statusText);
}
return response.json();
})
.then((data) => {
const documentId = data.documentId;
console.log('Document téléversé avec succès, ID :', documentId);
onUpload(documentId);
});
.catch((error) => console.error(error));
};
return (
<div>
<Modal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'}
ContentComponent={() => (
<FileUpload
onFileUpload={handleFileUpload}
fileToEdit={fileToEdit}
/>
)}
/>
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
title={groupToEdit ? "Modifier le groupe" : "Ajouter un groupe de fichiers"}
ContentComponent={() => (
<RegistrationFileGroupForm
onSubmit={handleGroupSubmit}
initialData={groupToEdit}
/>
)}
/>
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Groupes de fichiers</h2>
<button
onClick={() => setIsGroupModalOpen(true)}
className="flex items-center bg-blue-600 text-white p-2 rounded-full shadow hover:bg-blue-900 transition duration-200"
>
<FolderPlus className="w-5 h-5" />
</button>
</div>
<Table
data={groups}
columns={columnsGroups}
itemsPerPage={5}
currentPage={1}
totalPages={Math.ceil(groups.length / 5)}
/>
</div>
{groups.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Fichiers</h2>
<div className="flex items-center gap-4">
<select
className="border rounded p-2"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
>
<option value="">Tous les groupes</option>
{groups.map(group => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
<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"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<Table
data={filteredFiles}
columns={columnsFiles}
itemsPerPage={10}
currentPage={1}
totalPages={Math.ceil(filteredFiles.length / 10)}
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
import Table from '@/components/Table';
import InputText from '@/components/InputText';
import MultiSelect from '@/components/MultiSelect';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader';
export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) {
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState(null);
const [selectedGroups, setSelectedGroups] = useState([]); // Gestion des groupes sélectionnés
const [guardianDetails, setGuardianDetails] = useState([]);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const csrfToken = useCsrfToken();
const handleAddEmptyRequiredDocument = () => {
setEditingDocumentId('new');
setFormData({ name: '', description: '', groups: [] });
setSelectedGroups([]); // Réinitialiser les groupes sélectionnés
};
const handleEditDocument = (document) => {
setEditingDocumentId(document.id);
setFormData(document);
const initialSelectedGroups = document.groups.map((groupId) =>
groups.find((group) => group.id === groupId)
);
setSelectedGroups(initialSelectedGroups);
};
const handleSaveDocument = () => {
if (!formData.name) {
alert('Le nom de la pièce est requis.');
return;
}
const updatedFormData = {
...formData,
groups: selectedGroups.map((group) => group.id),
};
if (editingDocumentId === 'new') {
handleCreate(updatedFormData).then((createdDocument) => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
guardianDetails.forEach((guardian, index) => {
// Création des templates
const data = {
master: createdDocument?.id,
registration_form: guardian.registration_form
};
console.log(guardian)
createRegistrationParentFileTemplate(data, csrfToken)
.then(response => {
logger.debug('Template enregistré avec succès:', response);
})
.catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error);
});
});
});
} else {
handleEdit(editingDocumentId, updatedFormData).then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
});
}
};
const handleRemoveDocument = (id) => {
return handleDelete(id)
.then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
})
.catch((error) => {
logger.error(error);
});
};
const handleCancelEdit = () => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
};
const handleGroupChange = (selected) => {
setSelectedGroups(selected);
console.log('selected : ', selected)
// Extraire les guardians associés aux register_forms des groupes sélectionnés
const details = selected.flatMap(group =>
group.registration_forms.flatMap(form =>
form.guardians.map(guardian => ({
email: guardian.associated_profile_email,
last_name: form.last_name, // Extraire depuis form
first_name: form.first_name, // Extraire depuis form
registration_form: form.student_id // Utiliser student_id comme ID du register_form
}))
)
);
console.log("Guardians associés : ", details);
setGuardianDetails(details); // Mettre à jour la variable d'état avec les détails des guardians
};
const renderRequiredDocumentCell = (document, column) => {
const isEditing = editingDocumentId === document.id || (editingDocumentId === 'new' && !document.id);
if (isEditing) {
switch (column) {
case 'Nom de la pièce':
return (
<InputText
name="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Nom de la pièce"
className="w-full"
/>
);
case 'Description':
return (
<InputText
name="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Description"
className="w-full"
/>
);
case 'Dossiers d\'inscription':
return (
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
options={groups}
selectedOptions={selectedGroups}
onChange={handleGroupChange}
errorMsg={null}
/>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={handleSaveDocument}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={handleCancelEdit}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'Nom de la pièce':
return <span>{document.name}</span>;
case 'Description':
return <span>{document.description}</span>;
case 'Dossiers d\'inscription':
return (
<span>
{document.groups
.map((groupId) => groups.find((group) => group.id === groupId)?.name || 'Dossiers d\'inscription inconnu')
.join(', ')}
</span>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => handleEditDocument(document)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Attentions ! \nVous êtes sur le point de supprimer le document "${document.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
handleRemoveDocument(document.id)
.then(() => {
setPopupMessage(`Le document "${document.name}" a été correctement supprimé.`);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du document:', error);
setPopupMessage(`Erreur lors de la suppression du document "${document.name}".`);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
const columnsRequiredDocuments = [
{ name: 'Nom de la pièce', transform: (row) => renderRequiredDocumentCell(row, 'Nom de la pièce') },
{ name: 'Description', transform: (row) => renderRequiredDocumentCell(row, 'Description') },
{ name: 'Dossiers d\'inscription', transform: (row) => renderRequiredDocumentCell(row, 'Dossiers d\'inscription') },
{ name: 'Actions', transform: (row) => renderRequiredDocumentCell(row, 'Actions') },
];
return (
<div className="mt-12 w-4/5">
<SectionHeader
icon={FileText}
title="Pièces à fournir"
description="Configurez la liste des documents que les parents doivent fournir."
button={true}
onClick={handleAddEmptyRequiredDocument}
/>
<Table
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
columns={columnsRequiredDocuments}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);
}

View File

@ -1,20 +1,12 @@
import React, { useState } from 'react';
import {
Plus,
Trash2,
Edit3,
Check,
X,
Percent,
EuroIcon,
Tag,
} from 'lucide-react';
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import SectionHeader from '@/components/SectionHeader';
const DiscountsSection = ({
discounts,
@ -347,22 +339,15 @@ const DiscountsSection = ({
];
return (
<div className="space-y-4">
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Liste des réductions</h2>
</div>
<button
type="button"
<div className="space-y-4 mt-8">
<SectionHeader
icon={Tag}
discountStyle={true}
title={`${type == 0 ? "Liste des réductions sur les frais d'inscription" : 'Liste des réductions sur les frais de scolarité'}`}
description={`${subscriptionMode ? 'Sélectionnez' : 'Gérez'} ${type == 0 ? " vos réductions sur les frais d'inscription" : ' vos réductions sur les frais de scolarité'}`}
button={!subscriptionMode}
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={columns}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import FeesSection from '@/components/Structure/Tarification/FeesSection';
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
import PaymentPlanSelector from '@/components/PaymentPlanSelector';
@ -50,23 +50,22 @@ const FeesManagement = ({
};
return (
<div className="w-full mx-auto p-2 mt-6 space-y-6">
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">
Frais d&apos;inscription
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="w-full mx-auto mt-6">
<div className="w-4/5 mx-auto flex items-center mt-8">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">
Frais d'inscription
</span>
<hr className="flex-grow border-t-2 border-gray-300" />
</div>
<div className="mt-8 w-4/5">
<FeesSection
fees={registrationFees}
setFees={setRegistrationFees}
discounts={registrationDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_FEES_URL}`,
newData,
setRegistrationFees
)
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)
}
handleEdit={(id, updatedData) =>
handleEdit(
@ -82,7 +81,7 @@ const FeesManagement = ({
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={registrationDiscounts}
setDiscounts={setRegistrationDiscounts}
@ -112,7 +111,8 @@ const FeesManagement = ({
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 mt-4">
<PaymentPlanSelector
paymentPlans={registrationPaymentPlans}
setPaymentPlans={setRegistrationPaymentPlans}
@ -127,7 +127,7 @@ const FeesManagement = ({
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={registrationPaymentModes}
setPaymentModes={setRegistrationPaymentModes}
@ -143,11 +143,16 @@ const FeesManagement = ({
/>
</div>
</div>
<div className="w-4/5 mx-auto flex items-center mt-16">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">
Frais de scolarité
</span>
<hr className="flex-grow border-t-2 border-gray-300" />
</div>
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-8 w-4/5">
<FeesSection
fees={tuitionFees}
setFees={setTuitionFees}
@ -156,12 +161,7 @@ const FeesManagement = ({
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_FEES_URL}`,
id,
updatedData,
setTuitionFees
)
handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)
@ -169,7 +169,7 @@ const FeesManagement = ({
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={tuitionDiscounts}
setDiscounts={setTuitionDiscounts}
@ -189,17 +189,14 @@ const FeesManagement = ({
)
}
handleDelete={(id) =>
handleDelete(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
setTuitionDiscounts
)
handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)
}
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 mt-4">
<PaymentPlanSelector
paymentPlans={tuitionPaymentPlans}
setPaymentPlans={setTuitionPaymentPlans}
@ -214,7 +211,7 @@ const FeesManagement = ({
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={tuitionPaymentModes}
setPaymentModes={setTuitionPaymentModes}
@ -231,7 +228,6 @@ const FeesManagement = ({
</div>
</div>
</div>
</div>
);
};

View File

@ -1,20 +1,11 @@
import React, { useState } from 'react';
import {
Plus,
Trash2,
Edit3,
Check,
X,
EyeOff,
Eye,
CreditCard,
BookOpen,
} from 'lucide-react';
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { ESTABLISHMENT_ID } from '@/utils/Url';
@ -325,21 +316,13 @@ const FeesSection = ({
return (
<div className="space-y-4">
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Liste des frais</h2>
</div>
<button
type="button"
<SectionHeader
icon={CreditCard}
title={`${type == 0 ? "Liste des frais d'inscription" : 'Liste des frais de scolarité'}`}
description={`${subscriptionMode ? 'Sélectionnez' : 'Gérez'} ${type == 0 ? " vos frais d'inscription" : ' vos frais de scolarité'}`}
button={!subscriptionMode}
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={columns}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
const Table = ({
data,
@ -43,7 +43,16 @@ const Table = ({
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''}
`}
onClick={() => isSelectable && onRowClick && onRowClick(row)}
onClick={() => {
if (isSelectable && onRowClick) {
// Si la ligne est déjà sélectionnée, transmettre une indication explicite de désélection
if (selectedRows?.includes(row.id)) {
onRowClick({ deselected: true, row }); // Désélectionner
} else {
onRowClick(row); // Sélectionner
}
}
}}
>
{columns.map((column, colIndex) => (
<td
@ -83,6 +92,10 @@ Table.propTypes = {
currentPage: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
onRowClick: PropTypes.func,
selectedRows: PropTypes.arrayOf(PropTypes.any),
isSelectable: PropTypes.bool,
defaultTheme: PropTypes.string,
};
export default Table;

View File

@ -25,9 +25,11 @@ export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession`;
export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students`; // Récupère la liste des élèves inscrits ou en cours d'inscriptions
export const BE_SUBSCRIPTION_CHILDRENS_URL = `${BASE_URL}/Subscriptions/children`; // Récupère la liste des élèves d'un profil
export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`;
export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL = `${BASE_URL}/Subscriptions/registrationTemplateMasters`;
export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationTemplates`;
export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`;
export const BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL = `${BASE_URL}/Subscriptions/registrationSchoolFileMasters`;
export const BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationSchoolFileTemplates`;
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL = `${BASE_URL}/Subscriptions/registrationParentFileMasters`;
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`;
export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
//GESTION ECOLE