mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Sauvegarde des formulaires d'école dans les bons dossiers /
utilisation des bons composants dans les modales [N3WTS-17]
This commit is contained in:
@ -318,20 +318,27 @@ class RegistrationForm(models.Model):
|
|||||||
####### Formulaires masters (documents école, à signer ou pas) #######
|
####### Formulaires masters (documents école, à signer ou pas) #######
|
||||||
def registration_school_file_master_upload_to(instance, filename):
|
def registration_school_file_master_upload_to(instance, filename):
|
||||||
# Stocke les fichiers masters dans un dossier dédié
|
# 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):
|
class RegistrationSchoolFileMaster(models.Model):
|
||||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
is_required = models.BooleanField(default=False)
|
is_required = models.BooleanField(default=False)
|
||||||
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||||
# Nouveau champ pour formulaire existant (PDF, DOC, etc.)
|
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
upload_to=registration_school_file_master_upload_to,
|
upload_to=registration_school_file_master_upload_to,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Fichier du formulaire existant (PDF, DOC, etc.)"
|
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):
|
def __str__(self):
|
||||||
return f'{self.name} - {self.id}'
|
return f'{self.name} - {self.id}'
|
||||||
@ -342,6 +349,131 @@ class RegistrationSchoolFileMaster(models.Model):
|
|||||||
return self.file.url
|
return self.file.url
|
||||||
return None
|
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) #######
|
####### Parent files masters (documents à fournir par les parents) #######
|
||||||
class RegistrationParentFileMaster(models.Model):
|
class RegistrationParentFileMaster(models.Model):
|
||||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_file_masters', blank=True)
|
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):
|
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):
|
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}"
|
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||||
|
|||||||
@ -109,6 +109,7 @@ def create_templates_for_registration_form(register_form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
created = []
|
created = []
|
||||||
|
logger.info("util.create_templates_for_registration_form - create_templates_for_registration_form")
|
||||||
|
|
||||||
# Récupérer les masters du fileGroup courant
|
# Récupérer les masters du fileGroup courant
|
||||||
current_group = getattr(register_form, "fileGroup", None)
|
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()
|
school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct()
|
||||||
parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||||
|
logger.info("util.create_templates_for_registration_form - school_masters récupérés")
|
||||||
|
|
||||||
school_master_ids = {m.pk for m in school_masters}
|
school_master_ids = {m.pk for m in school_masters}
|
||||||
parent_master_ids = {m.pk for m in parent_masters}
|
parent_master_ids = {m.pk for m in parent_masters}
|
||||||
@ -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)
|
logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||||
|
|
||||||
# Créer les school templates manquants
|
# 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:
|
for m in school_masters:
|
||||||
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||||
if exists:
|
if exists:
|
||||||
@ -169,44 +172,61 @@ def create_templates_for_registration_form(register_form):
|
|||||||
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||||||
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||||||
|
|
||||||
# Si le master a un fichier uploadé (formulaire existant)
|
# --- Correction : Générer un nom de fichier unique uniquement si le master n'a pas de fichier ---
|
||||||
file_to_attach = None
|
file_name = None
|
||||||
if m.file:
|
if m.file and hasattr(m.file, 'name') and m.file.name:
|
||||||
import os
|
# Utiliser le nom du fichier tel qu'il est stocké dans le master (pas de suffixe aléatoire ici)
|
||||||
from django.core.files import File as DjangoFile
|
file_name = os.path.basename(m.file.name)
|
||||||
master_file_path = m.file.path
|
logger.info(f"util.create_templates_for_registration_form - file_name 1 : {file_name}")
|
||||||
if os.path.exists(master_file_path):
|
elif m.file:
|
||||||
filename = os.path.basename(master_file_path)
|
file_name = str(m.file)
|
||||||
# Générer le chemin cible pour le template élève
|
|
||||||
dest_path = registration_school_file_upload_to(None, filename)
|
|
||||||
dest_dir = os.path.dirname(os.path.join(settings.MEDIA_ROOT, dest_path))
|
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
|
||||||
# Copier le fichier dans le dossier cible
|
|
||||||
dest_full_path = os.path.join(settings.MEDIA_ROOT, dest_path)
|
|
||||||
with open(master_file_path, 'rb') as src, open(dest_full_path, 'wb') as dst:
|
|
||||||
dst.write(src.read())
|
|
||||||
# Préparer le File Django à attacher au template
|
|
||||||
with open(dest_full_path, 'rb') as f:
|
|
||||||
file_to_attach = DjangoFile(f, name=dest_path)
|
|
||||||
else:
|
else:
|
||||||
# Générer le PDF du template à partir du JSON du master
|
# Générer le PDF si besoin (rare ici)
|
||||||
try:
|
try:
|
||||||
pdf_file = generate_form_json_pdf(register_form, m.formMasterData)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la génération du PDF pour le template: {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,
|
master=m,
|
||||||
registration_form=register_form,
|
registration_form=register_form,
|
||||||
name=m.name or "",
|
name=m.name or "",
|
||||||
formTemplateData=m.formMasterData or [],
|
formTemplateData=m.formMasterData or [],
|
||||||
slug=slug,
|
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)
|
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
|
# Créer les parent templates manquants
|
||||||
for m in parent_masters:
|
for m in parent_masters:
|
||||||
|
|||||||
@ -139,6 +139,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
|||||||
|
|
||||||
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||||
if removed_group_ids:
|
if removed_group_ids:
|
||||||
|
logger.info("REMOVE IDs")
|
||||||
try:
|
try:
|
||||||
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||||
for rf in rfs_removed:
|
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
|
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||||
if added_group_ids:
|
if added_group_ids:
|
||||||
|
logger.info("ADD IDs")
|
||||||
try:
|
try:
|
||||||
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||||
for rf in rfs_added:
|
for rf in rfs_added:
|
||||||
|
|||||||
@ -106,7 +106,6 @@ export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
|||||||
body: data,
|
body: data,
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrfToken,
|
'X-CSRFToken': csrfToken,
|
||||||
// Pas de Content-Type, le navigateur gère pour FormData
|
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -187,10 +186,9 @@ export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
|
|||||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(data),
|
body: data,
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrfToken,
|
'X-CSRFToken': csrfToken,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import {
|
|||||||
FileUp,
|
FileUp,
|
||||||
PenTool,
|
PenTool,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
|
|
||||||
const FIELD_TYPES_ICON = {
|
const FIELD_TYPES_ICON = {
|
||||||
text: { icon: TextCursorInput },
|
text: { icon: TextCursorInput },
|
||||||
@ -520,29 +521,27 @@ export default function FormTemplateBuilder({
|
|||||||
Groupes d'inscription{' '}
|
Groupes d'inscription{' '}
|
||||||
<span className="text-red-500">*</span>
|
<span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
|
<div className="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||||
{groups && groups.length > 0 ? (
|
{groups && groups.length > 0 ? (
|
||||||
groups.map((group) => (
|
groups.map((group) => (
|
||||||
<label key={group.id} className="flex items-center">
|
<CheckBox
|
||||||
<input
|
key={group.id}
|
||||||
type="checkbox"
|
item={{ id: group.id }}
|
||||||
checked={selectedGroups.includes(group.id)}
|
formData={{
|
||||||
onChange={(e) => {
|
groups: selectedGroups
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedGroups([
|
|
||||||
...selectedGroups,
|
|
||||||
group.id,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setSelectedGroups(
|
|
||||||
selectedGroups.filter((id) => id !== group.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="mr-2 text-blue-600"
|
handleChange={() => {
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">{group.name}</span>
|
|
||||||
</label>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-sm">
|
<p className="text-gray-500 text-sm">
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Edit3,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
FileText,
|
FileText,
|
||||||
Star,
|
Star,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Plus
|
Plus,
|
||||||
|
Archive,
|
||||||
|
Eye
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||||
@ -37,6 +39,10 @@ import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModa
|
|||||||
import FileUpload from '@/components/Form/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import SectionTitle from '@/components/SectionTitle';
|
import SectionTitle from '@/components/SectionTitle';
|
||||||
import DropdownMenu from '@/components/DropdownMenu';
|
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) {
|
function getItemBgColor(type, selected, forceTheme = false) {
|
||||||
// Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné
|
// Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné
|
||||||
@ -140,7 +146,7 @@ function SimpleList({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={key}
|
key={key}
|
||||||
className={`flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-emerald-400' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix} ${extraZ} ${typeof itemClassName === 'function' ? itemClassName(item) : itemClassName}`}
|
className={`w-full flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-emerald-400' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix} ${extraZ} ${typeof itemClassName === 'function' ? itemClassName(item) : itemClassName}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!selectable || !onSelect) return;
|
if (!selectable || !onSelect) return;
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@ -191,6 +197,7 @@ export default function FilesGroupsManagement({
|
|||||||
csrfToken,
|
csrfToken,
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
}) {
|
}) {
|
||||||
|
const [showFilePreview, setShowFilePreview] = useState(false);
|
||||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||||
const [parentFiles, setParentFileMasters] = useState([]);
|
const [parentFiles, setParentFileMasters] = useState([]);
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
@ -208,7 +215,6 @@ export default function FilesGroupsManagement({
|
|||||||
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const [isFormBuilderOpen, setIsFormBuilderOpen] = useState(false);
|
|
||||||
const [isFileUploadOpen, setIsFileUploadOpen] = useState(false);
|
const [isFileUploadOpen, setIsFileUploadOpen] = useState(false);
|
||||||
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
||||||
const [editingParentFile, setEditingParentFile] = useState(null);
|
const [editingParentFile, setEditingParentFile] = useState(null);
|
||||||
@ -223,9 +229,10 @@ export default function FilesGroupsManagement({
|
|||||||
const handleDocDropdownSelect = (type) => {
|
const handleDocDropdownSelect = (type) => {
|
||||||
setIsDocDropdownOpen(false);
|
setIsDocDropdownOpen(false);
|
||||||
if (type === 'formulaire') {
|
if (type === 'formulaire') {
|
||||||
setIsFormBuilderOpen(true);
|
// Ouvre la modale unique en mode création
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setFileToEdit(null);
|
setFileToEdit(null);
|
||||||
|
setIsModalOpen(true);
|
||||||
} else if (type === 'formulaire_existant') {
|
} else if (type === 'formulaire_existant') {
|
||||||
setIsFileUploadPopupOpen(true);
|
setIsFileUploadPopupOpen(true);
|
||||||
setFileToEdit({});
|
setFileToEdit({});
|
||||||
@ -320,9 +327,16 @@ export default function FilesGroupsManagement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const editTemplateMaster = (file) => {
|
const editTemplateMaster = (file) => {
|
||||||
|
// 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);
|
setIsEditing(true);
|
||||||
setFileToEdit(file);
|
setFileToEdit(file);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSchoolFileMaster = ({ name, group_ids, formMasterData, file }) => {
|
const handleCreateSchoolFileMaster = ({ name, group_ids, formMasterData, file }) => {
|
||||||
@ -332,10 +346,23 @@ export default function FilesGroupsManagement({
|
|||||||
name,
|
name,
|
||||||
groups: group_ids,
|
groups: group_ids,
|
||||||
formMasterData,
|
formMasterData,
|
||||||
|
establishment: selectedEstablishmentId,
|
||||||
};
|
};
|
||||||
dataToSend.append('data', JSON.stringify(jsonData));
|
dataToSend.append('data', JSON.stringify(jsonData));
|
||||||
if (file) {
|
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)
|
createRegistrationSchoolFileMaster(dataToSend, csrfToken)
|
||||||
@ -363,15 +390,93 @@ export default function FilesGroupsManagement({
|
|||||||
group_ids,
|
group_ids,
|
||||||
formMasterData,
|
formMasterData,
|
||||||
id,
|
id,
|
||||||
|
file,
|
||||||
}) => {
|
}) => {
|
||||||
const data = {
|
// Correction : normaliser group_ids pour ne garder que les IDs (number/string)
|
||||||
name: name,
|
let normalizedGroupIds = [];
|
||||||
groups: group_ids,
|
if (Array.isArray(group_ids)) {
|
||||||
formMasterData: formMasterData,
|
normalizedGroupIds = group_ids.map(g =>
|
||||||
};
|
typeof g === 'object' && g !== null && 'id' in g ? g.id : g
|
||||||
logger.debug(data);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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) => {
|
.then((data) => {
|
||||||
setSchoolFileMasters((prevFichiers) =>
|
setSchoolFileMasters((prevFichiers) =>
|
||||||
prevFichiers.map((f) => (f.id === id ? data : f))
|
prevFichiers.map((f) => (f.id === id ? data : f))
|
||||||
@ -552,17 +657,25 @@ export default function FilesGroupsManagement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
const handleDelete = (id) => {
|
||||||
return deleteRegistrationParentFileMaster(id, csrfToken)
|
// Vérification avant suppression : afficher une popup de confirmation
|
||||||
.then(() => {
|
setRemovePopupMessage(
|
||||||
// Mettre à jour la liste des fichiers parents en supprimant l'élément correspondant
|
'Attention !\nVous êtes sur le point de supprimer la pièce à fournir.\nÊtes-vous sûr(e) de vouloir poursuivre l\'opération ?'
|
||||||
setParentFileMasters((prevFiles) =>
|
|
||||||
prevFiles.filter((file) => file.id !== id)
|
|
||||||
);
|
);
|
||||||
|
setRemovePopupOnConfirm(() => () => {
|
||||||
|
deleteRegistrationParentFileMaster(id, csrfToken)
|
||||||
|
.then(() => {
|
||||||
|
setParentFileMasters((prevFiles) => prevFiles.filter((file) => file.id !== id));
|
||||||
logger.debug('Document parent supprimé avec succès:', 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) => {
|
.catch((error) => {
|
||||||
logger.error('Erreur lors de la suppression du fichier parent:', 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
|
// Ouvre la modale de création d'une pièce à fournir
|
||||||
@ -575,6 +688,7 @@ export default function FilesGroupsManagement({
|
|||||||
const openEditParentFileModal = (file) => {
|
const openEditParentFileModal = (file) => {
|
||||||
setEditingParentFile(file);
|
setEditingParentFile(file);
|
||||||
setIsParentFileModalOpen(true);
|
setIsParentFileModalOpen(true);
|
||||||
|
setIsEditing(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ferme la modale de pièce à fournir
|
// Ferme la modale de pièce à fournir
|
||||||
@ -690,7 +804,13 @@ export default function FilesGroupsManagement({
|
|||||||
return count;
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Aide optionnelle */}
|
{/* Aide optionnelle */}
|
||||||
@ -724,17 +844,21 @@ export default function FilesGroupsManagement({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleGroupEdit(row); }}
|
onClick={(e) => { e.stopPropagation(); handleGroupEdit(row); }}
|
||||||
className="text-blue-500 hover:text-blue-700"
|
className="p-2 rounded-full hover:bg-gray-100 transition"
|
||||||
title="Modifier"
|
title="Modifier"
|
||||||
>
|
>
|
||||||
<Edit3 className="w-5 h-5" />
|
<span title="Editer le dossier">
|
||||||
|
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleGroupDelete(row.id); }}
|
onClick={(e) => { e.stopPropagation(); handleGroupDelete(row.id); }}
|
||||||
className="text-red-500 hover:text-red-700"
|
className="p-2 rounded-full hover:bg-gray-100 transition"
|
||||||
title="Supprimer"
|
title="Supprimer"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-5 h-5" />
|
<span title="Supprimer le dossier">
|
||||||
|
<Trash2 className="w-5 h-5 text-red-500 hover:text-red-700" />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -812,41 +936,38 @@ export default function FilesGroupsManagement({
|
|||||||
showGroups={false}
|
showGroups={false}
|
||||||
actionButtons={(row) => (
|
actionButtons={(row) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{row._type === 'emerald' ? (
|
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); editTemplateMaster(row); }}
|
onClick={(e) => {
|
||||||
className="text-blue-500 hover:text-blue-700"
|
e.stopPropagation();
|
||||||
|
if (row._type === 'emerald') {
|
||||||
|
editTemplateMaster(row);
|
||||||
|
} else {
|
||||||
|
openEditParentFileModal(row);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition"
|
||||||
title="Modifier"
|
title="Modifier"
|
||||||
>
|
>
|
||||||
<Edit3 className="w-5 h-5" />
|
<span title="Editer le document">
|
||||||
|
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); deleteTemplateMaster(row); }}
|
onClick={(e) => {
|
||||||
className="text-red-500 hover:text-red-700"
|
e.stopPropagation();
|
||||||
|
if (row._type === 'emerald') {
|
||||||
|
deleteTemplateMaster(row);
|
||||||
|
} else {
|
||||||
|
handleDelete(row.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition"
|
||||||
title="Supprimer"
|
title="Supprimer"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-5 h-5" />
|
<span title="Supprimer le document">
|
||||||
|
<Trash2 className="w-5 h-5 text-red-500 hover:text-red-700" />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={(e) => { e.stopPropagation(); openEditParentFileModal(row); }}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
title="Modifier"
|
|
||||||
>
|
|
||||||
<Edit3 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(row.id); }}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
title="Supprimer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -854,48 +975,90 @@ export default function FilesGroupsManagement({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals pour création/édition */}
|
{/* Popup pour création/édition d'un groupe de documents */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isFormBuilderOpen}
|
isOpen={isGroupModalOpen}
|
||||||
setIsOpen={setIsFormBuilderOpen}
|
setIsOpen={(isOpen) => {
|
||||||
title="Créer un formulaire personnalisé"
|
setIsGroupModalOpen(isOpen);
|
||||||
modalClassName="w-11/12 h-5/6"
|
if (!isOpen) {
|
||||||
>
|
setGroupToEdit(null);
|
||||||
<FormTemplateBuilder
|
}
|
||||||
onSave={(data) => {
|
|
||||||
handleCreateSchoolFileMaster(data);
|
|
||||||
setIsFormBuilderOpen(false);
|
|
||||||
}}
|
}}
|
||||||
groups={groups}
|
title={
|
||||||
isEditing={false}
|
groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
|
<RegistrationFileGroupForm
|
||||||
|
onSubmit={(data) => {
|
||||||
|
handleGroupSubmit(data);
|
||||||
|
setIsGroupModalOpen(false);
|
||||||
|
}}
|
||||||
|
initialData={groupToEdit}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Popup pour téléchargement d'un document existant */}
|
{/* Modals pour création/édition d'un formulaire dynamique */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
setIsOpen={(isOpen) => {
|
||||||
|
setIsModalOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setFileToEdit(null);
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire personnalisé'}
|
||||||
|
>
|
||||||
|
<div className="w-11/12 h-5/6 max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<FormTemplateBuilder
|
||||||
|
onSave={(data) => {
|
||||||
|
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}}
|
||||||
|
initialData={isEditing ? fileToEdit : undefined}
|
||||||
|
groups={groups}
|
||||||
|
isEditing={isEditing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Popup pour création/édition d'un formulaire d'école déjà existant */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isFileUploadPopupOpen}
|
isOpen={isFileUploadPopupOpen}
|
||||||
setIsOpen={setIsFileUploadPopupOpen}
|
setIsOpen={setIsFileUploadPopupOpen}
|
||||||
title="Télécharger un document existant"
|
title={fileToEdit && fileToEdit.id ? 'Modifier le document existant' : 'Télécharger un document existant'}
|
||||||
modalClassName="w-full max-w-md"
|
|
||||||
>
|
>
|
||||||
|
<div className="w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
{fileToEdit && fileToEdit.id ? (
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4 w-full"
|
||||||
onSubmit={e => {
|
onSubmit={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!fileToEdit?.name || !fileToEdit?.group_ids || !fileToEdit?.file) return;
|
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({
|
handleCreateSchoolFileMaster({
|
||||||
name: fileToEdit.name,
|
name: fileToEdit.name,
|
||||||
group_ids: fileToEdit.group_ids,
|
group_ids: fileToEdit.groups,
|
||||||
file: fileToEdit.file,
|
file: fileToEdit.file,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
setIsFileUploadPopupOpen(false);
|
setIsFileUploadPopupOpen(false);
|
||||||
setFileToEdit(null);
|
setFileToEdit(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<InputText
|
||||||
type="text"
|
label="Nom du document"
|
||||||
className="border rounded px-3 py-2"
|
name="name"
|
||||||
placeholder="Nom du document"
|
|
||||||
value={fileToEdit?.name || ''}
|
value={fileToEdit?.name || ''}
|
||||||
onChange={e => setFileToEdit({ ...fileToEdit, name: e.target.value })}
|
onChange={e => setFileToEdit({ ...fileToEdit, name: e.target.value })}
|
||||||
required
|
required
|
||||||
@ -904,27 +1067,121 @@ export default function FilesGroupsManagement({
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Groupes d'inscription <span className="text-red-500">*</span>
|
Groupes d'inscription <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
|
<div className="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||||
{groups && groups.length > 0 ? (
|
{groups && groups.length > 0 ? (
|
||||||
groups.map((group) => (
|
groups.map((group) => {
|
||||||
<label key={group.id} className="flex items-center">
|
const selectedGroupIds = (fileToEdit?.groups || []).map(g =>
|
||||||
<input
|
typeof g === 'object' && g !== null && 'id' in g ? g.id : g
|
||||||
type="checkbox"
|
);
|
||||||
checked={fileToEdit?.group_ids?.includes(group.id) || false}
|
return (
|
||||||
onChange={(e) => {
|
<CheckBox
|
||||||
let group_ids = fileToEdit?.group_ids || [];
|
key={group.id}
|
||||||
if (e.target.checked) {
|
item={{ id: group.id }}
|
||||||
group_ids = [...group_ids, group.id];
|
formData={{
|
||||||
} else {
|
groups: selectedGroupIds
|
||||||
group_ids = group_ids.filter((id) => id !== group.id);
|
|
||||||
}
|
|
||||||
setFileToEdit({ ...fileToEdit, group_ids });
|
|
||||||
}}
|
}}
|
||||||
className="mr-2 text-blue-600"
|
handleChange={() => {
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">{group.name}</span>
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Aucun groupe disponible
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Label document sélectionné sans icône œil */}
|
||||||
|
{fileToEdit?.file && (
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<FileText className="w-5 h-5 text-gray-600" />
|
||||||
|
<span className="text-sm truncate">{fileToEdit.file.name || fileToEdit.file.path || 'Document sélectionné'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FileUpload
|
||||||
|
selectionMessage="Sélectionnez le fichier du document"
|
||||||
|
onFileSelect={file =>
|
||||||
|
setFileToEdit({ ...fileToEdit, file })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
enable
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
text="Enregistrer"
|
||||||
|
className="mt-2"
|
||||||
|
disabled={
|
||||||
|
!fileToEdit?.name ||
|
||||||
|
!fileToEdit?.groups ||
|
||||||
|
fileToEdit.groups.length === 0 ||
|
||||||
|
!fileToEdit?.file
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4 w-full"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!fileToEdit?.name || !fileToEdit?.groups || fileToEdit.groups.length === 0 || !fileToEdit?.file) return;
|
||||||
|
handleCreateSchoolFileMaster({
|
||||||
|
name: fileToEdit.name,
|
||||||
|
group_ids: fileToEdit.groups,
|
||||||
|
file: fileToEdit.file,
|
||||||
|
});
|
||||||
|
setIsFileUploadPopupOpen(false);
|
||||||
|
setFileToEdit(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputText
|
||||||
|
label="Nom du document"
|
||||||
|
name="name"
|
||||||
|
value={fileToEdit?.name || ''}
|
||||||
|
onChange={e => setFileToEdit({ ...fileToEdit, name: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Groupes d'inscription <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
))
|
<div className="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||||
|
{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 (
|
||||||
|
<CheckBox
|
||||||
|
key={group.id}
|
||||||
|
item={{ id: group.id }}
|
||||||
|
formData={{
|
||||||
|
groups: selectedGroupIds
|
||||||
|
}}
|
||||||
|
handleChange={() => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-sm">
|
<p className="text-gray-500 text-sm">
|
||||||
Aucun groupe disponible
|
Aucun groupe disponible
|
||||||
@ -940,85 +1197,140 @@ export default function FilesGroupsManagement({
|
|||||||
required
|
required
|
||||||
enable
|
enable
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
|
primary
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-emerald-600 text-white px-4 py-2 rounded font-bold mt-2"
|
text="Enregistrer"
|
||||||
|
className="mt-2"
|
||||||
disabled={
|
disabled={
|
||||||
!fileToEdit?.name ||
|
!fileToEdit?.name ||
|
||||||
!fileToEdit?.group_ids ||
|
!fileToEdit?.groups ||
|
||||||
fileToEdit.group_ids.length === 0 ||
|
fileToEdit.groups.length === 0 ||
|
||||||
!fileToEdit?.file
|
!fileToEdit?.file
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
Créer le document
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Popup pour création/édition d'un document parent */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isParentFileModalOpen}
|
isOpen={isParentFileModalOpen}
|
||||||
setIsOpen={(open) => {
|
setIsOpen={(open) => {
|
||||||
setIsParentFileModalOpen(open);
|
setIsParentFileModalOpen(open);
|
||||||
if (!open) setEditingParentFile(null);
|
if (!open) setEditingParentFile(null);
|
||||||
}}
|
}}
|
||||||
title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
|
|
||||||
modalClassName="w-full max-w-md"
|
|
||||||
>
|
|
||||||
<ParentFiles
|
|
||||||
parentFiles={parentFiles}
|
|
||||||
groups={groups}
|
|
||||||
handleCreate={handleCreate}
|
|
||||||
handleEdit={handleEdit}
|
|
||||||
handleDelete={handleDelete}
|
|
||||||
singleForm // affiche uniquement le formulaire, pas la liste
|
|
||||||
initialData={editingParentFile}
|
|
||||||
onCancel={() => setIsParentFileModalOpen(false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Modals et popups pour édition */}
|
|
||||||
<Modal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
setIsOpen={(isOpen) => {
|
|
||||||
setIsModalOpen(isOpen);
|
|
||||||
if (!isOpen) {
|
|
||||||
setFileToEdit(null);
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'}
|
|
||||||
modalClassName="w-11/12 h-5/6"
|
|
||||||
>
|
|
||||||
<FormTemplateBuilder
|
|
||||||
onSave={(data) => {
|
|
||||||
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
|
|
||||||
setIsModalOpen(false);
|
|
||||||
}}
|
|
||||||
initialData={fileToEdit}
|
|
||||||
groups={groups}
|
|
||||||
isEditing={isEditing}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
isOpen={isGroupModalOpen}
|
|
||||||
setIsOpen={(isOpen) => {
|
|
||||||
setIsGroupModalOpen(isOpen);
|
|
||||||
if (!isOpen) {
|
|
||||||
setGroupToEdit(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={
|
title={
|
||||||
groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier"
|
editingParentFile && editingParentFile.id
|
||||||
|
? 'Modifier la pièce à fournir'
|
||||||
|
: 'Créer une pièce à fournir'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RegistrationFileGroupForm
|
<div className="w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
onSubmit={(data) => {
|
<form
|
||||||
handleGroupSubmit(data);
|
className="flex flex-col gap-4"
|
||||||
setIsGroupModalOpen(false);
|
onSubmit={e => {
|
||||||
|
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);
|
||||||
}}
|
}}
|
||||||
initialData={groupToEdit}
|
>
|
||||||
|
<InputText
|
||||||
|
label="Nom de la pièce à fournir"
|
||||||
|
name="name"
|
||||||
|
value={editingParentFile?.name || ''}
|
||||||
|
onChange={e => setEditingParentFile({ ...editingParentFile, name: e.target.value })}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
<InputText
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
value={editingParentFile?.description || ''}
|
||||||
|
onChange={e => setEditingParentFile({ ...editingParentFile, description: e.target.value })}
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Groupes d'inscription <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||||
|
{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 (
|
||||||
|
<CheckBox
|
||||||
|
key={group.id}
|
||||||
|
item={{ id: group.id }}
|
||||||
|
formData={{
|
||||||
|
groups: selectedGroupIds
|
||||||
|
}}
|
||||||
|
handleChange={() => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Aucun groupe disponible
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: 'is_required' }}
|
||||||
|
formData={{ is_required: !!editingParentFile?.is_required }}
|
||||||
|
handleChange={() =>
|
||||||
|
setEditingParentFile({
|
||||||
|
...editingParentFile,
|
||||||
|
is_required: !editingParentFile?.is_required,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fieldName="is_required"
|
||||||
|
itemLabelFunc={() => 'Requis'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
text="Enregistrer"
|
||||||
|
className="mt-2"
|
||||||
|
disabled={
|
||||||
|
!editingParentFile?.name ||
|
||||||
|
!editingParentFile?.groups ||
|
||||||
|
editingParentFile.groups.length === 0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Popup
|
<Popup
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import InputText from '@/components/Form/InputText';
|
||||||
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@ -18,38 +20,28 @@ export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
{/* Utilisation de InputText pour le nom du groupe */}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<InputText
|
||||||
Nom du groupe
|
label="Nom du groupe"
|
||||||
</label>
|
name="name"
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
<InputText
|
||||||
|
label="Description"
|
||||||
<div>
|
name="description"
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
rows={3}
|
required
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<Button
|
||||||
|
primary
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
text="Enregistrer"
|
||||||
>
|
/>
|
||||||
{initialData ? 'Modifier le groupe' : 'Créer le groupe'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user