mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
Compare commits
6 Commits
dd00cba385
...
abb4b525b2
| Author | SHA1 | Date | |
|---|---|---|---|
| abb4b525b2 | |||
| b4f70e6bad | |||
| 8549699dec | |||
| a034149eae | |||
| 12f5fc7aa9 | |||
| 2dc0dfa268 |
@ -2,11 +2,9 @@ from django.db import models
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger("SubscriptionModels")
|
logger = logging.getLogger("SubscriptionModels")
|
||||||
|
|
||||||
@ -316,14 +314,164 @@ 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):
|
||||||
|
# Stocke les fichiers masters dans un dossier dédié
|
||||||
|
# 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)
|
||||||
|
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):
|
def __str__(self):
|
||||||
return f'{self.group.name} - {self.id}'
|
return f'{self.name} - {self.id}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_url(self):
|
||||||
|
if self.file and hasattr(self.file, 'url'):
|
||||||
|
return self.file.url
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
affected_rf_ids = set()
|
||||||
|
is_new = self.pk is None
|
||||||
|
|
||||||
|
# Log création ou modification du master
|
||||||
|
if is_new:
|
||||||
|
logger.info(f"[FormPerso] Création master '{self.name}' pour établissement '{self.establishment}'")
|
||||||
|
else:
|
||||||
|
logger.info(f"[FormPerso] Modification master '{self.name}' (id={self.pk}) pour établissement '{self.establishment}'")
|
||||||
|
|
||||||
|
# --- Suppression de l'ancien fichier master si le nom change (form existant ou dynamique) ---
|
||||||
|
if self.pk:
|
||||||
|
try:
|
||||||
|
old = RegistrationSchoolFileMaster.objects.get(pk=self.pk)
|
||||||
|
if old.file and old.file.name:
|
||||||
|
old_filename = os.path.basename(old.file.name)
|
||||||
|
# Nouveau nom selon le type (dynamique ou existant)
|
||||||
|
if (
|
||||||
|
self.formMasterData
|
||||||
|
and isinstance(self.formMasterData, dict)
|
||||||
|
and self.formMasterData.get("fields")
|
||||||
|
):
|
||||||
|
new_filename = f"{self.name}.pdf"
|
||||||
|
else:
|
||||||
|
# Pour les forms existants, le nom attendu est self.name + extension du fichier existant
|
||||||
|
extension = os.path.splitext(old_filename)[1]
|
||||||
|
new_filename = f"{self.name}{extension}" if extension else self.name
|
||||||
|
if new_filename and old_filename != new_filename:
|
||||||
|
old_file_path = old.file.path
|
||||||
|
if os.path.exists(old_file_path):
|
||||||
|
try:
|
||||||
|
os.remove(old_file_path)
|
||||||
|
logger.info(f"[FormPerso] Suppression de l'ancien fichier master: {old_file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[FormPerso] Erreur suppression ancien fichier master: {e}")
|
||||||
|
# Correction du nom du fichier pour éviter le suffixe random
|
||||||
|
if (
|
||||||
|
not self.formMasterData
|
||||||
|
or not (isinstance(self.formMasterData, dict) and self.formMasterData.get("fields"))
|
||||||
|
):
|
||||||
|
# Si le fichier existe et le nom ne correspond pas, renommer le fichier physique et mettre à jour le FileField
|
||||||
|
if self.file and self.file.name:
|
||||||
|
current_filename = os.path.basename(self.file.name)
|
||||||
|
current_path = self.file.path
|
||||||
|
expected_filename = new_filename
|
||||||
|
expected_path = os.path.join(os.path.dirname(current_path), expected_filename)
|
||||||
|
if current_filename != expected_filename:
|
||||||
|
try:
|
||||||
|
if os.path.exists(current_path):
|
||||||
|
os.rename(current_path, expected_path)
|
||||||
|
self.file.name = os.path.join(os.path.dirname(self.file.name), expected_filename).replace("\\", "/")
|
||||||
|
logger.info(f"[FormPerso] Renommage du fichier master: {current_path} -> {expected_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[FormPerso] Erreur lors du renommage du fichier master: {e}")
|
||||||
|
except RegistrationSchoolFileMaster.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Traitement PDF dynamique AVANT le super().save() ---
|
||||||
|
if (
|
||||||
|
self.formMasterData
|
||||||
|
and isinstance(self.formMasterData, dict)
|
||||||
|
and self.formMasterData.get("fields")
|
||||||
|
):
|
||||||
|
from Subscriptions.util import generate_form_json_pdf
|
||||||
|
pdf_filename = f"{self.name}.pdf"
|
||||||
|
pdf_file = generate_form_json_pdf(self, self.formMasterData)
|
||||||
|
self.file.save(pdf_filename, pdf_file, save=False)
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Synchronisation des templates pour tous les dossiers d'inscription concernés (création ou modification)
|
||||||
|
try:
|
||||||
|
# Import local pour éviter le circular import
|
||||||
|
from Subscriptions.util import create_templates_for_registration_form
|
||||||
|
from Subscriptions.models import RegistrationForm, RegistrationSchoolFileTemplate
|
||||||
|
# Détermination des RF concernés
|
||||||
|
if is_new:
|
||||||
|
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))
|
||||||
|
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
|
||||||
|
if form_data_changed or name_changed:
|
||||||
|
logger.info(f"[FormPerso] Modification du contenu du master '{self.name}' (id={self.pk})")
|
||||||
|
except RegistrationSchoolFileMaster.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Pour chaque RF concerné, régénérer les templates
|
||||||
|
for rf_id in affected_rf_ids:
|
||||||
|
try:
|
||||||
|
rf = RegistrationForm.objects.get(pk=rf_id)
|
||||||
|
logger.info(f"[FormPerso] Synchronisation template pour élève '{rf.student.last_name}_{rf.student.first_name}' (RF id={rf.pk}) suite à modification/ajout du master '{self.name}'")
|
||||||
|
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):
|
||||||
|
logger.info(f"[FormPerso] Suppression master '{self.name}' (id={self.pk}) et tous ses templates")
|
||||||
|
# Import local pour éviter le circular import
|
||||||
|
from Subscriptions.models import RegistrationSchoolFileTemplate
|
||||||
|
templates = RegistrationSchoolFileTemplate.objects.filter(master=self)
|
||||||
|
for tmpl in templates:
|
||||||
|
logger.info(f"[FormPerso] Suppression template '{tmpl.name}' pour élève '{tmpl.registration_form.student.last_name}_{tmpl.registration_form.student.first_name}' (RF id={tmpl.registration_form.pk})")
|
||||||
|
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):
|
||||||
@ -337,10 +485,10 @@ class RegistrationParentFileMaster(models.Model):
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def registration_school_file_upload_to(instance, filename):
|
def registration_school_file_upload_to(instance, filename):
|
||||||
return f"registration_files/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/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||||
|
|
||||||
####### Formulaires templates (par dossier d'inscription) #######
|
####### Formulaires templates (par dossier d'inscription) #######
|
||||||
class RegistrationSchoolFileTemplate(models.Model):
|
class RegistrationSchoolFileTemplate(models.Model):
|
||||||
|
|||||||
@ -25,8 +25,6 @@ from .views import (
|
|||||||
|
|
||||||
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||||
from .views import (
|
from .views import (
|
||||||
registration_school_file_masters_views,
|
|
||||||
registration_school_file_templates_views,
|
|
||||||
get_school_file_templates_by_rf,
|
get_school_file_templates_by_rf,
|
||||||
get_parent_file_templates_by_rf
|
get_parent_file_templates_by_rf
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,6 +8,9 @@ from N3wtSchool import renderers
|
|||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
@ -100,11 +103,13 @@ def create_templates_for_registration_form(register_form):
|
|||||||
from Subscriptions.models import (
|
from Subscriptions.models import (
|
||||||
RegistrationSchoolFileMaster,
|
RegistrationSchoolFileMaster,
|
||||||
RegistrationSchoolFileTemplate,
|
RegistrationSchoolFileTemplate,
|
||||||
# RegistrationParentFileMaster,
|
RegistrationParentFileMaster,
|
||||||
# RegistrationParentFileTemplate,
|
RegistrationParentFileTemplate,
|
||||||
|
registration_school_file_upload_to,
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
@ -114,25 +119,26 @@ def create_templates_for_registration_form(register_form):
|
|||||||
for t in school_existing:
|
for t in school_existing:
|
||||||
try:
|
try:
|
||||||
if getattr(t, "file", None):
|
if getattr(t, "file", None):
|
||||||
|
logger.info("Deleted school template %s for RF %s", t.pk, register_form.pk)
|
||||||
t.file.delete(save=False)
|
t.file.delete(save=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
|
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
|
||||||
t.delete()
|
t.delete()
|
||||||
# parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
||||||
# for t in parent_existing:
|
for t in parent_existing:
|
||||||
# try:
|
try:
|
||||||
# if getattr(t, "file", None):
|
if getattr(t, "file", None):
|
||||||
# t.file.delete(save=False)
|
t.file.delete(save=False)
|
||||||
# except Exception:
|
except Exception:
|
||||||
# logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
||||||
# t.delete()
|
t.delete()
|
||||||
return created
|
return created
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
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}
|
||||||
|
|
||||||
# Supprimer les school templates obsolètes
|
# Supprimer les school templates obsolètes
|
||||||
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
|
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
|
||||||
@ -146,45 +152,119 @@ def create_templates_for_registration_form(register_form):
|
|||||||
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||||
|
|
||||||
# Supprimer les parent templates obsolètes
|
# Supprimer les parent templates obsolètes
|
||||||
# for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
||||||
# if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
||||||
# try:
|
try:
|
||||||
# if getattr(tmpl, "file", None):
|
if getattr(tmpl, "file", None):
|
||||||
# tmpl.file.delete(save=False)
|
tmpl.file.delete(save=False)
|
||||||
# except Exception:
|
except Exception:
|
||||||
# logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
||||||
# tmpl.delete()
|
tmpl.delete()
|
||||||
# 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 ou mettre à jour les existants si le master a changé
|
||||||
for m in school_masters:
|
for m in school_masters:
|
||||||
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
tmpl_qs = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form)
|
||||||
if exists:
|
tmpl = tmpl_qs.first() if tmpl_qs.exists() else None
|
||||||
continue
|
|
||||||
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}"
|
||||||
tmpl = RegistrationSchoolFileTemplate.objects.create(
|
|
||||||
|
file_name = None
|
||||||
|
if m.file and hasattr(m.file, 'name') and m.file.name:
|
||||||
|
file_name = os.path.basename(m.file.name)
|
||||||
|
elif m.file:
|
||||||
|
file_name = str(m.file)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
pdf_file = generate_form_json_pdf(register_form, m.formMasterData)
|
||||||
|
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_name = None
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
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
|
||||||
|
|
||||||
|
if tmpl:
|
||||||
|
template_file_name = os.path.basename(tmpl.file.name) if tmpl.file and tmpl.file.name else None
|
||||||
|
master_file_changed = template_file_name != file_name
|
||||||
|
# --- GESTION FORM EXISTANT : suppression ancien template si nom ou contenu master changé ---
|
||||||
|
if master_file_changed or (
|
||||||
|
master_file_path and os.path.exists(master_file_path) and
|
||||||
|
(not tmpl.file or not os.path.exists(abs_path) or os.path.getmtime(master_file_path) > os.path.getmtime(abs_path))
|
||||||
|
):
|
||||||
|
# Supprimer l'ancien fichier du template (même si le nom change)
|
||||||
|
if tmpl.file and tmpl.file.name:
|
||||||
|
old_template_path = os.path.join(settings.MEDIA_ROOT, tmpl.file.name)
|
||||||
|
if os.path.exists(old_template_path):
|
||||||
|
try:
|
||||||
|
os.remove(old_template_path)
|
||||||
|
logger.info(f"util.create_templates_for_registration_form - Suppression ancien fichier template: {old_template_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur suppression ancien fichier template: {e}")
|
||||||
|
# Copier le nouveau fichier du master (form existant)
|
||||||
|
if master_file_path and os.path.exists(master_file_path):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||||
|
import shutil
|
||||||
|
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}")
|
||||||
|
tmpl.file.name = upload_rel_path
|
||||||
|
tmpl.name = m.name or ""
|
||||||
|
tmpl.slug = slug
|
||||||
|
tmpl.formTemplateData = m.formMasterData or []
|
||||||
|
tmpl.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la copie du fichier master pour mise à jour du template: {e}")
|
||||||
|
created.append(tmpl)
|
||||||
|
logger.info("util.create_templates_for_registration_form - Mise à jour school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sinon, création du template comme avant
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
if file_name:
|
||||||
|
# Copier le fichier du master si besoin (form existant)
|
||||||
|
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}")
|
||||||
|
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:
|
||||||
# exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||||
# if exists:
|
if exists:
|
||||||
# continue
|
continue
|
||||||
# tmpl = RegistrationParentFileTemplate.objects.create(
|
tmpl = RegistrationParentFileTemplate.objects.create(
|
||||||
# master=m,
|
master=m,
|
||||||
# registration_form=register_form,
|
registration_form=register_form,
|
||||||
# file=None,
|
file=None,
|
||||||
# )
|
)
|
||||||
# created.append(tmpl)
|
created.append(tmpl)
|
||||||
# logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||||
|
|
||||||
return created
|
return created
|
||||||
|
|
||||||
@ -378,3 +458,51 @@ def getHistoricalYears(count=5):
|
|||||||
historical_years.append(f"{historical_start_year}-{historical_start_year + 1}")
|
historical_years.append(f"{historical_start_year}-{historical_start_year + 1}")
|
||||||
|
|
||||||
return historical_years
|
return historical_years
|
||||||
|
|
||||||
|
def generate_form_json_pdf(register_form, form_json):
|
||||||
|
"""
|
||||||
|
Génère un PDF du rendu du formulaire dynamique à partir du JSON (formConfig)
|
||||||
|
et l'associe au RegistrationSchoolFileTemplate.
|
||||||
|
Le PDF contient le titre, les labels et types de champs.
|
||||||
|
Retourne un ContentFile prêt à être utilisé dans un FileField, avec uniquement le nom du fichier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Récupérer le nom du formulaire
|
||||||
|
form_name = (form_json.get("title") or "formulaire").strip().replace(" ", "_")
|
||||||
|
filename = f"{form_name}.pdf"
|
||||||
|
|
||||||
|
# Générer le PDF
|
||||||
|
buffer = BytesIO()
|
||||||
|
c = canvas.Canvas(buffer, pagesize=A4)
|
||||||
|
y = 800
|
||||||
|
|
||||||
|
# Titre
|
||||||
|
c.setFont("Helvetica-Bold", 20)
|
||||||
|
c.drawString(100, y, form_json.get("title", "Formulaire"))
|
||||||
|
y -= 40
|
||||||
|
|
||||||
|
# Champs
|
||||||
|
c.setFont("Helvetica", 12)
|
||||||
|
fields = form_json.get("fields", [])
|
||||||
|
for field in fields:
|
||||||
|
label = field.get("label", field.get("id", ""))
|
||||||
|
ftype = field.get("type", "")
|
||||||
|
c.drawString(100, y, f"{label} [{ftype}]")
|
||||||
|
y -= 25
|
||||||
|
if y < 100:
|
||||||
|
c.showPage()
|
||||||
|
y = 800
|
||||||
|
|
||||||
|
c.save()
|
||||||
|
buffer.seek(0)
|
||||||
|
pdf_content = buffer.read()
|
||||||
|
|
||||||
|
# Si un fichier existe déjà sur le template, le supprimer (optionnel, à adapter selon usage)
|
||||||
|
if hasattr(register_form, "registration_file") and register_form.registration_file and register_form.registration_file.name:
|
||||||
|
existing_file_path = os.path.join(settings.MEDIA_ROOT, register_form.registration_file.name.lstrip('/'))
|
||||||
|
if os.path.exists(existing_file_path):
|
||||||
|
os.remove(existing_file_path)
|
||||||
|
register_form.registration_file.delete(save=False)
|
||||||
|
|
||||||
|
# Retourner le ContentFile avec uniquement le nom du fichier
|
||||||
|
return ContentFile(pdf_content, name=os.path.basename(filename))
|
||||||
|
|||||||
@ -10,14 +10,18 @@ from .register_form_views import (
|
|||||||
from .registration_school_file_masters_views import (
|
from .registration_school_file_masters_views import (
|
||||||
RegistrationSchoolFileMasterView,
|
RegistrationSchoolFileMasterView,
|
||||||
RegistrationSchoolFileMasterSimpleView,
|
RegistrationSchoolFileMasterSimpleView,
|
||||||
RegistrationParentFileMasterView,
|
|
||||||
RegistrationParentFileMasterSimpleView,
|
|
||||||
RegistrationParentFileTemplateSimpleView,
|
|
||||||
RegistrationParentFileTemplateView
|
|
||||||
)
|
)
|
||||||
from .registration_school_file_templates_views import (
|
from .registration_school_file_templates_views import (
|
||||||
RegistrationSchoolFileTemplateView,
|
RegistrationSchoolFileTemplateView,
|
||||||
RegistrationSchoolFileTemplateSimpleView,
|
RegistrationSchoolFileTemplateSimpleView
|
||||||
|
)
|
||||||
|
from .registration_parent_file_masters_views import (
|
||||||
|
RegistrationParentFileMasterView,
|
||||||
|
RegistrationParentFileMasterSimpleView
|
||||||
|
)
|
||||||
|
from .registration_parent_file_templates_views import (
|
||||||
|
RegistrationParentFileTemplateSimpleView,
|
||||||
|
RegistrationParentFileTemplateView
|
||||||
)
|
)
|
||||||
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||||
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
||||||
|
|||||||
@ -0,0 +1,272 @@
|
|||||||
|
from django.http.response import JsonResponse
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from Subscriptions.serializers import RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||||
|
from Subscriptions.models import (
|
||||||
|
RegistrationForm,
|
||||||
|
RegistrationParentFileMaster,
|
||||||
|
RegistrationParentFileTemplate
|
||||||
|
)
|
||||||
|
from N3wtSchool import bdd
|
||||||
|
import logging
|
||||||
|
import Subscriptions.util as util
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RegistrationParentFileMasterView(APIView):
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les fichiers parents liés à l'établissement
|
||||||
|
templates = RegistrationParentFileMaster.objects.filter(
|
||||||
|
groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau fichier parent",
|
||||||
|
request_body=RegistrationParentFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationParentFileMasterSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
logger.info(f"raw request.data: {request.data}")
|
||||||
|
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for serializer: {payload}")
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(data=payload, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
obj = serializer.save()
|
||||||
|
|
||||||
|
# Propager la création des templates côté serveur pour les RegistrationForm
|
||||||
|
try:
|
||||||
|
groups_qs = obj.groups.all()
|
||||||
|
if groups_qs.exists():
|
||||||
|
rfs = RegistrationForm.objects.filter(fileGroup__in=groups_qs).distinct()
|
||||||
|
for rf in rfs:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error creating templates for RF %s from parent master %s: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while propagating templates after parent master creation %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
return Response(RegistrationParentFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
logger.error(f"serializer errors: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationParentFileMasterSimpleView(APIView):
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un fichier parent spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileMasterSerializer,
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(template)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un fichier parent existant",
|
||||||
|
request_body=RegistrationParentFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileMasterSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
master = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if master is None:
|
||||||
|
return JsonResponse({'erreur': "Le master de fichier parent n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# snapshot des groups avant update
|
||||||
|
old_group_ids = set(master.groups.values_list('id', flat=True))
|
||||||
|
|
||||||
|
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for update serializer: {payload}")
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(master, data=payload, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
obj = serializer.save()
|
||||||
|
|
||||||
|
# groups après update
|
||||||
|
new_group_ids = set(obj.groups.values_list('id', flat=True))
|
||||||
|
|
||||||
|
removed_group_ids = old_group_ids - new_group_ids
|
||||||
|
added_group_ids = new_group_ids - old_group_ids
|
||||||
|
|
||||||
|
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||||
|
if removed_group_ids:
|
||||||
|
try:
|
||||||
|
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||||
|
for rf in rfs_removed:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error cleaning templates for RF %s after parent master %s group removal: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while processing RFs for removed groups after parent master update %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||||
|
if added_group_ids:
|
||||||
|
try:
|
||||||
|
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||||
|
for rf in rfs_added:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error creating templates for RF %s after parent master %s group addition: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while processing RFs for added groups after parent master update %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un fichier parent",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if template is not None:
|
||||||
|
template.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||||
|
templates = RegistrationParentFileTemplate.objects.filter(
|
||||||
|
master__groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau template d'inscription",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateSimpleView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un template d'inscription spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un template d'inscription existant",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un template d'inscription",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is not None:
|
||||||
|
# Suppression du fichier PDF associé avant suppression de l'objet
|
||||||
|
if template.file and template.file.name:
|
||||||
|
template.file.delete(save=False)
|
||||||
|
template.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
from django.http.response import JsonResponse
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from Subscriptions.serializers import RegistrationParentFileTemplateSerializer
|
||||||
|
from Subscriptions.models import (
|
||||||
|
RegistrationParentFileTemplate
|
||||||
|
)
|
||||||
|
from N3wtSchool import bdd
|
||||||
|
import logging
|
||||||
|
import Subscriptions.util as util
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||||
|
templates = RegistrationParentFileTemplate.objects.filter(
|
||||||
|
master__groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau template d'inscription",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateSimpleView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un template d'inscription spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un template d'inscription existant",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un template d'inscription",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is not None:
|
||||||
|
# Suppression du fichier PDF associé
|
||||||
|
if template.file and template.file.name:
|
||||||
|
template.file.delete(save=False)
|
||||||
|
template.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
@ -5,15 +5,12 @@ from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
import json
|
|
||||||
from django.http import QueryDict
|
|
||||||
|
|
||||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer
|
||||||
from Subscriptions.models import (
|
from Subscriptions.models import (
|
||||||
RegistrationForm,
|
RegistrationForm,
|
||||||
RegistrationSchoolFileMaster,
|
RegistrationSchoolFileMaster,
|
||||||
RegistrationParentFileMaster,
|
RegistrationSchoolFileTemplate
|
||||||
RegistrationParentFileTemplate
|
|
||||||
)
|
)
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
import logging
|
import logging
|
||||||
@ -142,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:
|
||||||
@ -154,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:
|
||||||
@ -179,188 +178,13 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
|||||||
def delete(self, request, id):
|
def delete(self, request, id):
|
||||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
if master is not None:
|
if master is not None:
|
||||||
|
# Supprimer tous les templates liés et leurs fichiers PDF
|
||||||
|
templates = RegistrationSchoolFileTemplate.objects.filter(master=master)
|
||||||
|
for template in templates:
|
||||||
|
if template.file and template.file.name:
|
||||||
|
template.file.delete(save=False)
|
||||||
|
template.delete()
|
||||||
master.delete()
|
master.delete()
|
||||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
return JsonResponse({'message': 'La suppression du master de template et des fichiers associés a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
class RegistrationParentFileMasterView(APIView):
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
|
||||||
manual_parameters=[
|
|
||||||
openapi.Parameter(
|
|
||||||
'establishment_id',
|
|
||||||
openapi.IN_QUERY,
|
|
||||||
description="ID de l'établissement",
|
|
||||||
type=openapi.TYPE_INTEGER,
|
|
||||||
required=True
|
|
||||||
)
|
|
||||||
],
|
|
||||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
|
||||||
)
|
|
||||||
def get(self, request):
|
|
||||||
establishment_id = request.GET.get('establishment_id')
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# Filtrer les fichiers parents liés à l'établissement
|
|
||||||
templates = RegistrationParentFileMaster.objects.filter(
|
|
||||||
groups__establishment__id=establishment_id
|
|
||||||
).distinct()
|
|
||||||
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Crée un nouveau fichier parent",
|
|
||||||
request_body=RegistrationParentFileMasterSerializer,
|
|
||||||
responses={
|
|
||||||
201: RegistrationParentFileMasterSerializer,
|
|
||||||
400: "Données invalides"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, request):
|
|
||||||
serializer = RegistrationParentFileMasterSerializer(data=request.data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
class RegistrationParentFileMasterSimpleView(APIView):
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Récupère un fichier parent spécifique",
|
|
||||||
responses={
|
|
||||||
200: RegistrationParentFileMasterSerializer,
|
|
||||||
404: "Fichier parent non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
|
||||||
if template is None:
|
|
||||||
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
serializer = RegistrationParentFileMasterSerializer(template)
|
|
||||||
return JsonResponse(serializer.data, safe=False)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Met à jour un fichier parent existant",
|
|
||||||
request_body=RegistrationParentFileMasterSerializer,
|
|
||||||
responses={
|
|
||||||
200: RegistrationParentFileMasterSerializer,
|
|
||||||
400: "Données invalides",
|
|
||||||
404: "Fichier parent non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
|
||||||
if template is None:
|
|
||||||
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
serializer = RegistrationParentFileMasterSerializer(template, data=request.data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Supprime un fichier parent",
|
|
||||||
responses={
|
|
||||||
204: "Suppression réussie",
|
|
||||||
404: "Fichier parent non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def delete(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
|
||||||
if template is not None:
|
|
||||||
template.delete()
|
|
||||||
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
|
||||||
else:
|
|
||||||
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
class RegistrationParentFileTemplateView(APIView):
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Récupère tous les templates parents pour un établissement donné",
|
|
||||||
manual_parameters=[
|
|
||||||
openapi.Parameter(
|
|
||||||
'establishment_id',
|
|
||||||
openapi.IN_QUERY,
|
|
||||||
description="ID de l'établissement",
|
|
||||||
type=openapi.TYPE_INTEGER,
|
|
||||||
required=True
|
|
||||||
)
|
|
||||||
],
|
|
||||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
|
||||||
)
|
|
||||||
def get(self, request):
|
|
||||||
establishment_id = request.GET.get('establishment_id')
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
|
||||||
templates = RegistrationParentFileTemplate.objects.filter(
|
|
||||||
master__groups__establishment__id=establishment_id
|
|
||||||
).distinct()
|
|
||||||
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Crée un nouveau template d'inscription",
|
|
||||||
request_body=RegistrationParentFileTemplateSerializer,
|
|
||||||
responses={
|
|
||||||
201: RegistrationParentFileTemplateSerializer,
|
|
||||||
400: "Données invalides"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, request):
|
|
||||||
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
class RegistrationParentFileTemplateSimpleView(APIView):
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Récupère un template d'inscription spécifique",
|
|
||||||
responses={
|
|
||||||
200: RegistrationParentFileTemplateSerializer,
|
|
||||||
404: "Template non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
|
||||||
if template is None:
|
|
||||||
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
serializer = RegistrationParentFileTemplateSerializer(template)
|
|
||||||
return JsonResponse(serializer.data, safe=False)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Met à jour un template d'inscription existant",
|
|
||||||
request_body=RegistrationParentFileTemplateSerializer,
|
|
||||||
responses={
|
|
||||||
200: RegistrationParentFileTemplateSerializer,
|
|
||||||
400: "Données invalides",
|
|
||||||
404: "Template non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
|
||||||
if template is None:
|
|
||||||
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
operation_description="Supprime un template d'inscription",
|
|
||||||
responses={
|
|
||||||
204: "Suppression réussie",
|
|
||||||
404: "Template non trouvé"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def delete(self, request, id):
|
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
|
||||||
if template is not None:
|
|
||||||
template.delete()
|
|
||||||
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
|
||||||
else:
|
|
||||||
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|||||||
@ -5,8 +5,7 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
import json
|
import os
|
||||||
from django.http import QueryDict
|
|
||||||
|
|
||||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||||
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
||||||
@ -209,6 +208,17 @@ class RegistrationSchoolFileTemplateSimpleView(APIView):
|
|||||||
def delete(self, request, id):
|
def delete(self, request, id):
|
||||||
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
|
template = bdd.getObject(_objectName=RegistrationSchoolFileTemplate, _columnName='id', _value=id)
|
||||||
if template is not None:
|
if template is not None:
|
||||||
|
# Suppression du fichier PDF associé
|
||||||
|
if template.file and template.file.name:
|
||||||
|
file_path = template.file.path
|
||||||
|
template.file.delete(save=False)
|
||||||
|
# Vérification post-suppression
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
logger.info(f"Fichier supprimé manuellement: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la suppression manuelle du fichier: {e}")
|
||||||
template.delete()
|
template.delete()
|
||||||
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
else:
|
else:
|
||||||
@ -390,6 +400,17 @@ class RegistrationParentFileTemplateSimpleView(APIView):
|
|||||||
def delete(self, request, id):
|
def delete(self, request, id):
|
||||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
if template is not None:
|
if template is not None:
|
||||||
|
# Suppression du fichier PDF associé
|
||||||
|
if template.file and template.file.name:
|
||||||
|
file_path = template.file.path
|
||||||
|
template.file.delete(save=False)
|
||||||
|
# Vérification post-suppression
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
logger.info(f"Fichier supprimé manuellement: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la suppression manuelle du fichier: {e}")
|
||||||
template.delete()
|
template.delete()
|
||||||
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -34,9 +34,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
fetchRegistrationFileGroups,
|
fetchRegistrationFileGroups,
|
||||||
fetchRegistrationSchoolFileMasters,
|
fetchRegistrationSchoolFileMasters,
|
||||||
fetchRegistrationParentFileMasters,
|
fetchRegistrationParentFileMasters
|
||||||
createRegistrationSchoolFileTemplate,
|
|
||||||
createRegistrationParentFileTemplate,
|
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
import { fetchProfiles } from '@/app/actions/authAction';
|
import { fetchProfiles } from '@/app/actions/authAction';
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
|
|||||||
@ -100,12 +100,12 @@ export async function createRegistrationFileGroup(groupData, csrfToken) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
||||||
|
// Toujours FormData, jamais JSON
|
||||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: data,
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrfToken,
|
'X-CSRFToken': csrfToken,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -186,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 },
|
||||||
@ -517,32 +518,30 @@ export default function FormTemplateBuilder({
|
|||||||
{/* Sélecteur de groupes */}
|
{/* Sélecteur de groupes */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
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([
|
handleChange={() => {
|
||||||
...selectedGroups,
|
let group_ids = selectedGroups;
|
||||||
group.id,
|
if (group_ids.includes(group.id)) {
|
||||||
]);
|
group_ids = group_ids.filter((id) => id !== group.id);
|
||||||
} else {
|
} else {
|
||||||
setSelectedGroups(
|
group_ids = [...group_ids, group.id];
|
||||||
selectedGroups.filter((id) => id !== group.id)
|
}
|
||||||
);
|
setSelectedGroups(group_ids);
|
||||||
}
|
}}
|
||||||
}}
|
fieldName="groups"
|
||||||
className="mr-2 text-blue-600"
|
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">
|
||||||
|
|||||||
@ -267,10 +267,10 @@ export default function DynamicFormsList({
|
|||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Ce formulaire n'est pas encore configuré.
|
Ce formulaire n'est pas encore configuré.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
Contactez l'administration pour plus d'informations.
|
Contactez l'administration pour plus d'informations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -285,7 +285,7 @@ export default function DynamicFormsList({
|
|||||||
Tous les formulaires ont été complétés !
|
Tous les formulaires ont été complétés !
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Vous pouvez maintenant passer à l'étape suivante.
|
Vous pouvez maintenant passer à l'étape suivante.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
85
Front-End/src/components/SectionHeaderDocument.js
Normal file
85
Front-End/src/components/SectionHeaderDocument.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
|
||||||
|
// Thèmes couleur selon le type
|
||||||
|
const THEME = {
|
||||||
|
groupe: {
|
||||||
|
bg: 'bg-blue-50',
|
||||||
|
border: 'border-blue-200',
|
||||||
|
iconBg: 'bg-blue-100',
|
||||||
|
icon: 'text-blue-600',
|
||||||
|
title: 'text-blue-800',
|
||||||
|
desc: 'text-blue-600',
|
||||||
|
button: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||||
|
buttonText: 'text-blue-700',
|
||||||
|
buttonHover: 'hover:bg-blue-100',
|
||||||
|
},
|
||||||
|
formulaire: {
|
||||||
|
bg: 'bg-emerald-50',
|
||||||
|
border: 'border-emerald-200',
|
||||||
|
iconBg: 'bg-emerald-100',
|
||||||
|
icon: 'text-emerald-600',
|
||||||
|
title: 'text-emerald-800',
|
||||||
|
desc: 'text-emerald-600',
|
||||||
|
button: 'bg-emerald-500 text-white hover:bg-emerald-600',
|
||||||
|
buttonText: 'text-emerald-700',
|
||||||
|
buttonHover: 'hover:bg-emerald-100',
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
bg: 'bg-orange-50',
|
||||||
|
border: 'border-orange-200',
|
||||||
|
iconBg: 'bg-orange-100',
|
||||||
|
icon: 'text-orange-500',
|
||||||
|
title: 'text-orange-700',
|
||||||
|
desc: 'text-orange-600',
|
||||||
|
button: 'bg-orange-500 text-white hover:bg-orange-600',
|
||||||
|
buttonText: 'text-orange-700',
|
||||||
|
buttonHover: 'hover:bg-orange-100',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SectionHeaderDocument = ({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
button = false,
|
||||||
|
buttonOpeningModal = false,
|
||||||
|
onClick = null,
|
||||||
|
className = '',
|
||||||
|
type = 'groupe', // 'groupe', 'formulaire', 'parent'
|
||||||
|
}) => {
|
||||||
|
const theme = THEME[type] || THEME.groupe;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-between border-b ${theme.border} ${theme.bg} px-2 py-3 mb-4 ${className}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{Icon && (
|
||||||
|
<span className={`${theme.iconBg} p-2 rounded-md flex items-center justify-center`}>
|
||||||
|
<Icon className={`w-6 h-6 ${theme.icon}`} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h2 className={`text-lg font-semibold ${theme.title}`}>{title}</h2>
|
||||||
|
{description && (
|
||||||
|
<p className={`text-xs ${theme.desc}`}>{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{button && onClick && (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={
|
||||||
|
buttonOpeningModal
|
||||||
|
? `flex items-center ${theme.button} px-3 py-1 rounded-md shadow transition`
|
||||||
|
: `flex items-center ${theme.buttonText} ${theme.buttonHover} px-2 py-1 rounded-md`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5 mr-1" />
|
||||||
|
<span className="text-sm font-medium">Ajouter</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionHeaderDocument;
|
||||||
209
Front-End/src/components/Structure/Files/CreateDocumentModal.js
Normal file
209
Front-End/src/components/Structure/Files/CreateDocumentModal.js
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { FolderPlus, FileText, FilePlus2, ArrowLeft, Settings2, Upload as UploadIcon } from 'lucide-react';
|
||||||
|
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||||
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
|
|
||||||
|
export default function CreateDocumentModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onCreateGroup,
|
||||||
|
onCreateParentFile,
|
||||||
|
onCreateSchoolFileMaster,
|
||||||
|
groups = [],
|
||||||
|
}) {
|
||||||
|
const [step, setStep] = useState('main'); // main | choose_form | form_builder | file_upload
|
||||||
|
const [fileName, setFileName] = useState('');
|
||||||
|
const [selectedGroupsFileUpload, setSelectedGroupsFileUpload] = useState([]);
|
||||||
|
const [uploadedFile, setUploadedFile] = useState(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setStep('main');
|
||||||
|
setFileName('');
|
||||||
|
setSelectedGroupsFileUpload([]);
|
||||||
|
setUploadedFile(null);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Handler pour chaque type
|
||||||
|
const handleSelect = (type) => {
|
||||||
|
if (type === 'groupe') {
|
||||||
|
setStep('main');
|
||||||
|
onCreateGroup();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
if (type === 'formulaire') {
|
||||||
|
setStep('choose_form');
|
||||||
|
}
|
||||||
|
if (type === 'parent') {
|
||||||
|
setStep('main');
|
||||||
|
onCreateParentFile();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retour au menu principal
|
||||||
|
const handleBack = () => setStep('main');
|
||||||
|
|
||||||
|
// Submit pour formulaire existant
|
||||||
|
const handleFileUploadSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile) return;
|
||||||
|
onCreateSchoolFileMaster({
|
||||||
|
name: fileName,
|
||||||
|
group_ids: selectedGroupsFileUpload,
|
||||||
|
file: uploadedFile,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
setIsOpen={onClose}
|
||||||
|
title="Créer un document"
|
||||||
|
modalClassName="w-full max-w-md"
|
||||||
|
>
|
||||||
|
{step === 'main' && (
|
||||||
|
<div className="flex flex-col gap-6 py-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-blue-50 hover:bg-blue-100 border border-blue-200 transition"
|
||||||
|
onClick={() => handleSelect('groupe')}
|
||||||
|
>
|
||||||
|
<FolderPlus className="w-6 h-6 text-blue-600" />
|
||||||
|
<span className="font-semibold text-blue-800">Dossier d&aposinscription</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-50 hover:bg-emerald-100 border border-emerald-200 transition"
|
||||||
|
onClick={() => handleSelect('formulaire')}
|
||||||
|
>
|
||||||
|
<FileText className="w-6 h-6 text-emerald-600" />
|
||||||
|
<span className="font-semibold text-emerald-800">Formulaire scolaire</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-orange-50 hover:bg-orange-100 border border-orange-200 transition"
|
||||||
|
onClick={() => handleSelect('parent')}
|
||||||
|
>
|
||||||
|
<FilePlus2 className="w-6 h-6 text-orange-500" />
|
||||||
|
<span className="font-semibold text-orange-700">Pièce à fournir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 'choose_form' && (
|
||||||
|
<div className="flex flex-col gap-4 py-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-100 hover:bg-emerald-200 border border-emerald-300 transition"
|
||||||
|
onClick={() => setStep('form_builder')}
|
||||||
|
>
|
||||||
|
<Settings2 className="w-6 h-6 text-emerald-700" />
|
||||||
|
<span className="font-semibold text-emerald-900">Formulaire personnalisé</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-gray-100 hover:bg-gray-200 border border-gray-300 transition"
|
||||||
|
onClick={() => setStep('file_upload')}
|
||||||
|
>
|
||||||
|
<UploadIcon className="w-6 h-6 text-gray-700" />
|
||||||
|
<span className="font-semibold text-gray-900">Importer un formulaire existant</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mt-2"
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>Retour</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 'form_builder' && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>Retour</span>
|
||||||
|
</button>
|
||||||
|
<FormTemplateBuilder
|
||||||
|
onSave={(data) => {
|
||||||
|
onCreateSchoolFileMaster(data);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
groups={groups}
|
||||||
|
isEditing={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 'file_upload' && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>Retour</span>
|
||||||
|
</button>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleFileUploadSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border rounded px-3 py-2"
|
||||||
|
placeholder="Nom du formulaire"
|
||||||
|
value={fileName}
|
||||||
|
onChange={e => setFileName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/* Sélecteur de groupes à cocher */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Groupes d'inscription <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||||
|
{groups && groups.length > 0 ? (
|
||||||
|
groups.map((group) => (
|
||||||
|
<label key={group.id} className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedGroupsFileUpload.includes(group.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedGroupsFileUpload([
|
||||||
|
...selectedGroupsFileUpload,
|
||||||
|
group.id,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setSelectedGroupsFileUpload(
|
||||||
|
selectedGroupsFileUpload.filter((id) => id !== group.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mr-2 text-blue-600"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{group.name}</span>
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Aucun groupe disponible
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FileUpload
|
||||||
|
selectionMessage="Sélectionnez le fichier du formulaire"
|
||||||
|
onFileSelect={setUploadedFile}
|
||||||
|
required
|
||||||
|
enable
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-emerald-600 text-white px-4 py-2 rounded font-bold mt-2"
|
||||||
|
disabled={!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile}
|
||||||
|
>
|
||||||
|
Créer le formulaire
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,8 +10,8 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
|||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
|
|
||||||
export default function FileUploadDocuSeal({
|
export default function FileUploadDocuSeal({
|
||||||
handleCreateTemplateMaster,
|
handleCreateSchoolFileMaster,
|
||||||
handleEditTemplateMaster,
|
handleEditSchoolFileMaster,
|
||||||
fileToEdit = null,
|
fileToEdit = null,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) {
|
}) {
|
||||||
@ -75,7 +75,7 @@ export default function FileUploadDocuSeal({
|
|||||||
const is_required = data.fields.length > 0;
|
const is_required = data.fields.length > 0;
|
||||||
if (fileToEdit) {
|
if (fileToEdit) {
|
||||||
logger.debug('Modification du template master:', templateMaster?.id);
|
logger.debug('Modification du template master:', templateMaster?.id);
|
||||||
handleEditTemplateMaster({
|
handleEditSchoolFileMaster({
|
||||||
name: uploadedFileName,
|
name: uploadedFileName,
|
||||||
group_ids: selectedGroups.map((group) => group.id),
|
group_ids: selectedGroups.map((group) => group.id),
|
||||||
id: templateMaster?.id,
|
id: templateMaster?.id,
|
||||||
@ -83,7 +83,7 @@ export default function FileUploadDocuSeal({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug('Création du template master:', templateMaster?.id);
|
logger.debug('Création du template master:', templateMaster?.id);
|
||||||
handleCreateTemplateMaster({
|
handleCreateSchoolFileMaster({
|
||||||
name: uploadedFileName,
|
name: uploadedFileName,
|
||||||
group_ids: selectedGroups.map((group) => group.id),
|
group_ids: selectedGroups.map((group) => group.id),
|
||||||
id: templateMaster?.id,
|
id: templateMaster?.id,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
267
Front-End/src/components/Structure/Files/ParentFiles.js
Normal file
267
Front-End/src/components/Structure/Files/ParentFiles.js
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import logger from '@/utils/logger';
|
||||||
|
import { Edit3, Trash2, Plus } from 'lucide-react';
|
||||||
|
|
||||||
|
function ParentFileForm({ initialData, groups, onSubmit, onCancel }) {
|
||||||
|
const [name, setName] = useState(initialData?.name || '');
|
||||||
|
const [description, setDescription] = useState(initialData?.description || '');
|
||||||
|
// Correction : s'assurer que selectedGroups ne contient que des IDs uniques
|
||||||
|
const [selectedGroups, setSelectedGroups] = useState(
|
||||||
|
Array.isArray(initialData?.groups)
|
||||||
|
? Array.from(
|
||||||
|
new Set(
|
||||||
|
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
const [isRequired, setIsRequired] = useState(initialData?.is_required || false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
setName(initialData.name || '');
|
||||||
|
setDescription(initialData.description || '');
|
||||||
|
setSelectedGroups(
|
||||||
|
Array.isArray(initialData.groups)
|
||||||
|
? Array.from(
|
||||||
|
new Set(
|
||||||
|
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
setIsRequired(initialData.is_required || false);
|
||||||
|
}
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!name || selectedGroups.length === 0) return;
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
groups: selectedGroups,
|
||||||
|
is_required: isRequired,
|
||||||
|
id: initialData?.id,
|
||||||
|
};
|
||||||
|
logger.debug('[ParentFileForm] handleSubmit data:', data);
|
||||||
|
onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4 py-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nom de la pièce <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
required
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Dossiers d'inscription <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedGroups}
|
||||||
|
onChange={e =>
|
||||||
|
setSelectedGroups(
|
||||||
|
Array.from(new Set(Array.from(e.target.selectedOptions, opt => Number(opt.value))))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{groups.map(group => (
|
||||||
|
<option key={`group-option-${group.id}`} value={group.id}>
|
||||||
|
{group.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="is_required"
|
||||||
|
checked={isRequired}
|
||||||
|
onChange={e => setIsRequired(e.target.checked)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<label htmlFor="is_required" className="text-sm text-gray-700">
|
||||||
|
Obligatoire
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
|
||||||
|
disabled={!name || selectedGroups.length === 0}
|
||||||
|
>
|
||||||
|
{initialData?.id ? 'Modifier' : 'Créer'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ParentFiles({
|
||||||
|
parentFiles,
|
||||||
|
groups,
|
||||||
|
handleCreate,
|
||||||
|
handleEdit,
|
||||||
|
handleDelete,
|
||||||
|
singleForm = false,
|
||||||
|
initialData = null,
|
||||||
|
onCancel,
|
||||||
|
}) {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(singleForm);
|
||||||
|
const [editingFile, setEditingFile] = useState(initialData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (singleForm) {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setEditingFile(initialData);
|
||||||
|
}
|
||||||
|
}, [singleForm, initialData]);
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
setEditingFile(null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (file) => {
|
||||||
|
setEditingFile(file);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setEditingFile(null);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = (data) => {
|
||||||
|
logger.debug('[ParentFiles] handleFormSubmit data:', data);
|
||||||
|
if (editingFile && editingFile.id) {
|
||||||
|
logger.debug('[ParentFiles] handleEdit called with:', data.id, data);
|
||||||
|
handleEdit(data.id, data).then(closeModal);
|
||||||
|
} else {
|
||||||
|
logger.debug('[ParentFiles] handleCreate called with:', data);
|
||||||
|
handleCreate(data).then(closeModal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (singleForm) {
|
||||||
|
return (
|
||||||
|
<ParentFileForm
|
||||||
|
initialData={editingFile}
|
||||||
|
groups={groups}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
onCancel={closeModal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-bold text-orange-700">Pièces à fournir</h2>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded shadow"
|
||||||
|
onClick={openCreateModal}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
<span>Ajouter une pièce</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<table className="min-w-full border border-gray-200 rounded bg-white">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-orange-50">
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Nom</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Description</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Dossiers</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Obligatoire</th>
|
||||||
|
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{parentFiles.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="text-center py-6 text-gray-400">Aucune pièce à fournir</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
parentFiles.map((file) => (
|
||||||
|
<tr key={file.id} className="hover:bg-orange-50">
|
||||||
|
<td className="px-3 py-2 border-b">{file.name}</td>
|
||||||
|
<td className="px-3 py-2 border-b">{file.description}</td>
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
{(file.groups || []).map(
|
||||||
|
gid => groups.find(g => g.id === gid)?.name || gid
|
||||||
|
).join(', ')}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 border-b text-center">
|
||||||
|
{file.is_required ? (
|
||||||
|
<span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs font-semibold">Oui</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs">Non</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 border-b text-center">
|
||||||
|
<button
|
||||||
|
className="text-blue-500 hover:text-blue-700 mr-2"
|
||||||
|
onClick={() => openEditModal(file)}
|
||||||
|
>
|
||||||
|
<Edit3 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
onClick={() => handleDelete(file.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
setIsOpen={closeModal}
|
||||||
|
title={editingFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
|
||||||
|
modalClassName="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<ParentFileForm
|
||||||
|
initialData={editingFile}
|
||||||
|
groups={groups}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
onCancel={closeModal}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,16 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
|
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
|
||||||
import InputText from '@/components/Form/InputText';
|
|
||||||
import MultiSelect from '@/components/Form/MultiSelect';
|
|
||||||
import Popup from '@/components/Popup';
|
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
|
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
|
||||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
import Popup from '@/components/Popup';
|
||||||
|
import InputText from '@/components/Form/InputText';
|
||||||
|
import MultiSelect from '@/components/Form/MultiSelect';
|
||||||
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
|
|
||||||
export default function ParentFilesSection({
|
export default function ParentFilesSection({
|
||||||
parentFiles,
|
parentFiles,
|
||||||
@ -18,6 +15,11 @@ export default function ParentFilesSection({
|
|||||||
handleCreate,
|
handleCreate,
|
||||||
handleEdit,
|
handleEdit,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
hideCreateButton = false,
|
||||||
|
tableContainerClass = '',
|
||||||
|
headerClassName = '',
|
||||||
|
TableComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
}) {
|
}) {
|
||||||
const [editingDocumentId, setEditingDocumentId] = useState(null);
|
const [editingDocumentId, setEditingDocumentId] = useState(null);
|
||||||
const [formData, setFormData] = useState(null);
|
const [formData, setFormData] = useState(null);
|
||||||
@ -325,27 +327,30 @@ export default function ParentFilesSection({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Ajout : écouteur d'event global pour déclencher la création depuis la popup centrale
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!hideCreateButton) return;
|
||||||
|
const handler = () => handleAddEmptyRequiredDocument();
|
||||||
|
window.addEventListener('parentFilesSection:create', handler);
|
||||||
|
return () => window.removeEventListener('parentFilesSection:create', handler);
|
||||||
|
}, [hideCreateButton]);
|
||||||
|
|
||||||
|
const Table = TableComponent || ((props) => <div />); // fallback
|
||||||
|
const SectionHeader = SectionHeaderComponent || ((props) => <div />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 w-4/5">
|
<div className={`w-full h-full flex flex-col ${tableContainerClass}`}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
icon={FileText}
|
|
||||||
title="Pièces à fournir"
|
title="Pièces à fournir"
|
||||||
description="Configurez la liste des documents que les parents doivent fournir."
|
description="Configurez la liste des documents que les parents doivent fournir."
|
||||||
button={true}
|
className={headerClassName}
|
||||||
onClick={handleAddEmptyRequiredDocument}
|
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
data={
|
data={
|
||||||
editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles
|
editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles
|
||||||
}
|
}
|
||||||
columns={columnsRequiredDocuments}
|
columns={columnsRequiredDocuments}
|
||||||
emptyMessage={
|
emptyMessage="Aucune pièce à fournir enregistrée"
|
||||||
<AlertMessage
|
|
||||||
type="warning"
|
|
||||||
title="Aucune pièce à fournir enregistrée"
|
|
||||||
message="Veuillez procéder à la création de nouvelles pièces à fournir par les parents"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Popup
|
<Popup
|
||||||
isOpen={removePopupVisible}
|
isOpen={removePopupVisible}
|
||||||
|
|||||||
@ -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
|
value={name}
|
||||||
type="text"
|
onChange={(e) => setName(e.target.value)}
|
||||||
value={name}
|
required
|
||||||
onChange={(e) => setName(e.target.value)}
|
/>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
<InputText
|
||||||
required
|
label="Description"
|
||||||
/>
|
name="description"
|
||||||
</div>
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
<div>
|
required
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
/>
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school",
|
"name": "n3wt-school",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n3wt-school",
|
"name": "n3wt-school",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.5.0",
|
"@commitlint/cli": "^19.5.0",
|
||||||
"@commitlint/config-conventional": "^19.5.0",
|
"@commitlint/config-conventional": "^19.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user