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 from xhtml2pdf import pisa
class PDFResult:
def __init__(self, content):
self.content = content
def render_to_pdf(template_src, context_dict={}): 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) template = get_template(template_src)
html = template.render(context_dict) html = template.render(context_dict)
result = BytesIO() result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result) pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
if pdf.err: if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain') # Lever une exception ou retourner None en cas d'erreur
return HttpResponse(result.getvalue(), content_type='application/pdf') 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, Fee,
Discount, Discount,
RegistrationFileGroup, RegistrationFileGroup,
RegistrationTemplateMaster, RegistrationSchoolFileMaster,
RegistrationTemplate RegistrationSchoolFileTemplate
) )
from Auth.models import Profile, ProfileRole from Auth.models import Profile, ProfileRole
from School.models import ( from School.models import (

View File

@ -9,6 +9,8 @@ from Establishment.models import Establishment
from datetime import datetime from datetime import datetime
import os
class Language(models.Model): class Language(models.Model):
""" """
Représente une langue parlée par lélève. Représente une langue parlée par lélève.
@ -83,7 +85,7 @@ class Student(models.Model):
siblings = models.ManyToManyField(Sibling, blank=True) siblings = models.ManyToManyField(Sibling, blank=True)
# Many-to-Many Relationship # 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 # Many-to-Many Relationship
spoken_languages = models.ManyToManyField(Language, blank=True) spoken_languages = models.ManyToManyField(Language, blank=True)
@ -163,19 +165,13 @@ class RegistrationFileGroup(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def __str__(self):
return f'{self.group.name} - {self.id}'
def registration_file_path(instance, filename): def registration_file_path(instance, filename):
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename # Génère le chemin : registration_files/dossier_rf_{student_id}/filename
return f'registration_files/dossier_rf_{instance.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 RegistrationForm(models.Model):
class RegistrationFormStatus(models.IntegerChoices): class RegistrationFormStatus(models.IntegerChoices):
RF_IDLE = 0, _('Pas de dossier d\'inscription') RF_IDLE = 0, _('Pas de dossier d\'inscription')
@ -211,7 +207,7 @@ class RegistrationForm(models.Model):
# Many-to-Many Relationship # Many-to-Many Relationship
discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms') discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms')
fileGroup = models.ForeignKey(RegistrationFileGroup, fileGroup = models.ForeignKey(RegistrationFileGroup,
on_delete=models.CASCADE, on_delete=models.SET_NULL,
related_name='register_forms', related_name='register_forms',
null=True, null=True,
blank=True) blank=True)
@ -223,16 +219,58 @@ class RegistrationForm(models.Model):
def __str__(self): def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name return "RF_" + self.student.last_name + "_" + self.student.first_name
def registration_file_upload_to(instance, filename): def save(self, *args, **kwargs):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/{filename}" # 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): # Appeler la méthode save originale
master = models.ForeignKey(RegistrationTemplateMaster, on_delete=models.CASCADE, related_name='templates', blank=True) 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) id = models.IntegerField(primary_key=True)
slug = models.CharField(max_length=255, default="") slug = models.CharField(max_length=255, default="")
name = 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) 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_file_upload_to) file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
def __str__(self): def __str__(self):
return self.name return self.name
@ -242,7 +280,41 @@ class RegistrationTemplate(models.Model):
""" """
Récupère tous les fichiers liés à un dossier dinscription donné. 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 = [] filenames = []
for reg_file in registration_files: for reg_file in registration_files:
filenames.append(reg_file.file.path) filenames.append(reg_file.file.path)

View File

@ -1,5 +1,5 @@
from rest_framework import serializers 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.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import ProfileRole, Profile from Auth.models import ProfileRole, Profile
@ -12,18 +12,43 @@ import pytz
from datetime import datetime from datetime import datetime
import Subscriptions.util as util import Subscriptions.util as util
class RegistrationTemplateMasterSerializer(serializers.ModelSerializer): class RegistrationSchoolFileMasterSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
class Meta: class Meta:
model = RegistrationTemplateMaster model = RegistrationSchoolFileMaster
fields = '__all__' fields = '__all__'
class RegistrationTemplateSerializer(serializers.ModelSerializer): class RegistrationParentFileMasterSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
class Meta: class Meta:
model = RegistrationTemplate model = RegistrationParentFileMaster
fields = '__all__' 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): class GuardianSimpleSerializer(serializers.ModelSerializer):
associated_profile_email = serializers.SerializerMethodField() associated_profile_email = serializers.SerializerMethodField()
@ -199,7 +224,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
sepa_file = serializers.FileField(required=False) sepa_file = serializers.FileField(required=False)
status_label = serializers.SerializerMethodField() status_label = serializers.SerializerMethodField()
formatted_last_update = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField()
registration_files = RegistrationTemplateSerializer(many=True, required=False) registration_files = RegistrationSchoolFileTemplateSerializer(many=True, required=False)
fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), 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) discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False)
totalRegistrationFees = serializers.SerializerMethodField() totalRegistrationFees = serializers.SerializerMethodField()
@ -280,7 +305,7 @@ class RegistrationFormByParentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RegistrationForm model = RegistrationForm
fields = ['student', 'status'] fields = ['student', 'status', 'sepa_file']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RegistrationFormByParentSerializer, self).__init__(*args, **kwargs) super(RegistrationFormByParentSerializer, self).__init__(*args, **kwargs)

View File

@ -7,16 +7,27 @@ from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archi
# SubClasses # SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView
# Files # 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 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 = [ urlpatterns = [
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"), 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]+)/resend$', resend, name="resend"),
re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"), 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]+)$', 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"), re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"),
# Page INSCRIPTION - Liste des élèves # 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/(?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'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'),
re_path(r'^registrationTemplateMasters/(?P<id>[0-9]+)$', RegistrationTemplateMasterSimpleView.as_view(), name='registrationTemplateMasters'), re_path(r'^registrationSchoolFileMasters/(?P<id>[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'),
re_path(r'^registrationTemplateMasters$', RegistrationTemplateMasterView.as_view(), name='registrationTemplateMasters'), 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'^registrationParentFileMasters/(?P<id>[0-9]+)$', RegistrationParentFileMasterSimpleView.as_view(), name='registrationParentFileMasters'),
re_path(r'^registrationTemplates$', RegistrationTemplateView.as_view(), name="registrationTemplates"), 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'), 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 from PyPDF2 import PdfMerger
import shutil import shutil
import logging
logger = logging.getLogger(__name__)
def recupereListeFichesInscription(): 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. Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
""" """
filename = filename.replace(" ", "_") filename = filename.replace(" ", "_")
data = { data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}", 'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'), 'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
@ -131,20 +133,37 @@ def rfToPDF(registerForm, filename):
# Générer le PDF # Générer le PDF
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) 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 # 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): if registerForm.registration_file and registerForm.registration_file.name:
os.remove(registerForm.registration_file.path) # 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) 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 # Enregistrer directement le fichier dans le champ registration_file
try:
registerForm.registration_file.save( registerForm.registration_file.save(
os.path.basename(filename), os.path.basename(filename), # Utiliser uniquement le nom de fichier
File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque File(BytesIO(pdf.content)),
save=True 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): 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 .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 RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView 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 .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView, DissociateGuardianView from .guardian_views import GuardianView, DissociateGuardianView
@ -10,14 +19,19 @@ __all__ = [
'send', 'send',
'resend', 'resend',
'archive', 'archive',
'RegistrationTemplateView', 'RegistrationSchoolFileTemplateView',
'RegistrationTemplateSimpleView', 'RegistrationSchoolFileTemplateSimpleView',
'RegistrationTemplateMasterView', 'RegistrationParentFileMasterSimpleView',
'RegistrationTemplateMasterSimpleView', 'RegistrationParentFileMasterView',
'RegistrationSchoolFileMasterView',
'RegistrationSchoolFileMasterSimpleView',
'RegistrationParentFileTemplateSimpleView',
'RegistrationParentFileTemplateView',
'RegistrationFileGroupView', 'RegistrationFileGroupView',
'RegistrationFileGroupSimpleView', 'RegistrationFileGroupSimpleView',
'get_registration_files_by_group', 'get_registration_files_by_group',
'get_templates_by_rf', 'get_school_file_templates_by_rf',
'get_parent_file_templates_by_rf'
'StudentView', 'StudentView',
'StudentListView', 'StudentListView',
'ChildrenListView', 'ChildrenListView',

View File

@ -14,9 +14,9 @@ from django.core.files import File
import Subscriptions.mailManager as mailer import Subscriptions.mailManager as mailer
import Subscriptions.util as util import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.pagination import CustomPagination 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 Subscriptions.automate import updateStateMachine
from N3wtSchool import settings, bdd from N3wtSchool import settings, bdd
@ -254,29 +254,36 @@ class RegisterFormWithIdView(APIView):
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
try: try:
# Génération de la fiche d'inscription au format PDF # 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) os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial # 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.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save() registerForm.save()
# Récupération des fichiers d'inscription # Récupération des fichiers d'inscription
fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk) # fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file: # if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path) # fileNames.insert(0, registerForm.registration_file.path)
# Création du fichier PDF Fusionné # # Création du fichier PDF Fusionné
merged_pdf_content = util.merge_files_pdf(fileNames) # 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 # 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') updateStateMachine(registerForm, 'EVENT_SIGNATURE')
except Exception as e: except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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é" operation_summary="Récupérer les fichiers à signer d'un dossier d'inscription donné"
) )
@api_view(['GET']) @api_view(['GET'])
def get_templates_by_rf(request, id): def get_school_file_templates_by_rf(request, id):
try: try:
templates = RegistrationTemplate.objects.filter(registration_form=id) # Récupérer les templates associés au RegistrationForm donné
templates_data = list(templates.values()) templates = RegistrationSchoolFileTemplate.objects.filter(registration_form=id)
return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist: # Sérialiser les données
return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404) 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 drf_yasg import openapi
from Subscriptions.serializers import RegistrationFileGroupSerializer from Subscriptions.serializers import RegistrationFileGroupSerializer
from Subscriptions.models import RegistrationFileGroup, RegistrationTemplateMaster from Subscriptions.models import RegistrationFileGroup, RegistrationSchoolFileMaster
from N3wtSchool import bdd from N3wtSchool import bdd
class RegistrationFileGroupView(APIView): class RegistrationFileGroupView(APIView):
@ -124,7 +124,7 @@ class RegistrationFileGroupSimpleView(APIView):
def get_registration_files_by_group(request, id): def get_registration_files_by_group(request, id):
try: try:
group = RegistrationFileGroup.objects.get(id=id) group = RegistrationFileGroup.objects.get(id=id)
templateMasters = RegistrationTemplateMaster.objects.filter(groups=group) templateMasters = RegistrationSchoolFileMaster.objects.filter(groups=group)
templates_data = list(templateMasters.values()) templates_data = list(templateMasters.values())
return JsonResponse(templates_data, safe=False) return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist: except RegistrationFileGroup.DoesNotExist:

View File

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

View File

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

View File

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

View File

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

View File

@ -2,21 +2,28 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Table from '@/components/Table'; 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 StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url'; 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 logger from '@/utils/logger';
import { BASE_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import ProfileSelector from '@/components/ProfileSelector'; import { useCsrfToken } from '@/context/CsrfContext';
export default function ParentHomePage() { export default function ParentHomePage() {
const [children, setChildren] = useState([]); const [children, setChildren] = useState([]);
const [userId, setUserId] = useState(null); const [userId, setUserId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const { user, selectedEstablishmentId } = useEstablishment(); 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 router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => { useEffect(() => {
const userIdFromSession = user.user_id; const userIdFromSession = user.user_id;
@ -27,17 +34,62 @@ export default function ParentHomePage() {
}); });
}, [selectedEstablishmentId]); }, [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) { function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`); logger.debug(`Edit dossier for student id: ${eleveId}`);
router.push( router.push(
`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}` `${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 = [ const childrenColumns = [
{ name: 'Nom', transform: (row) => `${row.student.last_name}` }, { name: 'Nom', transform: (row) => `${row.student.last_name}` },
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` }, { name: 'Prénom', transform: (row) => `${row.student.first_name}` },
@ -52,29 +104,76 @@ export default function ParentHomePage() {
{ {
name: 'Actions', name: 'Actions',
transform: (row) => ( transform: (row) => (
<div className="flex justify-center"> <div className="flex justify-center items-center gap-2">
{row.status === 2 && (
<button <button
className="p-2 hover:bg-gray-100 rounded-full transition-colors" className="text-blue-500 hover:text-blue-700"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleEdit(row.student.id); 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> </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> </div>
), ),
}, },
]; ];
const itemsPerPage = 5;
const totalPages = Math.ceil(children.length / itemsPerPage) || 1;
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
return ( return (
<div className="px-2 py-4 md:px-4 max-w-full"> <div className="px-2 py-4 md:px-4 max-w-full">
<div> <div>
@ -86,13 +185,29 @@ export default function ParentHomePage() {
<Table <Table
data={children} data={children}
columns={childrenColumns} columns={childrenColumns}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
defaultTheme="bg-gray-50" defaultTheme="bg-gray-50"
/> />
</div> </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>
</div> </div>
); );

View File

@ -1,7 +1,9 @@
import { import {
BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL, BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL, BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_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_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL, FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN, FE_API_DOCUSEAL_GENERATE_TOKEN,
@ -18,6 +20,8 @@ const requestResponseHandler = async (response) => {
throw error; throw error;
}; };
// FETCH requests
export async function fetchRegistrationFileGroups(establishment) { export async function fetchRegistrationFileGroups(establishment) {
const response = await fetch( const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`, `${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
@ -34,6 +38,68 @@ export async function fetchRegistrationFileGroups(establishment) {
return response.json(); 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) { export async function createRegistrationFileGroup(groupData, csrfToken) {
const response = await fetch( const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, `${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
@ -55,20 +121,55 @@ export async function createRegistrationFileGroup(groupData, csrfToken) {
return response.json(); return response.json();
} }
export async function deleteRegistrationFileGroup(groupId, csrfToken) { export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
const response = await fetch( return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, method: 'POST',
{ body: JSON.stringify(data),
method: 'DELETE',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
}, },
credentials: 'include', 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 ( export const editRegistrationFileGroup = async (
groupId, groupId,
@ -94,113 +195,9 @@ export const editRegistrationFileGroup = async (
return response.json(); return response.json();
}; };
export const fetchRegistrationFileFromGroup = async (groupId) => { export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
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) => {
return fetch( return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`, `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_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}`,
{ {
method: 'PUT', method: 'PUT',
body: JSON.stringify(data), body: JSON.stringify(data),
@ -213,6 +210,128 @@ export const editRegistrationTemplateMaster = (fileId, data, csrfToken) => {
).then(requestResponseHandler); ).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) => { export const cloneTemplate = (templateId, email, is_required) => {
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, { return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
method: 'POST', method: 'POST',

View File

@ -153,9 +153,27 @@ export async function getRegisterFormFileTemplate(fileId) {
return response.json(); return response.json();
} }
export const fetchTemplatesFromRegistrationFiles = async (id) => { export const fetchSchoolFileTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch( 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', credentials: 'include',
headers: { 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, className,
required, required,
}) { }) {
const handlePhoneChange = (phone) => {
// Appeler onChange avec un objet personnalisé
onChange({ target: { name, value: phone } });
};
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700"> <label htmlFor={name} className="block text-sm font-medium text-gray-700">
@ -21,7 +26,7 @@ export default function InputPhone({
<PhoneInput <PhoneInput
defaultCountry="fr" defaultCountry="fr"
value={value} value={value}
onChange={(phone) => onChange(phone)} onChange={handlePhoneChange}
inputProps={{ inputProps={{
name: name, name: name,
required: required, required: required,

View File

@ -1,19 +1,244 @@
import React from 'react'; import React, { useState } from 'react';
import Table from '@/components/Table'; 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 ( return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <span
<h2 className="text-xl font-bold mb-4 text-gray-800"> className={`px-2 py-1 rounded-md text-sm font-medium ${
Fichiers à uploader 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> </h2>
<Table <p className="text-sm text-gray-500 italic">
data={fileTemplates} Ajoutez les documents pour compléter votre inscription
columns={columns} </p>
itemsPerPage={5} </div>
currentPage={1} </div>
totalPages={1} </div>
onPageChange={() => {}} <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> </div>
); );

View File

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

View File

@ -5,23 +5,19 @@ import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { import {
fetchRegisterForm, fetchRegisterForm,
fetchTemplatesFromRegistrationFiles, fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction'; } from '@/app/actions/subscriptionAction';
import { import {
downloadTemplate, downloadTemplate,
createRegistrationTemplates, editRegistrationSchoolFileTemplates,
editRegistrationTemplates, editRegistrationParentFileTemplates,
deleteRegistrationTemplates,
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import { import {
fetchRegistrationPaymentModes, fetchRegistrationPaymentModes,
fetchTuitionPaymentModes, fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction'; } from '@/app/actions/schoolAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url'; 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 logger from '@/utils/logger';
import StudentInfoForm, { import StudentInfoForm, {
validateStudentInfo, validateStudentInfo,
@ -42,7 +38,6 @@ export default function InscriptionFormShared({
csrfToken, csrfToken,
selectedEstablishmentId, selectedEstablishmentId,
onSubmit, onSubmit,
cancelUrl,
errors = {}, // Nouvelle prop pour les erreurs errors = {}, // Nouvelle prop pour les erreurs
}) { }) {
// États pour gérer les données du formulaire // États pour gérer les données du formulaire
@ -69,13 +64,8 @@ export default function InscriptionFormShared({
// États pour la gestion des fichiers // États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]); const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]); const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [fileGroup, setFileGroup] = useState(null); const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [fileName, setFileName] = useState('');
const [file, setFile] = useState('');
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => { const isCurrentPageValid = () => {
@ -110,7 +100,6 @@ export default function InscriptionFormShared({
totalTuitionFees: data?.totalTuitionFees, totalTuitionFees: data?.totalTuitionFees,
}); });
setGuardians(data?.student?.guardians || []); setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
}); });
setIsLoading(false); setIsLoading(false);
@ -118,12 +107,23 @@ export default function InscriptionFormShared({
}, [studentId]); }, [studentId]);
useEffect(() => { useEffect(() => {
fetchTemplatesFromRegistrationFiles(studentId).then((data) => { fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setFileTemplates(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) { if (selectedEstablishmentId) {
// Fetch data for registration payment modes // Fetch data for registration payment modes
handleRegistrationPaymentModes(); handleRegistrationPaymentModes();
@ -164,71 +164,89 @@ export default function InscriptionFormShared({
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
// Gestion du téléversement de fichiers const handleFileUpload = (file, selectedFile) => {
const handleFileUpload = async (file, fileName) => { if (!file || !selectedFile) {
if (!file || !currentTemplateId || !formData.id) { logger.error('Données manquantes pour le téléversement.');
logger.error('Missing required data for upload'); 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; return;
} }
const data = new FormData(); // Créer un FormData avec un champ vide pour "file"
data.append('file', file); const updateData = new FormData();
data.append('name', fileName); updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try { return editRegistrationParentFileTemplates(
const response = await createRegistrationTemplates(data, csrfToken); templateId,
if (response) { updateData,
setUploadedFiles((prev) => { csrfToken
const newFiles = prev.filter( )
(f) => parseInt(f.template) !== currentTemplateId .then((response) => {
); logger.debug('Fichier supprimé avec succès dans la base :', response);
return [
...newFiles,
{
name: fileName,
template: currentTemplateId,
file: response.file,
},
];
});
// Rafraîchir les données du formulaire pour avoir les fichiers à jour // Mettre à jour l'état local pour refléter la suppression
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);
setUploadedFiles((prev) => 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) { return response;
logger.error('Error deleting file:', error); })
} .catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier dans la base :',
error
);
throw error;
});
}; };
// Soumission du formulaire // Soumission du formulaire
@ -268,90 +286,12 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1); 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 // Affichage du loader pendant le chargement
if (isLoading) return <Loader />; if (isLoading) return <Loader />;
// Rendu du composant // Rendu du composant
return ( return (
<div className="max-w-4xl mx-auto p-6"> <div className="mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken} /> <DjangoCSRFToken csrfToken={csrfToken} />
{/* Page 1 : Informations de l'élève et Responsables */} {/* Page 1 : Informations de l'élève et Responsables */}
@ -368,44 +308,45 @@ export default function InscriptionFormShared({
)} )}
{/* Pages suivantes : Section Fichiers d'inscription */} {/* 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"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */} {/* Titre du document */}
<div className="mb-4"> <div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800"> <h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name || {schoolFileTemplates[currentPage - 2].name ||
'Document sans nom'} 'Document sans nom'}
</h2> </h2>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description || {schoolFileTemplates[currentPage - 2].description ||
'Aucune description disponible pour ce document.'} 'Aucune description disponible pour ce document.'}
</p> </p>
</div> </div>
{/* Affichage du formulaire ou du document */} {/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === '' ? ( {schoolFileTemplates[currentPage - 2].file === null ? (
<DocusealForm <DocusealForm
id="docusealForm" id="docusealForm"
src={ src={
'https://docuseal.com/s/' + 'https://docuseal.com/s/' +
requiredFileTemplates[currentPage - 2].slug schoolFileTemplates[currentPage - 2].slug
} }
withDownloadButton={false} withDownloadButton={false}
onComplete={() => { onComplete={() => {
downloadTemplate(requiredFileTemplates[currentPage - 2].slug) downloadTemplate(schoolFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data)) .then((data) => fetch(data))
.then((response) => response.blob()) .then((response) => response.blob())
.then((blob) => { .then((blob) => {
const file = new File( const file = new File(
[blob], [blob],
`${requiredFileTemplates[currentPage - 2].name}.pdf`, `${schoolFileTemplates[currentPage - 2].name}.pdf`,
{ type: blob.type } { type: blob.type }
); );
const updateData = new FormData(); const updateData = new FormData();
updateData.append('file', file); updateData.append('file', file);
return editRegistrationTemplates( return editRegistrationSchoolFileTemplates(
requiredFileTemplates[currentPage - 2].id, schoolFileTemplates[currentPage - 2].id,
updateData, updateData,
csrfToken csrfToken
); );
@ -420,7 +361,7 @@ export default function InscriptionFormShared({
/> />
) : ( ) : (
<iframe <iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`} src={`${BASE_URL}/${schoolFileTemplates[currentPage - 2].file}`}
title="Document Viewer" title="Document Viewer"
className="w-full" className="w-full"
style={{ style={{
@ -430,22 +371,21 @@ export default function InscriptionFormShared({
/> />
)} )}
</div> </div>
</div>
)} )}
{/* Dernière page : Section Fichiers parents */} {/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && ( {currentPage === schoolFileTemplates.length + 2 && (
<>
<FilesToUpload <FilesToUpload
fileTemplates={fileTemplates.filter( parentFileTemplates={parentFileTemplates}
(template) => !template.is_required uploadedFiles={uploadedFiles}
)} onFileUpload={handleFileUpload}
columns={columns} onFileDelete={handleDeleteFile}
/> />
</>
)} )}
{/* Boutons de contrôle */} {/* Boutons de contrôle */}
<div className="flex justify-end space-x-4"> <div className="flex justify-center space-x-4">
<Button <Button
text="Sauvegarder" text="Sauvegarder"
onClick={handleSave} onClick={handleSave}
@ -462,7 +402,7 @@ export default function InscriptionFormShared({
}} }}
/> />
)} )}
{currentPage < requiredFileTemplates.length + 2 && ( {currentPage < schoolFileTemplates.length + 2 && (
<Button <Button
text="Suivant" text="Suivant"
onClick={(e) => { onClick={(e) => {
@ -479,57 +419,11 @@ export default function InscriptionFormShared({
name="Next" name="Next"
/> />
)} )}
{currentPage === requiredFileTemplates.length + 2 && ( {currentPage === schoolFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary /> <Button type="submit" text="Valider" primary />
)} )}
</div> </div>
</form> </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> </div>
); );
} }

View File

@ -1,25 +1,35 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { DocusealBuilder } from '@docuseal/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import { generateToken } from '@/app/actions/registerFileGroupAction'; import { generateToken } from '@/app/actions/registerFileGroupAction';
import {
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; 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({ export default function ValidateSubscription({
studentId, studentId,
firstName, firstName,
lastName, lastName,
paymentMode, paymentSepa,
file, file,
onAccept, onAccept,
}) { }) {
const [token, setToken] = useState(null); const [token, setToken] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState(''); const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedFile, setSelectedFile] = useState(null); // Nouvel état pour le fichier sélectionné
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`); const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA const [isSepa, setIsSepa] = useState(paymentSepa); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des pages 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(() => { useEffect(() => {
if (isSepa) { if (isSepa) {
@ -33,38 +43,67 @@ export default function ValidateSubscription({
} }
}, [isSepa]); }, [isSepa]);
const handleUpload = (detail) => { useEffect(() => {
logger.debug('Uploaded file detail:', detail); // Récupérer les fichiers schoolFileTemplates pour l'étudiant
setUploadedFileName(detail.name); 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 handleAccept = () => {
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input if (!selectedFile && isSepa) {
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
if (!file) {
logger.error('Aucun fichier sélectionné pour le champ SEPA.'); logger.error('Aucun fichier sélectionné pour le champ SEPA.');
return; return;
} }
// Ajouter le paramètre fusion dans l'URL
const fusionParam = mergeDocuments ? 'true' : 'false';
const data = { const data = {
status: 7, 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 // Appeler la fonction passée par le parent pour mettre à jour le RF
onAccept(data); onAccept(data);
}; };
const handleRefuse = () => { const handleToggleMergeDocuments = () => {
logger.debug("Dossier refusé pour l'étudiant:", studentId); // Inverser l'état de mergeDocuments
// Logique pour refuser l'inscription setMergeDocuments((prevState) => !prevState);
}; };
const isValidateButtonDisabled = isSepa && !uploadedFileName; const isValidateButtonDisabled = isSepa && !uploadedFileName;
const goToNextPage = () => { const goToNextPage = () => {
if (currentPage < (isSepa ? 2 : 1)) { const totalPages =
1 +
schoolFileTemplates.length +
parentFileTemplates.length +
(isSepa ? 1 : 0);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1); setCurrentPage(currentPage + 1);
} }
}; };
@ -75,30 +114,16 @@ export default function ValidateSubscription({
} }
}; };
return ( const totalPages =
<div className="p-8 space-y-6 bg-gray-50 rounded-lg shadow-lg"> 1 +
{/* Titre */} schoolFileTemplates.length +
<div className="flex items-center space-x-4"> parentFileTemplates.length +
<div className="bg-emerald-100 p-3 rounded-full shadow-md"> (isSepa ? 1 : 0);
<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>
{/* Contenu principal */} const renderContent = () => {
{currentPage === 1 && ( if (currentPage === 1) {
<div className="border p-6 rounded-lg shadow-md bg-white flex justify-center items-center"> // Page 1 : Afficher le PDF principal
return (
<iframe <iframe
src={pdfUrl} src={pdfUrl}
title="Aperçu du PDF" title="Aperçu du PDF"
@ -109,59 +134,82 @@ export default function ValidateSubscription({
border: 'none', border: 'none',
}} }}
/> />
</div> );
)} } else if (
currentPage > 1 &&
{currentPage === 2 && isSepa && ( currentPage <= 1 + schoolFileTemplates.length
<div className="border p-4 rounded-md shadow-md"> ) {
<h3 className="text-lg font-semibold mb-4"> // Pages des schoolFileTemplates
Sélection du mandat de pélèvement SEPA const index = currentPage - 2; // Décalage pour correspondre à l'index du tableau
</h3> return (
<div <iframe
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" src={`${BASE_URL}/${schoolFileTemplates[index]?.file}`}
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic title={`Document ${index + 1}`}
onDragOver={(e) => e.preventDefault()} className="w-full h-[900px] border rounded-lg"
onDrop={(e) => { style={{
e.preventDefault(); transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
const file = e.dataTransfer.files[0]; transformOrigin: 'top center',
if (file) { border: 'none',
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
}} }}
>
<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"> } else if (
Déposez votre fichier ici currentPage > 1 + schoolFileTemplates.length &&
</p> currentPage <= 1 + schoolFileTemplates.length + parentFileTemplates.length
<p className="text-sm text-gray-500 mt-2"> ) {
ou cliquez pour sélectionner un fichier PDF // Pages des parentFileTemplates
</p> const index = currentPage - 2 - schoolFileTemplates.length; // Décalage pour correspondre à l'index du tableau
</label> return (
</div> <iframe
{uploadedFileName && ( src={`${BASE_URL}/${parentFileTemplates[index]?.file}`}
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm"> title={`Document Parent ${index + 1}`}
<CloudUpload className="w-6 h-6 text-emerald-500" /> className="w-full h-[900px] border rounded-lg"
<p className="text-sm font-medium text-gray-800"> style={{
<span className="font-semibold">{uploadedFileName}</span> transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
</p> transformOrigin: 'top center',
</div> 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> </div>
)} )}
@ -174,7 +222,7 @@ export default function ValidateSubscription({
className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2" className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
/> />
)} )}
{currentPage < (isSepa ? 2 : 1) && ( {currentPage < totalPages && (
<Button <Button
text="Suivant" text="Suivant"
onClick={goToNextPage} onClick={goToNextPage}
@ -182,7 +230,7 @@ export default function ValidateSubscription({
className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-2" className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-2"
/> />
)} )}
{currentPage === (isSepa ? 2 : 1) && ( {currentPage === totalPages && (
<Button <Button
text="Valider" text="Valider"
onClick={handleAccept} 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) => ( {tabs.map((tab) => (
<button <button
key={tab.id} 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)} onClick={() => setActiveTab(tab.id)}
> >
{tab.label} {tab.label}

View File

@ -1,13 +1,4 @@
import { import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
Trash2,
Edit3,
Plus,
ZoomIn,
Users,
Check,
X,
Hand,
} from 'lucide-react';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
@ -21,6 +12,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { ESTABLISHMENT_ID } from '@/utils/Url'; import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import ClasseDetails from '@/components/ClasseDetails'; import ClasseDetails from '@/components/ClasseDetails';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = { const ItemTypes = {
TEACHER: 'teacher', TEACHER: 'teacher',
@ -553,19 +545,13 @@ const ClassesSection = ({
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <SectionHeader
<div className="flex items-center mb-4"> icon={Users}
<Users className="w-6 h-6 text-emerald-500 mr-2" /> title="Liste des classes"
<h2 className="text-xl font-semibold">Classes</h2> description="Gérez les classes de votre école"
</div> button={true}
<button
type="button"
onClick={handleAddClass} onClick={handleAddClass}
className="text-emerald-500 hover:text-emerald-700" />
>
<Plus className="w-5 h-5" />
</button>
</div>
<Table <Table
data={newClass ? [newClass, ...classes] : classes} data={newClass ? [newClass, ...classes] : classes}
columns={columns} 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 { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
@ -6,7 +6,9 @@ import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
const SpecialitiesSection = ({ const SpecialitiesSection = ({
specialities, specialities,
@ -26,6 +28,8 @@ const SpecialitiesSection = ({
const [removePopupMessage, setRemovePopupMessage] = useState(''); const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId } = useEstablishment();
// Récupération des messages d'erreur // Récupération des messages d'erreur
const getError = (field) => { const getError = (field) => {
return localErrors?.[field]?.[0]; return localErrors?.[field]?.[0];
@ -49,7 +53,13 @@ const SpecialitiesSection = ({
const handleSaveNewSpeciality = () => { const handleSaveNewSpeciality = () => {
if (newSpeciality.name) { 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) => { .then((createdSpeciality) => {
setSpecialities([createdSpeciality, ...specialities]); setSpecialities([createdSpeciality, ...specialities]);
setNewSpeciality(null); setNewSpeciality(null);
@ -234,19 +244,13 @@ const SpecialitiesSection = ({
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <SectionHeader
<div className="flex items-center mb-4"> icon={BookOpen}
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" /> title="Liste des spécialités"
<h2 className="text-xl font-semibold">Spécialités</h2> description="Gérez les spécialités de votre école"
</div> button={true}
<button
type="button"
onClick={handleAddSpeciality} onClick={handleAddSpeciality}
className="text-emerald-500 hover:text-emerald-700" />
>
<Plus className="w-5 h-5" />
</button>
</div>
<Table <Table
data={newSpeciality ? [newSpeciality, ...specialities] : specialities} data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns} columns={columns}

View File

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

View File

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

View File

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

View File

@ -1,36 +1,40 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import { Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react';
Plus,
Download,
Edit3,
Trash2,
FolderPlus,
Signature,
} from 'lucide-react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Table from '@/components/Table'; 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 { BASE_URL } from '@/utils/Url';
import { import {
// GET
fetchRegistrationFileGroups, fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
fetchRegistrationSchoolFileTemplates,
fetchRegistrationParentFileMasters,
// POST
createRegistrationFileGroup, createRegistrationFileGroup,
deleteRegistrationFileGroup, createRegistrationSchoolFileMaster,
createRegistrationParentFileMaster,
// PUT
editRegistrationFileGroup, editRegistrationFileGroup,
fetchRegistrationTemplateMaster, editRegistrationSchoolFileMaster,
createRegistrationTemplateMaster, editRegistrationParentFileMaster,
editRegistrationTemplateMaster, // DELETE
deleteRegistrationTemplateMaster, deleteRegistrationFileGroup,
fetchRegistrationTemplates, deleteRegistrationSchoolFileMaster,
deleteRegistrationParentFileMaster,
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
import SectionHeader from '@/components/SectionHeader';
export default function FilesGroupsManagement({ export default function FilesGroupsManagement({
csrfToken, csrfToken,
selectedEstablishmentId, selectedEstablishmentId,
}) { }) {
const [templateMasters, setTemplateMasters] = useState([]); const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [templates, setTemplates] = useState([]); const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -39,6 +43,10 @@ export default function FilesGroupsManagement({
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null); const [groupToEdit, setGroupToEdit] = useState(null);
const [reloadTemplates, setReloadTemplates] = useState(false); const [reloadTemplates, setReloadTemplates] = useState(false);
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({});
const [uploadedFileName, setUploadedFileName] = useState('');
const handleReloadTemplates = () => { const handleReloadTemplates = () => {
setReloadTemplates(true); setReloadTemplates(true);
@ -61,19 +69,28 @@ export default function FilesGroupsManagement({
useEffect(() => { useEffect(() => {
if (selectedEstablishmentId) { if (selectedEstablishmentId) {
Promise.all([ Promise.all([
fetchRegistrationTemplateMaster(), fetchRegistrationSchoolFileMasters(),
fetchRegistrationFileGroups(selectedEstablishmentId), fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationTemplates(), fetchRegistrationSchoolFileTemplates(),
fetchRegistrationParentFileMasters(),
]) ])
.then(([filesTemplateMasters, groupsData, filesTemplates]) => { .then(
([
dataSchoolFileMasters,
groupsData,
dataSchoolFileTemplates,
dataParentFileMasters,
]) => {
setGroups(groupsData); setGroups(groupsData);
setTemplates(filesTemplates); setSchoolFileTemplates(dataSchoolFileTemplates);
setParentFileMasters(dataParentFileMasters);
// Transformer chaque fichier pour inclure les informations complètes du groupe // Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesTemplateMasters.map((file) => const transformedFiles = dataSchoolFileMasters.map((file) =>
transformFileData(file, groupsData) transformFileData(file, groupsData)
); );
setTemplateMasters(transformedFiles); setSchoolFileMasters(transformedFiles);
}) }
)
.catch((err) => { .catch((err) => {
console.log(err.message); console.log(err.message);
}) })
@ -85,7 +102,7 @@ export default function FilesGroupsManagement({
const deleteTemplateMaster = (templateMaster) => { const deleteTemplateMaster = (templateMaster) => {
// Supprimer les clones associés via l'API DocuSeal // Supprimer les clones associés via l'API DocuSeal
const removeClonesPromises = templates const removeClonesPromises = schoolFileTemplates
.filter((template) => template.master === templateMaster.id) .filter((template) => template.master === templateMaster.id)
.map((template) => removeTemplate(template.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.'); logger.debug('Master et clones supprimés avec succès de DocuSeal.');
// Supprimer le template master de la base de données // Supprimer le template master de la base de données
deleteRegistrationTemplateMaster(templateMaster.id, csrfToken) deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
setTemplateMasters( setSchoolFileMasters(
templateMasters.filter( schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id (fichier) => fichier.id !== templateMaster.id
) )
); );
@ -175,11 +192,11 @@ export default function FilesGroupsManagement({
}; };
logger.debug(data); logger.debug(data);
createRegistrationTemplateMaster(data, csrfToken) createRegistrationSchoolFileMaster(data, csrfToken)
.then((data) => { .then((data) => {
// Transformer le nouveau fichier avec les informations du groupe // Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups); const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFiles) => [...prevFiles, transformedFile]); setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
setIsModalOpen(false); setIsModalOpen(false);
}) })
.catch((error) => { .catch((error) => {
@ -196,11 +213,11 @@ export default function FilesGroupsManagement({
}; };
logger.debug(data); logger.debug(data);
editRegistrationTemplateMaster(id, data, csrfToken) editRegistrationSchoolFileMaster(id, data, csrfToken)
.then((data) => { .then((data) => {
// Transformer le fichier mis à jour avec les informations du groupe // Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups); const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFichiers) => setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? transformedFile : f)) prevFichiers.map((f) => (f.id === id ? transformedFile : f))
); );
setIsModalOpen(false); setIsModalOpen(false);
@ -228,7 +245,13 @@ export default function FilesGroupsManagement({
alert("Erreur lors de l'opération sur le groupe"); alert("Erreur lors de l'opération sur le groupe");
}); });
} else { } 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) => { .then((newGroup) => {
setGroups([...groups, newGroup]); setGroups([...groups, newGroup]);
setIsGroupModalOpen(false); setIsGroupModalOpen(false);
@ -246,13 +269,13 @@ export default function FilesGroupsManagement({
}; };
const handleGroupDelete = (groupId) => { const handleGroupDelete = (groupId) => {
// Vérifier si des templateMasters utilisent ce groupe // Vérifier si des schoolFileMasters utilisent ce groupe
const filesInGroup = templateMasters.filter( const filesInGroup = schoolFileMasters.filter(
(file) => file.group && file.group.id === groupId (file) => file.group && file.group.id === groupId
); );
if (filesInGroup.length > 0) { if (filesInGroup.length > 0) {
alert( 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; 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; if (!selectedGroup) return true;
return ( return (
file.groups && file.groups &&
@ -288,9 +366,9 @@ export default function FilesGroupsManagement({
}); });
const columnsFiles = [ 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) => transform: (row) =>
row.groups && row.groups.length > 0 row.groups && row.groups.length > 0
? row.groups.map((group) => group.name).join(', ') ? row.groups.map((group) => group.name).join(', ')
@ -327,7 +405,7 @@ export default function FilesGroupsManagement({
]; ];
const columnsGroups = [ 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: 'Description', transform: (row) => row.description },
{ {
name: 'Actions', name: 'Actions',
@ -351,7 +429,8 @@ export default function FilesGroupsManagement({
]; ];
return ( return (
<div> <div className="w-full mx-auto mt-6">
{/* Modal pour les fichiers */}
<Modal <Modal
isOpen={isModalOpen} isOpen={isModalOpen}
setIsOpen={(isOpen) => { setIsOpen={(isOpen) => {
@ -362,7 +441,7 @@ export default function FilesGroupsManagement({
}} }}
title={isEditing ? 'Modification du document' : 'Ajouter un document'} title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => ( ContentComponent={() => (
<FileUpload <FileUploadDocuSeal
handleCreateTemplateMaster={handleCreateTemplateMaster} handleCreateTemplateMaster={handleCreateTemplateMaster}
handleEditTemplateMaster={handleEditTemplateMaster} handleEditTemplateMaster={handleEditTemplateMaster}
fileToEdit={fileToEdit} fileToEdit={fileToEdit}
@ -371,13 +450,15 @@ export default function FilesGroupsManagement({
)} )}
modalClassName="w-4/5 h-4/5" modalClassName="w-4/5 h-4/5"
/> />
{/* Modal pour les groupes */}
<Modal <Modal
isOpen={isGroupModalOpen} isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen} setIsOpen={setIsGroupModalOpen}
title={ title={
groupToEdit groupToEdit
? 'Modifier le groupe' ? 'Modifier le groupe'
: 'Ajouter un groupe de templateMasters' : 'Ajouter un groupe de schoolFileMasters'
} }
ContentComponent={() => ( ContentComponent={() => (
<RegistrationFileGroupForm <RegistrationFileGroupForm
@ -386,61 +467,45 @@ export default function FilesGroupsManagement({
/> />
)} )}
/> />
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4"> {/* Section Groupes de fichiers */}
<h2 className="text-xl font-bold">Groupes de fichiers</h2> <div className="mt-8 w-3/5">
<button <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)} 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> </div>
{groups.length > 0 && (
<div className="mt-8"> {/* Section Fichiers */}
<div className="flex justify-between items-center mb-4"> <div className="mt-12 mb-4 w-3/5">
<h2 className="text-xl font-bold">Fichiers</h2> <SectionHeader
<div className="flex items-center gap-4"> icon={Signature}
<select title="Formulaires à remplir"
className="border rounded p-2" description="Gérez les formulaires nécessitant une signature électronique."
value={selectedGroup || ''} button={true}
onChange={(e) => setSelectedGroup(e.target.value)} buttonOpeningModal={true}
>
<option value="">Tous les groupes</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
<button
onClick={() => { onClick={() => {
setIsModalOpen(true); setIsModalOpen(true);
setIsEditing(false); 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> </div>
)}
{/* Section Pièces à fournir */}
<ParentFilesSection
parentFiles={parentFiles}
setParentFileMasters={setParentFileMasters}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div> </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 React, { useState } from 'react';
import { import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
Plus,
Trash2,
Edit3,
Check,
X,
Percent,
EuroIcon,
Tag,
} from 'lucide-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox'; import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { ESTABLISHMENT_ID } from '@/utils/Url'; import { ESTABLISHMENT_ID } from '@/utils/Url';
import SectionHeader from '@/components/SectionHeader';
const DiscountsSection = ({ const DiscountsSection = ({
discounts, discounts,
@ -347,22 +339,15 @@ const DiscountsSection = ({
]; ];
return ( return (
<div className="space-y-4"> <div className="space-y-4 mt-8">
{!subscriptionMode && ( <SectionHeader
<div className="flex justify-between items-center"> icon={Tag}
<div className="flex items-center mb-4"> discountStyle={true}
<Tag className="w-6 h-6 text-emerald-500 mr-2" /> title={`${type == 0 ? "Liste des réductions sur les frais d'inscription" : 'Liste des réductions sur les frais de scolarité'}`}
<h2 className="text-xl font-semibold">Liste des réductions</h2> description={`${subscriptionMode ? 'Sélectionnez' : 'Gérez'} ${type == 0 ? " vos réductions sur les frais d'inscription" : ' vos réductions sur les frais de scolarité'}`}
</div> button={!subscriptionMode}
<button
type="button"
onClick={handleAddDiscount} onClick={handleAddDiscount}
className="text-emerald-500 hover:text-emerald-700" />
>
<Plus className="w-5 h-5" />
</button>
</div>
)}
<Table <Table
data={newDiscount ? [newDiscount, ...discounts] : discounts} data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={columns} 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 FeesSection from '@/components/Structure/Tarification/FeesSection';
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
import PaymentPlanSelector from '@/components/PaymentPlanSelector'; import PaymentPlanSelector from '@/components/PaymentPlanSelector';
@ -50,23 +50,22 @@ const FeesManagement = ({
}; };
return ( return (
<div className="w-full mx-auto p-2 mt-6 space-y-6"> <div className="w-full mx-auto mt-6">
<div className="bg-white p-2 rounded-lg shadow-md"> <div className="w-4/5 mx-auto flex items-center mt-8">
<h2 className="text-2xl font-semibold mb-4"> <hr className="flex-grow border-t-2 border-gray-300" />
Frais d&apos;inscription <span className="mx-4 text-gray-600 font-semibold">
</h2> Frais d'inscription
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> </span>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4"> <hr className="flex-grow border-t-2 border-gray-300" />
</div>
<div className="mt-8 w-4/5">
<FeesSection <FeesSection
fees={registrationFees} fees={registrationFees}
setFees={setRegistrationFees} setFees={setRegistrationFees}
discounts={registrationDiscounts} discounts={registrationDiscounts}
handleCreate={(newData) => handleCreate={(newData) =>
handleCreate( handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)
`${BE_SCHOOL_FEES_URL}`,
newData,
setRegistrationFees
)
} }
handleEdit={(id, updatedData) => handleEdit={(id, updatedData) =>
handleEdit( handleEdit(
@ -82,7 +81,7 @@ const FeesManagement = ({
type={0} type={0}
/> />
</div> </div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4"> <div className="mt-12 w-4/5">
<DiscountsSection <DiscountsSection
discounts={registrationDiscounts} discounts={registrationDiscounts}
setDiscounts={setRegistrationDiscounts} setDiscounts={setRegistrationDiscounts}
@ -112,7 +111,8 @@ const FeesManagement = ({
type={0} type={0}
/> />
</div> </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 <PaymentPlanSelector
paymentPlans={registrationPaymentPlans} paymentPlans={registrationPaymentPlans}
setPaymentPlans={setRegistrationPaymentPlans} setPaymentPlans={setRegistrationPaymentPlans}
@ -127,7 +127,7 @@ const FeesManagement = ({
type={0} type={0}
/> />
</div> </div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4"> <div className="col-span-1 mt-4">
<PaymentModeSelector <PaymentModeSelector
paymentModes={registrationPaymentModes} paymentModes={registrationPaymentModes}
setPaymentModes={setRegistrationPaymentModes} setPaymentModes={setRegistrationPaymentModes}
@ -143,11 +143,16 @@ const FeesManagement = ({
/> />
</div> </div>
</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>
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2> <div className="mt-8 w-4/5">
<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">
<FeesSection <FeesSection
fees={tuitionFees} fees={tuitionFees}
setFees={setTuitionFees} setFees={setTuitionFees}
@ -156,12 +161,7 @@ const FeesManagement = ({
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees) handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)
} }
handleEdit={(id, updatedData) => handleEdit={(id, updatedData) =>
handleEdit( handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)
`${BE_SCHOOL_FEES_URL}`,
id,
updatedData,
setTuitionFees
)
} }
handleDelete={(id) => handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees) handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)
@ -169,7 +169,7 @@ const FeesManagement = ({
type={1} type={1}
/> />
</div> </div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4"> <div className="mt-12 w-4/5">
<DiscountsSection <DiscountsSection
discounts={tuitionDiscounts} discounts={tuitionDiscounts}
setDiscounts={setTuitionDiscounts} setDiscounts={setTuitionDiscounts}
@ -189,17 +189,14 @@ const FeesManagement = ({
) )
} }
handleDelete={(id) => handleDelete={(id) =>
handleDelete( handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
setTuitionDiscounts
)
} }
onDiscountDelete={(id) => handleDiscountDelete(id, 1)} onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
type={1} type={1}
/> />
</div> </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 <PaymentPlanSelector
paymentPlans={tuitionPaymentPlans} paymentPlans={tuitionPaymentPlans}
setPaymentPlans={setTuitionPaymentPlans} setPaymentPlans={setTuitionPaymentPlans}
@ -214,7 +211,7 @@ const FeesManagement = ({
type={1} type={1}
/> />
</div> </div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4"> <div className="col-span-1 mt-4">
<PaymentModeSelector <PaymentModeSelector
paymentModes={tuitionPaymentModes} paymentModes={tuitionPaymentModes}
setPaymentModes={setTuitionPaymentModes} setPaymentModes={setTuitionPaymentModes}
@ -231,7 +228,6 @@ const FeesManagement = ({
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 = ({ const Table = ({
data, data,
@ -43,7 +43,16 @@ const Table = ({
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''} ${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''} ${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) => ( {columns.map((column, colIndex) => (
<td <td
@ -83,6 +92,10 @@ Table.propTypes = {
currentPage: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired, onPageChange: PropTypes.func.isRequired,
onRowClick: PropTypes.func,
selectedRows: PropTypes.arrayOf(PropTypes.any),
isSelectable: PropTypes.bool,
defaultTheme: PropTypes.string,
}; };
export default Table; 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_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_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_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_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`; export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
//GESTION ECOLE //GESTION ECOLE