mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
feat: Changement du rendu de la page des documents + gestion des
formulaires d'école déjà existants [N3WTS-17]
This commit is contained in:
@ -316,14 +316,31 @@ class RegistrationForm(models.Model):
|
||||
#############################################################
|
||||
|
||||
####### Formulaires masters (documents école, à signer ou pas) #######
|
||||
def registration_school_file_master_upload_to(instance, filename):
|
||||
# Stocke les fichiers masters dans un dossier dédié
|
||||
return f"registration_files/school_file_masters/{instance.pk}/{filename}"
|
||||
|
||||
class RegistrationSchoolFileMaster(models.Model):
|
||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||
name = models.CharField(max_length=255, default="")
|
||||
is_required = models.BooleanField(default=False)
|
||||
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||
# Nouveau champ pour formulaire existant (PDF, DOC, etc.)
|
||||
file = models.FileField(
|
||||
upload_to=registration_school_file_master_upload_to,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Fichier du formulaire existant (PDF, DOC, etc.)"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.group.name} - {self.id}'
|
||||
return f'{self.name} - {self.id}'
|
||||
|
||||
@property
|
||||
def file_url(self):
|
||||
if self.file and hasattr(self.file, 'url'):
|
||||
return self.file.url
|
||||
return None
|
||||
|
||||
####### Parent files masters (documents à fournir par les parents) #######
|
||||
class RegistrationParentFileMaster(models.Model):
|
||||
@ -337,10 +354,10 @@ class RegistrationParentFileMaster(models.Model):
|
||||
############################################################
|
||||
|
||||
def registration_school_file_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
|
||||
return f"registration_files/{instance.registration_form.establishment.name}/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}"
|
||||
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||
|
||||
####### Formulaires templates (par dossier d'inscription) #######
|
||||
class RegistrationSchoolFileTemplate(models.Model):
|
||||
|
||||
@ -25,8 +25,6 @@ from .views import (
|
||||
|
||||
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||
from .views import (
|
||||
registration_school_file_masters_views,
|
||||
registration_school_file_templates_views,
|
||||
get_school_file_templates_by_rf,
|
||||
get_parent_file_templates_by_rf
|
||||
)
|
||||
|
||||
@ -8,6 +8,9 @@ from N3wtSchool import renderers
|
||||
from N3wtSchool import bdd
|
||||
|
||||
from io import BytesIO
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files import File
|
||||
from pathlib import Path
|
||||
import os
|
||||
@ -100,8 +103,9 @@ def create_templates_for_registration_form(register_form):
|
||||
from Subscriptions.models import (
|
||||
RegistrationSchoolFileMaster,
|
||||
RegistrationSchoolFileTemplate,
|
||||
# RegistrationParentFileMaster,
|
||||
# RegistrationParentFileTemplate,
|
||||
RegistrationParentFileMaster,
|
||||
RegistrationParentFileTemplate,
|
||||
registration_school_file_upload_to,
|
||||
)
|
||||
|
||||
created = []
|
||||
@ -114,25 +118,26 @@ def create_templates_for_registration_form(register_form):
|
||||
for t in school_existing:
|
||||
try:
|
||||
if getattr(t, "file", None):
|
||||
logger.info("Deleted school template %s for RF %s", t.pk, register_form.pk)
|
||||
t.file.delete(save=False)
|
||||
except Exception:
|
||||
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
|
||||
t.delete()
|
||||
# parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
||||
# for t in parent_existing:
|
||||
# try:
|
||||
# if getattr(t, "file", None):
|
||||
# t.file.delete(save=False)
|
||||
# except Exception:
|
||||
# logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
||||
# t.delete()
|
||||
parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
||||
for t in parent_existing:
|
||||
try:
|
||||
if getattr(t, "file", None):
|
||||
t.file.delete(save=False)
|
||||
except Exception:
|
||||
logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
||||
t.delete()
|
||||
return created
|
||||
|
||||
school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct()
|
||||
# parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||
parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||
|
||||
school_master_ids = {m.pk for m in school_masters}
|
||||
#parent_master_ids = {m.pk for m in parent_masters}
|
||||
parent_master_ids = {m.pk for m in parent_masters}
|
||||
|
||||
# Supprimer les school templates obsolètes
|
||||
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
|
||||
@ -146,15 +151,15 @@ def create_templates_for_registration_form(register_form):
|
||||
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
|
||||
# Supprimer les parent templates obsolètes
|
||||
# for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
||||
# if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
||||
# try:
|
||||
# if getattr(tmpl, "file", None):
|
||||
# tmpl.file.delete(save=False)
|
||||
# except Exception:
|
||||
# logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
||||
# tmpl.delete()
|
||||
# logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
||||
if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
||||
try:
|
||||
if getattr(tmpl, "file", None):
|
||||
tmpl.file.delete(save=False)
|
||||
except Exception:
|
||||
logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
||||
tmpl.delete()
|
||||
logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
|
||||
# Créer les school templates manquants
|
||||
for m in school_masters:
|
||||
@ -163,28 +168,58 @@ def create_templates_for_registration_form(register_form):
|
||||
continue
|
||||
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||||
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||||
|
||||
# Si le master a un fichier uploadé (formulaire existant)
|
||||
file_to_attach = None
|
||||
if m.file:
|
||||
import os
|
||||
from django.core.files import File as DjangoFile
|
||||
master_file_path = m.file.path
|
||||
if os.path.exists(master_file_path):
|
||||
filename = os.path.basename(master_file_path)
|
||||
# Générer le chemin cible pour le template élève
|
||||
dest_path = registration_school_file_upload_to(None, filename)
|
||||
dest_dir = os.path.dirname(os.path.join(settings.MEDIA_ROOT, dest_path))
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
# Copier le fichier dans le dossier cible
|
||||
dest_full_path = os.path.join(settings.MEDIA_ROOT, dest_path)
|
||||
with open(master_file_path, 'rb') as src, open(dest_full_path, 'wb') as dst:
|
||||
dst.write(src.read())
|
||||
# Préparer le File Django à attacher au template
|
||||
with open(dest_full_path, 'rb') as f:
|
||||
file_to_attach = DjangoFile(f, name=dest_path)
|
||||
else:
|
||||
# Générer le PDF du template à partir du JSON du master
|
||||
try:
|
||||
pdf_file = generate_form_json_pdf(register_form, m.formMasterData)
|
||||
file_to_attach = pdf_file
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération du PDF pour le template: {e}")
|
||||
file_to_attach = None
|
||||
|
||||
tmpl = RegistrationSchoolFileTemplate.objects.create(
|
||||
master=m,
|
||||
registration_form=register_form,
|
||||
name=m.name or "",
|
||||
formTemplateData=m.formMasterData or [],
|
||||
slug=slug,
|
||||
file=file_to_attach,
|
||||
)
|
||||
created.append(tmpl)
|
||||
logger.info("Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
|
||||
# Créer les parent templates manquants
|
||||
# for m in parent_masters:
|
||||
# exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||
# if exists:
|
||||
# continue
|
||||
# tmpl = RegistrationParentFileTemplate.objects.create(
|
||||
# master=m,
|
||||
# registration_form=register_form,
|
||||
# file=None,
|
||||
# )
|
||||
# created.append(tmpl)
|
||||
# logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
for m in parent_masters:
|
||||
exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||
if exists:
|
||||
continue
|
||||
tmpl = RegistrationParentFileTemplate.objects.create(
|
||||
master=m,
|
||||
registration_form=register_form,
|
||||
file=None,
|
||||
)
|
||||
created.append(tmpl)
|
||||
logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
|
||||
return created
|
||||
|
||||
@ -377,4 +412,52 @@ def getHistoricalYears(count=5):
|
||||
historical_start_year = start_year - i
|
||||
historical_years.append(f"{historical_start_year}-{historical_start_year + 1}")
|
||||
|
||||
return historical_years
|
||||
return historical_years
|
||||
|
||||
def generate_form_json_pdf(register_form, form_json):
|
||||
"""
|
||||
Génère un PDF du rendu du formulaire dynamique à partir du JSON (formConfig)
|
||||
et l'associe au RegistrationSchoolFileTemplate.
|
||||
Le PDF contient le titre, les labels et types de champs.
|
||||
Retourne un ContentFile prêt à être utilisé dans un FileField, avec uniquement le nom du fichier.
|
||||
"""
|
||||
|
||||
# Récupérer le nom du formulaire
|
||||
form_name = (form_json.get("title") or "formulaire").strip().replace(" ", "_")
|
||||
filename = f"{form_name}.pdf"
|
||||
|
||||
# Générer le PDF
|
||||
buffer = BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=A4)
|
||||
y = 800
|
||||
|
||||
# Titre
|
||||
c.setFont("Helvetica-Bold", 20)
|
||||
c.drawString(100, y, form_json.get("title", "Formulaire"))
|
||||
y -= 40
|
||||
|
||||
# Champs
|
||||
c.setFont("Helvetica", 12)
|
||||
fields = form_json.get("fields", [])
|
||||
for field in fields:
|
||||
label = field.get("label", field.get("id", ""))
|
||||
ftype = field.get("type", "")
|
||||
c.drawString(100, y, f"{label} [{ftype}]")
|
||||
y -= 25
|
||||
if y < 100:
|
||||
c.showPage()
|
||||
y = 800
|
||||
|
||||
c.save()
|
||||
buffer.seek(0)
|
||||
pdf_content = buffer.read()
|
||||
|
||||
# Si un fichier existe déjà sur le template, le supprimer (optionnel, à adapter selon usage)
|
||||
if hasattr(register_form, "registration_file") and register_form.registration_file and register_form.registration_file.name:
|
||||
existing_file_path = os.path.join(settings.MEDIA_ROOT, register_form.registration_file.name.lstrip('/'))
|
||||
if os.path.exists(existing_file_path):
|
||||
os.remove(existing_file_path)
|
||||
register_form.registration_file.delete(save=False)
|
||||
|
||||
# Retourner le ContentFile avec uniquement le nom du fichier
|
||||
return ContentFile(pdf_content, name=os.path.basename(filename))
|
||||
|
||||
@ -10,14 +10,18 @@ from .register_form_views import (
|
||||
from .registration_school_file_masters_views import (
|
||||
RegistrationSchoolFileMasterView,
|
||||
RegistrationSchoolFileMasterSimpleView,
|
||||
RegistrationParentFileMasterView,
|
||||
RegistrationParentFileMasterSimpleView,
|
||||
RegistrationParentFileTemplateSimpleView,
|
||||
RegistrationParentFileTemplateView
|
||||
)
|
||||
from .registration_school_file_templates_views import (
|
||||
RegistrationSchoolFileTemplateView,
|
||||
RegistrationSchoolFileTemplateSimpleView,
|
||||
RegistrationSchoolFileTemplateSimpleView
|
||||
)
|
||||
from .registration_parent_file_masters_views import (
|
||||
RegistrationParentFileMasterView,
|
||||
RegistrationParentFileMasterSimpleView
|
||||
)
|
||||
from .registration_parent_file_templates_views import (
|
||||
RegistrationParentFileTemplateSimpleView,
|
||||
RegistrationParentFileTemplateView
|
||||
)
|
||||
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
||||
|
||||
@ -0,0 +1,272 @@
|
||||
from django.http.response import JsonResponse
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from Subscriptions.serializers import RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.models import (
|
||||
RegistrationForm,
|
||||
RegistrationParentFileMaster,
|
||||
RegistrationParentFileTemplate
|
||||
)
|
||||
from N3wtSchool import bdd
|
||||
import logging
|
||||
import Subscriptions.util as util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RegistrationParentFileMasterView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les fichiers parents liés à l'établissement
|
||||
templates = RegistrationParentFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
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):
|
||||
logger.info(f"raw request.data: {request.data}")
|
||||
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for serializer: {payload}")
|
||||
serializer = RegistrationParentFileMasterSerializer(data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
obj = serializer.save()
|
||||
|
||||
# Propager la création des templates côté serveur pour les RegistrationForm
|
||||
try:
|
||||
groups_qs = obj.groups.all()
|
||||
if groups_qs.exists():
|
||||
rfs = RegistrationForm.objects.filter(fileGroup__in=groups_qs).distinct()
|
||||
for rf in rfs:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf)
|
||||
except Exception as e:
|
||||
logger.exception("Error creating templates for RF %s from parent master %s: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while propagating templates after parent master creation %s", getattr(obj, 'pk', None))
|
||||
|
||||
return Response(RegistrationParentFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
logger.error(f"serializer errors: {serializer.errors}")
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationParentFileMasterSimpleView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||
@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):
|
||||
master = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||
if master is None:
|
||||
return JsonResponse({'erreur': "Le master de fichier parent n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# snapshot des groups avant update
|
||||
old_group_ids = set(master.groups.values_list('id', flat=True))
|
||||
|
||||
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for update serializer: {payload}")
|
||||
serializer = RegistrationParentFileMasterSerializer(master, data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
obj = serializer.save()
|
||||
|
||||
# groups après update
|
||||
new_group_ids = set(obj.groups.values_list('id', flat=True))
|
||||
|
||||
removed_group_ids = old_group_ids - new_group_ids
|
||||
added_group_ids = new_group_ids - old_group_ids
|
||||
|
||||
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||
if removed_group_ids:
|
||||
try:
|
||||
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||
for rf in rfs_removed:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf)
|
||||
except Exception as e:
|
||||
logger.exception("Error cleaning templates for RF %s after parent master %s group removal: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while processing RFs for removed groups after parent master update %s", getattr(obj, 'pk', None))
|
||||
|
||||
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||
if added_group_ids:
|
||||
try:
|
||||
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||
for rf in rfs_added:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf)
|
||||
except Exception as e:
|
||||
logger.exception("Error creating templates for RF %s after parent master %s group addition: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while processing RFs for added groups after parent master update %s", getattr(obj, 'pk', None))
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||
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 parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
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:
|
||||
# Suppression du fichier PDF associé avant suppression de l'objet
|
||||
if template.file and template.file.name:
|
||||
template.file.delete(save=False)
|
||||
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)
|
||||
@ -0,0 +1,111 @@
|
||||
from django.http.response import JsonResponse
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from Subscriptions.serializers import RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.models import (
|
||||
RegistrationParentFileTemplate
|
||||
)
|
||||
from N3wtSchool import bdd
|
||||
import logging
|
||||
import Subscriptions.util as util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RegistrationParentFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
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:
|
||||
# Suppression du fichier PDF associé
|
||||
if template.file and template.file.name:
|
||||
template.file.delete(save=False)
|
||||
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)
|
||||
@ -5,15 +5,12 @@ from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
import json
|
||||
from django.http import QueryDict
|
||||
|
||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer
|
||||
from Subscriptions.models import (
|
||||
RegistrationForm,
|
||||
RegistrationSchoolFileMaster,
|
||||
RegistrationParentFileMaster,
|
||||
RegistrationParentFileTemplate
|
||||
RegistrationSchoolFileTemplate
|
||||
)
|
||||
from N3wtSchool import bdd
|
||||
import logging
|
||||
@ -179,188 +176,13 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
def delete(self, request, id):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is not None:
|
||||
# Supprimer tous les templates liés et leurs fichiers PDF
|
||||
templates = RegistrationSchoolFileTemplate.objects.filter(master=master)
|
||||
for template in templates:
|
||||
if template.file and template.file.name:
|
||||
template.file.delete(save=False)
|
||||
template.delete()
|
||||
master.delete()
|
||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||
return JsonResponse({'message': 'La suppression du master de template et des fichiers associés a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le master de 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 pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les fichiers parents liés à l'établissement
|
||||
templates = RegistrationParentFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
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 parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Crée un nouveau template d'inscription",
|
||||
request_body=RegistrationParentFileTemplateSerializer,
|
||||
responses={
|
||||
201: RegistrationParentFileTemplateSerializer,
|
||||
400: "Données invalides"
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationParentFileTemplateSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère un template d'inscription spécifique",
|
||||
responses={
|
||||
200: RegistrationParentFileTemplateSerializer,
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def get(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationParentFileTemplateSerializer(template)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Met à jour un template d'inscription existant",
|
||||
request_body=RegistrationParentFileTemplateSerializer,
|
||||
responses={
|
||||
200: RegistrationParentFileTemplateSerializer,
|
||||
400: "Données invalides",
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprime un template d'inscription",
|
||||
responses={
|
||||
204: "Suppression réussie",
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is not None:
|
||||
template.delete()
|
||||
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@ -5,8 +5,7 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
import json
|
||||
from django.http import QueryDict
|
||||
import os
|
||||
|
||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
||||
@ -209,6 +208,17 @@ class RegistrationSchoolFileTemplateSimpleView(APIView):
|
||||
def delete(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
|
||||
if template is not None:
|
||||
# Suppression du fichier PDF associé
|
||||
if template.file and template.file.name:
|
||||
file_path = template.file.path
|
||||
template.file.delete(save=False)
|
||||
# Vérification post-suppression
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.info(f"Fichier supprimé manuellement: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression manuelle du fichier: {e}")
|
||||
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:
|
||||
@ -390,6 +400,17 @@ class RegistrationParentFileTemplateSimpleView(APIView):
|
||||
def delete(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is not None:
|
||||
# Suppression du fichier PDF associé
|
||||
if template.file and template.file.name:
|
||||
file_path = template.file.path
|
||||
template.file.delete(save=False)
|
||||
# Vérification post-suppression
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.info(f"Fichier supprimé manuellement: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression manuelle du fichier: {e}")
|
||||
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:
|
||||
|
||||
@ -34,9 +34,7 @@ import {
|
||||
import {
|
||||
fetchRegistrationFileGroups,
|
||||
fetchRegistrationSchoolFileMasters,
|
||||
fetchRegistrationParentFileMasters,
|
||||
createRegistrationSchoolFileTemplate,
|
||||
createRegistrationParentFileTemplate,
|
||||
fetchRegistrationParentFileMasters
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
|
||||
@ -100,12 +100,13 @@ export async function createRegistrationFileGroup(groupData, csrfToken) {
|
||||
}
|
||||
|
||||
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
||||
// Toujours FormData, jamais JSON
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
// Pas de Content-Type, le navigateur gère pour FormData
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
85
Front-End/src/components/SectionHeaderDocument.js
Normal file
85
Front-End/src/components/SectionHeaderDocument.js
Normal file
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
// Thèmes couleur selon le type
|
||||
const THEME = {
|
||||
groupe: {
|
||||
bg: 'bg-blue-50',
|
||||
border: 'border-blue-200',
|
||||
iconBg: 'bg-blue-100',
|
||||
icon: 'text-blue-600',
|
||||
title: 'text-blue-800',
|
||||
desc: 'text-blue-600',
|
||||
button: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||
buttonText: 'text-blue-700',
|
||||
buttonHover: 'hover:bg-blue-100',
|
||||
},
|
||||
formulaire: {
|
||||
bg: 'bg-emerald-50',
|
||||
border: 'border-emerald-200',
|
||||
iconBg: 'bg-emerald-100',
|
||||
icon: 'text-emerald-600',
|
||||
title: 'text-emerald-800',
|
||||
desc: 'text-emerald-600',
|
||||
button: 'bg-emerald-500 text-white hover:bg-emerald-600',
|
||||
buttonText: 'text-emerald-700',
|
||||
buttonHover: 'hover:bg-emerald-100',
|
||||
},
|
||||
parent: {
|
||||
bg: 'bg-orange-50',
|
||||
border: 'border-orange-200',
|
||||
iconBg: 'bg-orange-100',
|
||||
icon: 'text-orange-500',
|
||||
title: 'text-orange-700',
|
||||
desc: 'text-orange-600',
|
||||
button: 'bg-orange-500 text-white hover:bg-orange-600',
|
||||
buttonText: 'text-orange-700',
|
||||
buttonHover: 'hover:bg-orange-100',
|
||||
},
|
||||
};
|
||||
|
||||
const SectionHeaderDocument = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
button = false,
|
||||
buttonOpeningModal = false,
|
||||
onClick = null,
|
||||
className = '',
|
||||
type = 'groupe', // 'groupe', 'formulaire', 'parent'
|
||||
}) => {
|
||||
const theme = THEME[type] || THEME.groupe;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-between border-b ${theme.border} ${theme.bg} px-2 py-3 mb-4 ${className}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
{Icon && (
|
||||
<span className={`${theme.iconBg} p-2 rounded-md flex items-center justify-center`}>
|
||||
<Icon className={`w-6 h-6 ${theme.icon}`} />
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<h2 className={`text-lg font-semibold ${theme.title}`}>{title}</h2>
|
||||
{description && (
|
||||
<p className={`text-xs ${theme.desc}`}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{button && onClick && (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={
|
||||
buttonOpeningModal
|
||||
? `flex items-center ${theme.button} px-3 py-1 rounded-md shadow transition`
|
||||
: `flex items-center ${theme.buttonText} ${theme.buttonHover} px-2 py-1 rounded-md`
|
||||
}
|
||||
>
|
||||
<Plus className="w-5 h-5 mr-1" />
|
||||
<span className="text-sm font-medium">Ajouter</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeaderDocument;
|
||||
209
Front-End/src/components/Structure/Files/CreateDocumentModal.js
Normal file
209
Front-End/src/components/Structure/Files/CreateDocumentModal.js
Normal file
@ -0,0 +1,209 @@
|
||||
import React, { useState } from 'react';
|
||||
import Modal from '@/components/Modal';
|
||||
import { FolderPlus, FileText, FilePlus2, ArrowLeft, Settings2, Upload as UploadIcon } from 'lucide-react';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
|
||||
export default function CreateDocumentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateGroup,
|
||||
onCreateParentFile,
|
||||
onCreateSchoolFileMaster,
|
||||
groups = [],
|
||||
}) {
|
||||
const [step, setStep] = useState('main'); // main | choose_form | form_builder | file_upload
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [selectedGroupsFileUpload, setSelectedGroupsFileUpload] = useState([]);
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setStep('main');
|
||||
setFileName('');
|
||||
setSelectedGroupsFileUpload([]);
|
||||
setUploadedFile(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Handler pour chaque type
|
||||
const handleSelect = (type) => {
|
||||
if (type === 'groupe') {
|
||||
setStep('main');
|
||||
onCreateGroup();
|
||||
onClose();
|
||||
}
|
||||
if (type === 'formulaire') {
|
||||
setStep('choose_form');
|
||||
}
|
||||
if (type === 'parent') {
|
||||
setStep('main');
|
||||
onCreateParentFile();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Retour au menu principal
|
||||
const handleBack = () => setStep('main');
|
||||
|
||||
// Submit pour formulaire existant
|
||||
const handleFileUploadSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile) return;
|
||||
onCreateSchoolFileMaster({
|
||||
name: fileName,
|
||||
group_ids: selectedGroupsFileUpload,
|
||||
file: uploadedFile,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={onClose}
|
||||
title="Créer un document"
|
||||
modalClassName="w-full max-w-md"
|
||||
>
|
||||
{step === 'main' && (
|
||||
<div className="flex flex-col gap-6 py-4">
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-blue-50 hover:bg-blue-100 border border-blue-200 transition"
|
||||
onClick={() => handleSelect('groupe')}
|
||||
>
|
||||
<FolderPlus className="w-6 h-6 text-blue-600" />
|
||||
<span className="font-semibold text-blue-800">Dossier d&aposinscription</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-50 hover:bg-emerald-100 border border-emerald-200 transition"
|
||||
onClick={() => handleSelect('formulaire')}
|
||||
>
|
||||
<FileText className="w-6 h-6 text-emerald-600" />
|
||||
<span className="font-semibold text-emerald-800">Formulaire scolaire</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-orange-50 hover:bg-orange-100 border border-orange-200 transition"
|
||||
onClick={() => handleSelect('parent')}
|
||||
>
|
||||
<FilePlus2 className="w-6 h-6 text-orange-500" />
|
||||
<span className="font-semibold text-orange-700">Pièce à fournir</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{step === 'choose_form' && (
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-100 hover:bg-emerald-200 border border-emerald-300 transition"
|
||||
onClick={() => setStep('form_builder')}
|
||||
>
|
||||
<Settings2 className="w-6 h-6 text-emerald-700" />
|
||||
<span className="font-semibold text-emerald-900">Formulaire personnalisé</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-gray-100 hover:bg-gray-200 border border-gray-300 transition"
|
||||
onClick={() => setStep('file_upload')}
|
||||
>
|
||||
<UploadIcon className="w-6 h-6 text-gray-700" />
|
||||
<span className="font-semibold text-gray-900">Importer un formulaire existant</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mt-2"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Retour</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{step === 'form_builder' && (
|
||||
<div>
|
||||
<button
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Retour</span>
|
||||
</button>
|
||||
<FormTemplateBuilder
|
||||
onSave={(data) => {
|
||||
onCreateSchoolFileMaster(data);
|
||||
onClose();
|
||||
}}
|
||||
groups={groups}
|
||||
isEditing={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step === 'file_upload' && (
|
||||
<div>
|
||||
<button
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Retour</span>
|
||||
</button>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleFileUploadSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2"
|
||||
placeholder="Nom du formulaire"
|
||||
value={fileName}
|
||||
onChange={e => setFileName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{/* Sélecteur de groupes à cocher */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Groupes d'inscription <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||
{groups && groups.length > 0 ? (
|
||||
groups.map((group) => (
|
||||
<label key={group.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedGroupsFileUpload.includes(group.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedGroupsFileUpload([
|
||||
...selectedGroupsFileUpload,
|
||||
group.id,
|
||||
]);
|
||||
} else {
|
||||
setSelectedGroupsFileUpload(
|
||||
selectedGroupsFileUpload.filter((id) => id !== group.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mr-2 text-blue-600"
|
||||
/>
|
||||
<span className="text-sm">{group.name}</span>
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">
|
||||
Aucun groupe disponible
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FileUpload
|
||||
selectionMessage="Sélectionnez le fichier du formulaire"
|
||||
onFileSelect={setUploadedFile}
|
||||
required
|
||||
enable
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-emerald-600 text-white px-4 py-2 rounded font-bold mt-2"
|
||||
disabled={!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile}
|
||||
>
|
||||
Créer le formulaire
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -10,8 +10,8 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
export default function FileUploadDocuSeal({
|
||||
handleCreateTemplateMaster,
|
||||
handleEditTemplateMaster,
|
||||
handleCreateSchoolFileMaster,
|
||||
handleEditSchoolFileMaster,
|
||||
fileToEdit = null,
|
||||
onSuccess,
|
||||
}) {
|
||||
@ -75,7 +75,7 @@ export default function FileUploadDocuSeal({
|
||||
const is_required = data.fields.length > 0;
|
||||
if (fileToEdit) {
|
||||
logger.debug('Modification du template master:', templateMaster?.id);
|
||||
handleEditTemplateMaster({
|
||||
handleEditSchoolFileMaster({
|
||||
name: uploadedFileName,
|
||||
group_ids: selectedGroups.map((group) => group.id),
|
||||
id: templateMaster?.id,
|
||||
@ -83,7 +83,7 @@ export default function FileUploadDocuSeal({
|
||||
});
|
||||
} else {
|
||||
logger.debug('Création du template master:', templateMaster?.id);
|
||||
handleCreateTemplateMaster({
|
||||
handleCreateSchoolFileMaster({
|
||||
name: uploadedFileName,
|
||||
group_ids: selectedGroups.map((group) => group.id),
|
||||
id: templateMaster?.id,
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Download,
|
||||
Edit3,
|
||||
Trash2,
|
||||
FolderPlus,
|
||||
FileText,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import Table from '@/components/Table';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import {
|
||||
// GET
|
||||
fetchRegistrationFileGroups,
|
||||
@ -31,13 +25,110 @@ import {
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
|
||||
import logger from '@/utils/logger';
|
||||
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import ParentFiles from './ParentFiles';
|
||||
import Popup from '@/components/Popup';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
|
||||
import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
|
||||
function getItemBgColor(type, selected, forceTheme = false) {
|
||||
// Colonne gauche : bleu, sélectionné plus soutenu
|
||||
if (type === 'blue') {
|
||||
if (selected) return 'bg-blue-200';
|
||||
return 'bg-blue-50';
|
||||
}
|
||||
// Colonne droite : thème selon type, jamais sélectionné
|
||||
if (forceTheme) {
|
||||
if (type === 'emerald') return 'bg-emerald-50';
|
||||
if (type === 'orange') return 'bg-orange-50';
|
||||
return 'bg-gray-50';
|
||||
}
|
||||
return 'bg-white';
|
||||
}
|
||||
|
||||
function SimpleList({
|
||||
items,
|
||||
onSelect,
|
||||
selectedId,
|
||||
actionButtons,
|
||||
getItemType,
|
||||
title,
|
||||
minHeight = 'min-h-[200px]',
|
||||
selectable = true,
|
||||
forceTheme = false,
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded border border-gray-200 bg-white ${minHeight} flex flex-col`}>
|
||||
{title && (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3
|
||||
font-bold text-base
|
||||
border-b border-gray-300
|
||||
bg-gradient-to-r
|
||||
${title === "Dossiers d'inscription"
|
||||
? 'from-blue-100 to-blue-50 text-blue-800'
|
||||
: title === 'Documents'
|
||||
? 'from-emerald-100 to-orange-50 text-emerald-800'
|
||||
: 'from-gray-100 to-white text-gray-800'}
|
||||
rounded-t
|
||||
shadow-sm
|
||||
tracking-wide
|
||||
uppercase
|
||||
flex items-center
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<ul className="flex-1">
|
||||
{items.length === 0 ? (
|
||||
<li className="py-4 text-center text-gray-400">Aucun élément</li>
|
||||
) : (
|
||||
items.map((item, idx) => {
|
||||
// Correction : clé unique et stable même si plusieurs items ont le même id
|
||||
// On concatène l'id et l'index pour garantir l'unicité
|
||||
const key = `${item.id}-${idx}`;
|
||||
const selected = selectedId === item.id;
|
||||
const itemType = getItemType ? getItemType(item) : 'gray';
|
||||
const bgColor = getItemBgColor(itemType, selected, forceTheme);
|
||||
const zIndex =
|
||||
selectable && selected
|
||||
? 'z-10 relative'
|
||||
: selectable
|
||||
? 'z-0 relative'
|
||||
: '';
|
||||
const marginFix =
|
||||
selectable && idx !== items.length - 1
|
||||
? '-mb-[1px]'
|
||||
: '';
|
||||
return (
|
||||
<li
|
||||
key={key}
|
||||
className={`flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-blue-400' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix}`}
|
||||
onClick={() => {
|
||||
if (!selectable || !onSelect) return;
|
||||
if (selected) {
|
||||
onSelect(null);
|
||||
} else {
|
||||
onSelect(item.id);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
aria-selected={selected}
|
||||
role="option"
|
||||
>
|
||||
<span className="font-medium text-gray-800">{item.name}</span>
|
||||
{actionButtons && actionButtons(item)}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FilesGroupsManagement({
|
||||
csrfToken,
|
||||
@ -57,22 +148,34 @@ export default function FilesGroupsManagement({
|
||||
const [removePopupMessage, setRemovePopupMessage] = useState('');
|
||||
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [createModalKey, setCreateModalKey] = useState(0);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const { showNotification } = useNotification();
|
||||
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
||||
const [editingParentFile, setEditingParentFile] = useState(null);
|
||||
const [isFileUploadModalOpen, setIsFileUploadModalOpen] = useState(false);
|
||||
|
||||
const transformFileData = (file, groups) => {
|
||||
const groupInfos = file.groups.map(
|
||||
(groupId) =>
|
||||
groups.find((g) => g.id === groupId) || {
|
||||
id: groupId,
|
||||
name: 'Groupe inconnu',
|
||||
}
|
||||
);
|
||||
// file.groups peut contenir des IDs (number/string) ou des objets {id, name}
|
||||
const groupInfos = (file.groups || []).map((group) => {
|
||||
if (typeof group === 'object' && group !== null && 'id' in group) {
|
||||
// Déjà un objet groupe
|
||||
const found = groups.find((g) => g.id === group.id);
|
||||
return found || group;
|
||||
} else {
|
||||
// C'est un ID
|
||||
return groups.find((g) => g.id === group) || { id: group, name: 'Groupe inconnu' };
|
||||
}
|
||||
});
|
||||
return {
|
||||
...file,
|
||||
groups: groupInfos,
|
||||
};
|
||||
};
|
||||
|
||||
// Ne pas transformer ici, stocker la donnée brute
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
Promise.all([
|
||||
@ -83,11 +186,7 @@ export default function FilesGroupsManagement({
|
||||
.then(([dataSchoolFileMasters, groupsData, dataParentFileMasters]) => {
|
||||
setGroups(groupsData);
|
||||
setParentFileMasters(dataParentFileMasters);
|
||||
// Transformer chaque fichier pour inclure les informations complètes du groupe
|
||||
const transformedFiles = dataSchoolFileMasters.map((file) =>
|
||||
transformFileData(file, groupsData)
|
||||
);
|
||||
setSchoolFileMasters(transformedFiles);
|
||||
setSchoolFileMasters(dataSchoolFileMasters); // donnée brute
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.debug(err.message);
|
||||
@ -148,19 +247,22 @@ export default function FilesGroupsManagement({
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateTemplateMaster = ({ name, group_ids, formMasterData }) => {
|
||||
const data = {
|
||||
name: name,
|
||||
const handleCreateSchoolFileMaster = ({ name, group_ids, formMasterData, file }) => {
|
||||
// Toujours envoyer en FormData, même sans fichier
|
||||
const dataToSend = new FormData();
|
||||
const jsonData = {
|
||||
name,
|
||||
groups: group_ids,
|
||||
formMasterData: formMasterData, // Envoyer directement l'objet
|
||||
formMasterData,
|
||||
};
|
||||
logger.debug(data);
|
||||
dataToSend.append('data', JSON.stringify(jsonData));
|
||||
if (file) {
|
||||
dataToSend.append('file', file, file.path || file.name);
|
||||
}
|
||||
|
||||
createRegistrationSchoolFileMaster(data, csrfToken)
|
||||
createRegistrationSchoolFileMaster(dataToSend, csrfToken)
|
||||
.then((data) => {
|
||||
// Transformer le nouveau fichier avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
|
||||
setSchoolFileMasters((prevFiles) => [...prevFiles, data]);
|
||||
setIsModalOpen(false);
|
||||
showNotification(
|
||||
`Le formulaire "${name}" a été créé avec succès.`,
|
||||
@ -178,7 +280,7 @@ export default function FilesGroupsManagement({
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditTemplateMaster = ({
|
||||
const handleEditSchoolFileMaster = ({
|
||||
name,
|
||||
group_ids,
|
||||
formMasterData,
|
||||
@ -193,10 +295,8 @@ export default function FilesGroupsManagement({
|
||||
|
||||
editRegistrationSchoolFileMaster(id, data, csrfToken)
|
||||
.then((data) => {
|
||||
// Transformer le fichier mis à jour avec les informations du groupe
|
||||
const transformedFile = transformFileData(data, groups);
|
||||
setSchoolFileMasters((prevFichiers) =>
|
||||
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
|
||||
prevFichiers.map((f) => (f.id === id ? data : f))
|
||||
);
|
||||
setIsModalOpen(false);
|
||||
showNotification(
|
||||
@ -292,6 +392,10 @@ export default function FilesGroupsManagement({
|
||||
throw new Error('Erreur lors de la suppression du groupe.');
|
||||
}
|
||||
setGroups(groups.filter((group) => group.id !== groupId));
|
||||
// Si le groupe supprimé était sélectionné, on désélectionne
|
||||
if (String(selectedGroupId) === String(groupId)) {
|
||||
setSelectedGroupId(null);
|
||||
}
|
||||
setRemovePopupVisible(false);
|
||||
setIsLoading(false);
|
||||
showNotification('Groupe supprimé avec succès.', 'success', 'Succès');
|
||||
@ -331,15 +435,22 @@ export default function FilesGroupsManagement({
|
||||
};
|
||||
|
||||
const handleEdit = (id, updatedFile) => {
|
||||
logger.debug('[FilesGroupsManagement] handleEdit called with:', id, updatedFile);
|
||||
// Correction : vérifier si updatedFile est bien un objet (et pas juste un id)
|
||||
if (typeof updatedFile !== 'object' || updatedFile === null) {
|
||||
logger.error('[FilesGroupsManagement] handleEdit: updatedFile is not an object', updatedFile);
|
||||
return Promise.reject(new Error('updatedFile is not an object'));
|
||||
}
|
||||
logger.debug('[FilesGroupsManagement] handleEdit payload:', JSON.stringify(updatedFile));
|
||||
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('[FilesGroupsManagement] editRegistrationParentFileMaster response:', 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
|
||||
return modifiedFile;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
@ -369,77 +480,321 @@ export default function FilesGroupsManagement({
|
||||
});
|
||||
};
|
||||
|
||||
const filteredFiles = schoolFileMasters.filter((file) => {
|
||||
if (!selectedGroup) return true;
|
||||
// Ouvre la modale de création d'une pièce à fournir
|
||||
const openParentFileModal = () => {
|
||||
setEditingParentFile(null);
|
||||
setIsParentFileModalOpen(true);
|
||||
};
|
||||
|
||||
// Ouvre la modale d'édition d'une pièce à fournir
|
||||
const openEditParentFileModal = (file) => {
|
||||
setEditingParentFile(file);
|
||||
setIsParentFileModalOpen(true);
|
||||
};
|
||||
|
||||
// Ferme la modale de pièce à fournir
|
||||
const closeParentFileModal = () => {
|
||||
setEditingParentFile(null);
|
||||
setIsParentFileModalOpen(false);
|
||||
};
|
||||
|
||||
// Ouvre le menu de choix pour formulaire d'école
|
||||
const handleCreateFormMenu = () => {
|
||||
setIsCreateModalOpen(false);
|
||||
setTimeout(() => setIsCreateModalOpen(true), 0); // force le reset du menu
|
||||
};
|
||||
|
||||
// Ouvre la modale FormTemplateBuilder
|
||||
const handleOpenFormTemplateBuilder = () => {
|
||||
setIsModalOpen(true);
|
||||
setIsEditing(false);
|
||||
setFileToEdit(null);
|
||||
};
|
||||
|
||||
// Ouvre la modale FileUpload
|
||||
const handleOpenFileUpload = () => {
|
||||
setIsFileUploadModalOpen(true);
|
||||
};
|
||||
|
||||
// Ferme la modale FileUpload
|
||||
const handleCloseFileUpload = () => {
|
||||
setIsFileUploadModalOpen(false);
|
||||
};
|
||||
|
||||
// Soumission du formulaire existant (upload)
|
||||
const handleSubmitFileUpload = ({ name, group_ids, file }) => {
|
||||
// On centralise la logique dans handleCreateSchoolFileMaster
|
||||
handleCreateSchoolFileMaster({ name, group_ids, file });
|
||||
setIsFileUploadModalOpen(false);
|
||||
};
|
||||
|
||||
// Filtrage des formulaires et pièces selon le dossier sélectionné
|
||||
// Appliquer la transformation à la volée ici et comparer en string
|
||||
const filteredFiles = schoolFileMasters
|
||||
.map((file) => transformFileData(file, groups))
|
||||
.filter((file) => {
|
||||
if (!selectedGroupId) return false;
|
||||
return (
|
||||
file.groups &&
|
||||
file.groups.some((group) => String(group.id) === String(selectedGroupId))
|
||||
);
|
||||
});
|
||||
|
||||
const filteredParentFiles = parentFiles.filter((file) => {
|
||||
if (!selectedGroupId) return false;
|
||||
return (
|
||||
file.groups &&
|
||||
file.groups.some((group) => group.id === parseInt(selectedGroup))
|
||||
file.groups.map(String).includes(String(selectedGroupId))
|
||||
);
|
||||
});
|
||||
|
||||
const columnsFiles = [
|
||||
{ name: 'Nom du formulaire', transform: (row) => row.name },
|
||||
{
|
||||
name: "Dossiers d'inscription",
|
||||
transform: (row) =>
|
||||
row.groups && row.groups.length > 0
|
||||
? row.groups.map((group) => group.name).join(', ')
|
||||
: 'Aucun',
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => editTemplateMaster(row)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
title="Modifier le formulaire"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteTemplateMaster(row)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
title="Supprimer le formulaire"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// Fusion des deux types de documents pour la colonne de droite
|
||||
const mergedDocuments = [
|
||||
...filteredFiles.map((doc) => ({ ...doc, _type: 'emerald' })),
|
||||
...filteredParentFiles.map((doc) => ({ ...doc, _type: 'orange' })),
|
||||
];
|
||||
|
||||
const columnsGroups = [
|
||||
{ name: 'Nom du dossier', 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"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGroupDelete(row.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
// Nouvelle explication : adaptée au contexte "dossier lié à une classe/groupe de classes"
|
||||
const renderExplanation = () => (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex items-center gap-2 text-emerald-700 hover:text-emerald-900 font-medium mb-2"
|
||||
onClick={() => setShowHelp((v) => !v)}
|
||||
aria-expanded={showHelp}
|
||||
aria-controls="aide-inscription"
|
||||
>
|
||||
<span className="underline">{showHelp ? 'Masquer' : 'Afficher'} l’aide</span>
|
||||
<svg className={`w-4 h-4 transition-transform ${showHelp ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{showHelp && (
|
||||
<div id="aide-inscription" className="p-4 bg-blue-50 border border-blue-200 rounded mb-4">
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Gestion des dossiers et documents d'inscription
|
||||
</h2>
|
||||
<div className="text-gray-700 space-y-2">
|
||||
<p>
|
||||
<span className="font-semibold">1. Créez un ou plusieurs <span className="text-blue-700">dossiers d&aposinscription</span></span> :<br />
|
||||
Chaque dossier correspond à une classe ou un groupe de classes (ex : <span className="italic">Dossier Maternelles</span>, <span className="italic">Dossier Élémentaires</span>). Lors de la création d'une inscription élève, un seul dossier d'inscription sera rattaché à l'élève.
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">2. Pour chaque dossier, ajoutez des documents à fournir :</span>
|
||||
</p>
|
||||
<ul className="list-disc list-inside ml-6">
|
||||
<li>
|
||||
<span className="text-emerald-700 font-semibold">Formulaires personnalisés</span> : créés dynamiquement par l&aposécole, à remplir et/ou signer électroniquement par la famille (ex : autorisation de sortie, fiche sanitaire, etc.).
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-orange-700 font-semibold">Pièces à fournir</span> : documents à déposer par la famille (ex : RIB, justificatif de domicile, ou formulaire PDF à télécharger, remplir puis ré-uploader).
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
<span className="font-semibold">Astuce :</span> Commencez toujours par créer vos dossiers d&aposinscription (liés à vos classes) avant d&aposajouter des documents à fournir.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
// Correction : définition de handleBackToCreateMenu
|
||||
const handleBackToCreateMenu = () => {
|
||||
setCreateModalKey((k) => k + 1); // force le reset du composant modal
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
// Nouvelle disposition : sections côte à côte alignées
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Modal pour les formulaires */}
|
||||
{/* Aide optionnelle + bouton global de création */}
|
||||
<div className="mb-8">
|
||||
{renderExplanation()}
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="flex items-center justify-center gap-3 bg-emerald-500 hover:bg-emerald-600 text-white px-8 py-4 rounded-xl shadow transition text-lg font-bold"
|
||||
style={{ minWidth: '320px', maxWidth: '400px' }}
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
>
|
||||
<span className="text-2xl font-bold">+</span>
|
||||
<span>Créer un nouvel élément</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
{/* Colonne gauche : Dossiers d'inscription */}
|
||||
<div className="w-[30%] min-w-[260px]">
|
||||
<SimpleList
|
||||
items={groups}
|
||||
selectedId={selectedGroupId}
|
||||
onSelect={setSelectedGroupId}
|
||||
getItemType={() => 'blue'}
|
||||
title="Dossiers d'inscription"
|
||||
minHeight="min-h-[240px]"
|
||||
selectable={true}
|
||||
forceTheme={false}
|
||||
actionButtons={(row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleGroupEdit(row); }}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleGroupDelete(row.id); }}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Colonne droite : Documents (fusion des formulaires et pièces à fournir) */}
|
||||
<div className="w-[70%] min-w-[320px]">
|
||||
<div className="rounded border border-gray-200 bg-white min-h-[240px] flex flex-col">
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3
|
||||
font-bold text-base
|
||||
border-b border-gray-300
|
||||
bg-gradient-to-r
|
||||
from-emerald-100 to-orange-50 text-emerald-800
|
||||
rounded-t
|
||||
shadow-sm
|
||||
tracking-wide
|
||||
uppercase
|
||||
flex items-center
|
||||
`}
|
||||
>
|
||||
Documents
|
||||
</div>
|
||||
{selectedGroupId === null ? (
|
||||
<div className="flex items-center justify-center flex-1 bg-white text-gray-400 text-center text-base">
|
||||
Sélectionnez un dossier pour voir les documents associés.
|
||||
</div>
|
||||
) : (
|
||||
<SimpleList
|
||||
key={selectedGroupId}
|
||||
items={mergedDocuments}
|
||||
selectedId={null}
|
||||
onSelect={null}
|
||||
getItemType={(item) => item._type}
|
||||
// title="Documents" // Supprimé ici, header déjà affiché au-dessus
|
||||
minHeight="min-h-[240px]"
|
||||
selectable={false}
|
||||
forceTheme={true}
|
||||
actionButtons={(row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{row._type === 'emerald' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); editTemplateMaster(row); }}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); deleteTemplateMaster(row); }}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); openEditParentFileModal(row); }}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(row.id); }}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Modal de création centralisée */}
|
||||
<CreateDocumentModal
|
||||
key={createModalKey}
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onCreateGroup={() => {
|
||||
setIsGroupModalOpen(true);
|
||||
setIsCreateModalOpen(false);
|
||||
}}
|
||||
onCreateForm={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
setTimeout(() => setIsModalOpen(true), 0);
|
||||
}}
|
||||
onCreateParentFile={() => {
|
||||
openParentFileModal();
|
||||
setIsCreateModalOpen(false);
|
||||
}}
|
||||
onBack={handleBackToCreateMenu}
|
||||
onCreateSchoolFileMaster={handleCreateSchoolFileMaster}
|
||||
groups={groups}
|
||||
/>
|
||||
|
||||
{/* Modal pour upload de formulaire existant */}
|
||||
<Modal
|
||||
isOpen={isFileUploadModalOpen}
|
||||
setIsOpen={handleCloseFileUpload}
|
||||
title="Importer un formulaire existant"
|
||||
modalClassName="w-full max-w-md"
|
||||
>
|
||||
<FileUpload
|
||||
selectionMessage="Sélectionnez le fichier du formulaire"
|
||||
onFileSelect={(file) => {
|
||||
// Ici, il faut gérer le nom et les groupes (à adapter selon vos besoins UI)
|
||||
// Par exemple, ouvrir un petit formulaire pour compléter les infos puis submit
|
||||
}}
|
||||
onSubmit={handleSubmitFileUpload}
|
||||
required
|
||||
enable
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Modal création/édition pièce à fournir */}
|
||||
<Modal
|
||||
isOpen={isParentFileModalOpen}
|
||||
setIsOpen={(open) => {
|
||||
if (!open) closeParentFileModal();
|
||||
}}
|
||||
title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
|
||||
modalClassName="w-full max-w-md"
|
||||
>
|
||||
<ParentFiles
|
||||
parentFiles={parentFiles}
|
||||
groups={groups}
|
||||
handleCreate={handleCreate}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
singleForm // affiche uniquement le formulaire, pas la liste
|
||||
initialData={editingParentFile}
|
||||
onCancel={closeParentFileModal}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Modals et popups */}
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
setIsOpen={(isOpen) => {
|
||||
@ -447,100 +802,51 @@ export default function FilesGroupsManagement({
|
||||
if (!isOpen) {
|
||||
setFileToEdit(null);
|
||||
setIsEditing(false);
|
||||
// Retour au menu principal de création si fermeture
|
||||
handleBackToCreateMenu();
|
||||
}
|
||||
}}
|
||||
title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'}
|
||||
modalClassName="w-11/12 h-5/6"
|
||||
>
|
||||
<FormTemplateBuilder
|
||||
onSave={
|
||||
isEditing ? handleEditTemplateMaster : handleCreateTemplateMaster
|
||||
}
|
||||
onSave={(data) => {
|
||||
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
|
||||
}}
|
||||
initialData={fileToEdit}
|
||||
groups={groups}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Modal pour les groupes */}
|
||||
<Modal
|
||||
isOpen={isGroupModalOpen}
|
||||
setIsOpen={setIsGroupModalOpen}
|
||||
setIsOpen={(isOpen) => {
|
||||
setIsGroupModalOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
// Retour au menu principal de création si fermeture
|
||||
handleBackToCreateMenu();
|
||||
}
|
||||
}}
|
||||
title={
|
||||
groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier"
|
||||
}
|
||||
>
|
||||
<RegistrationFileGroupForm
|
||||
onSubmit={handleGroupSubmit}
|
||||
onSubmit={(data) => {
|
||||
handleGroupSubmit(data);
|
||||
}}
|
||||
initialData={groupToEdit}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Section Groupes de fichiers */}
|
||||
<div className="mt-8 w-3/5">
|
||||
<SectionHeader
|
||||
icon={FolderPlus}
|
||||
title="Dossiers d'inscriptions"
|
||||
description="Gérez les dossiers d'inscription pour organiser vos documents."
|
||||
button={true}
|
||||
buttonOpeningModal={true}
|
||||
onClick={() => setIsGroupModalOpen(true)}
|
||||
/>
|
||||
<Table
|
||||
data={groups}
|
||||
columns={columnsGroups}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
title="Aucun dossier d'inscription enregistré"
|
||||
message="Veuillez procéder à la création d'un nouveau dossier d'inscription"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Section Formulaires */}
|
||||
<div className="mt-12 mb-4 w-3/5">
|
||||
<SectionHeader
|
||||
icon={FileText}
|
||||
title="Formulaires personnalisés"
|
||||
description="Créez et gérez vos formulaires d'inscription personnalisés."
|
||||
button={true}
|
||||
buttonOpeningModal={true}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
setIsEditing(false);
|
||||
setFileToEdit(null);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
data={filteredFiles}
|
||||
columns={columnsFiles}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
title="Aucun formulaire enregistré"
|
||||
message="Veuillez procéder à la création d'un nouveau formulaire d'inscription"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Section Pièces à fournir */}
|
||||
<ParentFilesSection
|
||||
parentFiles={parentFiles}
|
||||
setParentFileMasters={setParentFileMasters}
|
||||
groups={groups}
|
||||
handleCreate={handleCreate}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
<Popup
|
||||
isOpen={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
/>
|
||||
{isLoading && <Loader />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
267
Front-End/src/components/Structure/Files/ParentFiles.js
Normal file
267
Front-End/src/components/Structure/Files/ParentFiles.js
Normal file
@ -0,0 +1,267 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Modal from '@/components/Modal';
|
||||
import logger from '@/utils/logger';
|
||||
import { Edit3, Trash2, Plus } from 'lucide-react';
|
||||
|
||||
function ParentFileForm({ initialData, groups, onSubmit, onCancel }) {
|
||||
const [name, setName] = useState(initialData?.name || '');
|
||||
const [description, setDescription] = useState(initialData?.description || '');
|
||||
// Correction : s'assurer que selectedGroups ne contient que des IDs uniques
|
||||
const [selectedGroups, setSelectedGroups] = useState(
|
||||
Array.isArray(initialData?.groups)
|
||||
? Array.from(
|
||||
new Set(
|
||||
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
|
||||
)
|
||||
)
|
||||
: []
|
||||
);
|
||||
const [isRequired, setIsRequired] = useState(initialData?.is_required || false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setName(initialData.name || '');
|
||||
setDescription(initialData.description || '');
|
||||
setSelectedGroups(
|
||||
Array.isArray(initialData.groups)
|
||||
? Array.from(
|
||||
new Set(
|
||||
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
|
||||
)
|
||||
)
|
||||
: []
|
||||
);
|
||||
setIsRequired(initialData.is_required || false);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name || selectedGroups.length === 0) return;
|
||||
const data = {
|
||||
name,
|
||||
description,
|
||||
groups: selectedGroups,
|
||||
is_required: isRequired,
|
||||
id: initialData?.id,
|
||||
};
|
||||
logger.debug('[ParentFileForm] handleSubmit data:', data);
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 py-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nom de la pièce <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
required
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Dossiers d&aposinscription <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
multiple
|
||||
value={selectedGroups}
|
||||
onChange={e =>
|
||||
setSelectedGroups(
|
||||
Array.from(new Set(Array.from(e.target.selectedOptions, opt => Number(opt.value))))
|
||||
)
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||
required
|
||||
>
|
||||
{groups.map(group => (
|
||||
<option key={`group-option-${group.id}`} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is_required"
|
||||
checked={isRequired}
|
||||
onChange={e => setIsRequired(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="is_required" className="text-sm text-gray-700">
|
||||
Obligatoire
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
|
||||
disabled={!name || selectedGroups.length === 0}
|
||||
>
|
||||
{initialData?.id ? 'Modifier' : 'Créer'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ParentFiles({
|
||||
parentFiles,
|
||||
groups,
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
singleForm = false,
|
||||
initialData = null,
|
||||
onCancel,
|
||||
}) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(singleForm);
|
||||
const [editingFile, setEditingFile] = useState(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (singleForm) {
|
||||
setIsModalOpen(true);
|
||||
setEditingFile(initialData);
|
||||
}
|
||||
}, [singleForm, initialData]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
setEditingFile(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditModal = (file) => {
|
||||
setEditingFile(file);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setEditingFile(null);
|
||||
setIsModalOpen(false);
|
||||
if (onCancel) onCancel();
|
||||
};
|
||||
|
||||
const handleFormSubmit = (data) => {
|
||||
logger.debug('[ParentFiles] handleFormSubmit data:', data);
|
||||
if (editingFile && editingFile.id) {
|
||||
logger.debug('[ParentFiles] handleEdit called with:', data.id, data);
|
||||
handleEdit(data.id, data).then(closeModal);
|
||||
} else {
|
||||
logger.debug('[ParentFiles] handleCreate called with:', data);
|
||||
handleCreate(data).then(closeModal);
|
||||
}
|
||||
};
|
||||
|
||||
if (singleForm) {
|
||||
return (
|
||||
<ParentFileForm
|
||||
initialData={editingFile}
|
||||
groups={groups}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-orange-700">Pièces à fournir</h2>
|
||||
<button
|
||||
className="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded shadow"
|
||||
onClick={openCreateModal}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Ajouter une pièce</span>
|
||||
</button>
|
||||
</div>
|
||||
<table className="min-w-full border border-gray-200 rounded bg-white">
|
||||
<thead>
|
||||
<tr className="bg-orange-50">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Nom</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Description</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Dossiers</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Obligatoire</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parentFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-6 text-gray-400">Aucune pièce à fournir</td>
|
||||
</tr>
|
||||
) : (
|
||||
parentFiles.map((file) => (
|
||||
<tr key={file.id} className="hover:bg-orange-50">
|
||||
<td className="px-3 py-2 border-b">{file.name}</td>
|
||||
<td className="px-3 py-2 border-b">{file.description}</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
{(file.groups || []).map(
|
||||
gid => groups.find(g => g.id === gid)?.name || gid
|
||||
).join(', ')}
|
||||
</td>
|
||||
<td className="px-3 py-2 border-b text-center">
|
||||
{file.is_required ? (
|
||||
<span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs font-semibold">Oui</span>
|
||||
) : (
|
||||
<span className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs">Non</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 border-b text-center">
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700 mr-2"
|
||||
onClick={() => openEditModal(file)}
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleDelete(file.id)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
setIsOpen={closeModal}
|
||||
title={editingFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
|
||||
modalClassName="w-full max-w-md"
|
||||
>
|
||||
<ParentFileForm
|
||||
initialData={editingFile}
|
||||
groups={groups}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={closeModal}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
|
||||
import Table from '@/components/Table';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import MultiSelect from '@/components/Form/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';
|
||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import Popup from '@/components/Popup';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import MultiSelect from '@/components/Form/MultiSelect';
|
||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||
|
||||
export default function ParentFilesSection({
|
||||
parentFiles,
|
||||
@ -18,6 +15,11 @@ export default function ParentFilesSection({
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
hideCreateButton = false,
|
||||
tableContainerClass = '',
|
||||
headerClassName = '',
|
||||
TableComponent,
|
||||
SectionHeaderComponent,
|
||||
}) {
|
||||
const [editingDocumentId, setEditingDocumentId] = useState(null);
|
||||
const [formData, setFormData] = useState(null);
|
||||
@ -325,27 +327,30 @@ export default function ParentFilesSection({
|
||||
},
|
||||
];
|
||||
|
||||
// Ajout : écouteur d'event global pour déclencher la création depuis la popup centrale
|
||||
React.useEffect(() => {
|
||||
if (!hideCreateButton) return;
|
||||
const handler = () => handleAddEmptyRequiredDocument();
|
||||
window.addEventListener('parentFilesSection:create', handler);
|
||||
return () => window.removeEventListener('parentFilesSection:create', handler);
|
||||
}, [hideCreateButton]);
|
||||
|
||||
const Table = TableComponent || ((props) => <div />); // fallback
|
||||
const SectionHeader = SectionHeaderComponent || ((props) => <div />);
|
||||
|
||||
return (
|
||||
<div className="mt-12 w-4/5">
|
||||
<div className={`w-full h-full flex flex-col ${tableContainerClass}`}>
|
||||
<SectionHeader
|
||||
icon={FileText}
|
||||
title="Pièces à fournir"
|
||||
description="Configurez la liste des documents que les parents doivent fournir."
|
||||
button={true}
|
||||
onClick={handleAddEmptyRequiredDocument}
|
||||
className={headerClassName}
|
||||
/>
|
||||
<Table
|
||||
data={
|
||||
editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles
|
||||
}
|
||||
columns={columnsRequiredDocuments}
|
||||
emptyMessage={
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
title="Aucune pièce à fournir enregistrée"
|
||||
message="Veuillez procéder à la création de nouvelles pièces à fournir par les parents"
|
||||
/>
|
||||
}
|
||||
emptyMessage="Aucune pièce à fournir enregistrée"
|
||||
/>
|
||||
<Popup
|
||||
isOpen={removePopupVisible}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "n3wt-school",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "n3wt-school",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
|
||||
Reference in New Issue
Block a user