From 0501c1dd7320d734ba6ae0b93ad4a270b903a5df Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 14 Mar 2026 11:26:20 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Finalisation=20de=20la=20validation=20/?= =?UTF-8?q?=20refus=20des=20documents=20sign=C3=A9s=20par=20les=20parents?= =?UTF-8?q?=20[N3WTS-2]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/N3wtSchool/mailManager.py | 68 ++++++++ Back-End/Subscriptions/models.py | 94 ++++++++--- .../templates/emails/refus_dossier.html | 67 ++++++++ .../templates/emails/validation_dossier.html | 85 ++++++++++ Back-End/Subscriptions/util.py | 110 +++++++++--- .../views/register_form_views.py | 71 +++++++- .../validateSubscription/page.js | 10 -- Front-End/src/components/Form/FormRenderer.js | 11 +- .../Inscription/DynamicFormsList.js | 157 +++++++++--------- 9 files changed, 537 insertions(+), 136 deletions(-) create mode 100644 Back-End/Subscriptions/templates/emails/refus_dossier.html create mode 100644 Back-End/Subscriptions/templates/emails/validation_dossier.html diff --git a/Back-End/N3wtSchool/mailManager.py b/Back-End/N3wtSchool/mailManager.py index e3eacbe..be5c2db 100644 --- a/Back-End/N3wtSchool/mailManager.py +++ b/Back-End/N3wtSchool/mailManager.py @@ -225,4 +225,72 @@ def sendRegisterTeacher(recipients, establishment_id): sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection) except Exception as e: errorMessage = str(e) + return errorMessage + + +def sendRefusDossier(recipients, establishment_id, student_name, notes): + """ + Envoie un email au parent pour l'informer que son dossier d'inscription + nécessite des corrections. + + Args: + recipients: Email du destinataire (parent) + establishment_id: ID de l'établissement + student_name: Nom complet de l'élève + notes: Motifs du refus (contenu du champ notes du RegistrationForm) + + Returns: + str: Message d'erreur si échec, chaîne vide sinon + """ + errorMessage = '' + try: + EMAIL_REFUS_SUBJECT = '[N3WT-SCHOOL] Dossier d\'inscription - Corrections requises' + context = { + 'BASE_URL': settings.BASE_URL, + 'URL_DJANGO': settings.URL_DJANGO, + 'student_name': student_name, + 'notes': notes + } + connection = getConnection(establishment_id) + subject = EMAIL_REFUS_SUBJECT + html_message = render_to_string('emails/refus_dossier.html', context) + sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection) + logger.info(f"Email de refus envoyé à {recipients} pour l'élève {student_name}") + except Exception as e: + errorMessage = str(e) + logger.error(f"Erreur lors de l'envoi de l'email de refus: {errorMessage}") + return errorMessage + + +def sendValidationDossier(recipients, establishment_id, student_name, class_name=None): + """ + Envoie un email au parent pour l'informer que le dossier d'inscription + a été validé. + + Args: + recipients: Email du destinataire (parent) + establishment_id: ID de l'établissement + student_name: Nom complet de l'élève + class_name: Nom de la classe attribuée (optionnel) + + Returns: + str: Message d'erreur si échec, chaîne vide sinon + """ + errorMessage = '' + try: + EMAIL_VALIDATION_SUBJECT = '[N3WT-SCHOOL] Dossier d\'inscription validé' + context = { + 'BASE_URL': settings.BASE_URL, + 'URL_DJANGO': settings.URL_DJANGO, + 'student_name': student_name, + 'class_name': class_name + } + connection = getConnection(establishment_id) + subject = EMAIL_VALIDATION_SUBJECT + html_message = render_to_string('emails/validation_dossier.html', context) + sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection) + logger.info(f"Email de validation envoyé à {recipients} pour l'élève {student_name}") + except Exception as e: + errorMessage = str(e) + logger.error(f"Erreur lors de l'envoi de l'email de validation: {errorMessage}") return errorMessage \ No newline at end of file diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 1cf7897..a54767d 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -214,9 +214,28 @@ class RegistrationFileGroup(models.Model): def __str__(self): return f'{self.group.name} - {self.id}' -def registration_file_path(instance, filename): - # Génère le chemin : registration_files/dossier_rf_{student_id}/filename - return f'registration_files/dossier_rf_{instance.student_id}/{filename}' +def registration_form_file_upload_to(instance, filename): + """ + Génère le chemin de stockage pour les fichiers du dossier d'inscription. + Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename + """ + est_name = instance.establishment.name if instance.establishment else "unknown_establishment" + student_last = instance.student.last_name if instance.student else "unknown" + student_first = instance.student.first_name if instance.student else "unknown" + return f"{est_name}/dossier_{student_last}_{student_first}/{filename}" + +def _delete_file_if_exists(file_field): + """ + Supprime le fichier physique s'il existe. + Utile pour éviter les suffixes automatiques Django lors du remplacement. + """ + if file_field and file_field.name: + try: + if hasattr(file_field, 'path') and os.path.exists(file_field.path): + os.remove(file_field.path) + logger.debug(f"Fichier supprimé: {file_field.path}") + except Exception as e: + logger.error(f"Erreur lors de la suppression du fichier {file_field.name}: {e}") class RegistrationForm(models.Model): class RegistrationFormStatus(models.IntegerChoices): @@ -238,17 +257,17 @@ class RegistrationForm(models.Model): notes = models.CharField(max_length=200, blank=True) registration_link_code = models.CharField(max_length=200, default="", blank=True) registration_file = models.FileField( - upload_to=registration_file_path, + upload_to=registration_form_file_upload_to, null=True, blank=True ) sepa_file = models.FileField( - upload_to=registration_file_path, + upload_to=registration_form_file_upload_to, null=True, blank=True ) fusion_file = models.FileField( - upload_to=registration_file_path, + upload_to=registration_form_file_upload_to, null=True, blank=True ) @@ -285,13 +304,23 @@ class RegistrationForm(models.Model): except RegistrationForm.DoesNotExist: old_fileGroup = None - # Vérifier si un fichier existant doit être remplacé + # Supprimer les anciens fichiers si remplacés (évite les suffixes Django) if self.pk: # Si l'objet existe déjà dans la base de données try: old_instance = RegistrationForm.objects.get(pk=self.pk) + + # Gestion du sepa_file if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file: - # Supprimer l'ancien fichier - old_instance.sepa_file.delete(save=False) + _delete_file_if_exists(old_instance.sepa_file) + + # Gestion du registration_file + if old_instance.registration_file and old_instance.registration_file != self.registration_file: + _delete_file_if_exists(old_instance.registration_file) + + # Gestion du fusion_file + if old_instance.fusion_file and old_instance.fusion_file != self.fusion_file: + _delete_file_if_exists(old_instance.fusion_file) + except RegistrationForm.DoesNotExist: pass # L'objet n'existe pas encore, rien à supprimer @@ -485,10 +514,18 @@ class RegistrationParentFileMaster(models.Model): ############################################################ def registration_school_file_upload_to(instance, filename): + """ + Génère le chemin pour les fichiers templates école. + Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename + """ return f"{instance.registration_form.establishment.name}/dossier_{instance.registration_form.student.last_name}_{instance.registration_form.student.first_name}/{filename}" def registration_parent_file_upload_to(instance, filename): - return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}" + """ + Génère le chemin pour les fichiers à fournir par les parents. + Structure : Etablissement/dossier_NomEleve_PrenomEleve/parent/filename + """ + return f"{instance.registration_form.establishment.name}/dossier_{instance.registration_form.student.last_name}_{instance.registration_form.student.first_name}/parent/{filename}" ####### Formulaires templates (par dossier d'inscription) ####### class RegistrationSchoolFileTemplate(models.Model): @@ -503,15 +540,31 @@ class RegistrationSchoolFileTemplate(models.Model): def __str__(self): return self.name + def save(self, *args, **kwargs): + # Supprimer l'ancien fichier si remplacé (évite les suffixes Django) + if self.pk: + try: + old_instance = RegistrationSchoolFileTemplate.objects.get(pk=self.pk) + if old_instance.file and old_instance.file != self.file: + _delete_file_if_exists(old_instance.file) + except RegistrationSchoolFileTemplate.DoesNotExist: + pass + super().save(*args, **kwargs) + @staticmethod def get_files_from_rf(register_form_id): """ Récupère tous les fichiers liés à un dossier d’inscription donné. + Ignore les fichiers qui n'existent pas physiquement. """ registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id) filenames = [] for reg_file in registration_files: - filenames.append(reg_file.file.path) + if reg_file.file and hasattr(reg_file.file, 'path'): + if os.path.exists(reg_file.file.path): + filenames.append(reg_file.file.path) + else: + logger.warning(f"Fichier introuvable ignoré: {reg_file.file.path}") return filenames class StudentCompetency(models.Model): @@ -544,20 +597,21 @@ class RegistrationParentFileTemplate(models.Model): isValidated = models.BooleanField(default=False) def __str__(self): - return self.name + return self.master.name if self.master else f"ParentFile_{self.pk}" def save(self, *args, **kwargs): - if self.pk: # Si l'objet existe déjà dans la base de données + # Supprimer l'ancien fichier si remplacé (évite les suffixes Django) + if self.pk: try: old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk) - if old_instance.file and (not self.file or self.file.name == ''): - if os.path.exists(old_instance.file.path): - old_instance.file.delete(save=False) - self.file = None - else: - print(f"Le fichier {old_instance.file.path} n'existe pas.") + # Si le fichier change ou est supprimé + if old_instance.file: + if old_instance.file != self.file or not self.file or self.file.name == '': + _delete_file_if_exists(old_instance.file) + if not self.file or self.file.name == '': + self.file = None except RegistrationParentFileTemplate.DoesNotExist: - print("Ancienne instance introuvable.") + pass super().save(*args, **kwargs) @staticmethod diff --git a/Back-End/Subscriptions/templates/emails/refus_dossier.html b/Back-End/Subscriptions/templates/emails/refus_dossier.html new file mode 100644 index 0000000..8e36c44 --- /dev/null +++ b/Back-End/Subscriptions/templates/emails/refus_dossier.html @@ -0,0 +1,67 @@ + + + + + Dossier d'inscription - Corrections requises + + + +
+
+ +

Dossier d'inscription - Corrections requises

+
+
+

Bonjour,

+

Nous avons examiné le dossier d'inscription de {{ student_name }} et certaines corrections sont nécessaires avant de pouvoir le valider.

+ +
+ Motif(s) :
+ {{ notes }} +
+ +

Veuillez vous connecter à votre espace pour effectuer les corrections demandées :

+

{{BASE_URL}}/users/login

+ +

Cordialement,

+

L'équipe N3wt School

+
+ +
+ + diff --git a/Back-End/Subscriptions/templates/emails/validation_dossier.html b/Back-End/Subscriptions/templates/emails/validation_dossier.html new file mode 100644 index 0000000..1454d21 --- /dev/null +++ b/Back-End/Subscriptions/templates/emails/validation_dossier.html @@ -0,0 +1,85 @@ + + + + + Dossier d'inscription validé + + + +
+
+ +

Dossier d'inscription validé

+
+
+

Bonjour,

+ +
+

Félicitations !

+

Le dossier d'inscription de {{ student_name }} a été validé.

+
+ + {% if class_name %} +
+ Classe attribuée : {{ class_name }} +
+ {% endif %} + +

Vous pouvez accéder à votre espace pour consulter les détails :

+

{{BASE_URL}}/users/login

+ +

Nous vous remercions de votre confiance et vous souhaitons une excellente année scolaire.

+ +

Cordialement,

+

L'équipe N3wt School

+
+ +
+ + diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index fbc834b..cb85046 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -19,7 +19,8 @@ from enum import Enum import random import string from rest_framework.parsers import JSONParser -from PyPDF2 import PdfMerger +from PyPDF2 import PdfMerger, PdfReader +from PyPDF2.errors import PdfReadError import shutil import logging @@ -31,6 +32,29 @@ from rest_framework import status logger = logging.getLogger(__name__) +def save_file_replacing_existing(file_field, filename, content, save=True): + """ + Sauvegarde un fichier en écrasant l'existant s'il porte le même nom. + Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf). + + Args: + file_field: Le FileField Django (ex: registerForm.registration_file) + filename: Le nom du fichier à sauvegarder + content: Le contenu du fichier (File, BytesIO, ContentFile, etc.) + save: Si True, sauvegarde l'instance parente + """ + # Supprimer l'ancien fichier s'il existe + if file_field and file_field.name: + try: + if hasattr(file_field, 'path') and os.path.exists(file_field.path): + os.remove(file_field.path) + logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}") + except Exception as e: + logger.error(f"[save_file] Erreur suppression ancien fichier: {e}") + + # Sauvegarder le nouveau fichier + file_field.save(filename, content, save=save) + def build_payload_from_request(request): """ Normalise la request en payload prêt à être donné au serializer. @@ -344,12 +368,70 @@ def getArgFromRequest(_argument, _request): def merge_files_pdf(file_paths): """ Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire. + Les fichiers non-PDF (images) sont convertis en PDF avant fusion. + Les fichiers invalides sont ignorés avec un log d'erreur. """ merger = PdfMerger() + files_added = 0 + + # Extensions d'images supportées + image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif'} - # Ajouter les fichiers valides au merger for file_path in file_paths: - merger.append(file_path) + # Vérifier que le fichier existe + if not os.path.exists(file_path): + logger.warning(f"[merge_files_pdf] Fichier introuvable, ignoré: {file_path}") + continue + + file_ext = os.path.splitext(file_path)[1].lower() + + # Si c'est une image, la convertir en PDF + if file_ext in image_extensions: + try: + from PIL import Image + from reportlab.lib.utils import ImageReader + + img = Image.open(file_path) + img_width, img_height = img.size + + # Créer un PDF en mémoire avec l'image + img_pdf = BytesIO() + c = canvas.Canvas(img_pdf, pagesize=(img_width, img_height)) + c.drawImage(file_path, 0, 0, width=img_width, height=img_height) + c.save() + img_pdf.seek(0) + + merger.append(img_pdf) + files_added += 1 + logger.debug(f"[merge_files_pdf] Image convertie et ajoutée: {file_path}") + except Exception as e: + logger.error(f"[merge_files_pdf] Erreur lors de la conversion de l'image {file_path}: {e}") + continue + + # Sinon, essayer de l'ajouter comme PDF + try: + # Valider que c'est un PDF lisible + with open(file_path, 'rb') as f: + PdfReader(f, strict=False) + + # Si la validation passe, ajouter au merger + merger.append(file_path) + files_added += 1 + logger.debug(f"[merge_files_pdf] PDF ajouté: {file_path}") + except PdfReadError as e: + logger.error(f"[merge_files_pdf] Fichier PDF invalide, ignoré: {file_path} - {e}") + except Exception as e: + logger.error(f"[merge_files_pdf] Erreur lors de la lecture du fichier {file_path}: {e}") + + if files_added == 0: + logger.warning("[merge_files_pdf] Aucun fichier valide à fusionner") + # Retourner un PDF vide + empty_pdf = BytesIO() + c = canvas.Canvas(empty_pdf, pagesize=A4) + c.drawString(100, 750, "Aucun document à afficher") + c.save() + empty_pdf.seek(0) + return empty_pdf # Sauvegarder le fichier fusionné en mémoire merged_pdf = BytesIO() @@ -378,25 +460,11 @@ def rfToPDF(registerForm, filename): if not pdf: raise ValueError("Erreur lors de la génération du PDF.") - # Vérifier si un fichier avec le même nom existe déjà et le supprimer - if registerForm.registration_file and registerForm.registration_file.name: - # Vérifiez si le chemin est déjà absolu ou relatif - if os.path.isabs(registerForm.registration_file.name): - existing_file_path = registerForm.registration_file.name - else: - existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/')) - - # Vérifier si le fichier existe et le supprimer - if os.path.exists(existing_file_path): - os.remove(existing_file_path) - registerForm.registration_file.delete(save=False) - else: - print(f'File does not exist: {existing_file_path}') - - # Enregistrer directement le fichier dans le champ registration_file + # Enregistrer directement le fichier dans le champ registration_file (écrase l'existant) try: - registerForm.registration_file.save( - os.path.basename(filename), # Utiliser uniquement le nom de fichier + save_file_replacing_existing( + registerForm.registration_file, + os.path.basename(filename), File(BytesIO(pdf.content)), save=True ) diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index b371ab2..5441703 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -9,6 +9,7 @@ from drf_yasg import openapi import json import os +import threading from django.core.files import File import N3wtSchool.mailManager as mailer @@ -359,6 +360,26 @@ class RegisterFormWithIdView(APIView): return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT: if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + # Envoi de l'email de refus aux responsables légaux + try: + student = registerForm.student + student_name = f"{student.first_name} {student.last_name}" + notes = registerForm.notes or "Aucun motif spécifié" + + guardians = student.guardians.all() + for guardian in guardians: + email = None + if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile: + email = guardian.profile_role.profile.email + if not email: + email = getattr(guardian, "email", None) + + if email: + logger.info(f"[RF_SENT] Envoi email de refus à {email} pour l'élève {student_name}") + mailer.sendRefusDossier(email, registerForm.establishment.pk, student_name, notes) + except Exception as e: + logger.error(f"[RF_SENT] Erreur lors de l'envoi de l'email de refus: {e}") + updateStateMachine(registerForm, 'EVENT_REFUSE') util.delete_registration_files(registerForm) elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT: @@ -401,14 +422,23 @@ class RegisterFormWithIdView(APIView): fileNames.extend(parent_file_templates) # Création du fichier PDF fusionné - merged_pdf_content = util.merge_files_pdf(fileNames) + merged_pdf_content = None + try: + merged_pdf_content = util.merge_files_pdf(fileNames) - # Mise à jour du champ registration_file avec le fichier fusionné - registerForm.fusion_file.save( - f"dossier_complet.pdf", - File(merged_pdf_content), - save=True - ) + # Mise à jour du champ fusion_file avec le fichier fusionné + util.save_file_replacing_existing( + registerForm.fusion_file, + "dossier_complet.pdf", + File(merged_pdf_content), + save=True + ) + except Exception as e: + logger.error(f"[RF_VALIDATED] Erreur lors de la fusion des fichiers: {e}") + finally: + # Libérer explicitement la mémoire du BytesIO + if merged_pdf_content is not None: + merged_pdf_content.close() # Valorisation des StudentCompetency pour l'élève try: student = registerForm.student @@ -450,6 +480,33 @@ class RegisterFormWithIdView(APIView): except Exception as e: logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}") + # Envoi de l'email de validation aux responsables légaux (en arrière-plan) + def send_validation_emails(): + try: + student = registerForm.student + student_name = f"{student.first_name} {student.last_name}" + class_name = None + if student.associated_class: + class_name = student.associated_class.atmosphere_name + + guardians = student.guardians.all() + for guardian in guardians: + email = None + if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile: + email = guardian.profile_role.profile.email + if not email: + email = getattr(guardian, "email", None) + + if email: + logger.info(f"[RF_VALIDATED] Envoi email de validation à {email} pour l'élève {student_name}") + mailer.sendValidationDossier(email, registerForm.establishment.pk, student_name, class_name) + except Exception as e: + logger.error(f"[RF_VALIDATED] Erreur lors de l'envoi de l'email de validation: {e}") + + # Lancer l'envoi d'email dans un thread séparé pour ne pas bloquer la réponse + email_thread = threading.Thread(target=send_validation_emails) + email_thread.start() + updateStateMachine(registerForm, 'EVENT_VALIDATE') # Retourner les données mises à jour diff --git a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js index 886b6a6..567074a 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js @@ -120,19 +120,9 @@ export default function Page() { editFn(templateId, updateData, csrfToken) .then((response) => { logger.debug(`Document ${validated ? 'validé' : 'refusé'} (type: ${type}, id: ${templateId})`, response); - showNotification( - `Le document a bien été ${validated ? 'validé' : 'refusé'}.`, - 'success', - 'Succès' - ); }) .catch((error) => { logger.error('Erreur lors de la validation/refus du document:', error); - showNotification( - `Erreur lors de la ${validated ? 'validation' : 'refus'} du document.`, - 'error', - 'Erreur' - ); }); }; diff --git a/Front-End/src/components/Form/FormRenderer.js b/Front-End/src/components/Form/FormRenderer.js index 0576174..3a0458f 100644 --- a/Front-End/src/components/Form/FormRenderer.js +++ b/Front-End/src/components/Form/FormRenderer.js @@ -1,5 +1,6 @@ import logger from '@/utils/logger'; import { useForm, Controller } from 'react-hook-form'; +import { useEffect } from 'react'; import SelectChoice from './SelectChoice'; import InputTextIcon from './InputTextIcon'; import * as LucideIcons from 'lucide-react'; @@ -28,6 +29,7 @@ export function getIcon(name) { export default function FormRenderer({ formConfig, csrfToken, + initialValues = {}, onFormSubmit = (data) => { alert(JSON.stringify(data, null, 2)); }, // Callback de soumission personnalisé (optionnel) @@ -37,7 +39,14 @@ export default function FormRenderer({ control, formState: { errors }, reset, - } = useForm(); + } = useForm({ defaultValues: initialValues }); + + // Réinitialiser le formulaire quand les valeurs initiales changent + useEffect(() => { + if (initialValues && Object.keys(initialValues).length > 0) { + reset(initialValues); + } + }, [initialValues, reset]); // Fonction utilitaire pour envoyer les données au backend const sendFormDataToBackend = async (formData) => { diff --git a/Front-End/src/components/Inscription/DynamicFormsList.js b/Front-End/src/components/Inscription/DynamicFormsList.js index 353076c..87b9b22 100644 --- a/Front-End/src/components/Inscription/DynamicFormsList.js +++ b/Front-End/src/components/Inscription/DynamicFormsList.js @@ -23,26 +23,6 @@ export default function DynamicFormsList({ onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent) }) { const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0); - // Remet formsValidation à false et formsData à undefined lors de la sélection d'un document refusé - useEffect(() => { - const currentTemplate = schoolFileTemplates[currentTemplateIndex]; - if ( - currentTemplate && - currentTemplate.isValidated === false && - formsValidation[currentTemplate.id] !== true - ) { - setFormsValidation((prev) => { - const newValidation = { ...prev }; - newValidation[currentTemplate.id] = false; - return newValidation; - }); - setFormsData((prev) => { - const newData = { ...prev }; - delete newData[currentTemplate.id]; - return newData; - }); - } - }, [currentTemplateIndex, schoolFileTemplates, formsValidation]); const [formsData, setFormsData] = useState({}); const [formsValidation, setFormsValidation] = useState({}); const fileInputRefs = React.useRef({}); @@ -51,37 +31,46 @@ export default function DynamicFormsList({ useEffect(() => { // Initialisation complète de formsValidation et formsData pour chaque template if (schoolFileTemplates && schoolFileTemplates.length > 0) { - // Initialiser formsData pour chaque template (avec données existantes ou objet vide) - const dataState = {}; - schoolFileTemplates.forEach((tpl) => { - if ( - existingResponses && - existingResponses[tpl.id] && - Object.keys(existingResponses[tpl.id]).length > 0 - ) { - dataState[tpl.id] = existingResponses[tpl.id]; - } else { - dataState[tpl.id] = {}; - } + // Fusionner avec l'état existant pour préserver les données locales + setFormsData((prevData) => { + const dataState = { ...prevData }; + schoolFileTemplates.forEach((tpl) => { + // Ne mettre à jour que si on n'a pas encore de données locales ou si les données du serveur ont changé + const hasLocalData = prevData[tpl.id] && Object.keys(prevData[tpl.id]).length > 0; + const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0; + + if (!hasLocalData && hasServerData) { + // Pas de données locales mais données serveur : utiliser les données serveur + dataState[tpl.id] = existingResponses[tpl.id]; + } else if (!hasLocalData && !hasServerData) { + // Pas de données du tout : initialiser à vide + dataState[tpl.id] = {}; + } + // Si hasLocalData : on garde les données locales existantes + }); + return dataState; }); - setFormsData(dataState); - // Initialiser formsValidation pour chaque template - const validationState = {}; - schoolFileTemplates.forEach((tpl) => { - if ( - existingResponses && - existingResponses[tpl.id] && - Object.keys(existingResponses[tpl.id]).length > 0 - ) { - validationState[tpl.id] = true; - } else { - validationState[tpl.id] = false; - } + // Fusionner avec l'état de validation existant + setFormsValidation((prevValidation) => { + const validationState = { ...prevValidation }; + schoolFileTemplates.forEach((tpl) => { + const hasLocalValidation = prevValidation[tpl.id] === true; + const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0; + + if (!hasLocalValidation && hasServerData) { + // Pas validé localement mais données serveur : marquer comme validé + validationState[tpl.id] = true; + } else if (validationState[tpl.id] === undefined) { + // Pas encore initialisé : initialiser à false + validationState[tpl.id] = false; + } + // Si hasLocalValidation : on garde l'état local existant + }); + return validationState; }); - setFormsValidation(validationState); } - }, [existingResponses]); + }, [existingResponses, schoolFileTemplates]); // Mettre à jour la validation globale quand la validation des formulaires change useEffect(() => { @@ -163,6 +152,8 @@ export default function DynamicFormsList({ return schoolFileTemplates[currentTemplateIndex]; }; + const currentTemplate = getCurrentTemplate(); + // Handler d'upload pour formulaire existant const handleUpload = async (file, selectedFile) => { if (!file || !selectedFile) return; @@ -399,6 +390,11 @@ export default function DynamicFormsList({ submitLabel: currentTemplate.formTemplateData?.submitLabel || 'Valider', }} + initialValues={ + formsData[currentTemplate.id] || + existingResponses[currentTemplate.id] || + {} + } onFormSubmit={(formData) => handleFormSubmit(formData, currentTemplate.id) } @@ -408,38 +404,45 @@ export default function DynamicFormsList({ ) : ( // Formulaire existant (PDF, image, etc.)
-
- {currentTemplate.file && currentTemplate.isValidated === true ? ( -