From db587ec74749ee6092696f8247434c556ea02128 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sun, 5 Apr 2026 16:06:04 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20signature=20=C3=A9lectronique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/migrations/0001_initial.py | 2 +- Back-End/Common/migrations/0001_initial.py | 2 +- .../Establishment/migrations/0001_initial.py | 2 +- .../migrations/0001_initial.py | 2 +- .../migrations/0001_initial.py | 2 +- Back-End/Planning/migrations/0001_initial.py | 2 +- Back-End/School/migrations/0001_initial.py | 4 +- Back-End/Settings/migrations/0001_initial.py | 2 +- .../Subscriptions/migrations/0001_initial.py | 5 +- Back-End/Subscriptions/models.py | 4 + Back-End/Subscriptions/serializers.py | 23 ++ .../templates/pdfs/dynamic_form.html | 245 +++++++++++++++++ .../Inscription/DynamicFormsList.js | 255 ++++++++++++++---- .../Inscription/InscriptionFormShared.js | 45 ++++ .../Inscription/ValidateSubscription.js | 85 +++++- .../Structure/Files/FilesGroupsManagement.js | 74 +++-- 16 files changed, 654 insertions(+), 100 deletions(-) create mode 100644 Back-End/Subscriptions/templates/pdfs/dynamic_form.html diff --git a/Back-End/Auth/migrations/0001_initial.py b/Back-End/Auth/migrations/0001_initial.py index 38f75fe..4d83049 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:05 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 6070e9f..2c148f5 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:04 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 e171153..953239e 100644 --- a/Back-End/Establishment/migrations/0001_initial.py +++ b/Back-End/Establishment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:04 import Establishment.models import django.contrib.postgres.fields diff --git a/Back-End/GestionMessagerie/migrations/0001_initial.py b/Back-End/GestionMessagerie/migrations/0001_initial.py index 8d92423..899add7 100644 --- a/Back-End/GestionMessagerie/migrations/0001_initial.py +++ b/Back-End/GestionMessagerie/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:05 import django.db.models.deletion import django.utils.timezone diff --git a/Back-End/GestionNotification/migrations/0001_initial.py b/Back-End/GestionNotification/migrations/0001_initial.py index 8198a27..768f5f1 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:05 import django.db.models.deletion from django.conf import settings diff --git a/Back-End/Planning/migrations/0001_initial.py b/Back-End/Planning/migrations/0001_initial.py index 7b95e54..2e6fac1 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:05 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 5b5aa28..5449b07 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:05 import django.contrib.postgres.fields import django.db.models.deletion @@ -124,7 +124,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100)), ('updated_date', models.DateTimeField(auto_now=True)), ('color_code', models.CharField(default='#FFFFFF', max_length=7)), - ('school_year', models.CharField(blank=True, max_length=9)), ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specialities', to='Establishment.establishment')), ], ), @@ -154,7 +153,6 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('last_name', models.CharField(max_length=100)), ('first_name', models.CharField(max_length=100)), - ('school_year', models.CharField(blank=True, max_length=9)), ('updated_date', models.DateTimeField(auto_now=True)), ('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='teacher_profile', to='Auth.profilerole')), ('specialities', models.ManyToManyField(blank=True, to='School.speciality')), diff --git a/Back-End/Settings/migrations/0001_initial.py b/Back-End/Settings/migrations/0001_initial.py index dfb4083..02fd817 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:04 import django.db.models.deletion from django.db import migrations, models diff --git a/Back-End/Subscriptions/migrations/0001_initial.py b/Back-End/Subscriptions/migrations/0001_initial.py index 5e846df..659161b 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 2026-04-05 08:05 +# Generated by Django 5.1.3 on 2026-04-05 14:04 import Subscriptions.models import django.db.models.deletion @@ -54,6 +54,8 @@ class Migration(migrations.Migration): ('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)), ('isValidated', models.BooleanField(blank=True, default=None, null=True)), + ('electronic_signature', models.TextField(blank=True, help_text='Signature électronique encodée en base64', null=True)), + ('electronic_signature_date', models.DateTimeField(blank=True, help_text='Date de la signature électronique', null=True)), ], ), migrations.CreateModel( @@ -169,6 +171,7 @@ class Migration(migrations.Migration): ('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)), + ('requires_electronic_signature', models.BooleanField(default=False, help_text='Si activé, le parent devra signer électroniquement ce document')), ('formMasterData', models.JSONField(blank=True, default=list, null=True)), ('file', models.FileField(blank=True, help_text='Fichier du formulaire existant (PDF, DOC, etc.)', null=True, upload_to=Subscriptions.models.registration_school_file_master_upload_to)), ('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')), diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 749b2c6..91a4413 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -384,6 +384,7 @@ class RegistrationSchoolFileMaster(models.Model): groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True) name = models.CharField(max_length=255, default="") is_required = models.BooleanField(default=False) + requires_electronic_signature = models.BooleanField(default=False, help_text="Si activé, le parent devra signer électroniquement ce document") formMasterData = models.JSONField(default=list, blank=True, null=True) file = models.FileField( upload_to=registration_school_file_master_upload_to, @@ -558,6 +559,9 @@ class RegistrationSchoolFileTemplate(models.Model): formTemplateData = models.JSONField(default=list, blank=True, null=True) # Tri-etat: None=en attente, True=valide, False=refuse isValidated = models.BooleanField(null=True, blank=True, default=None) + # Signature électronique (base64 SVG ou PNG) + electronic_signature = models.TextField(null=True, blank=True, help_text="Signature électronique encodée en base64") + electronic_signature_date = models.DateTimeField(null=True, blank=True, help_text="Date de la signature électronique") def __str__(self): return self.name diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index fec83fc..8748e39 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -57,6 +57,8 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) file_url = serializers.SerializerMethodField() master_file_url = serializers.SerializerMethodField() + requires_electronic_signature = serializers.SerializerMethodField() + is_electronically_signed = serializers.SerializerMethodField() class Meta: model = RegistrationSchoolFileTemplate @@ -72,6 +74,27 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer): return obj.master.file.url return None + def get_requires_electronic_signature(self, obj): + # Retourne si le document nécessite une signature électronique + if obj.master: + return obj.master.requires_electronic_signature + return False + + def get_is_electronically_signed(self, obj): + # Retourne True si le document a été signé électroniquement + return bool(obj.electronic_signature) + + def update(self, instance, validated_data): + # Auto-remplir la date de signature si electronic_signature est fournie + from django.utils import timezone + if 'electronic_signature' in validated_data and validated_data['electronic_signature']: + # Nouvelle signature ou re-signature : enregistrer la date + validated_data['electronic_signature_date'] = timezone.now() + # Si le document était refusé, le repasser en attente de validation + if instance.isValidated == False: + validated_data['isValidated'] = None + return super().update(instance, validated_data) + class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) file_url = serializers.SerializerMethodField() diff --git a/Back-End/Subscriptions/templates/pdfs/dynamic_form.html b/Back-End/Subscriptions/templates/pdfs/dynamic_form.html new file mode 100644 index 0000000..206b289 --- /dev/null +++ b/Back-End/Subscriptions/templates/pdfs/dynamic_form.html @@ -0,0 +1,245 @@ + + + + + {{ pdf_title }} + + + +
+

{{ pdf_title }}

+ + {% for field in fields %} + {% if field.kind == 'heading' %} + {% if field.level == 'heading1' %} +
{{ field.text }}
+ {% elif field.level == 'heading2' %} +
{{ field.text }}
+ {% elif field.level == 'heading3' %} +
{{ field.text }}
+ {% elif field.level == 'heading4' %} +
{{ field.text }}
+ {% elif field.level == 'heading5' %} +
{{ field.text }}
+ {% else %} +
{{ field.text }}
+ {% endif %} + {% elif field.kind == 'paragraph' %} +

{{ field.text }}

+ {% elif field.kind == 'file' %} +
+
{{ field.label }}
+
+ {% if field.has_preview %} + {% for preview in field.preview_pages %} +
+ {{ preview.alt }} +
+ {% endfor %} + {% if field.total_pages > 1 %} +
Aperçu de la première page sur {{ field.total_pages }} pages
+ {% endif %} + {% else %} +
Aucun document source fourni
+ {% endif %} +
+
+ {% elif field.kind == 'phone' %} +
+
{{ field.label }}
+ + + + + +
{{ field.prefix }} + {% if field.value %} + {{ field.value }} + {% else %} +   + {% endif %} +
+
+ {% elif field.kind == 'signature' %} +
+
{{ field.label }}
+
+ {% if field.signature_src %} + Signature + {% else %} +   + {% endif %} +
+
+ {% else %} +
+
{{ field.label }}
+
+ {% if field.value %} + {{ field.value }} + {% else %} +   + {% endif %} +
+ {% if field.field_type %} +
Type: {{ field.field_type }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ + \ No newline at end of file diff --git a/Front-End/src/components/Inscription/DynamicFormsList.js b/Front-End/src/components/Inscription/DynamicFormsList.js index f9a0639..701f5fa 100644 --- a/Front-End/src/components/Inscription/DynamicFormsList.js +++ b/Front-End/src/components/Inscription/DynamicFormsList.js @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react'; import FormRenderer from '@/components/Form/FormRenderer'; import FileUpload from '@/components/Form/FileUpload'; +import SignatureField from '@/components/Form/SignatureField'; +import Button from '@/components/Form/Button'; import { CheckCircle, Hourglass, @@ -9,6 +11,7 @@ import { Download, Upload, XCircle, + PenTool, } from 'lucide-react'; import logger from '@/utils/logger'; import { getSecureFileUrl } from '@/utils/fileUrl'; @@ -20,6 +23,7 @@ import { getSecureFileUrl } from '@/utils/fileUrl'; * @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis * @param {Boolean} enable - Si les formulaires sont modifiables * @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné + * @param {Function} onSignatureSubmit - Callback appelé quand une signature est soumise */ export default function DynamicFormsList({ schoolFileTemplates, @@ -27,7 +31,8 @@ export default function DynamicFormsList({ onFormSubmit, enable = true, onValidationChange, - onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent) + onFileUpload, + onSignatureSubmit, }) { const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0); const [formsData, setFormsData] = useState({}); @@ -82,6 +87,19 @@ export default function DynamicFormsList({ // vérifier si un fichier a déjà été uploadé sur le template const template = schoolFileTemplates.find((tpl) => tpl.id === templateId); if (template && template.file && !isDynamicForm(template)) { + // Si le document est refusé, on ne le considère pas comme complété + if (template.isValidated === false) { + return false; + } + return true; + } + + // Vérifier si le document a été signé électroniquement ET non refusé + if ( + template && + template.is_electronically_signed && + template.isValidated !== false + ) { return true; } @@ -249,6 +267,30 @@ export default function DynamicFormsList({ } }; + // Handler pour soumettre la signature électronique + const handleSignature = async (templateId, signatureData) => { + if (!signatureData || !templateId) return; + try { + if (onSignatureSubmit) { + await onSignatureSubmit(templateId, signatureData); + setFormsData((prev) => ({ + ...prev, + [templateId]: { ...prev[templateId], signed: true }, + })); + setFormsValidation((prev) => ({ + ...prev, + [templateId]: true, + })); + // Passer au formulaire suivant si disponible + if (currentTemplateIndex < schoolFileTemplates.length - 1) { + setCurrentTemplateIndex(currentTemplateIndex + 1); + } + } + } catch (error) { + logger.error('Erreur lors de la soumission de la signature :', error); + } + }; + if (!schoolFileTemplates || schoolFileTemplates.length === 0) { return (
@@ -325,7 +367,8 @@ export default function DynamicFormsList({ ? 'text-secondary font-semibold' : textClass; canEdit = false; - } else if (isValidated === false) { + } else if (isValidated === false && !isCompletedLocally) { + // Refusé uniquement si pas re-complété localement statusLabel = 'Refusé'; statusColor = 'red'; icon = ; @@ -337,30 +380,28 @@ export default function DynamicFormsList({ ? 'text-red-900 font-semibold' : 'text-red-700'; canEdit = true; + } else if (isCompletedLocally) { + statusLabel = 'Complété'; + statusColor = 'orange'; + icon = ; + bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50'; + borderClass = isActive + ? 'border border-orange-300' + : 'border border-orange-200'; + textClass = isActive + ? 'text-orange-900 font-semibold' + : 'text-orange-700'; + canEdit = true; } else { - if (isCompletedLocally) { - statusLabel = 'Complété'; - statusColor = 'orange'; - icon = ; - bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50'; - borderClass = isActive - ? 'border border-orange-300' - : 'border border-orange-200'; - textClass = isActive - ? 'text-orange-900 font-semibold' - : 'text-orange-700'; - canEdit = true; - } else { - statusLabel = 'À compléter'; - statusColor = 'gray'; - icon = ; - bgClass = isActive ? 'bg-gray-200' : ''; - borderClass = isActive ? 'border border-gray-300' : ''; - textClass = isActive - ? 'text-gray-900 font-semibold' - : 'text-gray-600'; - canEdit = true; - } + statusLabel = 'À compléter'; + statusColor = 'gray'; + icon = ; + bgClass = isActive ? 'bg-gray-200' : ''; + borderClass = isActive ? 'border border-gray-300' : ''; + textClass = isActive + ? 'text-gray-900 font-semibold' + : 'text-gray-600'; + canEdit = true; } return ( @@ -417,14 +458,14 @@ export default function DynamicFormsList({ Validé - ) : currentTemplate.isValidated === false ? ( - - Refusé - ) : hasLocalCompletion(currentTemplate.id) ? ( Complété + ) : currentTemplate.isValidated === false ? ( + + Refusé + ) : ( En attente @@ -507,39 +548,137 @@ export default function DynamicFormsList({ /> )} - {/* Cas non validé : bouton télécharger + upload */} + {/* Cas non validé */} {currentTemplate.isValidated !== true && (
- {/* Bouton télécharger le document source (fichier maître) */} - {(currentTemplate.master_file_url || - currentTemplate.file) && ( - + {/* Affichage du document à signer */} + {(currentTemplate.master_file_url || + currentTemplate.file) && ( +