diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index b112ba5..fbc834b 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -487,7 +487,12 @@ def generate_form_json_pdf(register_form, form_json): for field in fields: label = field.get("label", field.get("id", "")) ftype = field.get("type", "") - c.drawString(100, y, f"{label} [{ftype}]") + value = field.get("value", "") + # Afficher la valeur si elle existe + if value not in (None, ""): + c.drawString(100, y, f"{label} [{ftype}] : {value}") + else: + c.drawString(100, y, f"{label} [{ftype}]") y -= 25 if y < 100: c.showPage() diff --git a/Back-End/Subscriptions/views/registration_parent_file_masters_views.py b/Back-End/Subscriptions/views/registration_parent_file_masters_views.py index df91d4d..c75e6dd 100644 --- a/Back-End/Subscriptions/views/registration_parent_file_masters_views.py +++ b/Back-End/Subscriptions/views/registration_parent_file_masters_views.py @@ -6,11 +6,10 @@ 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.serializers import RegistrationParentFileMasterSerializer from Subscriptions.models import ( RegistrationForm, - RegistrationParentFileMaster, - RegistrationParentFileTemplate + RegistrationParentFileMaster ) from N3wtSchool import bdd import logging @@ -176,97 +175,3 @@ class RegistrationParentFileMasterSimpleView(APIView): 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) diff --git a/Back-End/Subscriptions/views/registration_school_file_templates_views.py b/Back-End/Subscriptions/views/registration_school_file_templates_views.py index 0f25471..50dc0fd 100644 --- a/Back-End/Subscriptions/views/registration_school_file_templates_views.py +++ b/Back-End/Subscriptions/views/registration_school_file_templates_views.py @@ -1,128 +1,21 @@ 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 from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status import os +import glob -from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer -from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate +from Subscriptions.serializers import RegistrationSchoolFileTemplateSerializer +from Subscriptions.models import RegistrationSchoolFileTemplate from N3wtSchool import bdd import logging +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser import Subscriptions.util as util logger = logging.getLogger(__name__) -class RegistrationSchoolFileMasterView(APIView): - parser_classes = [MultiPartParser, FormParser] - @swagger_auto_schema( - operation_description="Récupère tous les masters de templates d'inscription 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: RegistrationSchoolFileMasterSerializer(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 masters liés à l'établissement via groups.establishment - masters = RegistrationSchoolFileMaster.objects.filter( - groups__establishment__id=establishment_id - ).distinct() - serializer = RegistrationSchoolFileMasterSerializer(masters, many=True) - return Response(serializer.data) - - @swagger_auto_schema( - operation_description="Crée un nouveau master de template d'inscription", - request_body=RegistrationSchoolFileMasterSerializer, - responses={ - 201: RegistrationSchoolFileMasterSerializer, - 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 = RegistrationSchoolFileMasterSerializer(data=payload, partial=True) - if serializer.is_valid(): - obj = serializer.save() - return Response(RegistrationSchoolFileMasterSerializer(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 RegistrationSchoolFileMasterSimpleView(APIView): - @swagger_auto_schema( - operation_description="Récupère un master de template d'inscription spécifique", - responses={ - 200: RegistrationSchoolFileMasterSerializer, - 404: "Master non trouvé" - } - ) - def get(self, request, id): - master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) - if master is None: - return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationSchoolFileMasterSerializer(master) - return JsonResponse(serializer.data, safe=False) - - @swagger_auto_schema( - operation_description="Met à jour un master de template d'inscription existant", - request_body=RegistrationSchoolFileMasterSerializer, - responses={ - 200: RegistrationSchoolFileMasterSerializer, - 400: "Données invalides", - 404: "Master non trouvé" - } - ) - def put(self, request, id): - master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) - if master is None: - return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND) - - # 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 = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True) - if serializer.is_valid(): - serializer.save() - 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 master de template d'inscription", - responses={ - 204: "Suppression réussie", - 404: "Master non trouvé" - } - ) - def delete(self, request, id): - master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) - if master is not None: - master.delete() - return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK) - else: - return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - class RegistrationSchoolFileTemplateView(APIView): @swagger_auto_schema( operation_description="Récupère tous les templates d'inscription pour un établissement donné", @@ -165,6 +58,8 @@ class RegistrationSchoolFileTemplateView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class RegistrationSchoolFileTemplateSimpleView(APIView): + parser_classes = [MultiPartParser, FormParser, JSONParser] + @swagger_auto_schema( operation_description="Récupère un template d'inscription spécifique", responses={ @@ -189,12 +84,83 @@ class RegistrationSchoolFileTemplateSimpleView(APIView): } ) def put(self, request, id): + # Normaliser la payload (support form-data avec champ 'data' JSON ou fichier JSON) + payload, resp = util.build_payload_from_request(request) + if resp is not None: + return resp + + # Synchroniser fields[].value dans le payload AVANT le serializer (pour les formulaires dynamiques) + formTemplateData = payload.get('formTemplateData') + if formTemplateData and isinstance(formTemplateData, dict): + responses = None + if "responses" in formTemplateData: + resp = formTemplateData["responses"] + if isinstance(resp, dict) and "responses" in resp: + responses = resp["responses"] + elif isinstance(resp, dict): + responses = resp + if responses and "fields" in formTemplateData: + for field in formTemplateData["fields"]: + field_id = field.get("id") + if field_id and field_id in responses: + field["value"] = responses[field_id] + payload['formTemplateData'] = formTemplateData + template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id) if template is None: return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationSchoolFileTemplateSerializer(template, data=request.data) + + # Cas 1 : Upload d'un fichier existant (PDF/image) + if 'file' in request.FILES: + upload = request.FILES['file'] + file_field = template.file + upload_name = upload.name + upload_dir = os.path.dirname(file_field.path) if file_field and file_field.name else None + if upload_dir: + base_name, _ = os.path.splitext(upload_name) + pattern = os.path.join(upload_dir, f"{base_name}.*") + for f in glob.glob(pattern): + try: + if os.path.exists(f): + os.remove(f) + logger.info(f"Suppression du fichier existant (pattern): {f}") + except Exception as e: + logger.error(f"Erreur suppression fichier existant (pattern): {e}") + target_path = os.path.join(upload_dir, upload_name) + if os.path.exists(target_path): + try: + os.remove(target_path) + except Exception as e: + logger.error(f"Erreur suppression fichier cible: {e}") + # On écrase le fichier existant sans passer par le serializer + template.file.save(upload_name, upload, save=True) + return Response({'message': 'Template mis à jour avec succès', 'data': RegistrationSchoolFileTemplateSerializer(template).data}, status=status.HTTP_200_OK) + + # Cas 2 : Formulaire dynamique (JSON) + serializer = RegistrationSchoolFileTemplateSerializer(template, data=payload) if serializer.is_valid(): serializer.save() + # Régénérer le PDF si besoin + formTemplateData = serializer.validated_data.get('formTemplateData') + if ( + formTemplateData + and isinstance(formTemplateData, dict) + and formTemplateData.get("fields") + and hasattr(template, "file") + ): + old_pdf_name = None + if template.file and template.file.name: + old_pdf_name = os.path.basename(template.file.name) + try: + template.file.delete(save=False) + if os.path.exists(template.file.path): + os.remove(template.file.path) + except Exception as e: + logger.error(f"Erreur lors de la suppression du fichier existant: {e}") + from Subscriptions.util import generate_form_json_pdf + pdf_file = generate_form_json_pdf(template.registration_form, formTemplateData) + pdf_filename = old_pdf_name or f"{template.name}_{template.id}.pdf" + template.file.save(pdf_filename, pdf_file, save=True) 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) @@ -223,195 +189,3 @@ class RegistrationSchoolFileTemplateSimpleView(APIView): return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) else: return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -class RegistrationParentFileMasterView(APIView): - @swagger_auto_schema( - operation_description="Récupère tous les fichiers parents 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: - # 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: - return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Front-End/src/components/Inscription/DynamicFormsList.js b/Front-End/src/components/Inscription/DynamicFormsList.js index 20f97fc..e866b2e 100644 --- a/Front-End/src/components/Inscription/DynamicFormsList.js +++ b/Front-End/src/components/Inscription/DynamicFormsList.js @@ -1,8 +1,10 @@ 'use client'; import React, { useState, useEffect } from 'react'; import FormRenderer from '@/components/Form/FormRenderer'; -import { CheckCircle, Hourglass, FileText } from 'lucide-react'; +import FileUpload from '@/components/Form/FileUpload'; +import { CheckCircle, Hourglass, FileText, Download, Upload } from 'lucide-react'; import logger from '@/utils/logger'; +import { BASE_URL } from '@/utils/Url'; /** * Composant pour afficher et gérer les formulaires dynamiques d'inscription @@ -10,6 +12,7 @@ import logger from '@/utils/logger'; * @param {Object} existingResponses - Réponses déjà sauvegardées * @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis * @param {Boolean} enable - Si les formulaires sont modifiables + * @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné */ export default function DynamicFormsList({ schoolFileMasters, @@ -17,10 +20,12 @@ export default function DynamicFormsList({ onFormSubmit, enable = true, onValidationChange, + onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent) }) { const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0); const [formsData, setFormsData] = useState({}); const [formsValidation, setFormsValidation] = useState({}); + const fileInputRefs = React.useRef({}); // Initialiser les données avec les réponses existantes useEffect(() => { @@ -138,6 +143,27 @@ export default function DynamicFormsList({ return schoolFileMasters[currentTemplateIndex]; }; + // Handler d'upload pour formulaire existant + const handleUpload = async (file, selectedFile) => { + if (!file || !selectedFile) return; + try { + if (onFileUpload) { + await onFileUpload(file, selectedFile); + setFormsValidation((prev) => ({ + ...prev, + [selectedFile.id]: true, + })); + } + } catch (error) { + logger.error('Erreur lors de l\'upload du fichier :', error); + } + }; + + const isDynamicForm = (template) => + template.formTemplateData && + Array.isArray(template.formTemplateData.fields) && + template.formTemplateData.fields.length > 0; + if (!schoolFileMasters || schoolFileMasters.length === 0) { return (
@@ -223,13 +249,13 @@ export default function DynamicFormsList({

- {currentTemplate.formMasterData?.title || + {currentTemplate.formTemplateData?.title || currentTemplate.title || currentTemplate.name || 'Formulaire sans nom'}

- {currentTemplate.formMasterData?.description || + {currentTemplate.formTemplateData?.description || currentTemplate.description || 'Veuillez compléter ce formulaire pour continuer votre inscription.'}

@@ -239,39 +265,57 @@ export default function DynamicFormsList({
- {/* Vérifier si le formulaire maître a des données de configuration */} - {(currentTemplate.formMasterData?.fields && - currentTemplate.formMasterData.fields.length > 0) || - (currentTemplate.fields && currentTemplate.fields.length > 0) ? ( + {/* Affichage dynamique ou existant */} + {isDynamicForm(currentTemplate) ? ( handleFormSubmit(formData, currentTemplate.id) } /> ) : ( -
- -

- Ce formulaire n'est pas encore configuré. -

-

- Contactez l'administration pour plus d'informations. -

+ // Formulaire existant (PDF, image, etc.) +
+
+ +
+ {currentTemplate.name} +
+ {currentTemplate.file && ( + + + Télécharger le document + + )} +
+ {enable && ( + handleUpload(file, currentTemplate)} + required + enable + /> + )}
)}
diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index ea2937b..ceadc5a 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -5,15 +5,12 @@ import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import { fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles, - fetchRegistrationSchoolFileMasters, saveFormResponses, fetchFormResponses, autoSaveRegisterForm, } from '@/app/actions/subscriptionAction'; import { - downloadTemplate, editRegistrationSchoolFileTemplates, - editRegistrationParentFileTemplates, } from '@/app/actions/registerFileGroupAction'; import { fetchRegistrationPaymentModes, @@ -22,7 +19,7 @@ import { fetchTuitionPaymentPlans, } from '@/app/actions/schoolAction'; import { fetchProfiles } from '@/app/actions/authAction'; -import { BASE_URL, FE_PARENTS_HOME_URL } from '@/utils/Url'; +import { FE_PARENTS_HOME_URL } from '@/utils/Url'; import logger from '@/utils/logger'; import FilesToUpload from '@/components/Inscription/FilesToUpload'; import DynamicFormsList from '@/components/Inscription/DynamicFormsList'; @@ -32,7 +29,6 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie import SiblingInputFields from '@/components/Inscription/SiblingInputFields'; import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector'; import ProgressStep from '@/components/ProgressStep'; -import { CheckCircle, Hourglass } from 'lucide-react'; import { useRouter } from 'next/navigation'; /** @@ -47,7 +43,6 @@ export default function InscriptionFormShared({ studentId, csrfToken, selectedEstablishmentId, - apiDocuseal, onSubmit, errors = {}, // Nouvelle prop pour les erreurs enable = true, @@ -82,7 +77,7 @@ export default function InscriptionFormShared({ const [parentFileTemplates, setParentFileTemplates] = useState([]); const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [formResponses, setFormResponses] = useState({}); - const [currentPage, setCurrentPage] = useState(1); + const [currentPage, setCurrentPage] = useState(5); const [isPage1Valid, setIsPage1Valid] = useState(false); const [isPage2Valid, setIsPage2Valid] = useState(false); @@ -283,8 +278,8 @@ export default function InscriptionFormShared({ }); // Trouver le template correspondant pour récupérer sa configuration - const currentTemplate = schoolFileMasters.find( - (master) => master.id === templateId + const currentTemplate = schoolFileTemplates.find( + (template) => template.id === templateId ); if (!currentTemplate) { throw new Error(`Template avec l'ID ${templateId} non trouvé`); @@ -294,17 +289,16 @@ export default function InscriptionFormShared({ const formTemplateData = { id: currentTemplate.id, title: - currentTemplate.formMasterData?.title || + currentTemplate.formTemplateData?.title || currentTemplate.title || currentTemplate.name || 'Formulaire', fields: ( - currentTemplate.formMasterData?.fields || + currentTemplate.formTemplateData?.fields || currentTemplate.fields || [] ).map((field) => ({ ...field, - // Ajouter la réponse de l'utilisateur selon le type de champ ...(field.type === 'checkbox' ? { checked: formData[field.id] || false } : {}), @@ -315,8 +309,8 @@ export default function InscriptionFormShared({ ? { value: formData[field.id] || '' } : {}), })), - submitLabel: currentTemplate.formMasterData?.submitLabel || 'Valider', - responses: formData, // Garder aussi les réponses brutes pour facilité d'accès + submitLabel: currentTemplate.formTemplateData?.submitLabel || 'Valider', + responses: formData, }; // Sauvegarder les réponses du formulaire via l'API RegistrationSchoolFileTemplate @@ -331,18 +325,37 @@ export default function InscriptionFormShared({ ); logger.debug("Réponse de l'API:", result); - // Mettre à jour l'état local des réponses + // Prendre en compte la réponse du back pour mettre à jour les réponses locales + let newResponses = formData; + if ( + result && + result.data && + result.data.formTemplateData && + result.data.formTemplateData.responses && + result.data.formTemplateData.responses.responses + ) { + // Si la structure responses.responses existe, on la prend + newResponses = result.data.formTemplateData.responses.responses; + } else if ( + result && + result.data && + result.data.formTemplateData && + result.data.formTemplateData.responses + ) { + // Sinon, on prend responses directement + newResponses = result.data.formTemplateData.responses; + } + setFormResponses((prev) => ({ ...prev, - [templateId]: formData, + [templateId]: newResponses, })); - // Mettre à jour l'état local pour indiquer que le formulaire est complété - setSchoolFileMasters((prevMasters) => { - return prevMasters.map((master) => - master.id === templateId - ? { ...master, completed: true, responses: formData } - : master + setSchoolFileTemplates((prevTemplates) => { + return prevTemplates.map((template) => + template.id === templateId + ? { ...template, completed: true, responses: newResponses } + : template ); }); @@ -354,7 +367,6 @@ export default function InscriptionFormShared({ error: error.message, stack: error.stack, }); - // Afficher l'erreur à l'utilisateur alert(`Erreur lors de la sauvegarde du formulaire: ${error.message}`); return Promise.reject(error); } @@ -370,6 +382,56 @@ export default function InscriptionFormShared({ useEffect(() => { fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => { setSchoolFileTemplates(data); + + // Récupérer les réponses existantes pour chaque template + const fetchAllResponses = async () => { + const responsesMap = {}; + for (const template of data) { + if (template.id) { + try { + const templateData = await fetchFormResponses(template.id); + if (templateData && templateData.formTemplateData) { + if (templateData.formTemplateData.responses) { + responsesMap[template.id] = templateData.formTemplateData.responses; + } else { + // Extraire les réponses depuis les champs + const responses = {}; + if (templateData.formTemplateData.fields) { + templateData.formTemplateData.fields.forEach((field) => { + if ( + field.type === 'checkbox' && + field.checked !== undefined + ) { + responses[field.id] = field.checked; + } else if ( + field.type === 'radio' && + field.selected !== undefined + ) { + responses[field.id] = field.selected; + } else if ( + (field.type === 'text' || + field.type === 'textarea' || + field.type === 'email') && + field.value !== undefined + ) { + responses[field.id] = field.value; + } + }); + } + responsesMap[template.id] = responses; + } + } + } catch (error) { + logger.debug( + `Pas de données existantes pour le template ${template.id}:`, + error + ); + } + } + } + setFormResponses(responsesMap); + }; + fetchAllResponses(); }); fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => { @@ -392,66 +454,6 @@ export default function InscriptionFormShared({ .catch((error) => logger.error('Error fetching profiles : ', error)); if (selectedEstablishmentId) { - // Fetch data for school file masters - fetchRegistrationSchoolFileMasters(selectedEstablishmentId) - .then(async (data) => { - logger.debug('School file masters fetched:', data); - setSchoolFileMasters(data); - - // Récupérer les données existantes de chaque template - const responsesMap = {}; - for (const master of data) { - if (master.id) { - try { - const templateData = await fetchFormResponses(master.id); - if (templateData && templateData.formTemplateData) { - // Si on a les réponses brutes sauvegardées, les utiliser - if (templateData.formTemplateData.responses) { - responsesMap[master.id] = - templateData.formTemplateData.responses; - } else { - // Sinon, extraire les réponses depuis les champs - const responses = {}; - if (templateData.formTemplateData.fields) { - templateData.formTemplateData.fields.forEach((field) => { - if ( - field.type === 'checkbox' && - field.checked !== undefined - ) { - responses[field.id] = field.checked; - } else if ( - field.type === 'radio' && - field.selected !== undefined - ) { - responses[field.id] = field.selected; - } else if ( - (field.type === 'text' || - field.type === 'textarea' || - field.type === 'email') && - field.value !== undefined - ) { - responses[field.id] = field.value; - } - }); - } - responsesMap[master.id] = responses; - } - } - } catch (error) { - logger.debug( - `Pas de données existantes pour le template ${master.id}:`, - error - ); - // Ce n'est pas critique si un template n'a pas de données - } - } - } - setFormResponses(responsesMap); - }) - .catch((error) => - logger.error('Error fetching school file masters:', error) - ); - // Fetch data for registration payment modes handleRegistrationPaymentModes(); @@ -464,7 +466,7 @@ export default function InscriptionFormShared({ // Fetch data for tuition payment plans handleTuitionnPaymentPlans(); } - }, [selectedEstablishmentId]); + }, [studentId, selectedEstablishmentId]); const handleRegistrationPaymentModes = () => { fetchRegistrationPaymentModes(selectedEstablishmentId) @@ -514,10 +516,22 @@ export default function InscriptionFormShared({ ); } - const updateData = new FormData(); - updateData.append('file', file); + // Générer le nom du fichier : . + let extension = ''; + if (file.name && file.name.lastIndexOf('.') !== -1) { + extension = file.name.substring(file.name.lastIndexOf('.')); + } + // Nettoyer le nom du template pour éviter les caractères spéciaux + const cleanName = (selectedFile.name || 'document') + .replace(/[^a-zA-Z0-9_\-]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + const finalFileName = `${cleanName}${extension}`; - return editRegistrationParentFileTemplates( + const updateData = new FormData(); + updateData.append('file', file, finalFileName); + + return editRegistrationSchoolFileTemplates( selectedFile.id, updateData, csrfToken @@ -528,11 +542,10 @@ export default function InscriptionFormShared({ setUploadedFiles((prev) => { const updatedFiles = prev.map((uploadedFile) => uploadedFile.id === selectedFile.id - ? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé + ? { ...uploadedFile, fileName: response.data.file } : uploadedFile ); - // Si le fichier n'existe pas encore, l'ajouter if (!updatedFiles.find((file) => file.id === selectedFile.id)) { updatedFiles.push({ id: selectedFile.id, @@ -552,11 +565,11 @@ export default function InscriptionFormShared({ ) ); - return response; // Retourner la réponse pour signaler le succès + return response; }) .catch((error) => { logger.error('Erreur lors de la mise à jour du fichier :', error); - throw error; // Relancer l'erreur pour que l'appelant puisse la capturer + throw error; }); }; @@ -587,7 +600,7 @@ export default function InscriptionFormShared({ setUploadedFiles((prev) => prev.map((uploadedFile) => uploadedFile.id === templateId - ? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier + ? { ...uploadedFile, fileName: null, fileUrl: null } : uploadedFile ) ); @@ -786,11 +799,12 @@ export default function InscriptionFormShared({ {/* Page 5 : Formulaires dynamiques d'inscription */} {currentPage === 5 && ( )} diff --git a/Front-End/src/components/Structure/Files/CreateDocumentModal.js b/Front-End/src/components/Structure/Files/CreateDocumentModal.js deleted file mode 100644 index fd3fcac..0000000 --- a/Front-End/src/components/Structure/Files/CreateDocumentModal.js +++ /dev/null @@ -1,209 +0,0 @@ -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 ( - - {step === 'main' && ( -
- - - -
- )} - {step === 'choose_form' && ( -
- - - -
- )} - {step === 'form_builder' && ( -
- - { - onCreateSchoolFileMaster(data); - onClose(); - }} - groups={groups} - isEditing={false} - /> -
- )} - {step === 'file_upload' && ( -
- -
- setFileName(e.target.value)} - required - /> - {/* Sélecteur de groupes à cocher */} -
- -
- {groups && groups.length > 0 ? ( - groups.map((group) => ( - - )) - ) : ( -

- Aucun groupe disponible -

- )} -
-
- - - -
- )} -
- ); -} diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js index bbd3638..9f8a0ee 100644 --- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js +++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js @@ -6,8 +6,6 @@ import { Star, ChevronDown, Plus, - Archive, - Eye } from 'lucide-react'; import Modal from '@/components/Modal'; import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder'; @@ -31,11 +29,9 @@ import { } from '@/app/actions/registerFileGroupAction'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import logger from '@/utils/logger'; -import ParentFiles from './ParentFiles'; import Popup from '@/components/Popup'; import Loader from '@/components/Loader'; import { useNotification } from '@/context/NotificationContext'; -import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal'; import FileUpload from '@/components/Form/FileUpload'; import SectionTitle from '@/components/SectionTitle'; import DropdownMenu from '@/components/DropdownMenu';