diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 76420d2..7c28c0c 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -318,20 +318,27 @@ 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}" + # Utilise l'ID si le nom n'est pas encore disponible + est_name = None + if instance.establishment and instance.establishment.name: + est_name = instance.establishment.name + else: + # fallback si pas d'établissement (devrait être rare) + est_name = "unknown_establishment" + return f"{est_name}/Formulaires/{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.)" ) + establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='school_file_masters', null=True, blank=True) def __str__(self): return f'{self.name} - {self.id}' @@ -342,6 +349,131 @@ class RegistrationSchoolFileMaster(models.Model): return self.file.url return None + def save(self, *args, **kwargs): + import os + affected_rf_ids = set() + is_new = self.pk is None + + super().save(*args, **kwargs) + + # 2. Gestion des groupes pour la synchro des templates + if is_new: + from Subscriptions.models import RegistrationForm + new_groups = set(self.groups.values_list('id', flat=True)) + affected_rf_ids.update( + RegistrationForm.objects.filter(fileGroup__in=list(new_groups)).values_list('pk', flat=True) + ) + else: + try: + old = RegistrationSchoolFileMaster.objects.get(pk=self.pk) + old_groups = set(old.groups.values_list('id', flat=True)) + new_groups = set(self.groups.values_list('id', flat=True)) + from Subscriptions.models import RegistrationForm + affected_rf_ids.update( + RegistrationForm.objects.filter(fileGroup__in=list(old_groups | new_groups)).values_list('pk', flat=True) + ) + form_data_changed = ( + old.formMasterData != self.formMasterData + and self.formMasterData + and isinstance(self.formMasterData, dict) + and self.formMasterData.get("fields") + ) + name_changed = old.name != self.name + # --- Correction spécifique pour les fichiers existants (PDF, DOC, etc.) --- + # Si le nom change et qu'on a un fichier existant, on doit renommer physiquement le fichier + if old.file and not self.file and name_changed: + old_file_path = old.file.path + ext = os.path.splitext(old_file_path)[1] + clean_name = (self.name or 'document').replace(' ', '_').replace('/', '_') + new_filename = f"{clean_name}{ext}" + new_rel_path = os.path.join(os.path.dirname(old.file.name), new_filename) + new_abs_path = os.path.join(os.path.dirname(old_file_path), new_filename) + logger.info(f"Renommage fichier: {old_file_path} -> {new_abs_path}") + try: + if not os.path.exists(new_abs_path): + os.rename(old_file_path, new_abs_path) + self.file.name = new_rel_path + logger.info(f"self.file.name après renommage: {self.file.name}") + super().save(update_fields=["file"]) + else: + logger.info(f"Le fichier cible existe déjà: {new_abs_path}") + except Exception as e: + logger.error(f"Erreur lors du renommage du fichier master: {e}") + elif old.file and self.file and self.file != old.file: + old.file.delete(save=False) + elif old.file and form_data_changed: + old.file.delete(save=False) + self.file = None + except RegistrationSchoolFileMaster.DoesNotExist: + pass + + # Harmonisation : gestion du fichier (dynamique ou existant) sans appeler registration_school_file_master_upload_to + # Pour les formulaires dynamiques (PDF à générer) + if not self.file and self.formMasterData and isinstance(self.formMasterData, dict) and self.formMasterData.get("fields"): + try: + from Subscriptions.util import generate_form_json_pdf + pdf_filename = f"{self.name or 'formulaire'}.pdf" + abs_path = os.path.join(settings.MEDIA_ROOT, self.file.field.upload_to(self, pdf_filename)) + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + if os.path.exists(abs_path): + try: + os.remove(abs_path) + logger.info(f"Suppression fichier existant: {abs_path}") + except Exception as e: + logger.error(f"Erreur suppression fichier existant avant save: {e}") + pdf_file = generate_form_json_pdf(self, self.formMasterData) + logger.info(f"Sauvegarde PDF dynamique: {pdf_filename}") + self.file.save(pdf_filename, pdf_file, save=False) + logger.info(f"self.file.name après save PDF dynamique: {self.file.name}") + super().save(update_fields=["file"]) + except Exception as e: + logger.error(f"Erreur lors de la génération automatique du PDF pour le master {self.pk}: {e}") + # Pour les fichiers existants uploadés + elif self.file and hasattr(self.file, 'name') and self.name: + # On force le nom du fichier (nom du master + extension) si besoin + ext = os.path.splitext(self.file.name)[1] + clean_name = (self.name or 'document').replace(' ', '_').replace('/', '_') + final_file_name = f"{clean_name}{ext}" + # Si le nom ne correspond pas, on renomme le fichier physique et le FileField + if os.path.basename(self.file.name) != final_file_name: + current_path = self.file.path + abs_path = os.path.join(settings.MEDIA_ROOT, self.file.field.upload_to(self, final_file_name)) + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + if os.path.exists(abs_path): + try: + os.remove(abs_path) + logger.info(f"Suppression fichier existant: {abs_path}") + except Exception as e: + logger.error(f"Erreur suppression fichier existant avant renommage: {e}") + try: + os.rename(current_path, abs_path) + self.file.name = os.path.relpath(abs_path, settings.MEDIA_ROOT) + logger.info(f"Déplacement fichier existant: {current_path} -> {abs_path}") + super().save(update_fields=["file"]) + except Exception as e: + logger.error(f"Erreur lors du déplacement du fichier existant: {e}") + + # 4. Synchronisation des templates pour tous les dossiers d'inscription concernés (création ou modification) + try: + from Subscriptions.util import create_templates_for_registration_form + from Subscriptions.models import RegistrationForm + for rf_id in affected_rf_ids: + try: + rf = RegistrationForm.objects.get(pk=rf_id) + create_templates_for_registration_form(rf) + except Exception as e: + logger.error(f"Erreur lors de la synchronisation des templates pour RF {rf_id} après modification du master {self.pk}: {e}") + except Exception as e: + logger.error(f"Erreur globale lors de la synchronisation des templates après modification du master {self.pk}: {e}") + def delete(self, *args, **kwargs): + # Supprimer le fichier physique du master si présent + if self.file and hasattr(self.file, 'path') and os.path.exists(self.file.path): + try: + self.file.delete(save=False) + except Exception as e: + logger.error(f"Erreur lors de la suppression du fichier master: {e}") + super().delete(*args, **kwargs) + ####### Parent files masters (documents à fournir par les parents) ####### class RegistrationParentFileMaster(models.Model): groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_file_masters', blank=True) @@ -354,7 +486,7 @@ class RegistrationParentFileMaster(models.Model): ############################################################ def registration_school_file_upload_to(instance, filename): - return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/school/{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}" diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index d682fa3..e4c338e 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -109,6 +109,7 @@ def create_templates_for_registration_form(register_form): ) created = [] + logger.info("util.create_templates_for_registration_form - create_templates_for_registration_form") # Récupérer les masters du fileGroup courant current_group = getattr(register_form, "fileGroup", None) @@ -135,6 +136,7 @@ def create_templates_for_registration_form(register_form): school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct() parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct() + logger.info("util.create_templates_for_registration_form - school_masters récupérés") school_master_ids = {m.pk for m in school_masters} parent_master_ids = {m.pk for m in parent_masters} @@ -162,6 +164,7 @@ def create_templates_for_registration_form(register_form): logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk) # Créer les school templates manquants + logger.info("util.create_templates_for_registration_form - Créer les school templates manquants") for m in school_masters: exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists() if exists: @@ -169,44 +172,61 @@ def create_templates_for_registration_form(register_form): 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) + # --- Correction : Générer un nom de fichier unique uniquement si le master n'a pas de fichier --- + file_name = None + if m.file and hasattr(m.file, 'name') and m.file.name: + # Utiliser le nom du fichier tel qu'il est stocké dans le master (pas de suffixe aléatoire ici) + file_name = os.path.basename(m.file.name) + logger.info(f"util.create_templates_for_registration_form - file_name 1 : {file_name}") + elif m.file: + file_name = str(m.file) else: - # Générer le PDF du template à partir du JSON du master + # Générer le PDF si besoin (rare ici) try: pdf_file = generate_form_json_pdf(register_form, m.formMasterData) - file_to_attach = pdf_file + file_name = os.path.basename(pdf_file.name) except Exception as e: logger.error(f"Erreur lors de la génération du PDF pour le template: {e}") - file_to_attach = None + file_name = None - tmpl = RegistrationSchoolFileTemplate.objects.create( + logger.info(f"util.create_templates_for_registration_form - file_name : {file_name}") + + tmpl = RegistrationSchoolFileTemplate( master=m, registration_form=register_form, name=m.name or "", formTemplateData=m.formMasterData or [], slug=slug, - file=file_to_attach, ) + if file_name: + from django.core.files.base import ContentFile + # Vérifier si le fichier existe déjà dans MEDIA_ROOT (copie du master) + upload_rel_path = registration_school_file_upload_to( + type("Tmp", (), { + "registration_form": register_form, + "establishment": getattr(register_form, "establishment", None), + "student": getattr(register_form, "student", None) + })(), + file_name + ) + abs_path = os.path.join(settings.MEDIA_ROOT, upload_rel_path) + master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None + + # Si le fichier n'existe pas dans le dossier cible, le copier depuis le master + if master_file_path and not os.path.exists(abs_path): + try: + import shutil + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + shutil.copy2(master_file_path, abs_path) + logger.info(f"util.create_templates_for_registration_form - Copie du fichier master {master_file_path} -> {abs_path}") + except Exception as e: + logger.error(f"Erreur lors de la copie du fichier master pour le template: {e}") + + # Associer le fichier existant (ou copié) au template + tmpl.file.name = upload_rel_path + tmpl.save() created.append(tmpl) - logger.info("Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk) + logger.info("util.create_templates_for_registration_form - 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: diff --git a/Back-End/Subscriptions/views/registration_school_file_masters_views.py b/Back-End/Subscriptions/views/registration_school_file_masters_views.py index 2f794a6..2521536 100644 --- a/Back-End/Subscriptions/views/registration_school_file_masters_views.py +++ b/Back-End/Subscriptions/views/registration_school_file_masters_views.py @@ -139,6 +139,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView): # Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent) if removed_group_ids: + logger.info("REMOVE IDs") try: rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct() for rf in rfs_removed: @@ -151,6 +152,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView): # Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants if added_group_ids: + logger.info("ADD IDs") try: rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct() for rf in rfs_added: diff --git a/Front-End/src/app/actions/registerFileGroupAction.js b/Front-End/src/app/actions/registerFileGroupAction.js index f8a8f6c..1927804 100644 --- a/Front-End/src/app/actions/registerFileGroupAction.js +++ b/Front-End/src/app/actions/registerFileGroupAction.js @@ -106,7 +106,6 @@ export const createRegistrationSchoolFileMaster = (data, csrfToken) => { body: data, headers: { 'X-CSRFToken': csrfToken, - // Pas de Content-Type, le navigateur gère pour FormData }, credentials: 'include', }) @@ -187,10 +186,9 @@ export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => { `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`, { method: 'PUT', - body: JSON.stringify(data), + body: data, headers: { 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json', }, credentials: 'include', } diff --git a/Front-End/src/components/Form/FormTemplateBuilder.js b/Front-End/src/components/Form/FormTemplateBuilder.js index eafdb4a..3e8ef39 100644 --- a/Front-End/src/components/Form/FormTemplateBuilder.js +++ b/Front-End/src/components/Form/FormTemplateBuilder.js @@ -37,6 +37,7 @@ import { FileUp, PenTool, } from 'lucide-react'; +import CheckBox from '@/components/Form/CheckBox'; const FIELD_TYPES_ICON = { text: { icon: TextCursorInput }, @@ -520,29 +521,27 @@ export default function FormTemplateBuilder({ Groupes d'inscription{' '} * -
+
{groups && groups.length > 0 ? ( groups.map((group) => ( - + { + let group_ids = selectedGroups; + if (group_ids.includes(group.id)) { + group_ids = group_ids.filter((id) => id !== group.id); + } else { + group_ids = [...group_ids, group.id]; + } + setSelectedGroups(group_ids); + }} + fieldName="groups" + itemLabelFunc={() => group.name} + /> )) ) : (

diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js index bc75419..bbd3638 100644 --- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js +++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js @@ -1,11 +1,13 @@ import React, { useState, useEffect, useRef } from 'react'; import { - Edit3, + Edit, Trash2, FileText, Star, ChevronDown, - Plus + Plus, + Archive, + Eye } from 'lucide-react'; import Modal from '@/components/Modal'; import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder'; @@ -37,6 +39,10 @@ import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModa import FileUpload from '@/components/Form/FileUpload'; import SectionTitle from '@/components/SectionTitle'; import DropdownMenu from '@/components/DropdownMenu'; +import CheckBox from '@/components/Form/CheckBox'; +import Button from '@/components/Form/Button'; +import InputText from '@/components/Form/InputText'; +import { BASE_URL } from '@/utils/Url'; function getItemBgColor(type, selected, forceTheme = false) { // Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné @@ -140,7 +146,7 @@ function SimpleList({ return (

  • { if (!selectable || !onSelect) return; if (selected) { @@ -191,6 +197,7 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId, }) { + const [showFilePreview, setShowFilePreview] = useState(false); const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [parentFiles, setParentFileMasters] = useState([]); const [groups, setGroups] = useState([]); @@ -208,7 +215,6 @@ export default function FilesGroupsManagement({ const [selectedGroupId, setSelectedGroupId] = useState(null); const [showHelp, setShowHelp] = useState(false); const { showNotification } = useNotification(); - const [isFormBuilderOpen, setIsFormBuilderOpen] = useState(false); const [isFileUploadOpen, setIsFileUploadOpen] = useState(false); const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false); const [editingParentFile, setEditingParentFile] = useState(null); @@ -223,9 +229,10 @@ export default function FilesGroupsManagement({ const handleDocDropdownSelect = (type) => { setIsDocDropdownOpen(false); if (type === 'formulaire') { - setIsFormBuilderOpen(true); + // Ouvre la modale unique en mode création setIsEditing(false); setFileToEdit(null); + setIsModalOpen(true); } else if (type === 'formulaire_existant') { setIsFileUploadPopupOpen(true); setFileToEdit({}); @@ -320,9 +327,16 @@ export default function FilesGroupsManagement({ }; const editTemplateMaster = (file) => { - setIsEditing(true); - setFileToEdit(file); - setIsModalOpen(true); + // Si le formulaire n'est pas personnalisé, ouvrir la popup de téléchargement + if (!file.formMasterData || !Array.isArray(file.formMasterData.fields) || file.formMasterData.fields.length === 0) { + setFileToEdit(file); + setIsFileUploadPopupOpen(true); + setIsEditing(true); + } else { + setIsEditing(true); + setFileToEdit(file); + setIsModalOpen(true); + } }; const handleCreateSchoolFileMaster = ({ name, group_ids, formMasterData, file }) => { @@ -332,10 +346,23 @@ export default function FilesGroupsManagement({ name, groups: group_ids, formMasterData, + establishment: selectedEstablishmentId, }; dataToSend.append('data', JSON.stringify(jsonData)); if (file) { - dataToSend.append('file', file, file.path || file.name); + // Récupérer l'extension du fichier d'origine + let extension = ''; + if (file.name && file.name.lastIndexOf('.') !== -1) { + extension = file.name.substring(file.name.lastIndexOf('.')); + } + // Nettoyer le nom saisi pour le fichier (éviter les caractères spéciaux) + const cleanName = (name || 'document') + .replace(/[^a-zA-Z0-9_\-]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + // Générer le nom de fichier final + const finalFileName = `${cleanName}${extension}`; + dataToSend.append('file', file, finalFileName); } createRegistrationSchoolFileMaster(dataToSend, csrfToken) @@ -363,15 +390,93 @@ export default function FilesGroupsManagement({ group_ids, formMasterData, id, + file, }) => { - const data = { - name: name, - groups: group_ids, - formMasterData: formMasterData, - }; - logger.debug(data); + // Correction : normaliser group_ids pour ne garder que les IDs (number/string) + let normalizedGroupIds = []; + if (Array.isArray(group_ids)) { + normalizedGroupIds = group_ids.map(g => + typeof g === 'object' && g !== null && 'id' in g ? g.id : g + ); + } - editRegistrationSchoolFileMaster(id, data, csrfToken) + const dataToSend = new FormData(); + const jsonData = { + name: name, + groups: normalizedGroupIds, + formMasterData: formMasterData, + establishment: selectedEstablishmentId + }; + dataToSend.append('data', JSON.stringify(jsonData)); + + // Cas 1 : Nouveau fichier sélectionné (File/Blob) + if (file instanceof File || file instanceof Blob) { + let extension = ''; + if (file.name && file.name.lastIndexOf('.') !== -1) { + extension = file.name.substring(file.name.lastIndexOf('.')); + } + const cleanName = (name || 'document') + .replace(/[^a-zA-Z0-9_\-]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + const finalFileName = `${cleanName}${extension}`; + dataToSend.append('file', file, finalFileName); + } + // Cas 2 : Pas de nouveau fichier, mais le nom a changé → renvoyer le fichier existant avec le nouveau nom + else if (typeof file === 'string' && file) { + // Extraire l'extension du path existant + let extension = ''; + const lastDot = file.lastIndexOf('.'); + if (lastDot !== -1) { + extension = file.substring(lastDot); + } + const cleanName = (name || 'document') + .replace(/[^a-zA-Z0-9_\-]/g, '_') + .replace(/_+/g, '_') + .replace(/^_+|_+$/g, ''); + const finalFileName = `${cleanName}${extension}`; + // Correction : il faut récupérer le fichier à l'URL d'origine, pas à la nouvelle URL renommée + // On utilise le path original (file) pour le fetch, pas le chemin avec le nouveau nom + fetch(`${BASE_URL}${file}`) + .then(response => { + if (!response.ok) throw new Error('Fichier distant introuvable'); + return response.blob(); + }) + .then(blob => { + dataToSend.append('file', blob, finalFileName); + editRegistrationSchoolFileMaster(id, dataToSend, csrfToken) + .then((data) => { + setSchoolFileMasters((prevFichiers) => + prevFichiers.map((f) => (f.id === id ? data : f)) + ); + setIsModalOpen(false); + showNotification( + `Le formulaire "${name}" a été modifié avec succès.`, + 'success', + 'Succès' + ); + }) + .catch((error) => { + logger.error('Error editing form:', error); + showNotification( + 'Erreur lors de la modification du formulaire', + 'error', + 'Erreur' + ); + }); + }) + .catch((error) => { + logger.error('Erreur lors de la récupération du fichier existant pour renommage:', error); + showNotification( + 'Erreur lors de la récupération du fichier existant pour renommage', + 'error', + 'Erreur' + ); + }); + return; // On sort ici car l'appel API est fait dans le fetch + } + // Cas standard (nouveau fichier ou pas de renommage) + editRegistrationSchoolFileMaster(id, dataToSend, csrfToken) .then((data) => { setSchoolFileMasters((prevFichiers) => prevFichiers.map((f) => (f.id === id ? data : f)) @@ -552,17 +657,25 @@ export default function FilesGroupsManagement({ }; const handleDelete = (id) => { - return deleteRegistrationParentFileMaster(id, csrfToken) - .then(() => { - // Mettre à jour la liste des fichiers parents en supprimant l'élément correspondant - setParentFileMasters((prevFiles) => - prevFiles.filter((file) => file.id !== id) - ); - logger.debug('Document parent supprimé avec succès:', id); - }) - .catch((error) => { - logger.error('Erreur lors de la suppression du fichier parent:', error); - }); + // Vérification avant suppression : afficher une popup de confirmation + setRemovePopupMessage( + 'Attention !\nVous êtes sur le point de supprimer la pièce à fournir.\nÊtes-vous sûr(e) de vouloir poursuivre l\'opération ?' + ); + setRemovePopupOnConfirm(() => () => { + deleteRegistrationParentFileMaster(id, csrfToken) + .then(() => { + setParentFileMasters((prevFiles) => prevFiles.filter((file) => file.id !== id)); + logger.debug('Document parent supprimé avec succès:', id); + showNotification('La pièce à fournir a été supprimée avec succès.', 'success', 'Succès'); + setRemovePopupVisible(false); + }) + .catch((error) => { + logger.error('Erreur lors de la suppression du fichier parent:', error); + showNotification('Erreur lors de la suppression de la pièce à fournir.', 'error', 'Erreur'); + setRemovePopupVisible(false); + }); + }); + setRemovePopupVisible(true); }; // Ouvre la modale de création d'une pièce à fournir @@ -575,6 +688,7 @@ export default function FilesGroupsManagement({ const openEditParentFileModal = (file) => { setEditingParentFile(file); setIsParentFileModalOpen(true); + setIsEditing(true); }; // Ferme la modale de pièce à fournir @@ -690,316 +804,178 @@ export default function FilesGroupsManagement({ return count; }; - // Nouvelle disposition : sections côte à côte alignées + // Utilitaire pour ouvrir la modale FormTemplateBuilder (création ou édition) + const openFormBuilderModal = (editing = false, initialData = null) => { + setIsEditing(editing); + setFileToEdit(initialData); + setIsModalOpen(true); + }; + return (
    - {/* Aide optionnelle */} -
    {renderExplanation()}
    + {/* Aide optionnelle */} +
    {renderExplanation()}
    - {/* 2 colonnes : groupes à gauche, documents à droite */} -
    - {/* Colonne groupes (1/3) */} -
    -
    - -
    - -
    - 'blue'} - minHeight="min-h-[60px]" - selectable={true} - forceTheme={false} - groupDocCount={getGroupDocCount} - actionButtons={(row) => ( -
    - - -
    - )} - /> -
    - - {/* Colonne documents (2/3) */} -
    -
    - -
    - - - - - } - items={[ - { - type: 'item', - label: ( - - - Formulaire personnalisé - - ), - onClick: () => handleDocDropdownSelect('formulaire'), - }, - { - type: 'item', - label: ( - - - Formulaire existant - - ), - onClick: () => handleDocDropdownSelect('formulaire_existant'), - }, - { - type: 'item', - label: ( - - - Pièce à fournir - - ), - onClick: () => handleDocDropdownSelect('parent'), - }, - ]} - buttonClassName="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold" - menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20" - dropdownOpen={isDocDropdownOpen} - setDropdownOpen={setIsDocDropdownOpen} - /> -
    - {!selectedGroupId ? ( -
    - Sélectionner un dossier d'inscription + {/* 2 colonnes : groupes à gauche, documents à droite */} +
    + {/* Colonne groupes (1/3) */} +
    +
    + +
    +
    - ) : ( item._type} - minHeight="min-h-[240px]" - selectable={false} - forceTheme={true} - listClassName="" - itemClassName="text-gray-800 bg-white" - title="" - headerContent={null} - showGroups={false} + items={groups} + selectedId={selectedGroupId} + onSelect={handleGroupSelect} + getItemType={() => 'blue'} + minHeight="min-h-[60px]" + selectable={true} + forceTheme={false} + groupDocCount={getGroupDocCount} actionButtons={(row) => (
    - {row._type === 'emerald' ? ( - <> - - - - ) : ( - <> - - - - )} + +
    )} /> - )} -
    -
    - - {/* Modals pour création/édition */} - - { - handleCreateSchoolFileMaster(data); - setIsFormBuilderOpen(false); - }} - groups={groups} - isEditing={false} - /> - - - {/* Popup pour téléchargement d'un document existant */} - -
    { - e.preventDefault(); - if (!fileToEdit?.name || !fileToEdit?.group_ids || !fileToEdit?.file) return; - handleCreateSchoolFileMaster({ - name: fileToEdit.name, - group_ids: fileToEdit.group_ids, - file: fileToEdit.file, - }); - setIsFileUploadPopupOpen(false); - setFileToEdit(null); - }} - > - setFileToEdit({ ...fileToEdit, name: e.target.value })} - required - /> -
    - -
    - {groups && groups.length > 0 ? ( - groups.map((group) => ( - - )) - ) : ( -

    - Aucun groupe disponible -

    - )} -
    - - setFileToEdit({ ...fileToEdit, file }) - } - required - enable - /> - - -
    - { - setIsParentFileModalOpen(open); - if (!open) setEditingParentFile(null); - }} - title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'} - modalClassName="w-full max-w-md" - > - setIsParentFileModalOpen(false)} - /> - - - {/* Modals et popups pour édition */} - { - setIsModalOpen(isOpen); - if (!isOpen) { - setFileToEdit(null); - setIsEditing(false); - } - }} - title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'} - modalClassName="w-11/12 h-5/6" - > - { - (isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data); - setIsModalOpen(false); - }} - initialData={fileToEdit} - groups={groups} - isEditing={isEditing} - /> - + {/* Colonne documents (2/3) */} +
    +
    + +
    + + + + + } + items={[ + { + type: 'item', + label: ( + + + Formulaire personnalisé + + ), + onClick: () => handleDocDropdownSelect('formulaire'), + }, + { + type: 'item', + label: ( + + + Formulaire existant + + ), + onClick: () => handleDocDropdownSelect('formulaire_existant'), + }, + { + type: 'item', + label: ( + + + Pièce à fournir + + ), + onClick: () => handleDocDropdownSelect('parent'), + }, + ]} + buttonClassName="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold" + menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20" + dropdownOpen={isDocDropdownOpen} + setDropdownOpen={setIsDocDropdownOpen} + /> +
    + {!selectedGroupId ? ( +
    + Sélectionner un dossier d'inscription +
    + ) : ( + item._type} + minHeight="min-h-[240px]" + selectable={false} + forceTheme={true} + listClassName="" + itemClassName="text-gray-800 bg-white" + title="" + headerContent={null} + showGroups={false} + actionButtons={(row) => ( +
    + + +
    + )} + /> + )} +
    +
    + {/* Popup pour création/édition d'un groupe de documents */} { @@ -1012,13 +988,349 @@ export default function FilesGroupsManagement({ groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier" } > - { - handleGroupSubmit(data); - setIsGroupModalOpen(false); - }} - initialData={groupToEdit} - /> +
    + { + handleGroupSubmit(data); + setIsGroupModalOpen(false); + }} + initialData={groupToEdit} + /> +
    +
    + + {/* Modals pour création/édition d'un formulaire dynamique */} + { + setIsModalOpen(isOpen); + if (!isOpen) { + setFileToEdit(null); + setIsEditing(false); + } + }} + title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire personnalisé'} + > +
    + { + (isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data); + setIsModalOpen(false); + }} + initialData={isEditing ? fileToEdit : undefined} + groups={groups} + isEditing={isEditing} + /> +
    +
    + + {/* Popup pour création/édition d'un formulaire d'école déjà existant */} + +
    + {fileToEdit && fileToEdit.id ? ( +
    { + e.preventDefault(); + if (!fileToEdit?.name || !fileToEdit?.groups || fileToEdit.groups.length === 0 || !fileToEdit?.file) return; + if (isEditing) { + handleEditSchoolFileMaster({ + id: fileToEdit.id, + name: fileToEdit.name, + group_ids: fileToEdit.groups, + file: fileToEdit.file, + formMasterData: fileToEdit.formMasterData, + }); + } else { + handleCreateSchoolFileMaster({ + name: fileToEdit.name, + group_ids: fileToEdit.groups, + file: fileToEdit.file, + }); + } + setIsFileUploadPopupOpen(false); + setFileToEdit(null); + }} + > + setFileToEdit({ ...fileToEdit, name: e.target.value })} + required + /> +
    + +
    + {groups && groups.length > 0 ? ( + groups.map((group) => { + const selectedGroupIds = (fileToEdit?.groups || []).map(g => + typeof g === 'object' && g !== null && 'id' in g ? g.id : g + ); + return ( + { + let group_ids = selectedGroupIds; + if (group_ids.includes(group.id)) { + group_ids = group_ids.filter((id) => id !== group.id); + } else { + group_ids = [...group_ids, group.id]; + } + setFileToEdit({ ...fileToEdit, groups: group_ids }); + }} + fieldName="groups" + itemLabelFunc={() => group.name} + /> + ); + }) + ) : ( +

    + Aucun groupe disponible +

    + )} +
    +
    + {/* Label document sélectionné sans icône œil */} + {fileToEdit?.file && ( +
    + + {fileToEdit.file.name || fileToEdit.file.path || 'Document sélectionné'} +
    + )} + + setFileToEdit({ ...fileToEdit, file }) + } + required + enable + /> +
    +
    + + {/* Popup pour création/édition d'un document parent */} + { + setIsParentFileModalOpen(open); + if (!open) setEditingParentFile(null); + }} + title={ + editingParentFile && editingParentFile.id + ? 'Modifier la pièce à fournir' + : 'Créer une pièce à fournir' + } + > +
    +
    { + e.preventDefault(); + if ( + !editingParentFile?.name || + !editingParentFile?.groups || + editingParentFile.groups.length === 0 + ) return; + const payload = { + name: editingParentFile.name, + description: editingParentFile.description || '', + groups: editingParentFile.groups, + is_required: !!editingParentFile.is_required, + }; + if (editingParentFile?.id) { + handleEdit(editingParentFile.id, payload); + } else { + handleCreate(payload); + } + setIsParentFileModalOpen(false); + setEditingParentFile(null); + }} + > + setEditingParentFile({ ...editingParentFile, name: e.target.value })} + required + /> + setEditingParentFile({ ...editingParentFile, description: e.target.value })} + required={false} + /> +
    + +
    + {groups && groups.length > 0 ? ( + groups.map((group) => { + const selectedGroupIds = (editingParentFile?.groups || []).map(g => + typeof g === 'object' && g !== null && 'id' in g ? g.id : g + ); + return ( + { + let group_ids = selectedGroupIds; + if (group_ids.includes(group.id)) { + group_ids = group_ids.filter((id) => id !== group.id); + } else { + group_ids = [...group_ids, group.id]; + } + setEditingParentFile({ ...editingParentFile, groups: group_ids }); + }} + fieldName="groups" + itemLabelFunc={() => group.name} + /> + ); + }) + ) : ( +

    + Aucun groupe disponible +

    + )} +
    +
    +
    + + setEditingParentFile({ + ...editingParentFile, + is_required: !editingParentFile?.is_required, + }) + } + fieldName="is_required" + itemLabelFunc={() => 'Requis'} + /> +
    +
    -
    - - 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-blue-500 focus:border-blue-500" - required - /> -
    - -
    - -