diff --git a/Back-End/Auth/migrations/0001_initial.py b/Back-End/Auth/migrations/0001_initial.py
index f4492ff..6ed472e 100644
--- a/Back-End/Auth/migrations/0001_initial.py
+++ b/Back-End/Auth/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.contrib.auth.models
import django.contrib.auth.validators
diff --git a/Back-End/Common/migrations/0001_initial.py b/Back-End/Common/migrations/0001_initial.py
index 5725c9e..b45b14a 100644
--- a/Back-End/Common/migrations/0001_initial.py
+++ b/Back-End/Common/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.db.models.deletion
from django.db import migrations, models
diff --git a/Back-End/Establishment/migrations/0001_initial.py b/Back-End/Establishment/migrations/0001_initial.py
index 4ec0546..c718cca 100644
--- a/Back-End/Establishment/migrations/0001_initial.py
+++ b/Back-End/Establishment/migrations/0001_initial.py
@@ -1,5 +1,6 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
+import Establishment.models
import django.contrib.postgres.fields
from django.db import migrations, models
@@ -24,6 +25,7 @@ class Migration(migrations.Migration):
('licence_code', models.CharField(blank=True, max_length=100)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
+ ('logo', models.FileField(blank=True, null=True, upload_to=Establishment.models.registration_logo_upload_to)),
],
),
]
diff --git a/Back-End/Establishment/migrations/0002_establishment_api_docuseal.py b/Back-End/Establishment/migrations/0002_establishment_api_docuseal.py
deleted file mode 100644
index 568376b..0000000
--- a/Back-End/Establishment/migrations/0002_establishment_api_docuseal.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.1.3 on 2025-05-30 07:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('Establishment', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='establishment',
- name='api_docuseal',
- field=models.CharField(blank=True, max_length=255, null=True),
- ),
- ]
diff --git a/Back-End/Establishment/migrations/0003_establishment_logo.py b/Back-End/Establishment/migrations/0003_establishment_logo.py
deleted file mode 100644
index 1d89e51..0000000
--- a/Back-End/Establishment/migrations/0003_establishment_logo.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 5.1.3 on 2025-05-31 09:56
-
-import Establishment.models
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('Establishment', '0002_establishment_api_docuseal'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='establishment',
- name='logo',
- field=models.FileField(blank=True, null=True, upload_to=Establishment.models.registration_logo_upload_to),
- ),
- ]
diff --git a/Back-End/GestionMessagerie/migrations/0001_initial.py b/Back-End/GestionMessagerie/migrations/0001_initial.py
index 914c079..c36fba9 100644
--- a/Back-End/GestionMessagerie/migrations/0001_initial.py
+++ b/Back-End/GestionMessagerie/migrations/0001_initial.py
@@ -1,6 +1,8 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.db.models.deletion
+import django.utils.timezone
+import uuid
from django.conf import settings
from django.db import migrations, models
@@ -14,6 +16,39 @@ class Migration(migrations.Migration):
]
operations = [
+ migrations.CreateModel(
+ name='Conversation',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=255, null=True)),
+ ('conversation_type', models.CharField(choices=[('private', 'Privée'), ('group', 'Groupe')], default='private', max_length=10)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('last_activity', models.DateTimeField(default=django.utils.timezone.now)),
+ ('is_active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('content', models.TextField()),
+ ('message_type', models.CharField(choices=[('text', 'Texte'), ('file', 'Fichier'), ('image', 'Image'), ('system', 'Système')], default='text', max_length=10)),
+ ('file_url', models.URLField(blank=True, null=True)),
+ ('file_name', models.CharField(blank=True, max_length=255, null=True)),
+ ('file_size', models.BigIntegerField(blank=True, null=True)),
+ ('file_type', models.CharField(blank=True, max_length=100, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('is_edited', models.BooleanField(default=False)),
+ ('is_deleted', models.BooleanField(default=False)),
+ ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='GestionMessagerie.conversation')),
+ ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['created_at'],
+ },
+ ),
migrations.CreateModel(
name='Messagerie',
fields=[
@@ -27,4 +62,40 @@ class Migration(migrations.Migration):
('emetteur', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='messages_envoyes', to=settings.AUTH_USER_MODEL)),
],
),
+ migrations.CreateModel(
+ name='UserPresence',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('status', models.CharField(choices=[('online', 'En ligne'), ('away', 'Absent'), ('busy', 'Occupé'), ('offline', 'Hors ligne')], default='offline', max_length=10)),
+ ('last_seen', models.DateTimeField(default=django.utils.timezone.now)),
+ ('is_typing_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='typing_users', to='GestionMessagerie.conversation')),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='presence', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ConversationParticipant',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('joined_at', models.DateTimeField(auto_now_add=True)),
+ ('last_read_at', models.DateTimeField(default=django.utils.timezone.now)),
+ ('is_active', models.BooleanField(default=True)),
+ ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='GestionMessagerie.conversation')),
+ ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_participants', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('conversation', 'participant')},
+ },
+ ),
+ migrations.CreateModel(
+ name='MessageRead',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('read_at', models.DateTimeField(auto_now_add=True)),
+ ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_by', to='GestionMessagerie.message')),
+ ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_messages', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('message', 'participant')},
+ },
+ ),
]
diff --git a/Back-End/GestionMessagerie/migrations/0002_conversation_message_userpresence_and_more.py b/Back-End/GestionMessagerie/migrations/0002_conversation_message_userpresence_and_more.py
deleted file mode 100644
index abe623e..0000000
--- a/Back-End/GestionMessagerie/migrations/0002_conversation_message_userpresence_and_more.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Generated by Django 5.1.3 on 2025-05-30 07:40
-
-import django.db.models.deletion
-import django.utils.timezone
-import uuid
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('GestionMessagerie', '0001_initial'),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Conversation',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('name', models.CharField(blank=True, max_length=255, null=True)),
- ('conversation_type', models.CharField(choices=[('private', 'Privée'), ('group', 'Groupe')], default='private', max_length=10)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('last_activity', models.DateTimeField(default=django.utils.timezone.now)),
- ('is_active', models.BooleanField(default=True)),
- ],
- ),
- migrations.CreateModel(
- name='Message',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('content', models.TextField()),
- ('message_type', models.CharField(choices=[('text', 'Texte'), ('file', 'Fichier'), ('image', 'Image'), ('system', 'Système')], default='text', max_length=10)),
- ('file_url', models.URLField(blank=True, null=True)),
- ('file_name', models.CharField(blank=True, max_length=255, null=True)),
- ('file_size', models.BigIntegerField(blank=True, null=True)),
- ('file_type', models.CharField(blank=True, max_length=100, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('is_edited', models.BooleanField(default=False)),
- ('is_deleted', models.BooleanField(default=False)),
- ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='GestionMessagerie.conversation')),
- ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'ordering': ['created_at'],
- },
- ),
- migrations.CreateModel(
- name='UserPresence',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('status', models.CharField(choices=[('online', 'En ligne'), ('away', 'Absent'), ('busy', 'Occupé'), ('offline', 'Hors ligne')], default='offline', max_length=10)),
- ('last_seen', models.DateTimeField(default=django.utils.timezone.now)),
- ('is_typing_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='typing_users', to='GestionMessagerie.conversation')),
- ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='presence', to=settings.AUTH_USER_MODEL)),
- ],
- ),
- migrations.CreateModel(
- name='ConversationParticipant',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('joined_at', models.DateTimeField(auto_now_add=True)),
- ('last_read_at', models.DateTimeField(default=django.utils.timezone.now)),
- ('is_active', models.BooleanField(default=True)),
- ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='GestionMessagerie.conversation')),
- ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_participants', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'unique_together': {('conversation', 'participant')},
- },
- ),
- migrations.CreateModel(
- name='MessageRead',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('read_at', models.DateTimeField(auto_now_add=True)),
- ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_by', to='GestionMessagerie.message')),
- ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_messages', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'unique_together': {('message', 'participant')},
- },
- ),
- ]
diff --git a/Back-End/GestionNotification/migrations/0001_initial.py b/Back-End/GestionNotification/migrations/0001_initial.py
index 3517e45..4500122 100644
--- a/Back-End/GestionNotification/migrations/0001_initial.py
+++ b/Back-End/GestionNotification/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.db.models.deletion
from django.conf import settings
diff --git a/Back-End/N3wtSchool/Configuration/application.default.json b/Back-End/N3wtSchool/Configuration/application.default.json
deleted file mode 100644
index 3e9effc..0000000
--- a/Back-End/N3wtSchool/Configuration/application.default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "hostSMTP": "",
- "portSMTP": 25,
- "username": "",
- "password": "",
- "useSSL": false,
- "useTLS": false
-}
\ No newline at end of file
diff --git a/Back-End/N3wtSchool/mailManager.py b/Back-End/N3wtSchool/mailManager.py
index 183c1b4..7bf9cce 100644
--- a/Back-End/N3wtSchool/mailManager.py
+++ b/Back-End/N3wtSchool/mailManager.py
@@ -17,9 +17,12 @@ def getConnection(id_establishement):
try:
# Récupérer l'instance de l'établissement
establishment = Establishment.objects.get(id=id_establishement)
+ logger.info(f"Establishment trouvé: {establishment.name} (ID: {id_establishement})")
+
try:
# Récupérer les paramètres SMTP associés à l'établissement
smtp_settings = SMTPSettings.objects.get(establishment=establishment)
+ logger.info(f"Paramètres SMTP trouvés pour {establishment.name}: {smtp_settings.smtp_server}:{smtp_settings.smtp_port}")
# Créer une connexion SMTP avec les paramètres récupérés
connection = get_connection(
@@ -32,9 +35,11 @@ def getConnection(id_establishement):
)
return connection
except SMTPSettings.DoesNotExist:
+ logger.warning(f"Aucun paramètre SMTP spécifique trouvé pour l'établissement {establishment.name} (ID: {id_establishement})")
# Aucun paramètre SMTP spécifique, retournera None
return None
except Establishment.DoesNotExist:
+ logger.error(f"Aucun établissement trouvé avec l'ID {id_establishement}")
raise NotFound(f"Aucun établissement trouvé avec l'ID {id_establishement}")
def sendMail(subject, message, recipients, cc=[], bcc=[], attachments=[], connection=None):
@@ -53,11 +58,13 @@ def sendMail(subject, message, recipients, cc=[], bcc=[], attachments=[], connec
plain_message = strip_tags(message)
if connection is not None:
from_email = username
+ logger.info(f"Utilisation de la connexion SMTP spécifique: {username}")
else:
from_email = settings.EMAIL_HOST_USER
-
+ logger.info(f"Utilisation de la configuration SMTP par défaut: {from_email}")
logger.info(f"From email: {from_email}")
+ logger.info(f"Configuration par défaut - Host: {settings.EMAIL_HOST}, Port: {settings.EMAIL_PORT}, Use TLS: {settings.EMAIL_USE_TLS}")
email = EmailMultiAlternatives(
subject=subject,
@@ -79,6 +86,8 @@ def sendMail(subject, message, recipients, cc=[], bcc=[], attachments=[], connec
return Response({'message': 'Email envoyé avec succès.'}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Erreur lors de l'envoi de l'email: {str(e)}")
+ logger.error(f"Settings : {connection}")
+ logger.error(f"Settings : {connection}")
logger.error(f"Type d'erreur: {type(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
diff --git a/Back-End/Planning/migrations/0001_initial.py b/Back-End/Planning/migrations/0001_initial.py
index e6472c6..3d41fdb 100644
--- a/Back-End/Planning/migrations/0001_initial.py
+++ b/Back-End/Planning/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.db.models.deletion
from django.db import migrations, models
diff --git a/Back-End/School/migrations/0001_initial.py b/Back-End/School/migrations/0001_initial.py
index 4289620..d0eb4e4 100644
--- a/Back-End/School/migrations/0001_initial.py
+++ b/Back-End/School/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.contrib.postgres.fields
import django.db.models.deletion
diff --git a/Back-End/Settings/migrations/0001_initial.py b/Back-End/Settings/migrations/0001_initial.py
index c05340d..ebc8792 100644
--- a/Back-End/Settings/migrations/0001_initial.py
+++ b/Back-End/Settings/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import django.db.models.deletion
from django.db import migrations, models
diff --git a/Back-End/Subscriptions/Configuration/inscriptions.json b/Back-End/Subscriptions/Configuration/inscriptions.json
deleted file mode 100644
index 0170a43..0000000
--- a/Back-End/Subscriptions/Configuration/inscriptions.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "activationMailRelance": "Oui",
- "delaiRelance": "30",
- "ambiances": [
- "2-3 ans",
- "3-6 ans",
- "6-12 ans"
- ],
- "genres": [
- "Fille",
- "Garçon"
- ],
- "modesPaiement": [
- "Chèque",
- "Virement",
- "Prélèvement SEPA"
- ]
-}
\ No newline at end of file
diff --git a/Back-End/Subscriptions/management/__init__.py b/Back-End/Subscriptions/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Back-End/Subscriptions/management/commands/__init__.py b/Back-End/Subscriptions/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Back-End/Subscriptions/management/commands/test_email.py b/Back-End/Subscriptions/management/commands/test_email.py
new file mode 100644
index 0000000..d938b23
--- /dev/null
+++ b/Back-End/Subscriptions/management/commands/test_email.py
@@ -0,0 +1,43 @@
+"""
+Management command pour tester la configuration email Django
+"""
+from django.core.management.base import BaseCommand
+from django.core.mail import send_mail
+from django.conf import settings
+from N3wtSchool.mailManager import getConnection, sendMail
+import logging
+
+logger = logging.getLogger(__name__)
+
+class Command(BaseCommand):
+ help = 'Test de la configuration email'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--establishment-id', type=int, help='ID de l\'établissement pour test')
+ parser.add_argument('--email', type=str, default='test@example.com', help='Email de destination')
+
+ def handle(self, *args, **options):
+ self.stdout.write("=== Test de configuration email ===")
+
+ # Affichage de la configuration
+ self.stdout.write(f"EMAIL_HOST: {settings.EMAIL_HOST}")
+ self.stdout.write(f"EMAIL_PORT: {settings.EMAIL_PORT}")
+ self.stdout.write(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}")
+ self.stdout.write(f"EMAIL_HOST_PASSWORD: {settings.EMAIL_HOST_PASSWORD}")
+ self.stdout.write(f"EMAIL_USE_TLS: {settings.EMAIL_USE_TLS}")
+ self.stdout.write(f"EMAIL_USE_SSL: {settings.EMAIL_USE_SSL}")
+ self.stdout.write(f"EMAIL_BACKEND: {settings.EMAIL_BACKEND}")
+
+ # Test 1: Configuration par défaut Django
+ self.stdout.write("\n--- Test : Configuration EMAIL par défaut ---")
+ try:
+ result = send_mail(
+ 'Test Django Email',
+ 'Ceci est un test de la configuration email par défaut.',
+ settings.EMAIL_HOST_USER,
+ [options['email']],
+ fail_silently=False,
+ )
+ self.stdout.write(self.style.SUCCESS(f"✅ Email envoyé avec succès (résultat: {result})"))
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f"❌ Erreur: {e}"))
\ No newline at end of file
diff --git a/Back-End/Subscriptions/migrations/0001_initial.py b/Back-End/Subscriptions/migrations/0001_initial.py
index afd9c5c..d74fbcb 100644
--- a/Back-End/Subscriptions/migrations/0001_initial.py
+++ b/Back-End/Subscriptions/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2025-05-28 11:14
+# Generated by Django 5.1.3 on 2025-11-30 11:02
import Subscriptions.models
import django.db.models.deletion
@@ -46,10 +46,11 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RegistrationSchoolFileTemplate',
fields=[
- ('id', models.IntegerField(primary_key=True, serialize=False)),
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.CharField(default='', max_length=255)),
('name', models.CharField(default='', max_length=255)),
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
+ ('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
],
),
migrations.CreateModel(
@@ -153,7 +154,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='', max_length=255)),
- ('description', models.CharField(blank=True, null=True)),
+ ('description', models.CharField(blank=True, max_length=500, null=True)),
('is_required', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, related_name='parent_file_masters', to='Subscriptions.registrationfilegroup')),
],
@@ -161,9 +162,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RegistrationSchoolFileMaster',
fields=[
- ('id', models.IntegerField(primary_key=True, serialize=False)),
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='', max_length=255)),
('is_required', models.BooleanField(default=False)),
+ ('formMasterData', models.JSONField(blank=True, default=list, null=True)),
('groups', models.ManyToManyField(blank=True, related_name='school_file_masters', to='Subscriptions.registrationfilegroup')),
],
),
diff --git a/Back-End/Subscriptions/migrations/0002_alter_registrationparentfilemaster_description.py b/Back-End/Subscriptions/migrations/0002_alter_registrationparentfilemaster_description.py
deleted file mode 100644
index 82d5627..0000000
--- a/Back-End/Subscriptions/migrations/0002_alter_registrationparentfilemaster_description.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.1.3 on 2025-05-30 07:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('Subscriptions', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='registrationparentfilemaster',
- name='description',
- field=models.CharField(blank=True, max_length=500, null=True),
- ),
- ]
diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py
index e3aeabd..e9d4ee5 100644
--- a/Back-End/Subscriptions/serializers.py
+++ b/Back-End/Subscriptions/serializers.py
@@ -1,15 +1,15 @@
from rest_framework import serializers
from .models import (
- RegistrationFileGroup,
- RegistrationForm,
- Student,
- Guardian,
- Sibling,
+ RegistrationFileGroup,
+ RegistrationForm,
+ Student,
+ Guardian,
+ Sibling,
Language,
- RegistrationSchoolFileMaster,
- RegistrationSchoolFileTemplate,
- RegistrationParentFileMaster,
- RegistrationParentFileTemplate,
+ RegistrationSchoolFileMaster,
+ RegistrationSchoolFileTemplate,
+ RegistrationParentFileMaster,
+ RegistrationParentFileTemplate,
AbsenceManagement,
BilanCompetence
)
@@ -95,7 +95,7 @@ class RegistrationFormSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationForm
fields = ['student_id', 'last_name', 'first_name', 'guardians']
-
+
def get_last_name(self, obj):
return obj.student.last_name
@@ -164,12 +164,20 @@ class StudentSerializer(serializers.ModelSerializer):
if guardian_id:
# Si un ID est fourni, récupérer ou mettre à jour le Guardian existant
- guardian_instance, created = Guardian.objects.update_or_create(
- id=guardian_id,
- defaults=guardian_data
- )
- guardians_ids.append(guardian_instance.id)
- continue
+ try:
+ guardian_instance = Guardian.objects.get(id=guardian_id)
+ # Mettre à jour explicitement tous les champs y compris birth_date, profession, address
+ for field, value in guardian_data.items():
+ if field != 'id': # Ne pas mettre à jour l'ID
+ setattr(guardian_instance, field, value)
+ guardian_instance.save()
+ guardians_ids.append(guardian_instance.id)
+ continue
+ except Guardian.DoesNotExist:
+ # Si le guardian n'existe pas, créer un nouveau
+ guardian_instance = Guardian.objects.create(**guardian_data)
+ guardians_ids.append(guardian_instance.id)
+ continue
if profile_role_data:
# Vérifiez si 'profile_data' est fourni pour créer un nouveau profil
diff --git a/Back-End/Subscriptions/tasks.py b/Back-End/Subscriptions/tasks.py
index cae3930..14f8778 100644
--- a/Back-End/Subscriptions/tasks.py
+++ b/Back-End/Subscriptions/tasks.py
@@ -5,6 +5,8 @@ from Subscriptions.automate import Automate_RF_Register, updateStateMachine
from .models import RegistrationForm
from GestionMessagerie.models import Messagerie
from N3wtSchool import settings, bdd
+from N3wtSchool.mailManager import sendMail, getConnection
+from django.template.loader import render_to_string
import requests
import logging
logger = logging.getLogger(__name__)
@@ -26,17 +28,82 @@ def send_notification(dossier):
# Changer l'état de l'automate
updateStateMachine(dossier, 'EVENT_FOLLOW_UP')
- url = settings.URL_DJANGO + 'GestionMessagerie/message'
+ # Envoyer un email de relance aux responsables
+ try:
+ # Récupérer l'établissement du dossier
+ establishment_id = dossier.establishment.id
- destinataires = dossier.eleve.profiles.all()
- for destinataire in destinataires:
- message = {
- "objet": "[RELANCE]",
- "destinataire" : destinataire.id,
- "corpus": "RELANCE pour le dossier d'inscription"
+ # Obtenir la connexion SMTP pour cet établissement
+ connection = getConnection(establishment_id)
+
+ # Préparer le contenu de l'email
+ subject = f"[RELANCE] Dossier d'inscription en attente - {dossier.eleve.first_name} {dossier.eleve.last_name}"
+
+ context = {
+ 'student_name': f"{dossier.eleve.first_name} {dossier.eleve.last_name}",
+ 'deadline_date': (timezone.now() - timezone.timedelta(days=settings.EXPIRATION_DI_NB_DAYS)).strftime('%d/%m/%Y'),
+ 'establishment_name': dossier.establishment.name,
+ 'base_url': settings.BASE_URL
}
- response = requests.post(url, json=message)
+ # Utiliser un template HTML pour l'email (si disponible)
+ try:
+ html_message = render_to_string('emails/relance_signature.html', context)
+ except:
+ # Si pas de template, message simple
+ html_message = f"""
+
+
+ Relance - Dossier d'inscription en attente
+ Bonjour,
+ Le dossier d'inscription de {context['student_name']} est en attente de signature depuis plus de {settings.EXPIRATION_DI_NB_DAYS} jours.
+ Merci de vous connecter à votre espace pour finaliser l'inscription.
+ Cordialement, L'équipe {context['establishment_name']}
+
+
+ """
+
+ # Récupérer les emails des responsables
+ destinataires = []
+ profiles = dossier.eleve.profiles.all()
+ for profile in profiles:
+ if profile.email:
+ destinataires.append(profile.email)
+
+ if destinataires:
+ # Envoyer l'email
+ result = sendMail(
+ subject=subject,
+ message=html_message,
+ recipients=destinataires,
+ connection=connection
+ )
+ logger.info(f"Email de relance envoyé pour le dossier {dossier.id} à {destinataires}")
+ else:
+ logger.warning(f"Aucun email trouvé pour les responsables du dossier {dossier.id}")
+
+ except Exception as e:
+ logger.error(f"Erreur lors de l'envoi de l'email de relance pour le dossier {dossier.id}: {str(e)}")
+
+ # En cas d'erreur email, utiliser la messagerie interne comme fallback
+ try:
+ url = settings.URL_DJANGO + 'GestionMessagerie/send-message/'
+
+ # Créer ou récupérer une conversation avec chaque responsable
+ destinataires = dossier.eleve.profiles.all()
+ for destinataire in destinataires:
+ message_data = {
+ "conversation_id": None, # Sera géré par l'API
+ "sender_id": 1, # ID du système ou admin
+ "content": f"RELANCE pour le dossier d'inscription de {dossier.eleve.first_name} {dossier.eleve.last_name}"
+ }
+
+ response = requests.post(url, json=message_data)
+ if response.status_code != 201:
+ logger.error(f"Erreur lors de l'envoi du message interne: {response.text}")
+
+ except Exception as inner_e:
+ logger.error(f"Erreur lors de l'envoi du message interne de fallback: {str(inner_e)}")
# subject = f"Dossier d'inscription non signé - {dossier.objet}"
# message = f"Le dossier d'inscription avec l'objet '{dossier.objet}' n'a pas été signé depuis {dossier.created_at}."
diff --git a/Back-End/Subscriptions/templates/emails/relance_signature.html b/Back-End/Subscriptions/templates/emails/relance_signature.html
new file mode 100644
index 0000000..411920f
--- /dev/null
+++ b/Back-End/Subscriptions/templates/emails/relance_signature.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Relance - Dossier d'inscription
+
+
+
+
+
+
+
+ ⚠️
+ Attention : Votre dossier d'inscription nécessite votre attention
+
+
+
+
Bonjour,
+
+
+
Dossier d'inscription de : {{ student_name }}
+
En attente depuis le : {{ deadline_date }}
+
+
+
Nous vous informons que le dossier d'inscription mentionné ci-dessus est en attente de finalisation depuis plus de {{ deadline_date }}.
+
+
Action requise :
+
+ Connectez-vous à votre espace personnel
+ Vérifiez les documents manquants
+ Complétez et signez les formulaires en attente
+
+
+
+
+
Si vous rencontrez des difficultés ou avez des questions concernant ce dossier, n'hésitez pas à nous contacter.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py
index 16f2d33..f4e0a1e 100644
--- a/Back-End/Subscriptions/util.py
+++ b/Back-End/Subscriptions/util.py
@@ -35,6 +35,18 @@ def build_payload_from_request(request):
- supporte application/json ou form-data simple
Retour: (payload_dict, None) ou (None, Response erreur)
"""
+ # Si c'est du JSON pur (Content-Type: application/json)
+ if hasattr(request, 'content_type') and 'application/json' in request.content_type:
+ try:
+ # request.data contient déjà le JSON parsé par Django REST
+ payload = dict(request.data) if hasattr(request.data, 'items') else request.data
+ logger.info(f"JSON payload extracted: {payload}")
+ return payload, None
+ except Exception as e:
+ logger.error(f'Error processing JSON: {e}')
+ return None, Response({'error': "Invalid JSON", 'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Cas multipart/form-data avec champ 'data'
data_field = request.data.get('data') if hasattr(request.data, 'get') else None
if data_field:
try:
diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py
index 18479f5..d7e6e93 100644
--- a/Back-End/Subscriptions/views/register_form_views.py
+++ b/Back-End/Subscriptions/views/register_form_views.py
@@ -17,10 +17,10 @@ import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.pagination import CustomSubscriptionPagination
from Subscriptions.models import (
- Guardian,
- RegistrationForm,
- RegistrationSchoolFileTemplate,
- RegistrationFileGroup,
+ Guardian,
+ RegistrationForm,
+ RegistrationSchoolFileTemplate,
+ RegistrationFileGroup,
RegistrationParentFileTemplate,
StudentCompetency
)
@@ -431,6 +431,262 @@ class RegisterFormWithIdView(APIView):
# Retourner les données mises à jour
return JsonResponse(studentForm_serializer.data, safe=False)
+ @swagger_auto_schema(
+ request_body=openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ 'student_data': openapi.Schema(type=openapi.TYPE_STRING, description='JSON string des données étudiant'),
+ 'guardians_data': openapi.Schema(type=openapi.TYPE_STRING, description='JSON string des données responsables'),
+ 'siblings_data': openapi.Schema(type=openapi.TYPE_STRING, description='JSON string des données fratrie'),
+ 'payment_data': openapi.Schema(type=openapi.TYPE_STRING, description='JSON string des données de paiement'),
+ 'current_page': openapi.Schema(type=openapi.TYPE_INTEGER, description='Page actuelle du formulaire'),
+ 'auto_save': openapi.Schema(type=openapi.TYPE_BOOLEAN, description='Indicateur auto-save'),
+ }
+ ),
+ responses={200: RegistrationFormSerializer()},
+ operation_description="Auto-sauvegarde partielle d'un dossier d'inscription.",
+ operation_summary="Auto-sauvegarder un dossier d'inscription"
+ )
+ @method_decorator(csrf_protect, name='dispatch')
+ @method_decorator(ensure_csrf_cookie, name='dispatch')
+ def patch(self, request, id):
+ """
+ Auto-sauvegarde partielle d'un dossier d'inscription.
+ Cette méthode est optimisée pour les sauvegardes automatiques périodiques.
+ """
+ try:
+ # Récupérer le dossier d'inscription
+ registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
+ if not registerForm:
+ return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND)
+
+ # Préparer les données à mettre à jour
+ update_data = {}
+
+ # Traiter les données étudiant si présentes
+ if 'student_data' in request.data:
+ try:
+ student_data = json.loads(request.data['student_data'])
+
+ # Extraire les données de paiement des données étudiant
+ payment_fields = ['registration_payment', 'tuition_payment', 'registration_payment_plan', 'tuition_payment_plan']
+ payment_data = {}
+
+ for field in payment_fields:
+ if field in student_data:
+ payment_data[field] = student_data.pop(field)
+
+ # Si nous avons des données de paiement, les traiter
+ if payment_data:
+ logger.debug(f"Auto-save: extracted payment_data from student_data = {payment_data}")
+
+ # Traiter les données de paiement
+ payment_updates = {}
+
+ # Gestion du mode de paiement d'inscription
+ if 'registration_payment' in payment_data and payment_data['registration_payment']:
+ try:
+ from School.models import PaymentMode
+ payment_mode = PaymentMode.objects.get(id=payment_data['registration_payment'])
+ registerForm.registration_payment = payment_mode
+ payment_updates['registration_payment'] = payment_mode.id
+ except PaymentMode.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentMode with id {payment_data['registration_payment']} not found")
+
+ # Gestion du mode de paiement de scolarité
+ if 'tuition_payment' in payment_data and payment_data['tuition_payment']:
+ try:
+ from School.models import PaymentMode
+ payment_mode = PaymentMode.objects.get(id=payment_data['tuition_payment'])
+ registerForm.tuition_payment = payment_mode
+ payment_updates['tuition_payment'] = payment_mode.id
+ except PaymentMode.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentMode with id {payment_data['tuition_payment']} not found")
+
+ # Gestion du plan de paiement d'inscription
+ if 'registration_payment_plan' in payment_data and payment_data['registration_payment_plan']:
+ try:
+ from School.models import PaymentPlan
+ payment_plan = PaymentPlan.objects.get(id=payment_data['registration_payment_plan'])
+ registerForm.registration_payment_plan = payment_plan
+ payment_updates['registration_payment_plan'] = payment_plan.id
+ except PaymentPlan.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentPlan with id {payment_data['registration_payment_plan']} not found")
+
+ # Gestion du plan de paiement de scolarité
+ if 'tuition_payment_plan' in payment_data and payment_data['tuition_payment_plan']:
+ try:
+ from School.models import PaymentPlan
+ payment_plan = PaymentPlan.objects.get(id=payment_data['tuition_payment_plan'])
+ registerForm.tuition_payment_plan = payment_plan
+ payment_updates['tuition_payment_plan'] = payment_plan.id
+ except PaymentPlan.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentPlan with id {payment_data['tuition_payment_plan']} not found")
+
+ # Sauvegarder les modifications de paiement
+ if payment_updates:
+ registerForm.save()
+ logger.debug(f"Auto-save: Payment data updated - {payment_updates}")
+
+ update_data['student'] = student_data
+ except json.JSONDecodeError:
+ logger.warning("Auto-save: Invalid JSON in student_data")
+
+ # Traiter les données des responsables si présentes
+ if 'guardians_data' in request.data:
+ try:
+ guardians_data = json.loads(request.data['guardians_data'])
+ logger.debug(f"Auto-save: guardians_data = {guardians_data}")
+
+ # Enregistrer directement chaque guardian avec le modèle
+ for i, guardian_data in enumerate(guardians_data):
+ guardian_id = guardian_data.get('id')
+ if guardian_id:
+ try:
+ # Récupérer le guardian existant et mettre à jour ses champs
+ guardian = Guardian.objects.get(id=guardian_id)
+
+ # Mettre à jour les champs si ils sont présents
+ if 'birth_date' in guardian_data and guardian_data['birth_date']:
+ guardian.birth_date = guardian_data['birth_date']
+ if 'profession' in guardian_data:
+ guardian.profession = guardian_data['profession']
+ if 'address' in guardian_data:
+ guardian.address = guardian_data['address']
+ if 'phone' in guardian_data:
+ guardian.phone = guardian_data['phone']
+ if 'first_name' in guardian_data:
+ guardian.first_name = guardian_data['first_name']
+ if 'last_name' in guardian_data:
+ guardian.last_name = guardian_data['last_name']
+
+ guardian.save()
+ logger.debug(f"Guardian {i}: Updated birth_date={guardian.birth_date}, profession={guardian.profession}, address={guardian.address}")
+
+ except Guardian.DoesNotExist:
+ logger.warning(f"Auto-save: Guardian with id {guardian_id} not found")
+
+ except json.JSONDecodeError:
+ logger.warning("Auto-save: Invalid JSON in guardians_data")
+
+ # Traiter les données de la fratrie si présentes
+ if 'siblings_data' in request.data:
+ try:
+ siblings_data = json.loads(request.data['siblings_data'])
+ logger.debug(f"Auto-save: siblings_data = {siblings_data}")
+
+ # Enregistrer directement chaque sibling avec le modèle
+ for i, sibling_data in enumerate(siblings_data):
+ sibling_id = sibling_data.get('id')
+ if sibling_id:
+ try:
+ # Récupérer le sibling existant et mettre à jour ses champs
+ from Subscriptions.models import Sibling
+ sibling = Sibling.objects.get(id=sibling_id)
+
+ # Mettre à jour les champs si ils sont présents
+ if 'first_name' in sibling_data:
+ sibling.first_name = sibling_data['first_name']
+ if 'last_name' in sibling_data:
+ sibling.last_name = sibling_data['last_name']
+ if 'birth_date' in sibling_data and sibling_data['birth_date']:
+ sibling.birth_date = sibling_data['birth_date']
+
+ sibling.save()
+ logger.debug(f"Sibling {i}: Updated first_name={sibling.first_name}, last_name={sibling.last_name}, birth_date={sibling.birth_date}")
+
+ except Sibling.DoesNotExist:
+ logger.warning(f"Auto-save: Sibling with id {sibling_id} not found")
+
+ except json.JSONDecodeError:
+ logger.warning("Auto-save: Invalid JSON in siblings_data")
+
+ # Traiter les données de paiement si présentes
+ if 'payment_data' in request.data:
+ try:
+ payment_data = json.loads(request.data['payment_data'])
+ logger.debug(f"Auto-save: payment_data = {payment_data}")
+
+ # Mettre à jour directement les champs de paiement du formulaire
+ payment_updates = {}
+
+ # Gestion du mode de paiement d'inscription
+ if 'registration_payment' in payment_data and payment_data['registration_payment']:
+ try:
+ from School.models import PaymentMode
+ payment_mode = PaymentMode.objects.get(id=payment_data['registration_payment'])
+ registerForm.registration_payment = payment_mode
+ payment_updates['registration_payment'] = payment_mode.id
+ except PaymentMode.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentMode with id {payment_data['registration_payment']} not found")
+
+ # Gestion du mode de paiement de scolarité
+ if 'tuition_payment' in payment_data and payment_data['tuition_payment']:
+ try:
+ from School.models import PaymentMode
+ payment_mode = PaymentMode.objects.get(id=payment_data['tuition_payment'])
+ registerForm.tuition_payment = payment_mode
+ payment_updates['tuition_payment'] = payment_mode.id
+ except PaymentMode.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentMode with id {payment_data['tuition_payment']} not found")
+
+ # Gestion du plan de paiement d'inscription
+ if 'registration_payment_plan' in payment_data and payment_data['registration_payment_plan']:
+ try:
+ from School.models import PaymentPlan
+ payment_plan = PaymentPlan.objects.get(id=payment_data['registration_payment_plan'])
+ registerForm.registration_payment_plan = payment_plan
+ payment_updates['registration_payment_plan'] = payment_plan.id
+ except PaymentPlan.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentPlan with id {payment_data['registration_payment_plan']} not found")
+
+ # Gestion du plan de paiement de scolarité
+ if 'tuition_payment_plan' in payment_data and payment_data['tuition_payment_plan']:
+ try:
+ from School.models import PaymentPlan
+ payment_plan = PaymentPlan.objects.get(id=payment_data['tuition_payment_plan'])
+ registerForm.tuition_payment_plan = payment_plan
+ payment_updates['tuition_payment_plan'] = payment_plan.id
+ except PaymentPlan.DoesNotExist:
+ logger.warning(f"Auto-save: PaymentPlan with id {payment_data['tuition_payment_plan']} not found")
+
+ # Sauvegarder les modifications de paiement
+ if payment_updates:
+ registerForm.save()
+ logger.debug(f"Auto-save: Payment data updated - {payment_updates}")
+
+ except json.JSONDecodeError:
+ logger.warning("Auto-save: Invalid JSON in payment_data")
+
+ # Mettre à jour la page actuelle si présente
+ if 'current_page' in request.data:
+ try:
+ current_page = int(request.data['current_page'])
+ # Vous pouvez sauvegarder cette info dans un champ du modèle si nécessaire
+ logger.debug(f"Auto-save: current_page = {current_page}")
+ except (ValueError, TypeError):
+ logger.warning("Auto-save: Invalid current_page value")
+
+ # Effectuer la mise à jour partielle seulement si nous avons des données
+ if update_data:
+ serializer = RegistrationFormSerializer(registerForm, data=update_data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ logger.debug(f"Auto-save successful for student {id}")
+ return JsonResponse({"status": "auto_save_success", "timestamp": util._now().isoformat()}, safe=False)
+ else:
+ logger.warning(f"Auto-save validation errors: {serializer.errors}")
+ # Pour l'auto-save, on retourne un succès même en cas d'erreur de validation
+ return JsonResponse({"status": "auto_save_partial", "errors": serializer.errors}, safe=False)
+ else:
+ # Pas de données à sauvegarder, mais on retourne un succès
+ return JsonResponse({"status": "auto_save_no_data"}, safe=False)
+
+ except Exception as e:
+ logger.error(f"Auto-save error for student {id}: {str(e)}")
+ # Pour l'auto-save, on ne retourne pas d'erreur HTTP pour éviter d'interrompre l'UX
+ return JsonResponse({"status": "auto_save_failed", "error": str(e)}, safe=False)
+
@swagger_auto_schema(
responses={204: 'No Content'},
operation_description="Supprime un dossier d'inscription donné.",
diff --git a/Back-End/Subscriptions/views/registration_school_file_masters_views.py b/Back-End/Subscriptions/views/registration_school_file_masters_views.py
index f91afd9..5dc2d81 100644
--- a/Back-End/Subscriptions/views/registration_school_file_masters_views.py
+++ b/Back-End/Subscriptions/views/registration_school_file_masters_views.py
@@ -1,19 +1,18 @@
from django.http.response import JsonResponse
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
-from rest_framework.parsers import MultiPartParser, FormParser
+from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
import json
from django.http import QueryDict
-from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
+from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.models import (
RegistrationForm,
- RegistrationSchoolFileMaster,
- RegistrationSchoolFileTemplate,
- RegistrationParentFileMaster,
+ RegistrationSchoolFileMaster,
+ RegistrationParentFileMaster,
RegistrationParentFileTemplate
)
from N3wtSchool import bdd
@@ -23,7 +22,8 @@ import Subscriptions.util as util
logger = logging.getLogger(__name__)
class RegistrationSchoolFileMasterView(APIView):
- parser_classes = [MultiPartParser, FormParser]
+ parser_classes = [MultiPartParser, FormParser, JSONParser]
+
@swagger_auto_schema(
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
manual_parameters=[
@@ -64,6 +64,7 @@ class RegistrationSchoolFileMasterView(APIView):
if resp:
return resp
+
logger.info(f"payload for serializer: {payload}")
serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True)
if serializer.is_valid():
@@ -90,6 +91,7 @@ class RegistrationSchoolFileMasterView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationSchoolFileMasterSimpleView(APIView):
+ parser_classes = [MultiPartParser, FormParser, JSONParser]
@swagger_auto_schema(
operation_description="Récupère un master de template d'inscription spécifique",
responses={
@@ -126,6 +128,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
if resp:
return resp
+
logger.info(f"payload for update serializer: {payload}")
serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True)
if serializer.is_valid():
diff --git a/Back-End/start.py b/Back-End/start.py
index c957899..c265a97 100644
--- a/Back-End/start.py
+++ b/Back-End/start.py
@@ -12,21 +12,32 @@ def run_command(command):
return process.returncode
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
+flush_data = os.getenv('flush_data', 'false').lower() == 'true'
+migrate_data = os.getenv('migrate_data', 'false').lower() == 'true'
watch_mode = os.getenv('DJANGO_WATCH', 'false').lower() == 'true'
+collect_static_cmd = [
+ ["python", "manage.py", "collectstatic", "--noinput"]
+]
+
+flush_data_cmd = [
+ ["python", "manage.py", "flush", "--noinput"]
+]
+
+migrate_commands = [
+ ["python", "manage.py", "makemigrations", "Common", "--noinput"],
+ ["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
+ ["python", "manage.py", "makemigrations", "Settings", "--noinput"],
+ ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
+ ["python", "manage.py", "makemigrations", "Planning", "--noinput"],
+ ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
+ ["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
+ ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
+ ["python", "manage.py", "makemigrations", "Auth", "--noinput"],
+ ["python", "manage.py", "makemigrations", "School", "--noinput"]
+]
+
commands = [
- ["python", "manage.py", "collectstatic", "--noinput"],
- #["python", "manage.py", "flush", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Common", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Settings", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Planning", "--noinput"],
- # ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
- # ["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
- # ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
- # ["python", "manage.py", "makemigrations", "Auth", "--noinput"],
- # ["python", "manage.py", "makemigrations", "School", "--noinput"],
["python", "manage.py", "migrate", "--noinput"]
]
@@ -45,14 +56,29 @@ def run_daphne():
return 0
if __name__ == "__main__":
+
+ for command in collect_static_cmd:
+ if run_command(command) != 0:
+ exit(1)
+
+ if flush_data:
+ for command in flush_data_cmd:
+ if run_command(command) != 0:
+ exit(1)
+
+ if migrate_data:
+ for command in migrate_commands:
+ if run_command(command) != 0:
+ exit(1)
+
for command in commands:
if run_command(command) != 0:
exit(1)
- #if test_mode:
- # for test_command in test_commands:
- # if run_command(test_command) != 0:
- # exit(1)
+ if test_mode:
+ for test_command in test_commands:
+ if run_command(test_command) != 0:
+ exit(1)
if watch_mode:
celery_worker = subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"])
diff --git a/Front-End/src/app/[locale]/page.js b/Front-End/src/app/[locale]/page.js
index 831066e..0e97ae7 100644
--- a/Front-End/src/app/[locale]/page.js
+++ b/Front-End/src/app/[locale]/page.js
@@ -15,7 +15,6 @@ export default function Home() {
{t('welcomeParents')}
{t('pleaseLogin')}
-
);
}
diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js
index 04d246b..9949bb9 100644
--- a/Front-End/src/app/actions/subscriptionAction.js
+++ b/Front-End/src/app/actions/subscriptionAction.js
@@ -6,10 +6,13 @@ import {
BE_SUBSCRIPTION_ABSENCES_URL,
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
BE_SUBSCRIPTION_SEARCH_STUDENTS_URL,
+ BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
+ BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
} from '@/utils/Url';
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
+import logger from '@/utils/logger';
export const editStudentCompetencies = (data, csrfToken) => {
const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
@@ -83,6 +86,45 @@ export const editRegisterForm = (id, data, csrfToken) => {
.catch(errorHandler);
};
+export const autoSaveRegisterForm = async (id, data, csrfToken) => {
+ try {
+ // Version allégée pour auto-save - ne pas envoyer tous les fichiers
+ const autoSaveData = new FormData();
+
+ // Ajouter seulement les données textuelles pour l'auto-save
+ if (data.student) {
+ autoSaveData.append('student_data', JSON.stringify(data.student));
+ }
+ if (data.guardians) {
+ autoSaveData.append('guardians_data', JSON.stringify(data.guardians));
+ }
+ if (data.siblings) {
+ autoSaveData.append('siblings_data', JSON.stringify(data.siblings));
+ }
+ if (data.currentPage) {
+ autoSaveData.append('current_page', data.currentPage);
+ }
+ autoSaveData.append('auto_save', 'true');
+
+ return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
+ method: 'PATCH', // Utiliser PATCH pour les mises à jour partielles
+ headers: {
+ 'X-CSRFToken': csrfToken,
+ },
+ body: autoSaveData,
+ credentials: 'include',
+ })
+ .then(requestResponseHandler)
+ .catch(() => {
+ // Silent fail pour l'auto-save
+ logger.debug('Auto-save failed silently');
+ });
+ } catch (error) {
+ // Silent fail pour l'auto-save
+ logger.debug('Auto-save error:', error);
+ }
+};
+
export const createRegisterForm = (data, csrfToken) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
return fetch(url, {
@@ -302,3 +344,68 @@ export const deleteAbsences = (id, csrfToken) => {
credentials: 'include',
});
};
+
+/**
+ * Récupère les formulaires maîtres d'inscription pour un établissement
+ * @param {number} establishmentId - ID de l'établissement
+ * @returns {Promise} Liste des formulaires
+ */
+export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
+ const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishmentId}`;
+
+ return fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ })
+ .then(requestResponseHandler)
+ .catch(errorHandler);
+};
+
+/**
+ * Sauvegarde les réponses d'un formulaire dans RegistrationSchoolFileTemplate
+ * @param {number} templateId - ID du RegistrationSchoolFileTemplate
+ * @param {Object} formTemplateData - Données du formulaire à sauvegarder
+ * @param {string} csrfToken - Token CSRF
+ * @returns {Promise} Résultat de la sauvegarde
+ */
+export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
+ const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
+
+ const payload = {
+ formTemplateData: formTemplateData,
+ };
+
+ return fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken,
+ },
+ body: JSON.stringify(payload),
+ credentials: 'include',
+ })
+ .then(requestResponseHandler)
+ .catch(errorHandler);
+};
+
+/**
+ * Récupère les données sauvegardées d'un RegistrationSchoolFileTemplate
+ * @param {number} templateId - ID du RegistrationSchoolFileTemplate
+ * @returns {Promise} Template avec formTemplateData
+ */
+export const fetchFormResponses = (templateId) => {
+ const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
+
+ return fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ })
+ .then(requestResponseHandler)
+ .catch(errorHandler);
+};
diff --git a/Front-End/src/components/AutoSaveIndicator.js b/Front-End/src/components/AutoSaveIndicator.js
new file mode 100644
index 0000000..90139f2
--- /dev/null
+++ b/Front-End/src/components/AutoSaveIndicator.js
@@ -0,0 +1,63 @@
+'use client';
+import React from 'react';
+import { CheckCircle } from 'lucide-react';
+
+/**
+ * Composant indicateur de sauvegarde automatique
+ * @param {Boolean} isSaving - Si la sauvegarde est en cours
+ * @param {Date} lastSaved - Date de la dernière sauvegarde
+ * @param {Boolean} autoSaveEnabled - Si l'auto-save est activée
+ * @param {Function} onToggleAutoSave - Callback pour activer/désactiver l'auto-save
+ */
+export default function AutoSaveIndicator({
+ isSaving = false,
+ lastSaved = null,
+ autoSaveEnabled = true,
+ onToggleAutoSave = null,
+}) {
+ if (!autoSaveEnabled && !lastSaved && !isSaving) {
+ return null;
+ }
+
+ return (
+
+
+ {isSaving ? (
+ <>
+
+
+ Sauvegarde en cours...
+
+ >
+ ) : lastSaved ? (
+ <>
+
+
+ Sauvegardé à {lastSaved.toLocaleTimeString()}
+
+ >
+ ) : (
+
Auto-sauvegarde activée
+ )}
+
+
+ {onToggleAutoSave && (
+
+ {autoSaveEnabled ? '✓ Auto-save' : '○ Auto-save'}
+
+ )}
+
+ );
+}
diff --git a/Front-End/src/components/Form/AddFieldModal.js b/Front-End/src/components/Form/AddFieldModal.js
index 6d0eff1..c98ef6b 100644
--- a/Front-End/src/components/Form/AddFieldModal.js
+++ b/Front-End/src/components/Form/AddFieldModal.js
@@ -6,6 +6,7 @@ import Button from './Button';
import IconSelector from './IconSelector';
import * as LucideIcons from 'lucide-react';
import { FIELD_TYPES } from './FormTypes';
+import FIELD_TYPES_WITH_ICONS from './FieldTypesWithIcons';
export default function AddFieldModal({
isOpen,
@@ -55,6 +56,10 @@ export default function AddFieldModal({
acceptTypes: '',
maxSize: 5,
checked: false,
+ signatureData: '',
+ backgroundColor: '#ffffff',
+ penColor: '#000000',
+ penWidth: 2,
validation: {
pattern: '',
minLength: '',
@@ -62,6 +67,16 @@ export default function AddFieldModal({
},
};
+ // Si un type a été présélectionné depuis le sélecteur de type
+ if (editingField && !isEditing) {
+ // S'assurer que le type est correctement défini
+ if (typeof editingField.type === 'string') {
+ defaultValues.type = editingField.type;
+ } else if (editingField.value) {
+ defaultValues.type = editingField.value;
+ }
+ }
+
setCurrentField(defaultValues);
// Réinitialiser le formulaire avec les valeurs de l'élément à éditer
@@ -75,17 +90,32 @@ export default function AddFieldModal({
acceptTypes: defaultValues.acceptTypes,
maxSize: defaultValues.maxSize,
checked: defaultValues.checked,
+ signatureData: defaultValues.signatureData,
+ backgroundColor: defaultValues.backgroundColor,
+ penColor: defaultValues.penColor,
+ penWidth: defaultValues.penWidth,
validation: defaultValues.validation,
});
}
- }, [isOpen, editingField, reset]);
+ }, [isOpen, editingField, reset, isEditing]);
// Ajouter une option au select
- const addOption = () => {
+ const addOption = (e) => {
+ // Arrêter la propagation de l'événement pour éviter que le clic n'atteigne l'arrière-plan
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
if (newOption.trim()) {
+ // Vérifie si options existe et est un tableau, sinon initialise comme tableau vide
+ const currentOptions = Array.isArray(currentField.options)
+ ? currentField.options
+ : [];
+
setCurrentField({
...currentField,
- options: [...currentField.options, newOption.trim()],
+ options: [...currentOptions, newOption.trim()],
});
setNewOption('');
}
@@ -93,7 +123,12 @@ export default function AddFieldModal({
// Supprimer une option du select
const removeOption = (index) => {
- const newOptions = currentField.options.filter((_, i) => i !== index);
+ // Vérifie si options existe et est un tableau, sinon initialise comme tableau vide
+ const currentOptions = Array.isArray(currentField.options)
+ ? currentField.options
+ : [];
+ const newOptions = currentOptions.filter((_, i) => i !== index);
+
setCurrentField({ ...currentField, options: newOptions });
};
@@ -141,15 +176,28 @@ export default function AddFieldModal({
name="type"
selected={value}
callback={(e) => {
- onChange(e.target.value);
+ const newType = e.target.value;
+ onChange(newType);
+
+ // Assurons-nous que les options restent un tableau si on sélectionne select ou radio
+ let updatedOptions = currentField.options;
+
+ // Si options n'existe pas ou n'est pas un tableau, initialiser comme tableau vide
+ if (!updatedOptions || !Array.isArray(updatedOptions)) {
+ updatedOptions = [];
+ }
+
setCurrentField({
...currentField,
- type: e.target.value,
+ type: newType,
+ options: updatedOptions,
});
}}
- choices={FIELD_TYPES}
+ choices={FIELD_TYPES_WITH_ICONS}
placeHolder="Sélectionner un type"
required
+ showIcons={true}
+ customSelect={true}
/>
)}
/>
@@ -353,21 +401,22 @@ export default function AddFieldModal({
/>
- {currentField.options.map((option, index) => (
-
-
{option}
-
removeOption(index)}
- className="text-red-500 hover:text-red-700"
+ {Array.isArray(currentField.options) &&
+ currentField.options.map((option, index) => (
+
- ✕
-
-
- ))}
+ {option}
+ removeOption(index)}
+ className="text-red-500 hover:text-red-700"
+ >
+ ✕
+
+
+ ))}
)}
@@ -396,21 +445,22 @@ export default function AddFieldModal({
/>
- {currentField.options.map((option, index) => (
-
-
{option}
-
removeOption(index)}
- className="text-red-500 hover:text-red-700"
+ {Array.isArray(currentField.options) &&
+ currentField.options.map((option, index) => (
+
- ✕
-
-
- ))}
+ {option}
+ removeOption(index)}
+ className="text-red-500 hover:text-red-700"
+ >
+ ✕
+
+
+ ))}
)}
@@ -484,6 +534,81 @@ export default function AddFieldModal({
>
)}
+ {currentField.type === 'signature' && (
+ <>
+ (
+
+
+ Couleur de fond
+
+ {
+ onChange(e.target.value);
+ setCurrentField({
+ ...currentField,
+ backgroundColor: e.target.value,
+ });
+ }}
+ className="w-full h-10 border border-gray-300 rounded-md cursor-pointer"
+ />
+
+ )}
+ />
+ (
+
+
+ Couleur du stylo
+
+ {
+ onChange(e.target.value);
+ setCurrentField({
+ ...currentField,
+ penColor: e.target.value,
+ });
+ }}
+ className="w-full h-10 border border-gray-300 rounded-md cursor-pointer"
+ />
+
+ )}
+ />
+ (
+ {
+ onChange(parseInt(e.target.value));
+ setCurrentField({
+ ...currentField,
+ penWidth: parseInt(e.target.value) || 2,
+ });
+ }}
+ />
+ )}
+ />
+ >
+ )}
+
{currentField.type === 'checkbox' && (
<>
diff --git a/Front-End/src/components/Form/FieldTypeSelector.js b/Front-End/src/components/Form/FieldTypeSelector.js
new file mode 100644
index 0000000..8c8f074
--- /dev/null
+++ b/Front-End/src/components/Form/FieldTypeSelector.js
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import { FIELD_TYPES } from './FormTypes';
+import * as LucideIcons from 'lucide-react';
+import Button from './Button';
+
+// Utiliser les mêmes icônes que dans FormTemplateBuilder
+const FIELD_TYPES_ICON = {
+ text: { icon: LucideIcons.TextCursorInput },
+ email: { icon: LucideIcons.AtSign },
+ phone: { icon: LucideIcons.Phone },
+ date: { icon: LucideIcons.Calendar },
+ select: { icon: LucideIcons.ChevronDown },
+ radio: { icon: LucideIcons.Radio },
+ checkbox: { icon: LucideIcons.CheckSquare },
+ toggle: { icon: LucideIcons.ToggleLeft },
+ file: { icon: LucideIcons.FileUp },
+ signature: { icon: LucideIcons.PenTool },
+ textarea: { icon: LucideIcons.Type },
+ paragraph: { icon: LucideIcons.AlignLeft },
+ heading1: { icon: LucideIcons.Heading1 },
+ heading2: { icon: LucideIcons.Heading2 },
+ heading3: { icon: LucideIcons.Heading3 },
+ heading4: { icon: LucideIcons.Heading4 },
+ heading5: { icon: LucideIcons.Heading5 },
+ heading6: { icon: LucideIcons.Heading6 },
+};
+
+export default function FieldTypeSelector({ isOpen, onClose, onSelect }) {
+ const [searchTerm, setSearchTerm] = useState('');
+
+ if (!isOpen) return null;
+
+ // Filtrer les types de champs selon le terme de recherche
+ const filteredFieldTypes = FIELD_TYPES.filter((fieldType) =>
+ fieldType.label.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ const selectFieldType = (fieldType) => {
+ onSelect(fieldType);
+ onClose();
+ };
+
+ return (
+
+
+
+
+ Choisir un type de champ ({filteredFieldTypes.length} /{' '}
+ {FIELD_TYPES.length} types)
+
+
+ ✕
+
+
+
+ {/* Barre de recherche */}
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full px-4 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+
+ {searchTerm && (
+ setSearchTerm('')}
+ className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
+ >
+
+
+ )}
+
+
+
+
+ {filteredFieldTypes.map((fieldType) => {
+ const IconComponent = FIELD_TYPES_ICON[fieldType.value]?.icon;
+ return (
+ selectFieldType(fieldType)}
+ className="p-5 rounded-lg border-2 border-gray-200 bg-gray-50
+ hover:bg-blue-50 hover:border-blue-300 hover:shadow-md hover:scale-105
+ flex flex-col items-center justify-center gap-4 min-h-[140px] w-full
+ transition-all duration-200"
+ title={fieldType.label}
+ >
+ {IconComponent && (
+
+ )}
+
+ {fieldType.label}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+}
diff --git a/Front-End/src/components/Form/FieldTypesWithIcons.js b/Front-End/src/components/Form/FieldTypesWithIcons.js
new file mode 100644
index 0000000..3079cdf
--- /dev/null
+++ b/Front-End/src/components/Form/FieldTypesWithIcons.js
@@ -0,0 +1,73 @@
+import * as LucideIcons from 'lucide-react';
+import { FIELD_TYPES } from './FormTypes';
+
+// Associer les icônes à chaque type de champ
+export const FIELD_TYPES_WITH_ICONS = FIELD_TYPES.map((fieldType) => {
+ let icon = null;
+
+ switch (fieldType.value) {
+ case 'text':
+ icon = LucideIcons.TextCursorInput;
+ break;
+ case 'email':
+ icon = LucideIcons.AtSign;
+ break;
+ case 'phone':
+ icon = LucideIcons.Phone;
+ break;
+ case 'date':
+ icon = LucideIcons.Calendar;
+ break;
+ case 'select':
+ icon = LucideIcons.ChevronDown;
+ break;
+ case 'radio':
+ icon = LucideIcons.Radio;
+ break;
+ case 'checkbox':
+ icon = LucideIcons.CheckSquare;
+ break;
+ case 'toggle':
+ icon = LucideIcons.ToggleLeft;
+ break;
+ case 'file':
+ icon = LucideIcons.FileUp;
+ break;
+ case 'signature':
+ icon = LucideIcons.PenTool;
+ break;
+ case 'textarea':
+ icon = LucideIcons.Type;
+ break;
+ case 'paragraph':
+ icon = LucideIcons.AlignLeft;
+ break;
+ case 'heading1':
+ icon = LucideIcons.Heading1;
+ break;
+ case 'heading2':
+ icon = LucideIcons.Heading2;
+ break;
+ case 'heading3':
+ icon = LucideIcons.Heading3;
+ break;
+ case 'heading4':
+ icon = LucideIcons.Heading4;
+ break;
+ case 'heading5':
+ icon = LucideIcons.Heading5;
+ break;
+ case 'heading6':
+ icon = LucideIcons.Heading6;
+ break;
+ default:
+ break;
+ }
+
+ return {
+ ...fieldType,
+ icon,
+ };
+});
+
+export default FIELD_TYPES_WITH_ICONS;
diff --git a/Front-End/src/components/Form/FormRenderer.js b/Front-End/src/components/Form/FormRenderer.js
index d0a62a3..0576174 100644
--- a/Front-End/src/components/Form/FormRenderer.js
+++ b/Front-End/src/components/Form/FormRenderer.js
@@ -11,6 +11,7 @@ import CheckBox from './CheckBox';
import ToggleSwitch from './ToggleSwitch';
import InputPhone from './InputPhone';
import FileUpload from './FileUpload';
+import SignatureField from './SignatureField';
/*
* Récupère une icône Lucide par son nom.
@@ -24,46 +25,8 @@ export function getIcon(name) {
}
}
-const formConfigTest = {
- id: 0,
- title: 'Mon formulaire dynamique',
- submitLabel: 'Envoyer',
- fields: [
- { id: 'name', label: 'Nom', type: 'text', required: true },
- { id: 'email', label: 'Email', type: 'email' },
- {
- id: 'email2',
- label: 'Email',
- type: 'text',
- icon: 'Mail',
- },
- {
- id: 'role',
- label: 'Rôle',
- type: 'select',
- options: ['Admin', 'Utilisateur', 'Invité'],
- required: true,
- },
- {
- type: 'paragraph',
- text: "Bonjour, Bienvenue dans ce formulaire d'inscription haha",
- },
- {
- id: 'birthdate',
- label: 'Date de naissance',
- type: 'date',
- icon: 'Calendar',
- },
- {
- id: 'textarea',
- label: 'toto',
- type: 'textarea',
- },
- ],
-};
-
export default function FormRenderer({
- formConfig = formConfigTest,
+ formConfig,
csrfToken,
onFormSubmit = (data) => {
alert(JSON.stringify(data, null, 2));
@@ -109,7 +72,8 @@ export default function FormRenderer({
const hasFiles = Object.keys(data).some((key) => {
return (
data[key] instanceof FileList ||
- (data[key] && data[key][0] instanceof File)
+ (data[key] && data[key][0] instanceof File) ||
+ (typeof data[key] === 'string' && data[key].startsWith('data:image'))
);
});
@@ -118,7 +82,7 @@ export default function FormRenderer({
const formData = new FormData();
// Ajouter l'ID du formulaire
- formData.append('formId', formConfig.id.toString());
+ formData.append('formId', (formConfig?.id || 'unknown').toString());
// Traiter chaque champ et ses valeurs
Object.keys(data).forEach((key) => {
@@ -134,6 +98,29 @@ export default function FormRenderer({
formData.append(`files.${key}`, value[i]);
}
}
+ } else if (
+ typeof value === 'string' &&
+ value.startsWith('data:image')
+ ) {
+ // Gérer les signatures (SVG ou images base64)
+ if (value.includes('svg+xml')) {
+ // Gérer les signatures SVG
+ const svgData = value.split(',')[1];
+ const svgBlob = new Blob([atob(svgData)], {
+ type: 'image/svg+xml',
+ });
+ formData.append(`files.${key}`, svgBlob, `signature_${key}.svg`);
+ } else {
+ // Gérer les images base64 classiques
+ const byteString = atob(value.split(',')[1]);
+ const ab = new ArrayBuffer(byteString.length);
+ const ia = new Uint8Array(ab);
+ for (let i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
+ }
+ const blob = new Blob([ab], { type: 'image/png' });
+ formData.append(`files.${key}`, blob, `signature_${key}.png`);
+ }
} else {
// Gérer les autres types de champs
formData.append(
@@ -154,7 +141,7 @@ export default function FormRenderer({
} else {
// Pas de fichier, on peut utiliser JSON
const formattedData = {
- formId: formConfig.id,
+ formId: formConfig?.id || 'unknown',
responses: { ...data },
};
@@ -189,10 +176,10 @@ export default function FormRenderer({
>
{csrfToken ?
: null}
- {formConfig.title}
+ {formConfig?.title || 'Formulaire'}
- {formConfig.fields.map((field) => (
+ {(formConfig?.fields || []).map((field) => (
)}
+ {field.type === 'signature' && (
+
(
+
+
+ {errors[field.id] && (
+
+ {field.required
+ ? `${field.label} est requis`
+ : 'Champ invalide'}
+
+ )}
+
+ )}
+ />
+ )}
))}
diff --git a/Front-End/src/components/Form/FormTemplateBuilder.js b/Front-End/src/components/Form/FormTemplateBuilder.js
index 8e0231a..e837bf5 100644
--- a/Front-End/src/components/Form/FormTemplateBuilder.js
+++ b/Front-End/src/components/Form/FormTemplateBuilder.js
@@ -3,6 +3,7 @@ import { useForm } from 'react-hook-form';
import InputTextIcon from './InputTextIcon';
import FormRenderer from './FormRenderer';
import AddFieldModal from './AddFieldModal';
+import FieldTypeSelector from './FieldTypeSelector';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
@@ -34,6 +35,7 @@ import {
ToggleLeft,
CheckSquare,
FileUp,
+ PenTool,
} from 'lucide-react';
const FIELD_TYPES_ICON = {
@@ -46,6 +48,7 @@ const FIELD_TYPES_ICON = {
checkbox: { icon: CheckSquare },
toggle: { icon: ToggleLeft },
file: { icon: FileUp },
+ signature: { icon: PenTool },
textarea: { icon: Type },
paragraph: { icon: AlignLeft },
heading1: { icon: Heading1 },
@@ -168,15 +171,26 @@ const DraggableFieldItem = ({
);
};
-export default function FormTemplateBuilder() {
+export default function FormTemplateBuilder({
+ onSave,
+ initialData,
+ groups,
+ isEditing,
+}) {
const [formConfig, setFormConfig] = useState({
- id: 0,
- title: 'Nouveau formulaire',
+ id: initialData?.id || 0,
+ title: initialData?.name || 'Nouveau formulaire',
submitLabel: 'Envoyer',
- fields: [],
+ fields: initialData?.formMasterData?.fields || [],
});
+ const [selectedGroups, setSelectedGroups] = useState(
+ initialData?.groups?.map((g) => g.id) || []
+ );
+
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
+ const [showFieldTypeSelector, setShowFieldTypeSelector] = useState(false);
+ const [selectedFieldType, setSelectedFieldType] = useState(null);
const [editingIndex, setEditingIndex] = useState(-1);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState({ type: '', text: '' });
@@ -185,6 +199,19 @@ export default function FormTemplateBuilder() {
const { reset: resetField } = useForm();
+ // Initialiser les données du formulaire quand initialData change
+ useEffect(() => {
+ if (initialData) {
+ setFormConfig({
+ id: initialData.id || 0,
+ title: initialData.name || 'Nouveau formulaire',
+ submitLabel: 'Envoyer',
+ fields: initialData.formMasterData?.fields || [],
+ });
+ setSelectedGroups(initialData.groups?.map((g) => g.id) || []);
+ }
+ }, [initialData]);
+
// Gérer l'affichage du bouton de défilement
useEffect(() => {
const handleScroll = () => {
@@ -235,7 +262,9 @@ export default function FormTemplateBuilder() {
? undefined
: generateFieldId(data.label || 'field'),
options: ['select', 'radio'].includes(data.type)
- ? currentField.options
+ ? Array.isArray(currentField.options)
+ ? currentField.options
+ : []
: undefined,
icon: data.icon || currentField.icon || undefined,
placeholder: data.placeholder || undefined,
@@ -345,35 +374,36 @@ export default function FormTemplateBuilder() {
return;
}
+ if (selectedGroups.length === 0) {
+ setSaveMessage({
+ type: 'error',
+ text: "Sélectionnez au moins un groupe d'inscription",
+ });
+ return;
+ }
+
setSaving(true);
setSaveMessage({ type: '', text: '' });
try {
- // Simulation d'envoi au backend (à remplacer par l'appel API réel)
- // const response = await fetch('/api/form-templates', {
- // method: 'POST',
- // headers: {
- // 'Content-Type': 'application/json',
- // },
- // body: JSON.stringify(formConfig),
- // });
+ const dataToSave = {
+ name: formConfig.title,
+ group_ids: selectedGroups,
+ formMasterData: formConfig,
+ };
- // if (!response.ok) {
- // throw new Error('Erreur lors de l\'enregistrement du formulaire');
- // }
+ if (isEditing && initialData) {
+ dataToSave.id = initialData.id;
+ }
- // const data = await response.json();
-
- // Simulation d'une réponse du backend
- await new Promise((resolve) => setTimeout(resolve, 1000));
+ if (onSave) {
+ onSave(dataToSave);
+ }
setSaveMessage({
type: 'success',
text: 'Formulaire enregistré avec succès',
});
-
- // Si le backend renvoie un ID, on peut mettre à jour l'ID du formulaire
- // setFormConfig({ ...formConfig, id: data.id });
} catch (error) {
setSaveMessage({
type: 'error',
@@ -385,6 +415,13 @@ export default function FormTemplateBuilder() {
}
};
+ // Fonction pour gérer la sélection d'un type de champ
+ const handleFieldTypeSelect = (fieldType) => {
+ setSelectedFieldType(fieldType);
+ setShowFieldTypeSelector(false);
+ setShowAddFieldModal(true);
+ };
+
return (
@@ -476,6 +513,44 @@ export default function FormTemplateBuilder() {
})
}
/>
+
+ {/* Sélecteur de groupes */}
+
+
+ Groupes d'inscription{' '}
+ *
+
+
+ {groups && groups.length > 0 ? (
+ groups.map((group) => (
+
+ {
+ if (e.target.checked) {
+ setSelectedGroups([
+ ...selectedGroups,
+ group.id,
+ ]);
+ } else {
+ setSelectedGroups(
+ selectedGroups.filter((id) => id !== group.id)
+ );
+ }
+ }}
+ className="mr-2 text-blue-600"
+ />
+ {group.name}
+
+ ))
+ ) : (
+
+ Aucun groupe disponible
+
+ )}
+
+
{/* Liste des champs */}
@@ -487,7 +562,8 @@ export default function FormTemplateBuilder() {
{
setEditingIndex(-1);
- setShowAddFieldModal(true);
+ setSelectedFieldType(null);
+ setShowFieldTypeSelector(true);
}}
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
title="Ajouter un champ"
@@ -504,7 +580,8 @@ export default function FormTemplateBuilder() {
{
setEditingIndex(-1);
- setShowAddFieldModal(true);
+ setSelectedFieldType(null);
+ setShowFieldTypeSelector(true);
}}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
>
@@ -593,11 +670,22 @@ export default function FormTemplateBuilder() {
onClose={() => setShowAddFieldModal(false)}
onSubmit={handleFieldSubmit}
editingField={
- editingIndex >= 0 ? formConfig.fields[editingIndex] : null
+ editingIndex >= 0
+ ? formConfig.fields[editingIndex]
+ : selectedFieldType
+ ? { type: selectedFieldType.value || selectedFieldType }
+ : null
}
editingIndex={editingIndex}
/>
+ {/* Sélecteur de type de champ */}
+ setShowFieldTypeSelector(false)}
+ onSelect={handleFieldTypeSelect}
+ />
+
{/* Bouton flottant pour remonter en haut */}
{showScrollButton && (
diff --git a/Front-End/src/components/Form/FormTypes.js b/Front-End/src/components/Form/FormTypes.js
index 9aff34f..f57b5f2 100644
--- a/Front-End/src/components/Form/FormTypes.js
+++ b/Front-End/src/components/Form/FormTypes.js
@@ -8,6 +8,7 @@ export const FIELD_TYPES = [
{ value: 'checkbox', label: 'Case à cocher' },
{ value: 'toggle', label: 'Interrupteur' },
{ value: 'file', label: 'Upload de fichier' },
+ { value: 'signature', label: 'Signature' },
{ value: 'textarea', label: 'Zone de texte riche' },
{ value: 'paragraph', label: 'Paragraphe' },
{ value: 'heading1', label: 'Titre 1' },
diff --git a/Front-End/src/components/Form/SelectChoice.js b/Front-End/src/components/Form/SelectChoice.js
index d35c25e..b8a2243 100644
--- a/Front-End/src/components/Form/SelectChoice.js
+++ b/Front-End/src/components/Form/SelectChoice.js
@@ -1,3 +1,5 @@
+import React, { useState, useRef, useEffect } from 'react';
+
export default function SelectChoice({
type,
name,
@@ -11,8 +13,46 @@ export default function SelectChoice({
errorLocalMsg,
IconItem,
disabled = false,
+ // Nouveaux paramètres
+ showIcons = false, // Activer l'affichage des icônes dans les options
+ customSelect = false, // Utiliser une liste personnalisée au lieu du select natif
}) {
const isPlaceholderSelected = selected === ''; // Vérifie si le placeholder est sélectionné
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedLabel, setSelectedLabel] = useState('');
+ const dropdownRef = useRef(null);
+
+ // Ferme le dropdown si on clique en dehors
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ }
+
+ if (customSelect) {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }
+ }, [customSelect]);
+
+ // Met à jour le label affiché quand selected change
+ useEffect(() => {
+ if (selected && choices) {
+ const selectedOption = choices.find(
+ (option) => option.value === selected
+ );
+ if (selectedOption) {
+ setSelectedLabel(selectedOption.label);
+ }
+ } else {
+ setSelectedLabel('');
+ }
+ }, [selected, choices]);
+
+ // Affiche soit le select natif, soit notre version personnalisée avec icônes
return (
<>
@@ -23,45 +63,172 @@ export default function SelectChoice({
{label}
{required &&
* }
-
- {IconItem && (
-
- { }
-
- )}
-
+ !disabled && setIsOpen(!isOpen)}
+ >
+ {IconItem && (
+
+ { }
+
+ )}
+
+ {isPlaceholderSelected ? (
+
+ {placeHolder?.toLowerCase()}
+
+ ) : (
+
+ {showIcons && selected && choices && (
+
+ {(() => {
+ const selectedOption = choices.find(
+ (option) => option.value === selected
+ );
+ if (selectedOption && selectedOption.icon) {
+ const IconComponent = selectedOption.icon;
+ return (
+
+ );
+ }
+ return null;
+ })()}
+
+ )}
+ {selectedLabel}
+
+ )}
+
+
+
+
+ {isOpen && (
+
+
{
+ if (!disabled) {
+ const event = { target: { value: '' } };
+ callback(event);
+ setIsOpen(false);
+ }
+ }}
+ >
+ {placeHolder?.toLowerCase()}
+
+ {choices.map(({ value, label, icon: Icon }) => (
+
{
+ if (!disabled) {
+ const event = { target: { value } };
+ callback(event);
+ setIsOpen(false);
+ }
+ }}
+ >
+
+ {showIcons && Icon && (
+
+
+
+ )}
+ {label}
+
+
+ ))}
+
+ )}
+
+ {/* Input caché pour la compatibilité avec les formulaires */}
+ {}} // Évite l'avertissement React pour input non contrôlé
+ />
+
+ ) : (
+ // Version standard avec select natif (pour la rétrocompatibilité)
+
- {/* Placeholder en italique */}
-
- {placeHolder?.toLowerCase()}
-
- {/* Autres options sans italique */}
- {choices.map(({ value, label }) => (
-
- {label}
+ {IconItem && (
+
+ { }
+
+ )}
+
+ {/* Placeholder en italique */}
+
+ {placeHolder?.toLowerCase()}
- ))}
-
-
+ {/* Autres options sans italique */}
+ {choices.map(({ value, label }) => (
+
+ {label}
+
+ ))}
+
+
+ )}
+
{errorMsg &&
{errorMsg}
}
>
diff --git a/Front-End/src/components/Form/SignatureField.js b/Front-End/src/components/Form/SignatureField.js
new file mode 100644
index 0000000..95d04dd
--- /dev/null
+++ b/Front-End/src/components/Form/SignatureField.js
@@ -0,0 +1,346 @@
+import React, { useRef, useEffect, useState, useCallback } from 'react';
+import { RotateCcw } from 'lucide-react';
+
+const SignatureField = ({
+ label = 'Signature',
+ required = false,
+ onChange,
+ value,
+ disabled = false,
+ readOnly = false,
+ backgroundColor = '#ffffff',
+ penColor = '#000000',
+ penWidth = 2,
+}) => {
+ const canvasRef = useRef(null);
+ const [isDrawing, setIsDrawing] = useState(false);
+ const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 });
+ const [isEmpty, setIsEmpty] = useState(true);
+ const [svgPaths, setSvgPaths] = useState([]);
+ const [currentPath, setCurrentPath] = useState('');
+ const [smoothingPoints, setSmoothingPoints] = useState([]);
+
+ // Initialiser le canvas
+ const initializeCanvas = useCallback(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const context = canvas.getContext('2d');
+
+ // Support High DPI / Retina displays
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const displayWidth = 400;
+ const displayHeight = 200;
+
+ // Ajuster la taille physique du canvas pour la haute résolution
+ canvas.width = displayWidth * devicePixelRatio;
+ canvas.height = displayHeight * devicePixelRatio;
+
+ // Maintenir la taille d'affichage
+ canvas.style.width = displayWidth + 'px';
+ canvas.style.height = displayHeight + 'px';
+
+ // Adapter le contexte à la densité de pixels
+ context.scale(devicePixelRatio, devicePixelRatio);
+
+ // Améliorer l'anti-aliasing et le rendu
+ context.imageSmoothingEnabled = true;
+ context.imageSmoothingQuality = 'high';
+ context.textRenderingOptimization = 'optimizeQuality';
+
+ // Configuration du style de dessin
+ context.fillStyle = backgroundColor;
+ context.fillRect(0, 0, displayWidth, displayHeight);
+ context.strokeStyle = penColor;
+ context.lineWidth = penWidth;
+ context.lineCap = 'round';
+ context.lineJoin = 'round';
+ context.globalCompositeOperation = 'source-over';
+ }, [backgroundColor, penColor, penWidth]);
+
+ useEffect(() => {
+ initializeCanvas();
+
+ // Si une valeur est fournie (signature existante), la charger
+ if (value && value !== '') {
+ const canvas = canvasRef.current;
+ const context = canvas.getContext('2d');
+
+ if (value.includes('svg+xml')) {
+ // Charger une signature SVG
+ const svgData = atob(value.split(',')[1]);
+ const img = new Image();
+ const svg = new Blob([svgData], {
+ type: 'image/svg+xml;charset=utf-8',
+ });
+ const url = URL.createObjectURL(svg);
+
+ img.onload = () => {
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.fillStyle = backgroundColor;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.drawImage(img, 0, 0);
+ setIsEmpty(false);
+ URL.revokeObjectURL(url);
+ };
+ img.src = url;
+ } else {
+ // Charger une image classique
+ const img = new Image();
+ img.onload = () => {
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.fillStyle = backgroundColor;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.drawImage(img, 0, 0);
+ setIsEmpty(false);
+ };
+ img.src = value;
+ }
+ }
+ }, [value, initializeCanvas, backgroundColor]);
+
+ // Obtenir les coordonnées relatives au canvas
+ const getCanvasPosition = (e) => {
+ const canvas = canvasRef.current;
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.style.width
+ ? parseFloat(canvas.style.width) / rect.width
+ : 1;
+ const scaleY = canvas.style.height
+ ? parseFloat(canvas.style.height) / rect.height
+ : 1;
+
+ return {
+ x: (e.clientX - rect.left) * scaleX,
+ y: (e.clientY - rect.top) * scaleY,
+ };
+ };
+
+ // Obtenir les coordonnées pour les événements tactiles
+ const getTouchPosition = (e) => {
+ const canvas = canvasRef.current;
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.style.width
+ ? parseFloat(canvas.style.width) / rect.width
+ : 1;
+ const scaleY = canvas.style.height
+ ? parseFloat(canvas.style.height) / rect.height
+ : 1;
+
+ return {
+ x: (e.touches[0].clientX - rect.left) * scaleX,
+ y: (e.touches[0].clientY - rect.top) * scaleY,
+ };
+ };
+
+ // Commencer le dessin
+ const startDrawing = useCallback(
+ (e) => {
+ if (disabled || readOnly) return;
+
+ e.preventDefault();
+ setIsDrawing(true);
+
+ const pos = e.type.includes('touch')
+ ? getTouchPosition(e)
+ : getCanvasPosition(e);
+ setLastPosition(pos);
+
+ // Commencer un nouveau path SVG
+ setCurrentPath(`M ${pos.x},${pos.y}`);
+ setSmoothingPoints([pos]);
+ },
+ [disabled, readOnly]
+ );
+
+ // Dessiner
+ const draw = useCallback(
+ (e) => {
+ if (!isDrawing || disabled || readOnly) return;
+
+ e.preventDefault();
+ const canvas = canvasRef.current;
+ const context = canvas.getContext('2d');
+
+ const currentPos = e.type.includes('touch')
+ ? getTouchPosition(e)
+ : getCanvasPosition(e);
+
+ // Calculer la distance pour déterminer si on doit interpoler
+ const distance = Math.sqrt(
+ Math.pow(currentPos.x - lastPosition.x, 2) +
+ Math.pow(currentPos.y - lastPosition.y, 2)
+ );
+
+ // Si la distance est grande, interpoler pour un tracé plus lisse
+ if (distance > 2) {
+ const midPoint = {
+ x: (lastPosition.x + currentPos.x) / 2,
+ y: (lastPosition.y + currentPos.y) / 2,
+ };
+
+ // Utiliser une courbe quadratique pour un tracé plus lisse
+ context.beginPath();
+ context.moveTo(lastPosition.x, lastPosition.y);
+ context.quadraticCurveTo(
+ lastPosition.x,
+ lastPosition.y,
+ midPoint.x,
+ midPoint.y
+ );
+ context.stroke();
+
+ setLastPosition(midPoint);
+ setCurrentPath(
+ (prev) =>
+ prev +
+ ` Q ${lastPosition.x},${lastPosition.y} ${midPoint.x},${midPoint.y}`
+ );
+ } else {
+ // Tracé direct pour les mouvements lents
+ context.beginPath();
+ context.moveTo(lastPosition.x, lastPosition.y);
+ context.lineTo(currentPos.x, currentPos.y);
+ context.stroke();
+
+ setLastPosition(currentPos);
+ setCurrentPath((prev) => prev + ` L ${currentPos.x},${currentPos.y}`);
+ }
+
+ setIsEmpty(false);
+ },
+ [isDrawing, lastPosition, disabled]
+ );
+
+ // Arrêter le dessin
+ const stopDrawing = useCallback(
+ (e) => {
+ if (!isDrawing) return;
+
+ e.preventDefault();
+ setIsDrawing(false);
+
+ // Ajouter le path terminé aux paths SVG
+ if (currentPath) {
+ setSvgPaths((prev) => [...prev, currentPath]);
+ setCurrentPath('');
+ }
+
+ // Notifier le parent du changement avec SVG
+ if (onChange) {
+ const newPaths = [...svgPaths, currentPath].filter((p) => p.length > 0);
+ const svgData = generateSVG(newPaths);
+ onChange(svgData);
+ }
+ },
+ [isDrawing, onChange, svgPaths, currentPath]
+ );
+
+ // Générer le SVG à partir des paths
+ const generateSVG = (paths) => {
+ const svgContent = `
+
+ ${paths
+ .map(
+ (path) =>
+ ` `
+ )
+ .join('\n ')}
+ `;
+
+ return `data:image/svg+xml;base64,${btoa(svgContent)}`;
+ };
+
+ // Effacer la signature
+ const clearSignature = () => {
+ const canvas = canvasRef.current;
+ const context = canvas.getContext('2d');
+
+ // Effacer en tenant compte des dimensions d'affichage
+ const displayWidth = 400;
+ const displayHeight = 200;
+
+ context.clearRect(0, 0, displayWidth, displayHeight);
+ context.fillStyle = backgroundColor;
+ context.fillRect(0, 0, displayWidth, displayHeight);
+ setIsEmpty(true);
+ setSvgPaths([]);
+ setCurrentPath('');
+ setSmoothingPoints([]);
+
+ if (onChange) {
+ onChange('');
+ }
+ };
+
+ return (
+
+ {label && (
+
+ {label}
+ {required && * }
+
+ )}
+
+
+
+
+
+
+ {readOnly
+ ? isEmpty
+ ? 'Aucune signature'
+ : 'Signature'
+ : isEmpty
+ ? 'Signez dans la zone ci-dessus'
+ : 'Signature capturée'}
+
+
+ {!readOnly && (
+
+
+
+ Effacer
+
+
+ )}
+
+
+ {required && isEmpty && (
+
+ La signature est obligatoire
+
+ )}
+
+
+ );
+};
+
+export default SignatureField;
diff --git a/Front-End/src/components/Inscription/DynamicFormsList.js b/Front-End/src/components/Inscription/DynamicFormsList.js
new file mode 100644
index 0000000..df12c76
--- /dev/null
+++ b/Front-End/src/components/Inscription/DynamicFormsList.js
@@ -0,0 +1,295 @@
+'use client';
+import React, { useState, useEffect } from 'react';
+import FormRenderer from '@/components/Form/FormRenderer';
+import { CheckCircle, Hourglass, FileText } from 'lucide-react';
+import logger from '@/utils/logger';
+
+/**
+ * Composant pour afficher et gérer les formulaires dynamiques d'inscription
+ * @param {Array} schoolFileMasters - Liste des formulaires maîtres
+ * @param {Object} existingResponses - Réponses déjà sauvegardées
+ * @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
+ * @param {Boolean} enable - Si les formulaires sont modifiables
+ */
+export default function DynamicFormsList({
+ schoolFileMasters,
+ existingResponses = {},
+ onFormSubmit,
+ enable = true,
+ onValidationChange,
+}) {
+ const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
+ const [formsData, setFormsData] = useState({});
+ const [formsValidation, setFormsValidation] = useState({});
+
+ // Initialiser les données avec les réponses existantes
+ useEffect(() => {
+ if (existingResponses && Object.keys(existingResponses).length > 0) {
+ setFormsData(existingResponses);
+
+ // Marquer les formulaires avec réponses comme valides
+ const validationState = {};
+ Object.keys(existingResponses).forEach((formId) => {
+ if (
+ existingResponses[formId] &&
+ Object.keys(existingResponses[formId]).length > 0
+ ) {
+ validationState[formId] = true;
+ }
+ });
+ setFormsValidation(validationState);
+ }
+ }, [existingResponses]);
+
+ // Debug: Log des formulaires maîtres reçus
+ useEffect(() => {
+ logger.debug(
+ 'DynamicFormsList - Formulaires maîtres reçus:',
+ schoolFileMasters
+ );
+ }, [schoolFileMasters]);
+
+ // Mettre à jour la validation globale quand la validation des formulaires change
+ useEffect(() => {
+ const allFormsValid = schoolFileMasters.every(
+ (master, index) => formsValidation[master.id] === true
+ );
+
+ if (onValidationChange) {
+ onValidationChange(allFormsValid);
+ }
+ }, [formsValidation, schoolFileMasters, onValidationChange]);
+
+ /**
+ * Gère la soumission d'un formulaire individuel
+ */
+ const handleFormSubmit = async (formData, templateId) => {
+ try {
+ logger.debug('Soumission du formulaire:', { templateId, formData });
+
+ // Sauvegarder les données du formulaire
+ setFormsData((prev) => ({
+ ...prev,
+ [templateId]: formData,
+ }));
+
+ // Marquer le formulaire comme complété
+ setFormsValidation((prev) => ({
+ ...prev,
+ [templateId]: true,
+ }));
+
+ // Appeler le callback parent
+ if (onFormSubmit) {
+ await onFormSubmit(formData, templateId);
+ }
+
+ // Passer au formulaire suivant si disponible
+ if (currentTemplateIndex < schoolFileMasters.length - 1) {
+ setCurrentTemplateIndex(currentTemplateIndex + 1);
+ }
+
+ logger.debug('Formulaire soumis avec succès');
+ } catch (error) {
+ logger.error('Erreur lors de la soumission du formulaire:', error);
+ }
+ };
+
+ /**
+ * Gère les changements de validation d'un formulaire
+ */
+ const handleFormValidationChange = (isValid, templateId) => {
+ setFormsValidation((prev) => ({
+ ...prev,
+ [templateId]: isValid,
+ }));
+ };
+
+ /**
+ * Vérifie si un formulaire est complété
+ */
+ const isFormCompleted = (templateId) => {
+ return (
+ formsValidation[templateId] === true ||
+ (formsData[templateId] &&
+ Object.keys(formsData[templateId]).length > 0) ||
+ (existingResponses[templateId] &&
+ Object.keys(existingResponses[templateId]).length > 0)
+ );
+ };
+
+ /**
+ * Obtient l'icône de statut d'un formulaire
+ */
+ const getFormStatusIcon = (templateId, isActive) => {
+ if (isFormCompleted(templateId)) {
+ return ;
+ }
+ if (isActive) {
+ return ;
+ }
+ return ;
+ };
+
+ /**
+ * Obtient le formulaire actuel à afficher
+ */
+ const getCurrentTemplate = () => {
+ return schoolFileMasters[currentTemplateIndex];
+ };
+
+ if (!schoolFileMasters || schoolFileMasters.length === 0) {
+ return (
+
+
+
Aucun formulaire à compléter
+
+ );
+ }
+
+ const currentTemplate = getCurrentTemplate();
+
+ return (
+
+ {/* Liste des formulaires */}
+
+
+ Formulaires à compléter
+
+
+ {
+ Object.keys(formsValidation).filter((id) => formsValidation[id])
+ .length
+ }{' '}
+ / {schoolFileMasters.length} complétés
+
+
+
+ {schoolFileMasters.map((master, index) => {
+ const isActive = index === currentTemplateIndex;
+ const isCompleted = isFormCompleted(master.id);
+
+ return (
+ setCurrentTemplateIndex(index)}
+ >
+
+ {getFormStatusIcon(master.id, isActive)}
+
+
+
+ {master.formMasterData?.title ||
+ master.title ||
+ master.name ||
+ 'Formulaire sans nom'}
+
+ {isCompleted ? (
+
+ Complété -{' '}
+ {
+ Object.keys(
+ formsData[master.id] ||
+ existingResponses[master.id] ||
+ {}
+ ).length
+ }{' '}
+ réponse(s)
+
+ ) : (
+
+ {master.formMasterData?.fields || master.fields
+ ? `${(master.formMasterData?.fields || master.fields).length} champ(s)`
+ : 'À compléter'}
+
+ )}
+
+
+ );
+ })}
+
+
+
+ {/* Affichage du formulaire actuel */}
+
+ {currentTemplate && (
+
+
+
+ {currentTemplate.formMasterData?.title ||
+ currentTemplate.title ||
+ currentTemplate.name ||
+ 'Formulaire sans nom'}
+
+
+ {currentTemplate.formMasterData?.description ||
+ currentTemplate.description ||
+ 'Veuillez compléter ce formulaire pour continuer votre inscription.'}
+
+
+ Formulaire {currentTemplateIndex + 1} sur{' '}
+ {schoolFileMasters.length}
+
+
+
+ {/* Vérifier si le formulaire maître a des données de configuration */}
+ {(currentTemplate.formMasterData?.fields &&
+ currentTemplate.formMasterData.fields.length > 0) ||
+ (currentTemplate.fields && currentTemplate.fields.length > 0) ? (
+
+ handleFormSubmit(formData, currentTemplate.id)
+ }
+ />
+ ) : (
+
+
+
+ Ce formulaire n'est pas encore configuré.
+
+
+ Contactez l'administration pour plus d'informations.
+
+
+ )}
+
+ )}
+
+ {/* Message de fin */}
+ {currentTemplateIndex >= schoolFileMasters.length && (
+
+
+
+ Tous les formulaires ont été complétés !
+
+
+ Vous pouvez maintenant passer à l'étape suivante.
+
+
+ )}
+
+
+ );
+}
diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js
index 006eec3..ea2937b 100644
--- a/Front-End/src/components/Inscription/InscriptionFormShared.js
+++ b/Front-End/src/components/Inscription/InscriptionFormShared.js
@@ -5,6 +5,10 @@ import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import {
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
+ fetchRegistrationSchoolFileMasters,
+ saveFormResponses,
+ fetchFormResponses,
+ autoSaveRegisterForm,
} from '@/app/actions/subscriptionAction';
import {
downloadTemplate,
@@ -21,7 +25,8 @@ import { fetchProfiles } from '@/app/actions/authAction';
import { BASE_URL, FE_PARENTS_HOME_URL } from '@/utils/Url';
import logger from '@/utils/logger';
import FilesToUpload from '@/components/Inscription/FilesToUpload';
-import { DocusealForm } from '@docuseal/react';
+import DynamicFormsList from '@/components/Inscription/DynamicFormsList';
+import AutoSaveIndicator from '@/components/AutoSaveIndicator';
import StudentInfoForm from '@/components/Inscription/StudentInfoForm';
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
import SiblingInputFields from '@/components/Inscription/SiblingInputFields';
@@ -75,6 +80,8 @@ export default function InscriptionFormShared({
const [uploadedFiles, setUploadedFiles] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = useState([]);
+ const [schoolFileMasters, setSchoolFileMasters] = useState([]);
+ const [formResponses, setFormResponses] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [isPage1Valid, setIsPage1Valid] = useState(false);
@@ -90,6 +97,9 @@ export default function InscriptionFormShared({
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
const [profiles, setProfiles] = useState([]);
+ const [isSaving, setIsSaving] = useState(false);
+ const [lastSaved, setLastSaved] = useState(null);
+ const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const router = useRouter();
@@ -121,18 +131,18 @@ export default function InscriptionFormShared({
}, [schoolFileTemplates]);
useEffect(() => {
- // Vérifier si tous les templates ont leur champ "file" différent de null
- const allSigned = schoolFileTemplates.every(
- (template) => template.file !== null
- );
+ // Vérifier si tous les formulaires maîtres sont complétés
+ const allCompleted =
+ schoolFileMasters.length === 0 ||
+ schoolFileMasters.every((master) => master.completed === true);
- // Mettre à jour isPage4Valid en fonction de cette condition
- setIsPage5Valid(allSigned);
+ // Mettre à jour isPage5Valid en fonction de cette condition
+ setIsPage5Valid(allCompleted);
- if (allSigned) {
+ if (allCompleted) {
setCurrentTemplateIndex(0);
}
- }, [schoolFileTemplates]);
+ }, [schoolFileMasters]);
useEffect(() => {
// Vérifier si tous les documents avec is_required = true ont leur champ "file" différent de null
@@ -145,57 +155,216 @@ export default function InscriptionFormShared({
logger.debug(allRequiredUploaded);
}, [parentFileTemplates]);
- const handleTemplateSigned = (index) => {
- const template = schoolFileTemplates[index];
+ // Auto-sauvegarde périodique (toutes les 30 secondes)
+ useEffect(() => {
+ if (!enable || !autoSaveEnabled) return;
- if (!template) {
- logger.error("Template introuvable pour l'index donné.");
+ const interval = setInterval(() => {
+ autoSave();
+ }, 30000); // 30 secondes
+
+ return () => clearInterval(interval);
+ }, [enable, autoSaveEnabled, formData, guardians, siblings]);
+
+ // Auto-sauvegarde quand les données changent (avec debounce)
+ useEffect(() => {
+ if (!enable || !autoSaveEnabled) return;
+
+ const timeout = setTimeout(() => {
+ autoSave();
+ }, 2000); // Attendre 2 secondes après le dernier changement
+
+ return () => clearTimeout(timeout);
+ }, [formData, guardians, siblings]);
+
+ /**
+ * Fonction d'auto-sauvegarde qui sauvegarde les données en cours
+ */
+ const autoSave = async () => {
+ if (!autoSaveEnabled || !studentId || isSaving) {
return;
}
- // Télécharger le template
- downloadTemplate(template.slug, selectedEstablishmentId, apiDocuseal)
- .then((downloadUrl) => fetch(downloadUrl))
- .then((response) => {
- if (!response.ok) {
- throw new Error('Erreur lors du téléchargement du fichier.');
- }
- return response.blob();
- })
- .then((blob) => {
- const file = new File([blob], `${template.name}.pdf`, {
- type: blob.type,
- });
-
- // Préparer les données pour la mise à jour
- const updateData = new FormData();
- updateData.append('file', file);
-
- // Mettre à jour le template via l'API
- return editRegistrationSchoolFileTemplates(
- template.id,
- updateData,
- csrfToken
- );
- })
- .then((updatedTemplate) => {
- logger.debug('Template mis à jour avec succès :', updatedTemplate);
-
- // Mettre à jour l'état local de schoolFileTemplates
- setSchoolFileTemplates((prevTemplates) => {
- const updatedTemplates = prevTemplates.map((t, i) =>
- i === index ? { ...t, file: updatedTemplate.data.file } : t
- );
- logger.debug(
- 'État schoolFileTemplates mis à jour :',
- updatedTemplates
- );
- return updatedTemplates;
- });
- })
- .catch((error) => {
- logger.error('Erreur lors de la mise à jour du template :', error);
+ try {
+ setIsSaving(true);
+ logger.debug('Auto-sauvegarde en cours...', {
+ studentId,
+ formDataKeys: Object.keys(formData),
+ paymentFields: {
+ registration_payment: formData.registration_payment,
+ registration_payment_plan: formData.registration_payment_plan,
+ tuition_payment: formData.tuition_payment,
+ tuition_payment_plan: formData.tuition_payment_plan,
+ },
+ guardians: guardians.length,
+ siblings: siblings.length,
+ currentPage,
});
+
+ // Fonction helper pour nettoyer les données avant sauvegarde
+ const cleanDataForAutoSave = (data) => {
+ const cleaned = {};
+ Object.keys(data).forEach((key) => {
+ const value = data[key];
+ // Garder seulement les valeurs non-vides et valides
+ if (value !== null && value !== undefined && value !== '') {
+ // Pour les dates, vérifier le format
+ if (key === 'birth_date' && value) {
+ // Vérifier que la date est dans un format valide
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
+ if (dateRegex.test(value)) {
+ cleaned[key] = value;
+ }
+ }
+ // Pour les codes postaux, vérifier que c'est un nombre
+ else if (key === 'birth_postal_code' && value) {
+ if (!isNaN(value) && value.toString().trim() !== '') {
+ cleaned[key] = parseInt(value);
+ }
+ }
+ // Pour les champs de paiement, toujours les inclure s'ils ont une valeur
+ else if (key.includes('payment') && value) {
+ cleaned[key] = value;
+ }
+ // Pour les autres champs, garder la valeur si elle n'est pas vide
+ else if (value.toString().trim() !== '') {
+ cleaned[key] = value;
+ }
+ }
+ });
+ return cleaned;
+ };
+
+ // Préparer les données à sauvegarder avec nettoyage
+ const cleanedFormData = cleanDataForAutoSave(formData);
+
+ const dataToSave = {
+ student: cleanedFormData,
+ guardians: guardians.filter(
+ (guardian) =>
+ guardian &&
+ (guardian.first_name || guardian.last_name || guardian.email)
+ ),
+ siblings: siblings.filter(
+ (sibling) => sibling && (sibling.first_name || sibling.last_name)
+ ),
+ currentPage: currentPage,
+ };
+
+ // Utiliser la fonction d'auto-save dédiée
+ await autoSaveRegisterForm(studentId, dataToSave, csrfToken);
+
+ setLastSaved(new Date());
+ logger.debug('Auto-sauvegarde réussie');
+ } catch (error) {
+ logger.error("Erreur lors de l'auto-sauvegarde:", error);
+ // Ne pas afficher d'erreur à l'utilisateur pour l'auto-save
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ /**
+ * Gère la sauvegarde à chaque changement d'étape
+ */
+ const saveStepData = async () => {
+ await autoSave();
+ };
+
+ /**
+ * Gère la soumission d'un formulaire dynamique
+ */
+ const handleDynamicFormSubmit = async (formData, templateId) => {
+ try {
+ logger.debug('Soumission du formulaire dynamique:', {
+ templateId,
+ formData,
+ csrfToken: !!csrfToken,
+ });
+
+ // Trouver le template correspondant pour récupérer sa configuration
+ const currentTemplate = schoolFileMasters.find(
+ (master) => master.id === templateId
+ );
+ if (!currentTemplate) {
+ throw new Error(`Template avec l'ID ${templateId} non trouvé`);
+ }
+
+ // Construire la structure complète avec la configuration et les réponses
+ const formTemplateData = {
+ id: currentTemplate.id,
+ title:
+ currentTemplate.formMasterData?.title ||
+ currentTemplate.title ||
+ currentTemplate.name ||
+ 'Formulaire',
+ fields: (
+ currentTemplate.formMasterData?.fields ||
+ currentTemplate.fields ||
+ []
+ ).map((field) => ({
+ ...field,
+ // Ajouter la réponse de l'utilisateur selon le type de champ
+ ...(field.type === 'checkbox'
+ ? { checked: formData[field.id] || false }
+ : {}),
+ ...(field.type === 'radio' ? { selected: formData[field.id] } : {}),
+ ...(field.type === 'text' ||
+ field.type === 'textarea' ||
+ field.type === 'email'
+ ? { value: formData[field.id] || '' }
+ : {}),
+ })),
+ submitLabel: currentTemplate.formMasterData?.submitLabel || 'Valider',
+ responses: formData, // Garder aussi les réponses brutes pour facilité d'accès
+ };
+
+ // Sauvegarder les réponses du formulaire via l'API RegistrationSchoolFileTemplate
+ logger.debug('Appel API saveFormResponses avec:', {
+ templateId,
+ formTemplateData,
+ });
+ const result = await saveFormResponses(
+ templateId,
+ formTemplateData,
+ csrfToken
+ );
+ logger.debug("Réponse de l'API:", result);
+
+ // Mettre à jour l'état local des réponses
+ setFormResponses((prev) => ({
+ ...prev,
+ [templateId]: formData,
+ }));
+
+ // Mettre à jour l'état local pour indiquer que le formulaire est complété
+ setSchoolFileMasters((prevMasters) => {
+ return prevMasters.map((master) =>
+ master.id === templateId
+ ? { ...master, completed: true, responses: formData }
+ : master
+ );
+ });
+
+ logger.debug('Formulaire dynamique sauvegardé avec succès');
+ return Promise.resolve();
+ } catch (error) {
+ logger.error('Erreur lors de la soumission du formulaire dynamique:', {
+ templateId,
+ error: error.message,
+ stack: error.stack,
+ });
+ // Afficher l'erreur à l'utilisateur
+ alert(`Erreur lors de la sauvegarde du formulaire: ${error.message}`);
+ return Promise.reject(error);
+ }
+ };
+
+ /**
+ * Gère les changements de validation des formulaires dynamiques
+ */
+ const handleDynamicFormsValidationChange = (isValid) => {
+ setIsPage5Valid(isValid);
};
useEffect(() => {
@@ -223,6 +392,66 @@ export default function InscriptionFormShared({
.catch((error) => logger.error('Error fetching profiles : ', error));
if (selectedEstablishmentId) {
+ // Fetch data for school file masters
+ fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
+ .then(async (data) => {
+ logger.debug('School file masters fetched:', data);
+ setSchoolFileMasters(data);
+
+ // Récupérer les données existantes de chaque template
+ const responsesMap = {};
+ for (const master of data) {
+ if (master.id) {
+ try {
+ const templateData = await fetchFormResponses(master.id);
+ if (templateData && templateData.formTemplateData) {
+ // Si on a les réponses brutes sauvegardées, les utiliser
+ if (templateData.formTemplateData.responses) {
+ responsesMap[master.id] =
+ templateData.formTemplateData.responses;
+ } else {
+ // Sinon, extraire les réponses depuis les champs
+ const responses = {};
+ if (templateData.formTemplateData.fields) {
+ templateData.formTemplateData.fields.forEach((field) => {
+ if (
+ field.type === 'checkbox' &&
+ field.checked !== undefined
+ ) {
+ responses[field.id] = field.checked;
+ } else if (
+ field.type === 'radio' &&
+ field.selected !== undefined
+ ) {
+ responses[field.id] = field.selected;
+ } else if (
+ (field.type === 'text' ||
+ field.type === 'textarea' ||
+ field.type === 'email') &&
+ field.value !== undefined
+ ) {
+ responses[field.id] = field.value;
+ }
+ });
+ }
+ responsesMap[master.id] = responses;
+ }
+ }
+ } catch (error) {
+ logger.debug(
+ `Pas de données existantes pour le template ${master.id}:`,
+ error
+ );
+ // Ce n'est pas critique si un template n'a pas de données
+ }
+ }
+ }
+ setFormResponses(responsesMap);
+ })
+ .catch((error) =>
+ logger.error('Error fetching school file masters:', error)
+ );
+
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
@@ -423,11 +652,16 @@ export default function InscriptionFormShared({
onSubmit(formDataToSend);
};
- const handleNextPage = () => {
+ const handleNextPage = async () => {
+ // Sauvegarder avant de passer à l'étape suivante
+ await saveStepData();
+ setHasInteracted(false);
setCurrentPage(currentPage + 1);
};
- const handlePreviousPage = () => {
+ const handlePreviousPage = async () => {
+ // Sauvegarder avant de revenir à l'étape précédente
+ await saveStepData();
setCurrentPage(currentPage - 1);
};
@@ -479,7 +713,18 @@ export default function InscriptionFormShared({
setStep={setCurrentPage}
isStepValid={isStepValid}
/>
-
+
+ {/* Indicateur de sauvegarde automatique */}
+ {enable && (
+
setAutoSaveEnabled(!autoSaveEnabled)}
+ />
+ )}
+
+
{/* Page 1 : Informations sur l'élève */}
{currentPage === 1 && (
)}
- {/* Page 5 : Section Fichiers d'inscription */}
+ {/* Page 5 : Formulaires dynamiques d'inscription */}
{currentPage === 5 && (
-
- {/* Liste des états de signature */}
-
-
- Documents
-
-
- {schoolFileTemplates.map((template, index) => (
- setCurrentTemplateIndex(index)} // Mettre à jour l'index du template actuel
- >
-
- {template.file !== null ? (
-
- ) : (
-
- )}
-
- {template.name || 'Document sans nom'}
-
- ))}
-
-
-
- {/* Affichage du fichier actuel */}
-
- {currentTemplateIndex < schoolFileTemplates.length && (
-
-
- {schoolFileTemplates[currentTemplateIndex].name ||
- 'Document sans nom'}
-
-
- {schoolFileTemplates[currentTemplateIndex].description ||
- 'Aucune description disponible pour ce document.'}
-
-
- {schoolFileTemplates[currentTemplateIndex].file === null ? (
-
- handleTemplateSigned(currentTemplateIndex)
- }
- />
- ) : (
-
- )}
-
- )}
-
- {/* Message de fin */}
- {currentTemplateIndex >= schoolFileTemplates.length && (
-
- Tous les formulaires ont été signés avec succès !
-
- )}
-
-
+
)}
{/* Dernière page : Section Fichiers parents */}
diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js
index 23a6841..3965db7 100644
--- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js
+++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js
@@ -1,13 +1,20 @@
import React, { useState, useEffect } from 'react';
-import { Download, Edit3, Trash2, FolderPlus, Signature, AlertTriangle } from 'lucide-react';
+import {
+ Download,
+ Edit3,
+ Trash2,
+ FolderPlus,
+ FileText,
+ AlertTriangle,
+} from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
+import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
import { BASE_URL } from '@/utils/Url';
import {
// GET
fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
- fetchRegistrationSchoolFileTemplates,
fetchRegistrationParentFileMasters,
// POST
createRegistrationFileGroup,
@@ -20,7 +27,7 @@ import {
// DELETE
deleteRegistrationFileGroup,
deleteRegistrationSchoolFileMaster,
- deleteRegistrationParentFileMaster
+ deleteRegistrationParentFileMaster,
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
@@ -34,10 +41,9 @@ import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal'
export default function FilesGroupsManagement({
csrfToken,
- selectedEstablishmentId
+ selectedEstablishmentId,
}) {
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
- const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
@@ -46,7 +52,6 @@ export default function FilesGroupsManagement({
const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
- const [reloadTemplates, setReloadTemplates] = useState(false);
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
@@ -54,10 +59,6 @@ export default function FilesGroupsManagement({
const [isLoading, setIsLoading] = useState(false);
const { showNotification } = useNotification();
- const handleReloadTemplates = () => {
- setReloadTemplates(true);
- };
-
const transformFileData = (file, groups) => {
const groupInfos = file.groups.map(
(groupId) =>
@@ -77,79 +78,67 @@ export default function FilesGroupsManagement({
Promise.all([
fetchRegistrationSchoolFileMasters(selectedEstablishmentId),
fetchRegistrationFileGroups(selectedEstablishmentId),
- fetchRegistrationSchoolFileTemplates(selectedEstablishmentId),
fetchRegistrationParentFileMasters(selectedEstablishmentId),
])
- .then(
- ([
- dataSchoolFileMasters,
- groupsData,
- dataSchoolFileTemplates,
- dataParentFileMasters,
- ]) => {
- setGroups(groupsData);
- setSchoolFileTemplates(dataSchoolFileTemplates);
- setParentFileMasters(dataParentFileMasters);
- // Transformer chaque fichier pour inclure les informations complètes du groupe
- const transformedFiles = dataSchoolFileMasters.map((file) =>
- transformFileData(file, groupsData)
- );
- setSchoolFileMasters(transformedFiles);
- }
- )
+ .then(([dataSchoolFileMasters, groupsData, dataParentFileMasters]) => {
+ setGroups(groupsData);
+ setParentFileMasters(dataParentFileMasters);
+ // Transformer chaque fichier pour inclure les informations complètes du groupe
+ const transformedFiles = dataSchoolFileMasters.map((file) =>
+ transformFileData(file, groupsData)
+ );
+ setSchoolFileMasters(transformedFiles);
+ })
.catch((err) => {
logger.debug(err.message);
- })
- .finally(() => {
- setReloadTemplates(false);
});
}
- }, [reloadTemplates, selectedEstablishmentId]);
+ }, [selectedEstablishmentId]);
const deleteTemplateMaster = (templateMaster) => {
setRemovePopupVisible(true);
setRemovePopupMessage(
- `Attentions ! \nVous êtes sur le point de supprimer le document "${templateMaster.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
+ `Attention ! \nVous êtes sur le point de supprimer le formulaire "${templateMaster.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
setIsLoading(true);
- // Supprimer le template master de la base de données
- deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
- .then((response) => {
- if (response.ok) {
- setSchoolFileMasters(
- schoolFileMasters.filter(
- (fichier) => fichier.id !== templateMaster.id
- )
- );
- showNotification(
- `Le document "${templateMaster.name}" a été correctement supprimé.`,
- 'success',
- 'Succès'
- );
- setRemovePopupVisible(false);
- setIsLoading(false);
- } else {
+ // Supprimer le formulaire de la base de données
+ deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
+ .then((response) => {
+ if (response.ok) {
+ setSchoolFileMasters(
+ schoolFileMasters.filter(
+ (fichier) => fichier.id !== templateMaster.id
+ )
+ );
+ showNotification(
+ `Le formulaire "${templateMaster.name}" a été correctement supprimé.`,
+ 'success',
+ 'Succès'
+ );
+ setRemovePopupVisible(false);
+ } else {
+ showNotification(
+ `Erreur lors de la suppression du formulaire "${templateMaster.name}".`,
+ 'error',
+ 'Erreur'
+ );
+ setRemovePopupVisible(false);
+ }
+ })
+ .catch((error) => {
+ logger.error('Error deleting file from database:', error);
showNotification(
- `Erreur lors de la suppression du document "${templateMaster.name}".`,
+ `Erreur lors de la suppression du formulaire "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
+ })
+ .finally(() => {
setIsLoading(false);
- }
- })
- .catch((error) => {
- logger.error('Error deleting file from database:', error);
- showNotification(
- `Erreur lors de la suppression du document "${templateMaster.name}".`,
- 'error',
- 'Erreur'
- );
- setRemovePopupVisible(false);
- setIsLoading(false);
- });
+ });
});
};
@@ -159,12 +148,11 @@ export default function FilesGroupsManagement({
setIsModalOpen(true);
};
- const handleCreateTemplateMaster = ({ name, group_ids, id, is_required }) => {
+ const handleCreateTemplateMaster = ({ name, group_ids, formMasterData }) => {
const data = {
name: name,
- id: id,
groups: group_ids,
- is_required: is_required,
+ formMasterData: formMasterData, // Envoyer directement l'objet
};
logger.debug(data);
@@ -174,18 +162,32 @@ export default function FilesGroupsManagement({
const transformedFile = transformFileData(data, groups);
setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
setIsModalOpen(false);
+ showNotification(
+ `Le formulaire "${name}" a été créé avec succès.`,
+ 'success',
+ 'Succès'
+ );
})
.catch((error) => {
- logger.error('Error uploading file:', error);
+ logger.error('Error creating form:', error);
+ showNotification(
+ 'Erreur lors de la création du formulaire',
+ 'error',
+ 'Erreur'
+ );
});
};
- const handleEditTemplateMaster = ({ name, group_ids, id, is_required }) => {
+ const handleEditTemplateMaster = ({
+ name,
+ group_ids,
+ formMasterData,
+ id,
+ }) => {
const data = {
name: name,
- id: id,
groups: group_ids,
- is_required: is_required,
+ formMasterData: formMasterData,
};
logger.debug(data);
@@ -197,11 +199,16 @@ export default function FilesGroupsManagement({
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
);
setIsModalOpen(false);
+ showNotification(
+ `Le formulaire "${name}" a été modifié avec succès.`,
+ 'success',
+ 'Succès'
+ );
})
.catch((error) => {
- logger.error('Error editing file:', error);
+ logger.error('Error editing form:', error);
showNotification(
- 'Erreur lors de la modification du fichier',
+ 'Erreur lors de la modification du formulaire',
'error',
'Erreur'
);
@@ -383,24 +390,17 @@ export default function FilesGroupsManagement({
name: 'Actions',
transform: (row) => (
- {row.file && (
-
-
-
- )}
editTemplateMaster(row)}
className="text-blue-500 hover:text-blue-700"
+ title="Modifier le formulaire"
>
deleteTemplateMaster(row)}
className="text-red-500 hover:text-red-700"
+ title="Supprimer le formulaire"
>
@@ -439,23 +439,26 @@ export default function FilesGroupsManagement({
return (
- {/* Modal pour les fichiers */}
+ {/* Modal pour les formulaires */}
{
setIsModalOpen(isOpen);
if (!isOpen) {
setFileToEdit(null);
+ setIsEditing(false);
}
}}
- title={isEditing ? 'Modification du document' : 'Ajouter un document'}
- modalClassName="w-4/5 h-4/5"
+ title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'}
+ modalClassName="w-11/12 h-5/6"
>
-
@@ -496,17 +499,18 @@ export default function FilesGroupsManagement({
/>
- {/* Section Fichiers */}
+ {/* Section Formulaires */}
{
setIsModalOpen(true);
setIsEditing(false);
+ setFileToEdit(null);
}}
/>
}
/>
diff --git a/conf/backend.env.default b/conf/backend.env.default
index 3753350..427497d 100644
--- a/conf/backend.env.default
+++ b/conf/backend.env.default
@@ -1,6 +1,8 @@
TZ="Europe/Paris"
TEST_MODE=true
+FLUSH_DATA=false
+MIGRATE_DATA=false
CSRF_COOKIE_SECURE=true
CSRF_COOKIE_DOMAIN=".localhost"
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080