mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-06-04 13:26:11 +00:00
Compare commits
15 Commits
4431c428d3
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 4aacdc1c28 | |||
| 9e8201a1ec | |||
| 40b2c8d1e5 | |||
| 9d97023cae | |||
| cb782fa109 | |||
| 92c3183153 | |||
| db587ec747 | |||
| a81b76ecea | |||
| e9a30b7bde | |||
| ff1d113698 | |||
| 12a6ad1d61 | |||
| 856443d4ed | |||
| ace4dcbf07 | |||
| 61f63f9dc9 | |||
| d9e998d2ff |
@ -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.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
|
|||||||
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -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 Establishment.models
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
|
|||||||
@ -72,11 +72,11 @@ class ChatConsumer(AsyncWebsocketConsumer):
|
|||||||
if presence:
|
if presence:
|
||||||
await self.broadcast_presence_update(self.user_id, 'online')
|
await self.broadcast_presence_update(self.user_id, 'online')
|
||||||
|
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
# Envoyer les statuts de présence existants des autres utilisateurs connectés
|
# Envoyer les statuts de présence existants des autres utilisateurs connectés
|
||||||
await self.send_existing_user_presences()
|
await self.send_existing_user_presences()
|
||||||
|
|
||||||
await self.accept()
|
|
||||||
|
|
||||||
logger.info(f"User {self.user_id} connected to chat")
|
logger.info(f"User {self.user_id} connected to chat")
|
||||||
|
|
||||||
async def send_existing_user_presences(self):
|
async def send_existing_user_presences(self):
|
||||||
|
|||||||
@ -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.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@ -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.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@ -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.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -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.contrib.postgres.fields
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -124,7 +124,6 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
('color_code', models.CharField(default='#FFFFFF', max_length=7)),
|
('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')),
|
('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')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('last_name', models.CharField(max_length=100)),
|
('last_name', models.CharField(max_length=100)),
|
||||||
('first_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)),
|
('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')),
|
('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')),
|
('specialities', models.ManyToManyField(blank=True, to='School.speciality')),
|
||||||
|
|||||||
@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -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 Subscriptions.models
|
||||||
import django.db.models.deletion
|
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)),
|
('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)),
|
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
|
||||||
('isValidated', models.BooleanField(blank=True, default=None, 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(
|
migrations.CreateModel(
|
||||||
@ -169,6 +171,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(default='', max_length=255)),
|
('name', models.CharField(default='', max_length=255)),
|
||||||
('is_required', models.BooleanField(default=False)),
|
('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)),
|
('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)),
|
('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')),
|
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')),
|
||||||
|
|||||||
@ -384,6 +384,7 @@ class RegistrationSchoolFileMaster(models.Model):
|
|||||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
is_required = models.BooleanField(default=False)
|
is_required = models.BooleanField(default=False)
|
||||||
|
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)
|
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
upload_to=registration_school_file_master_upload_to,
|
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)
|
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
||||||
# Tri-etat: None=en attente, True=valide, False=refuse
|
# Tri-etat: None=en attente, True=valide, False=refuse
|
||||||
isValidated = models.BooleanField(null=True, blank=True, default=None)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@ -57,6 +57,8 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
|
|||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
file_url = serializers.SerializerMethodField()
|
file_url = serializers.SerializerMethodField()
|
||||||
master_file_url = serializers.SerializerMethodField()
|
master_file_url = serializers.SerializerMethodField()
|
||||||
|
requires_electronic_signature = serializers.SerializerMethodField()
|
||||||
|
is_electronically_signed = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RegistrationSchoolFileTemplate
|
model = RegistrationSchoolFileTemplate
|
||||||
@ -72,6 +74,27 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
|
|||||||
return obj.master.file.url
|
return obj.master.file.url
|
||||||
return None
|
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):
|
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
file_url = serializers.SerializerMethodField()
|
file_url = serializers.SerializerMethodField()
|
||||||
|
|||||||
245
Back-End/Subscriptions/templates/pdfs/dynamic_form.html
Normal file
245
Back-End/Subscriptions/templates/pdfs/dynamic_form.html
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>{{ pdf_title }}</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1.4cm 1.6cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.35;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
border-left: 1px dashed #cbd5e1;
|
||||||
|
border-right: 1px dashed #cbd5e1;
|
||||||
|
padding: 0 22px 8px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading1 {
|
||||||
|
font-size: 26pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 18px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading2 {
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 14px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading3 {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading4,
|
||||||
|
.heading5,
|
||||||
|
.heading6 {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-block {
|
||||||
|
margin: 10px 0 14px 0;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #475569;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 8px 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-prefix {
|
||||||
|
width: 78px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-value {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-card {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-page {
|
||||||
|
margin: 8px 0;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-page img {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-file {
|
||||||
|
border: 2px dashed #94a3b8;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 18px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-box {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
height: 84px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-box img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 70px;
|
||||||
|
margin: 6px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-shell">
|
||||||
|
<h1 class="title">{{ pdf_title }}</h1>
|
||||||
|
|
||||||
|
{% for field in fields %}
|
||||||
|
{% if field.kind == 'heading' %}
|
||||||
|
{% if field.level == 'heading1' %}
|
||||||
|
<div class="heading1">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading2' %}
|
||||||
|
<div class="heading2">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading3' %}
|
||||||
|
<div class="heading3">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading4' %}
|
||||||
|
<div class="heading4">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading5' %}
|
||||||
|
<div class="heading5">{{ field.text }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="heading6">{{ field.text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.kind == 'paragraph' %}
|
||||||
|
<p class="paragraph">{{ field.text }}</p>
|
||||||
|
{% elif field.kind == 'file' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="file-card">
|
||||||
|
{% if field.has_preview %}
|
||||||
|
{% for preview in field.preview_pages %}
|
||||||
|
<div class="preview-page">
|
||||||
|
<img src="{{ preview.src }}" alt="{{ preview.alt }}" width="{{ preview.width }}" height="{{ preview.height }}" />
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if field.total_pages > 1 %}
|
||||||
|
<div class="preview-meta">Aperçu de la première page sur {{ field.total_pages }} pages</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-file">Aucun document source fourni</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif field.kind == 'phone' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<table class="phone-table">
|
||||||
|
<tr>
|
||||||
|
<td class="phone-prefix">{{ field.prefix }}</td>
|
||||||
|
<td class="phone-value">
|
||||||
|
{% if field.value %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% else %}
|
||||||
|
<span class="input-placeholder"> </span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% elif field.kind == 'signature' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="signature-box">
|
||||||
|
{% if field.signature_src %}
|
||||||
|
<img src="{{ field.signature_src }}" alt="Signature" />
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="input-box">
|
||||||
|
{% if field.value %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% else %}
|
||||||
|
<span class="input-placeholder"> </span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if field.field_type %}
|
||||||
|
<div class="field-meta">Type: {{ field.field_type }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1 +1 @@
|
|||||||
__version__ = "0.0.3"
|
__version__ = "1.0.0"
|
||||||
|
|||||||
70
CHANGELOG.md
70
CHANGELOG.md
@ -2,6 +2,76 @@
|
|||||||
|
|
||||||
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
### [1.0.0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.3...1.0.0) (2026-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* **design-system:** add design system documentation and AI agent instructions ([cb76a23](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb76a23d02a9c0f27c9403e4d06cebfcc65d886b))
|
||||||
|
|
||||||
|
|
||||||
|
### Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
* Gestion de l'arborescence des documents d'école en fonction des requêtes CRUD [N3WTS-17] ([abb4b52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/abb4b525b296ba01fce037c6c63fb7766cfbc9b3))
|
||||||
|
* Ajout bouton de refus de dossier avec zone de saisie de motif [N3WTS-2] ([3779a47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3779a474171d7df716e3b0d06a26f7f9b69356fa))
|
||||||
|
* Ajout d'un système d'historisation et d'export de données en CSV [N3WTS-5] ([f091fa0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f091fa0432135bb5478793ea92b97d13c3c68a2f))
|
||||||
|
* Ajout d'un système de notation par classe et par matière et par élève [N3WTS-6] ([905fa5d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/905fa5dbfb3d50710a3aa04d9b28664c1c86b991))
|
||||||
|
* Ajout de la commande npm permettant de creer un etablissement ([09b1541](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/09b1541dc83b28dcba0aead633755d58e9a534fa))
|
||||||
|
* Ajout des composants manquant dans le FormTemplateBuilder [N3WTS-17] ([5e62ee5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5e62ee510074e30cd33802aae20d3d1c297619a3))
|
||||||
|
* Ajout FormTemplateBuilder [N3WTS-17] ([e89d2fc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e89d2fc4c34a99c6b67827bb895ad31a5eff10e6))
|
||||||
|
* **backend,frontend:** régénération et visualisation inline de la fiche élève PDF ([e37aee2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e37aee2abcf7ed69145d217a30e2dce037d933d0))
|
||||||
|
* Changement du rendu de la page des documents + gestion des ([12f5fc7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/12f5fc7aa9de10fc5591495b9ede478593c1c21a))
|
||||||
|
* creation d'un FormRenderer.js pour creer un formulaire dynamique [NEWTS-17] ([9481a01](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9481a0132d2e1f18da5fc632d924adde04d316d4))
|
||||||
|
* Début de suppression de docuseal côté Front [#N3WTS-17] ([1e5bc6c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1e5bc6ccba9053246e4767aa6c2da46ff776203e)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Envoi mail d'inscription au second responsable [N3WTS-1] ([d66db1b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d66db1b019f2480a8a418acc982a6f9132ff3342))
|
||||||
|
* Envoi mail d'inscription aux enseignants [N3WTS-1] ([bd7dc2b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/bd7dc2b0c228b26e31c930342679e30aafa101e1))
|
||||||
|
* Finalisation de la validation / refus des documents signés par les parents [N3WTS-2] ([0501c1d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0501c1dd7320d734ba6ae0b93ad4a270b903a5df))
|
||||||
|
* Finalisation formulaire dynamique ([90b0d14](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/90b0d144184a1c402aedfe3f70647c4bb43f8c37))
|
||||||
|
* **frontend:** fusion liste des frais et message compte existant [#NEWTS-9] ([e30a41a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e30a41a58b199b68eca687a159736b2025e9f22d)), closes [#NEWTS-9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-9)
|
||||||
|
* **frontend:** refonte mobile planning et ameliorations suivi pedagogique [#NEWTS-4] ([4248a58](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4248a589c5f300fe0702b1c20cae5abaeba32688)), closes [#NEWTS-4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-4) [#NEWTS-4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-4)
|
||||||
|
* Gestion de l'affichage des documents validés et non validés sur la page parent [N3WTS-2] ([4f7d7d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f7d7d002442bb0cb04063734d97f993a40474a7))
|
||||||
|
* Gestion de la sidebar [N3WTS-8] ([ddcaba3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ddcaba382e6573f4a628a691bc0f20226a954fd7))
|
||||||
|
* Gestion du refus définitif d'un dossier [N3WTS-2] ([2fef6d6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2fef6d61a4d38f82c28587a9642aef65b5fd387a))
|
||||||
|
* lister uniquement les élèves inscrits dans une classe [N3WTS-6] ([6fb3c5c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6fb3c5cdb40f05b9c34b96334e785bc184751e4f))
|
||||||
|
* Mise à jour de la page parent ([2d678b7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2d678b732f1be19b4653fbe68486455326da2157))
|
||||||
|
* Page Inscriptions : suppression de la possibilité de créer un nouveau DI [N3WTS-8] ([195579e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/195579e21710427d303e10c97b4a4438ec381da4))
|
||||||
|
* Page Structure : suppression de la possibilité de faire des actions d'admin [N3WTS-8] ([05c68eb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05c68ebfaa0ff30a70ca9c42bcb306261c0fbb85))
|
||||||
|
* Précablage du formulaire dynamique [N3WTS-17] ([dd00cba](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dd00cba385a131ed7db66656738340234bcf5e73))
|
||||||
|
* push test [#N3WTS-17] ([0fb668b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0fb668b21284a682a8dd5addd14e47c580723f13)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Réorganisation items dans la page [N3WTS-17] ([8549699](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8549699dec253a9763a4074e05130c4d7d40ac6e))
|
||||||
|
* Sauvegarde des formulaires d'école dans les bons dossiers / ([b4f70e6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b4f70e6bad2fa31b6c1fd68d8538962b9d5e2650))
|
||||||
|
* Securisation du Backend ([fa84309](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fa843097ba9490c5083b652ec2dc9a9b28c096a0))
|
||||||
|
* Securisation du téléchargement de fichier ([a329126](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a3291262d80d2bddfc11412f55bc271ea6bf8219))
|
||||||
|
* Traitement de clonages des templates de documents dans le back ([7486f6c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7486f6c5ce6c5aac2b051469903ef7b2936f5634)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Validation document par document [N3WTS-2] ([8fd1b62](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8fd1b62ec09a663f40262b406c2f17f64a8d5a2f))
|
||||||
|
* WIP finalisation partie signature des parents [N3WTS-17] ([9dff32b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9dff32b388fd27a899a44e3424bb7d24b2b26afd))
|
||||||
|
|
||||||
|
|
||||||
|
### Corrections de bugs
|
||||||
|
|
||||||
|
* Boutons de navigation + mise en page de l'aperçu du formulaire dynamique ([762dede](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/762dede0af37c41b2c1616216bd9f986d1e0c57f))
|
||||||
|
* Changement des niveaux de logs [N3WTS-1] ([26d4b56](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/26d4b5633f7ebf5630105c3e9db802cdaeee4e37))
|
||||||
|
* Chat getSession + passage en asyn ces getWebSocketUrl et connectToChat ([409cf05](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/409cf05f1a4554263d5f08185bce97d9425287fa))
|
||||||
|
* coorection démarrage ([2579af9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2579af9b8b397d24e66ee08365e345f81bfb00cf))
|
||||||
|
* Coquille [N3WTS-17] ([a034149](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a034149eae7f3b746af32c392499217cf73ab582))
|
||||||
|
* correction du téléchargement du fichier ([053140c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/053140c8be1e10ac9b127cfb47b5691e70dd26e0))
|
||||||
|
* Edition d'un teacher, champ email désactivé [N3WTS-1] ([176edc5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/176edc5c452fa7b03da266f5a227c730ff4ad525))
|
||||||
|
* Emploi du temps pour les classes de l'année scolaire en cours ([5bbbcb9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5bbbcb9dc1f688cd68f47f8f7629aa03678ba582))
|
||||||
|
* Lint ([2dc0dfa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2dc0dfa26889b319f8c6cfb08b1701f8540d06ed))
|
||||||
|
* messagerie ([a81b76e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a81b76eceac0919fc1873cdf8362e5fe55b97adb))
|
||||||
|
* Mise à jour des plannings ([12939fc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/12939fca85b139a2b0178b11a4a29ee5e97800ff))
|
||||||
|
* Mise en page sur absence de frais ou de tarifs lors de la création d'un DI ([ccdbae1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ccdbae1c08e1d1edb3bc1e3e14e8244f196cac60))
|
||||||
|
* Mise en place de l'auto reload pour Daphne [[#65](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/65)] ([7f002e2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f002e2e6a93d651a8d9c2a03baa0f1035844f96))
|
||||||
|
* Ne pas envoyer de mail lors de la création d'un DI ([3c72666](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c7266608d45cd1650775c9a2060dd1007280096))
|
||||||
|
* On n'historise plus les matières ni les enseignants ([f9c0585](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f9c0585b3061a6ae4a8978c9f0329c8710f9bd60))
|
||||||
|
* Réintégration du bouton de Bilan de compétence + harmonisation des paths d'upload de fichier ([2a223fe](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2a223fe3dd9dfae71117f0f47c07aee8a8893de5))
|
||||||
|
* Revue des modales de création de groupes / formulaire ([1f2a1b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1f2a1b88acab312dc90dcf4ba4d55048a3f302d9))
|
||||||
|
* sélection enseignants dans les plannings ([4431c42](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4431c428d3709ef3b2e8f66edd13beed73e42709))
|
||||||
|
* signature électronique ([92c3183](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92c318315335d126ccd6d8637c0381a3367b7a4f))
|
||||||
|
* Suppression d'un PROFILE si aucun PROFILE_ROLE n'y est associé [N3WTS-1] ([92c6a31](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92c6a31740b930fb5363b9dbf91b664f78c7f1a6))
|
||||||
|
* Suppression envoi mail / création page feedback ([4c56cb6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c56cb647470f86151b1ff78c318e0b6468f009d))
|
||||||
|
* Upload document ([b0e04e3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b0e04e3adc480005b0d0d74bbc0ac28f1f9d6e4b))
|
||||||
|
|
||||||
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.3",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@ -34,12 +34,13 @@ import Footer from '@/components/Footer';
|
|||||||
import MobileTopbar from '@/components/MobileTopbar';
|
import MobileTopbar from '@/components/MobileTopbar';
|
||||||
import { RIGHTS } from '@/utils/rights';
|
import { RIGHTS } from '@/utils/rights';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
import { useChatConnection } from '@/context/ChatConnectionContext';
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { profileRole, establishments, clearContext } =
|
const { profileRole, establishments, clearContext } = useEstablishment();
|
||||||
useEstablishment();
|
const { totalUnreadCount, resetUnreadCount } = useChatConnection();
|
||||||
|
|
||||||
const sidebarItems = {
|
const sidebarItems = {
|
||||||
admin: {
|
admin: {
|
||||||
@ -83,6 +84,7 @@ export default function Layout({ children }) {
|
|||||||
name: t('messagerie'),
|
name: t('messagerie'),
|
||||||
url: FE_ADMIN_MESSAGERIE_URL,
|
url: FE_ADMIN_MESSAGERIE_URL,
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
|
badge: totalUnreadCount,
|
||||||
},
|
},
|
||||||
feedback: {
|
feedback: {
|
||||||
id: 'feedback',
|
id: 'feedback',
|
||||||
@ -119,7 +121,11 @@ export default function Layout({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fermer la sidebar quand on change de page sur mobile
|
// Fermer la sidebar quand on change de page sur mobile
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
}, [pathname]);
|
// Réinitialiser le compteur non lu quand on ouvre la messagerie
|
||||||
|
if (pathname?.includes('/messagerie')) {
|
||||||
|
resetUnreadCount();
|
||||||
|
}
|
||||||
|
}, [pathname, resetUnreadCount]);
|
||||||
|
|
||||||
// Filtrage dynamique des items de la sidebar selon le rôle
|
// Filtrage dynamique des items de la sidebar selon le rôle
|
||||||
let sidebarItemsToDisplay = Object.values(sidebarItems);
|
let sidebarItemsToDisplay = Object.values(sidebarItems);
|
||||||
|
|||||||
@ -4,10 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { MessageSquare, Settings, Home } from 'lucide-react';
|
import { MessageSquare, Settings, Home } from 'lucide-react';
|
||||||
import {
|
import { FE_PARENTS_HOME_URL, FE_PARENTS_MESSAGERIE_URL } from '@/utils/Url';
|
||||||
FE_PARENTS_HOME_URL,
|
|
||||||
FE_PARENTS_MESSAGERIE_URL
|
|
||||||
} from '@/utils/Url';
|
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
import { disconnect } from '@/app/actions/authAction';
|
import { disconnect } from '@/app/actions/authAction';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
@ -15,6 +12,7 @@ import MobileTopbar from '@/components/MobileTopbar';
|
|||||||
import { RIGHTS } from '@/utils/rights';
|
import { RIGHTS } from '@/utils/rights';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
|
import { useChatConnection } from '@/context/ChatConnectionContext';
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -22,6 +20,7 @@ export default function Layout({ children }) {
|
|||||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { clearContext } = useEstablishment();
|
const { clearContext } = useEstablishment();
|
||||||
|
const { totalUnreadCount, resetUnreadCount } = useChatConnection();
|
||||||
const softwareName = 'N3WT School';
|
const softwareName = 'N3WT School';
|
||||||
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
||||||
|
|
||||||
@ -41,7 +40,8 @@ export default function Layout({ children }) {
|
|||||||
name: 'Messagerie',
|
name: 'Messagerie',
|
||||||
url: FE_PARENTS_MESSAGERIE_URL,
|
url: FE_PARENTS_MESSAGERIE_URL,
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
}
|
badge: totalUnreadCount,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Déterminer la page actuelle pour la sidebar
|
// Déterminer la page actuelle pour la sidebar
|
||||||
@ -70,7 +70,11 @@ export default function Layout({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fermer la sidebar quand on change de page sur mobile
|
// Fermer la sidebar quand on change de page sur mobile
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
}, [pathname]);
|
// Réinitialiser le compteur non lu quand on ouvre la messagerie
|
||||||
|
if (pathname?.includes('/messagerie')) {
|
||||||
|
resetUnreadCount();
|
||||||
|
}
|
||||||
|
}, [pathname, resetUnreadCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
|
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
|
||||||
|
|||||||
@ -80,12 +80,7 @@ export default function Page() {
|
|||||||
return <Loader />; // Affichez le composant Loader
|
return <Loader />; // Affichez le composant Loader
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen flex items-center justify-center p-4 bg-neutral">
|
||||||
className="min-h-screen flex items-center justify-center p-4"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, #80fdd6 100%, #2bb180 100%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<Logo className="h-150 w-150" />
|
<Logo className="h-150 w-150" />
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function Footer({ softwareName, softwareVersion }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-light flex items-center justify-between">
|
<div className="text-sm font-light flex items-center justify-between">
|
||||||
<div className="text-sm font-light mr-4">
|
<div className="text-sm font-light mr-4">
|
||||||
{softwareName} - {softwareVersion}
|
{softwareName} - {softwareVersion}
|
||||||
</div>
|
</div>
|
||||||
<Logo className="w-8 h-8" />
|
<Logo className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import FormRenderer from '@/components/Form/FormRenderer';
|
import FormRenderer from '@/components/Form/FormRenderer';
|
||||||
import FileUpload from '@/components/Form/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
|
import SignatureField from '@/components/Form/SignatureField';
|
||||||
|
import Button from '@/components/Form/Button';
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Hourglass,
|
Hourglass,
|
||||||
@ -9,6 +11,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
PenTool,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
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 {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
|
||||||
* @param {Boolean} enable - Si les formulaires sont modifiables
|
* @param {Boolean} enable - Si les formulaires sont modifiables
|
||||||
* @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné
|
* @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({
|
export default function DynamicFormsList({
|
||||||
schoolFileTemplates,
|
schoolFileTemplates,
|
||||||
@ -27,7 +31,8 @@ export default function DynamicFormsList({
|
|||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
enable = true,
|
enable = true,
|
||||||
onValidationChange,
|
onValidationChange,
|
||||||
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
|
onFileUpload,
|
||||||
|
onSignatureSubmit,
|
||||||
}) {
|
}) {
|
||||||
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
||||||
const [formsData, setFormsData] = useState({});
|
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
|
// vérifier si un fichier a déjà été uploadé sur le template
|
||||||
const template = schoolFileTemplates.find((tpl) => tpl.id === templateId);
|
const template = schoolFileTemplates.find((tpl) => tpl.id === templateId);
|
||||||
if (template && template.file && !isDynamicForm(template)) {
|
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;
|
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) {
|
if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
@ -325,7 +367,8 @@ export default function DynamicFormsList({
|
|||||||
? 'text-secondary font-semibold'
|
? 'text-secondary font-semibold'
|
||||||
: textClass;
|
: textClass;
|
||||||
canEdit = false;
|
canEdit = false;
|
||||||
} else if (isValidated === false) {
|
} else if (isValidated === false && !isCompletedLocally) {
|
||||||
|
// Refusé uniquement si pas re-complété localement
|
||||||
statusLabel = 'Refusé';
|
statusLabel = 'Refusé';
|
||||||
statusColor = 'red';
|
statusColor = 'red';
|
||||||
icon = <Hourglass className="w-5 h-5 text-red-500" />;
|
icon = <Hourglass className="w-5 h-5 text-red-500" />;
|
||||||
@ -337,30 +380,28 @@ export default function DynamicFormsList({
|
|||||||
? 'text-red-900 font-semibold'
|
? 'text-red-900 font-semibold'
|
||||||
: 'text-red-700';
|
: 'text-red-700';
|
||||||
canEdit = true;
|
canEdit = true;
|
||||||
|
} else if (isCompletedLocally) {
|
||||||
|
statusLabel = 'Complété';
|
||||||
|
statusColor = 'orange';
|
||||||
|
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
|
||||||
|
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 {
|
} else {
|
||||||
if (isCompletedLocally) {
|
statusLabel = 'À compléter';
|
||||||
statusLabel = 'Complété';
|
statusColor = 'gray';
|
||||||
statusColor = 'orange';
|
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
|
||||||
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
|
bgClass = isActive ? 'bg-gray-200' : '';
|
||||||
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
|
borderClass = isActive ? 'border border-gray-300' : '';
|
||||||
borderClass = isActive
|
textClass = isActive
|
||||||
? 'border border-orange-300'
|
? 'text-gray-900 font-semibold'
|
||||||
: 'border border-orange-200';
|
: 'text-gray-600';
|
||||||
textClass = isActive
|
canEdit = true;
|
||||||
? 'text-orange-900 font-semibold'
|
|
||||||
: 'text-orange-700';
|
|
||||||
canEdit = true;
|
|
||||||
} else {
|
|
||||||
statusLabel = 'À compléter';
|
|
||||||
statusColor = 'gray';
|
|
||||||
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
|
|
||||||
bgClass = isActive ? 'bg-gray-200' : '';
|
|
||||||
borderClass = isActive ? 'border border-gray-300' : '';
|
|
||||||
textClass = isActive
|
|
||||||
? 'text-gray-900 font-semibold'
|
|
||||||
: 'text-gray-600';
|
|
||||||
canEdit = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -417,14 +458,14 @@ export default function DynamicFormsList({
|
|||||||
<span className="px-2 py-0.5 rounded bg-primary/10 text-secondary text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-primary/10 text-secondary text-sm font-semibold">
|
||||||
Validé
|
Validé
|
||||||
</span>
|
</span>
|
||||||
) : currentTemplate.isValidated === false ? (
|
|
||||||
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
|
||||||
Refusé
|
|
||||||
</span>
|
|
||||||
) : hasLocalCompletion(currentTemplate.id) ? (
|
) : hasLocalCompletion(currentTemplate.id) ? (
|
||||||
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">
|
||||||
Complété
|
Complété
|
||||||
</span>
|
</span>
|
||||||
|
) : currentTemplate.isValidated === false ? (
|
||||||
|
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
||||||
|
Refusé
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-700 text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-700 text-sm font-semibold">
|
||||||
En attente
|
En attente
|
||||||
@ -507,39 +548,137 @@ export default function DynamicFormsList({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cas non validé : bouton télécharger + upload */}
|
{/* Cas non validé */}
|
||||||
{currentTemplate.isValidated !== true && (
|
{currentTemplate.isValidated !== true && (
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
{/* Bouton télécharger le document source (fichier maître) */}
|
{/* Document à signer électroniquement */}
|
||||||
{(currentTemplate.master_file_url ||
|
{currentTemplate?.requires_electronic_signature ? (
|
||||||
currentTemplate.file) && (
|
<>
|
||||||
<a
|
{/* Affichage du document à signer */}
|
||||||
href={getSecureFileUrl(
|
{(currentTemplate.master_file_url ||
|
||||||
currentTemplate.master_file_url ||
|
currentTemplate.file) && (
|
||||||
currentTemplate.file
|
<iframe
|
||||||
|
src={getSecureFileUrl(
|
||||||
|
currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file
|
||||||
|
)}
|
||||||
|
title={currentTemplate.name}
|
||||||
|
className="w-full border rounded"
|
||||||
|
style={{ height: '500px', border: 'none' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
|
||||||
download
|
|
||||||
>
|
|
||||||
<Download className="w-5 h-5" />
|
|
||||||
Télécharger le document
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Composant d'upload */}
|
{/* Afficher si déjà signé et non refusé */}
|
||||||
{enable && (
|
{currentTemplate.is_electronically_signed &&
|
||||||
<FileUpload
|
currentTemplate.isValidated !== false ? (
|
||||||
key={currentTemplate.id}
|
<div className="flex items-center gap-2 p-4 bg-green-50 border border-green-200 rounded-lg w-full max-w-md">
|
||||||
selectionMessage={'Sélectionnez le fichier du document'}
|
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||||
onFileSelect={(file) =>
|
<div>
|
||||||
handleUpload(file, currentTemplate)
|
<p className="font-medium text-green-800">
|
||||||
}
|
Document signé électroniquement
|
||||||
existingFile={
|
</p>
|
||||||
currentTemplate.file_url || currentTemplate.file
|
{currentTemplate.electronic_signature_date && (
|
||||||
}
|
<p className="text-sm text-green-600">
|
||||||
required
|
Signé le{' '}
|
||||||
enable={true}
|
{new Date(
|
||||||
/>
|
currentTemplate.electronic_signature_date
|
||||||
|
).toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Signature électronique si pas encore signé ou refusé */
|
||||||
|
enable && (
|
||||||
|
<div className="w-full max-w-md mt-4">
|
||||||
|
{/* Message si document refusé */}
|
||||||
|
{currentTemplate.isValidated === false && (
|
||||||
|
<div className="flex items-center gap-2 p-3 mb-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<XCircle className="w-5 h-5 text-red-500" />
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
Ce document a été refusé. Veuillez le
|
||||||
|
signer à nouveau.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SignatureField
|
||||||
|
label="Signature électronique"
|
||||||
|
required={true}
|
||||||
|
value={
|
||||||
|
formsData[currentTemplate.id]?.signature || ''
|
||||||
|
}
|
||||||
|
onChange={(signatureData) => {
|
||||||
|
setFormsData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[currentTemplate.id]: {
|
||||||
|
...(prev[currentTemplate.id] || {}),
|
||||||
|
signature: signatureData,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
text="Signer le document"
|
||||||
|
onClick={() =>
|
||||||
|
handleSignature(
|
||||||
|
currentTemplate.id,
|
||||||
|
formsData[currentTemplate.id]?.signature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!formsData[currentTemplate.id]?.signature
|
||||||
|
}
|
||||||
|
className="mt-4 w-full"
|
||||||
|
icon={<PenTool className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Document classique : télécharger + upload */}
|
||||||
|
{(currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file) && (
|
||||||
|
<a
|
||||||
|
href={getSecureFileUrl(
|
||||||
|
currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file
|
||||||
|
)}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
Télécharger le document
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Composant d'upload */}
|
||||||
|
{enable && (
|
||||||
|
<FileUpload
|
||||||
|
key={currentTemplate.id}
|
||||||
|
selectionMessage={
|
||||||
|
'Sélectionnez le fichier du document'
|
||||||
|
}
|
||||||
|
onFileSelect={(file) =>
|
||||||
|
handleUpload(file, currentTemplate)
|
||||||
|
}
|
||||||
|
existingFile={
|
||||||
|
currentTemplate.file_url || currentTemplate.file
|
||||||
|
}
|
||||||
|
required
|
||||||
|
enable={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -639,6 +639,50 @@ export default function InscriptionFormShared({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSignatureSubmit = (templateId, signatureData) => {
|
||||||
|
if (!templateId || !signatureData) {
|
||||||
|
logger.error('Données manquantes pour la signature.');
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('Données manquantes pour la signature.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = new FormData();
|
||||||
|
updateData.append('electronic_signature', signatureData);
|
||||||
|
|
||||||
|
return editRegistrationSchoolFileTemplates(templateId, updateData, csrfToken)
|
||||||
|
.then((response) => {
|
||||||
|
logger.debug('Signature électronique enregistrée avec succès :', response);
|
||||||
|
|
||||||
|
// Mettre à jour schoolFileTemplates avec la signature
|
||||||
|
setSchoolFileTemplates((prevTemplates) =>
|
||||||
|
prevTemplates.map((template) =>
|
||||||
|
template.id === templateId
|
||||||
|
? {
|
||||||
|
...template,
|
||||||
|
electronic_signature: response.data.electronic_signature,
|
||||||
|
electronic_signature_date: response.data.electronic_signature_date,
|
||||||
|
is_electronically_signed: true,
|
||||||
|
// Repasser en attente si le document était refusé
|
||||||
|
isValidated:
|
||||||
|
response.data.isValidated !== undefined
|
||||||
|
? response.data.isValidated
|
||||||
|
: template.isValidated === false
|
||||||
|
? null
|
||||||
|
: template.isValidated,
|
||||||
|
}
|
||||||
|
: template
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Erreur lors de l\'enregistrement de la signature :', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteFile = (templateId) => {
|
const handleDeleteFile = (templateId) => {
|
||||||
const fileToDelete = uploadedFiles.find(
|
const fileToDelete = uploadedFiles.find(
|
||||||
(file) => parseInt(file.id) === templateId && file.fileName
|
(file) => parseInt(file.id) === templateId && file.fileName
|
||||||
@ -970,6 +1014,7 @@ export default function InscriptionFormShared({
|
|||||||
onValidationChange={handleDynamicFormsValidationChange}
|
onValidationChange={handleDynamicFormsValidationChange}
|
||||||
enable={enable}
|
enable={enable}
|
||||||
onFileUpload={handleSchoolFileUpload}
|
onFileUpload={handleSchoolFileUpload}
|
||||||
|
onSignatureSubmit={handleSignatureSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { School, FileText } from 'lucide-react';
|
import { School, FileText, PenTool } from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
@ -161,6 +161,10 @@ export default function ValidateSubscription({
|
|||||||
name: template.name || 'Document scolaire',
|
name: template.name || 'Document scolaire',
|
||||||
file: template.file,
|
file: template.file,
|
||||||
description: template.description,
|
description: template.description,
|
||||||
|
is_electronically_signed: template.is_electronically_signed,
|
||||||
|
electronic_signature: template.electronic_signature,
|
||||||
|
electronic_signature_date: template.electronic_signature_date,
|
||||||
|
requires_electronic_signature: template.requires_electronic_signature,
|
||||||
})),
|
})),
|
||||||
...parentFileTemplates.map((template) => ({
|
...parentFileTemplates.map((template) => ({
|
||||||
name: template.master_name || 'Document parent',
|
name: template.master_name || 'Document parent',
|
||||||
@ -213,9 +217,19 @@ export default function ValidateSubscription({
|
|||||||
<div className="w-3/4">
|
<div className="w-3/4">
|
||||||
{currentTemplateIndex < allTemplates.length && (
|
{currentTemplateIndex < allTemplates.length && (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
{allTemplates[currentTemplateIndex].name || 'Document sans nom'}
|
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
</h3>
|
{allTemplates[currentTemplateIndex].name ||
|
||||||
|
'Document sans nom'}
|
||||||
|
</h3>
|
||||||
|
{/* Badge signature électronique */}
|
||||||
|
{allTemplates[currentTemplateIndex].is_electronically_signed && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-blue-100 text-blue-700 text-sm font-medium">
|
||||||
|
<PenTool className="w-4 h-4" />
|
||||||
|
Signé électroniquement
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
src={
|
src={
|
||||||
allTemplates[currentTemplateIndex].type === 'main'
|
allTemplates[currentTemplateIndex].type === 'main'
|
||||||
@ -229,10 +243,49 @@ export default function ValidateSubscription({
|
|||||||
}
|
}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{
|
style={{
|
||||||
height: '75vh',
|
height: allTemplates[currentTemplateIndex]
|
||||||
|
.is_electronically_signed
|
||||||
|
? '60vh'
|
||||||
|
: '75vh',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Affichage de la signature électronique */}
|
||||||
|
{allTemplates[currentTemplateIndex].is_electronically_signed &&
|
||||||
|
allTemplates[currentTemplateIndex].electronic_signature && (
|
||||||
|
<div className="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<PenTool className="w-5 h-5 text-blue-600" />
|
||||||
|
<span className="font-medium text-gray-800">
|
||||||
|
Signature électronique
|
||||||
|
</span>
|
||||||
|
{allTemplates[currentTemplateIndex]
|
||||||
|
.electronic_signature_date && (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
- Signé le{' '}
|
||||||
|
{new Date(
|
||||||
|
allTemplates[
|
||||||
|
currentTemplateIndex
|
||||||
|
].electronic_signature_date
|
||||||
|
).toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
allTemplates[currentTemplateIndex].electronic_signature
|
||||||
|
}
|
||||||
|
alt="Signature électronique"
|
||||||
|
className="max-w-xs h-auto border border-gray-300 rounded bg-white p-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -262,7 +315,27 @@ export default function ValidateSubscription({
|
|||||||
<FileText className="w-5 h-5 text-green-600" />
|
<FileText className="w-5 h-5 text-green-600" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1">{template.name}</span>
|
<span className="flex-1 flex items-center gap-2">
|
||||||
|
{template.name}
|
||||||
|
{/* Badge signature électronique */}
|
||||||
|
{template.is_electronically_signed && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 text-xs font-medium"
|
||||||
|
title={`Signé le ${template.electronic_signature_date ? new Date(template.electronic_signature_date).toLocaleDateString('fr-FR') : ''}`}
|
||||||
|
>
|
||||||
|
<PenTool className="w-3 h-3" />
|
||||||
|
Signé
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* Alerte si signature requise mais pas signée */}
|
||||||
|
{template.requires_electronic_signature &&
|
||||||
|
!template.is_electronically_signed && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-medium">
|
||||||
|
<PenTool className="w-3 h-3" />
|
||||||
|
À signer
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
{/* 2 boutons : Validé / Refusé (sauf fiche élève) */}
|
{/* 2 boutons : Validé / Refusé (sauf fiche élève) */}
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<span className="ml-2 flex gap-1">
|
<span className="ml-2 flex gap-1">
|
||||||
|
|||||||
@ -3,14 +3,21 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import ProfileSelector from '@/components/ProfileSelector';
|
import ProfileSelector from '@/components/ProfileSelector';
|
||||||
|
|
||||||
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
|
const SidebarItem = ({ icon: Icon, text, active, url, onClick, badge }) => (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer hover:bg-primary/10 ${
|
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer hover:bg-primary/10 ${
|
||||||
active ? 'bg-primary/5 text-primary' : 'text-gray-600'
|
active ? 'bg-primary/5 text-primary' : 'text-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<div className="relative shrink-0">
|
||||||
|
<Icon size={20} />
|
||||||
|
{badge > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-0.5 bg-red-500 text-white text-[10px] font-label font-semibold rounded-full flex items-center justify-center leading-none">
|
||||||
|
{badge > 99 ? '99+' : badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -45,6 +52,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) {
|
|||||||
text={item.name}
|
text={item.name}
|
||||||
active={item.id === selectedItem}
|
active={item.id === selectedItem}
|
||||||
url={item.url}
|
url={item.url}
|
||||||
|
badge={item.badge}
|
||||||
onClick={() => handleItemClick(item.url)}
|
onClick={() => handleItemClick(item.url)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -348,6 +348,7 @@ export default function FilesGroupsManagement({
|
|||||||
group_ids,
|
group_ids,
|
||||||
formMasterData,
|
formMasterData,
|
||||||
file,
|
file,
|
||||||
|
requires_electronic_signature,
|
||||||
},
|
},
|
||||||
onCreated
|
onCreated
|
||||||
) => {
|
) => {
|
||||||
@ -358,6 +359,7 @@ export default function FilesGroupsManagement({
|
|||||||
groups: group_ids,
|
groups: group_ids,
|
||||||
formMasterData,
|
formMasterData,
|
||||||
establishment: selectedEstablishmentId,
|
establishment: selectedEstablishmentId,
|
||||||
|
requires_electronic_signature: requires_electronic_signature || false,
|
||||||
};
|
};
|
||||||
dataToSend.append('data', JSON.stringify(jsonData));
|
dataToSend.append('data', JSON.stringify(jsonData));
|
||||||
if (file) {
|
if (file) {
|
||||||
@ -402,6 +404,7 @@ export default function FilesGroupsManagement({
|
|||||||
formMasterData,
|
formMasterData,
|
||||||
id,
|
id,
|
||||||
file,
|
file,
|
||||||
|
requires_electronic_signature,
|
||||||
}) => {
|
}) => {
|
||||||
// Correction : normaliser group_ids pour ne garder que les IDs (number/string)
|
// Correction : normaliser group_ids pour ne garder que les IDs (number/string)
|
||||||
let normalizedGroupIds = [];
|
let normalizedGroupIds = [];
|
||||||
@ -417,6 +420,7 @@ export default function FilesGroupsManagement({
|
|||||||
groups: normalizedGroupIds,
|
groups: normalizedGroupIds,
|
||||||
formMasterData: formMasterData,
|
formMasterData: formMasterData,
|
||||||
establishment: selectedEstablishmentId,
|
establishment: selectedEstablishmentId,
|
||||||
|
requires_electronic_signature: requires_electronic_signature || false,
|
||||||
};
|
};
|
||||||
dataToSend.append('data', JSON.stringify(jsonData));
|
dataToSend.append('data', JSON.stringify(jsonData));
|
||||||
|
|
||||||
@ -803,18 +807,12 @@ export default function FilesGroupsManagement({
|
|||||||
à droite de la liste des documents pour ajouter :
|
à droite de la liste des documents pour ajouter :
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside ml-6">
|
<ul className="list-disc list-inside ml-6">
|
||||||
<li>
|
|
||||||
<span className="text-yellow-700 font-semibold">
|
|
||||||
Formulaire personnalisé
|
|
||||||
</span>{' '}
|
|
||||||
: créé dynamiquement par l'école, à remplir et/ou signer
|
|
||||||
électroniquement par la famille.
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<span className="text-black font-semibold">
|
<span className="text-black font-semibold">
|
||||||
Formulaire existant
|
Formulaire existant
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
: importez un PDF ou autre document à faire remplir.
|
: importez un PDF ou autre document à faire remplir. Vous pouvez
|
||||||
|
activer la signature électronique.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="text-orange-700 font-semibold">
|
<span className="text-orange-700 font-semibold">
|
||||||
@ -962,16 +960,6 @@ export default function FilesGroupsManagement({
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
items={[
|
items={[
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
label: (
|
|
||||||
<span className="flex items-center">
|
|
||||||
<Star className="w-5 h-5 mr-2 text-yellow-600" />
|
|
||||||
Formulaire personnalisé
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
onClick: () => handleDocDropdownSelect('formulaire'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: (
|
label: (
|
||||||
@ -1117,12 +1105,16 @@ export default function FilesGroupsManagement({
|
|||||||
group_ids: fileToEdit.groups,
|
group_ids: fileToEdit.groups,
|
||||||
file: fileToEdit.file,
|
file: fileToEdit.file,
|
||||||
formMasterData: fileToEdit.formMasterData,
|
formMasterData: fileToEdit.formMasterData,
|
||||||
|
requires_electronic_signature:
|
||||||
|
fileToEdit.requires_electronic_signature || false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleCreateSchoolFileMaster({
|
handleCreateSchoolFileMaster({
|
||||||
name: fileToEdit.name,
|
name: fileToEdit.name,
|
||||||
group_ids: fileToEdit.groups,
|
group_ids: fileToEdit.groups,
|
||||||
file: fileToEdit.file,
|
file: fileToEdit.file,
|
||||||
|
requires_electronic_signature:
|
||||||
|
fileToEdit.requires_electronic_signature || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsFileUploadPopupOpen(false);
|
setIsFileUploadPopupOpen(false);
|
||||||
@ -1199,6 +1191,22 @@ export default function FilesGroupsManagement({
|
|||||||
required
|
required
|
||||||
enable
|
enable
|
||||||
/>
|
/>
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: 'signature' }}
|
||||||
|
formData={{
|
||||||
|
requires_electronic_signature:
|
||||||
|
fileToEdit?.requires_electronic_signature || false,
|
||||||
|
}}
|
||||||
|
handleChange={() =>
|
||||||
|
setFileToEdit({
|
||||||
|
...fileToEdit,
|
||||||
|
requires_electronic_signature:
|
||||||
|
!fileToEdit?.requires_electronic_signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fieldName="requires_electronic_signature"
|
||||||
|
itemLabelFunc={() => 'À signer électroniquement'}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -1224,13 +1232,13 @@ export default function FilesGroupsManagement({
|
|||||||
!fileToEdit?.file
|
!fileToEdit?.file
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
handleCreateSchoolFileMaster(
|
handleCreateSchoolFileMaster({
|
||||||
{
|
name: fileToEdit.name,
|
||||||
name: fileToEdit.name,
|
group_ids: fileToEdit.groups,
|
||||||
group_ids: fileToEdit.groups,
|
file: fileToEdit.file,
|
||||||
file: fileToEdit.file,
|
requires_electronic_signature:
|
||||||
}
|
fileToEdit.requires_electronic_signature || false,
|
||||||
);
|
});
|
||||||
setIsFileUploadPopupOpen(false);
|
setIsFileUploadPopupOpen(false);
|
||||||
setFileToEdit(null);
|
setFileToEdit(null);
|
||||||
}}
|
}}
|
||||||
@ -1294,6 +1302,22 @@ export default function FilesGroupsManagement({
|
|||||||
required
|
required
|
||||||
enable
|
enable
|
||||||
/>
|
/>
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: 'signature' }}
|
||||||
|
formData={{
|
||||||
|
requires_electronic_signature:
|
||||||
|
fileToEdit?.requires_electronic_signature || false,
|
||||||
|
}}
|
||||||
|
handleChange={() =>
|
||||||
|
setFileToEdit({
|
||||||
|
...fileToEdit,
|
||||||
|
requires_electronic_signature:
|
||||||
|
!fileToEdit?.requires_electronic_signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fieldName="requires_electronic_signature"
|
||||||
|
itemLabelFunc={() => 'À signer électroniquement'}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@ -6,9 +6,12 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSession, getSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { WS_CHAT_URL } from '@/utils/Url';
|
import {
|
||||||
|
WS_CHAT_URL,
|
||||||
|
BE_GESTIONMESSAGERIE_CONVERSATIONS_URL,
|
||||||
|
} from '@/utils/Url';
|
||||||
|
|
||||||
const ChatConnectionContext = createContext();
|
const ChatConnectionContext = createContext();
|
||||||
|
|
||||||
@ -22,6 +25,8 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
const [reconnectAttempts, setReconnectAttempts] = useState(0);
|
const [reconnectAttempts, setReconnectAttempts] = useState(0);
|
||||||
const [currentUserId, setCurrentUserId] = useState(null);
|
const [currentUserId, setCurrentUserId] = useState(null);
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
|
const isConnectedRef = useRef(false);
|
||||||
|
const [totalUnreadCount, setTotalUnreadCount] = useState(0);
|
||||||
|
|
||||||
// Système de callbacks pour les messages
|
// Système de callbacks pour les messages
|
||||||
const messageCallbacksRef = useRef(new Set());
|
const messageCallbacksRef = useRef(new Set());
|
||||||
@ -54,15 +59,13 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Configuration WebSocket
|
// Configuration WebSocket
|
||||||
const getWebSocketUrl = async (userId) => {
|
const getWebSocketUrl = (userId) => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
logger.warn('ChatConnection: No user ID provided for WebSocket URL');
|
logger.warn('ChatConnection: No user ID provided for WebSocket URL');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forcer un refresh de session pour obtenir un token JWT valide
|
const token = session?.user?.token;
|
||||||
const freshSession = await getSession();
|
|
||||||
const token = freshSession?.user?.token;
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@ -71,11 +74,8 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construire l'URL WebSocket avec le token
|
|
||||||
const baseUrl = WS_CHAT_URL(userId);
|
const baseUrl = WS_CHAT_URL(userId);
|
||||||
const wsUrl = `${baseUrl}?token=${encodeURIComponent(token)}`;
|
return `${baseUrl}?token=${encodeURIComponent(token)}`;
|
||||||
|
|
||||||
return wsUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connexion WebSocket
|
// Connexion WebSocket
|
||||||
@ -108,7 +108,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = await getWebSocketUrl(userIdToUse);
|
const wsUrl = getWebSocketUrl(userIdToUse);
|
||||||
|
|
||||||
if (!wsUrl) {
|
if (!wsUrl) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -123,12 +123,15 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
'ChatConnection: Connected successfully for user:',
|
'ChatConnection: Connected successfully for user:',
|
||||||
userIdToUse
|
userIdToUse
|
||||||
);
|
);
|
||||||
|
isConnectedRef.current = true;
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setConnectionStatus('connected');
|
setConnectionStatus('connected');
|
||||||
setReconnectAttempts(0);
|
setReconnectAttempts(0);
|
||||||
|
fetchInitialUnreadCount();
|
||||||
};
|
};
|
||||||
|
|
||||||
websocketRef.current.onclose = (event) => {
|
websocketRef.current.onclose = (event) => {
|
||||||
|
isConnectedRef.current = false;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
|
|
||||||
@ -149,6 +152,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
websocketRef.current.onerror = (error) => {
|
websocketRef.current.onerror = (error) => {
|
||||||
logger.error('ChatConnection: WebSocket error', error);
|
logger.error('ChatConnection: WebSocket error', error);
|
||||||
setConnectionStatus('error');
|
setConnectionStatus('error');
|
||||||
|
isConnectedRef.current = false;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,6 +165,17 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
handlePresenceUpdate(data);
|
handlePresenceUpdate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incrémenter le compteur de messages non lus si c'est un message entrant
|
||||||
|
if (data.type === 'new_message' && data.message) {
|
||||||
|
const senderId = String(
|
||||||
|
data.message.sender?.id ?? data.message.sender_id ?? ''
|
||||||
|
);
|
||||||
|
const currentId = String(session?.user?.user_id ?? '');
|
||||||
|
if (senderId && currentId && senderId !== currentId) {
|
||||||
|
setTotalUnreadCount((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notifier tous les callbacks enregistrés
|
// Notifier tous les callbacks enregistrés
|
||||||
notifyMessageCallbacks(data);
|
notifyMessageCallbacks(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -170,6 +185,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('ChatConnection: Error creating WebSocket', error);
|
logger.error('ChatConnection: Error creating WebSocket', error);
|
||||||
setConnectionStatus('error');
|
setConnectionStatus('error');
|
||||||
|
isConnectedRef.current = false;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -186,6 +202,7 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
websocketRef.current = null;
|
websocketRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isConnectedRef.current = false;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
setReconnectAttempts(0);
|
setReconnectAttempts(0);
|
||||||
@ -207,24 +224,51 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
// Obtenir la référence WebSocket pour les composants qui en ont besoin
|
// Obtenir la référence WebSocket pour les composants qui en ont besoin
|
||||||
const getWebSocket = () => websocketRef.current;
|
const getWebSocket = () => websocketRef.current;
|
||||||
|
|
||||||
|
// Réinitialiser le compteur de messages non lus (ex: quand on ouvre la messagerie)
|
||||||
|
const resetUnreadCount = useCallback(() => {
|
||||||
|
setTotalUnreadCount(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Récupérer le total initial des messages non lus depuis l'API
|
||||||
|
const fetchInitialUnreadCount = useCallback(async () => {
|
||||||
|
const token = session?.user?.token;
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(BE_GESTIONMESSAGERIE_CONVERSATIONS_URL, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = await resp.json();
|
||||||
|
const conversations = Array.isArray(data) ? data : (data.results ?? []);
|
||||||
|
const total = conversations.reduce(
|
||||||
|
(sum, conv) => sum + (conv.unread_count || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
setTotalUnreadCount(total);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
'ChatConnection: impossible de récupérer le compteur non lu initial',
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [session?.user?.token]);
|
||||||
|
|
||||||
// Effet pour la gestion de la session et connexion automatique
|
// Effet pour la gestion de la session et connexion automatique
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Si la session change vers authenticated et qu'on a un user_id, essayer de se connecter
|
if (
|
||||||
if (status === 'authenticated' && session?.user?.user_id && !isConnected) {
|
status === 'authenticated' &&
|
||||||
|
session?.user?.user_id &&
|
||||||
|
!isConnectedRef.current
|
||||||
|
) {
|
||||||
connectToChat(session.user.user_id);
|
connectToChat(session.user.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si la session devient unauthenticated, déconnecter
|
if (status === 'unauthenticated' && isConnectedRef.current) {
|
||||||
if (status === 'unauthenticated' && isConnected) {
|
|
||||||
disconnectFromChat();
|
disconnectFromChat();
|
||||||
}
|
}
|
||||||
}, [
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
status,
|
}, [status, session?.user?.user_id]);
|
||||||
session?.user?.user_id,
|
|
||||||
isConnected,
|
|
||||||
connectToChat,
|
|
||||||
disconnectFromChat,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Nettoyage à la destruction du composant
|
// Nettoyage à la destruction du composant
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -241,14 +285,16 @@ export const ChatConnectionProvider = ({ children }) => {
|
|||||||
const value = {
|
const value = {
|
||||||
isConnected,
|
isConnected,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
userPresences, // Ajouter les présences utilisateur
|
userPresences,
|
||||||
connectToChat,
|
connectToChat,
|
||||||
disconnectFromChat,
|
disconnectFromChat,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getWebSocket,
|
getWebSocket,
|
||||||
reconnectAttempts,
|
reconnectAttempts,
|
||||||
maxReconnectAttempts,
|
maxReconnectAttempts,
|
||||||
addMessageCallback, // Ajouter cette fonction
|
addMessageCallback,
|
||||||
|
totalUnreadCount,
|
||||||
|
resetUnreadCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { logger } from '@/utils/logger';
|
|
||||||
import { getToken } from 'next-auth/jwt';
|
import { getToken } from 'next-auth/jwt';
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL;
|
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
@ -49,7 +48,6 @@ export default async function handler(req, res) {
|
|||||||
const buffer = Buffer.from(await backendRes.arrayBuffer());
|
const buffer = Buffer.from(await backendRes.arrayBuffer());
|
||||||
return res.send(buffer);
|
return res.send(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Download proxy error:', error);
|
|
||||||
return res.status(500).json({ error: 'Erreur lors du téléchargement' });
|
return res.status(500).json({ error: 'Erreur lors du téléchargement' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
JenkinsFile
85
JenkinsFile
@ -1,85 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent any
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DOCKER_REGISTRY = 'git.v0id.ovh'
|
|
||||||
ORGANIZATION = "n3wt-innov"
|
|
||||||
APP_NAME = 'n3wt-school'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Déclencher uniquement sur les tags
|
|
||||||
triggers {
|
|
||||||
issueCommentTrigger('.*deploy.*')
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage('Vérification du Tag') {
|
|
||||||
when {
|
|
||||||
expression { env.TAG_NAME != null }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
// Extraire la version du tag
|
|
||||||
env.VERSION = env.TAG_NAME
|
|
||||||
echo "Version détectée: ${env.VERSION}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Build Docker Images') {
|
|
||||||
when {
|
|
||||||
expression { env.TAG_NAME != null }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
// Donner les permissions d'exécution au script
|
|
||||||
sh 'chmod +x ./ci-scripts/makeDocker.sh'
|
|
||||||
|
|
||||||
// Exécuter le script avec la version
|
|
||||||
sh """
|
|
||||||
./ci-scripts/makeDocker.sh ${env.VERSION}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Push sur Registry') {
|
|
||||||
when {
|
|
||||||
expression { env.TAG_NAME != null }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
withCredentials([usernamePassword(
|
|
||||||
credentialsId: 'docker-registry-credentials',
|
|
||||||
usernameVariable: 'REGISTRY_USER',
|
|
||||||
passwordVariable: 'REGISTRY_PASS'
|
|
||||||
)]) {
|
|
||||||
// Login au registry
|
|
||||||
sh "docker login ${DOCKER_REGISTRY} -u ${REGISTRY_USER} -p ${REGISTRY_PASS}"
|
|
||||||
|
|
||||||
// Push des images
|
|
||||||
sh """
|
|
||||||
docker push ${DOCKER_REGISTRY}/${ORGANIZATION}/${APP_NAME}/frontend:${env.VERSION}
|
|
||||||
docker push ${DOCKER_REGISTRY}/${ORGANIZATION}/${APP_NAME}/backend:${env.VERSION}
|
|
||||||
docker push ${DOCKER_REGISTRY}/${ORGANIZATION}/${APP_NAME}/frontend:latest
|
|
||||||
docker push ${DOCKER_REGISTRY}/${ORGANIZATION}/${APP_NAME}/backend:latest
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
success {
|
|
||||||
echo "Build et push des images Docker réussis pour la version ${env.VERSION}"
|
|
||||||
}
|
|
||||||
failure {
|
|
||||||
echo "Échec du build ou du push des images Docker"
|
|
||||||
}
|
|
||||||
always {
|
|
||||||
// Nettoyage
|
|
||||||
sh 'docker system prune -f'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
README.md
27
README.md
@ -24,7 +24,7 @@ Maquette figma : https://www.figma.com/design/1BtWHIQlJDTeue2oYblefV/Maquette-Lo
|
|||||||
|
|
||||||
Lien de téléchargement : https://www.docker.com/get-started/
|
Lien de téléchargement : https://www.docker.com/get-started/
|
||||||
|
|
||||||
# Lancement de monteschool
|
# Lancement du projet
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
@ -36,7 +36,7 @@ Lancement du front end
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
se connecter à localhost:8080
|
- se connecter à localhost:8080 pour le backend localhost:3000 pour le front
|
||||||
|
|
||||||
# Installation et développement en local
|
# Installation et développement en local
|
||||||
|
|
||||||
@ -57,25 +57,6 @@ npm i
|
|||||||
npm run format
|
npm run format
|
||||||
```
|
```
|
||||||
|
|
||||||
# Faire une livraison Mise en Production
|
# Mise en Production, Préparation de la release
|
||||||
|
|
||||||
```sh
|
- [MO_PREPARATION_MISE_EN_PROD](./docs/MEP/MO_PRE_MEP.md)
|
||||||
# Faire la première release (1.0.0)
|
|
||||||
npm run release -- --first-release
|
|
||||||
|
|
||||||
# Faire une prerelease (RC,alpha,beta)
|
|
||||||
npm run release -- --prerelease <name>
|
|
||||||
|
|
||||||
|
|
||||||
# Faire une release
|
|
||||||
npm run release
|
|
||||||
|
|
||||||
# Forcer la release sur un mode particulier (majeur, mineur ou patch)
|
|
||||||
# npm run script
|
|
||||||
npm run release -- --release-as minor
|
|
||||||
# Or
|
|
||||||
npm run release -- --release-as 1.1.0
|
|
||||||
|
|
||||||
# ignorer les hooks de commit lors de la release
|
|
||||||
npm run release -- --no-verify
|
|
||||||
```
|
|
||||||
|
|||||||
90
ci/build.Jenkinsfile
Normal file
90
ci/build.Jenkinsfile
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
pipeline {
|
||||||
|
|
||||||
|
agent {
|
||||||
|
label "SLAVE-N3WT"
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
timestamps()
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DOCKER_REGISTRY = "git.v0id.ovh"
|
||||||
|
ORG_NAME = "n3wt-innov"
|
||||||
|
APP_NAME = "n3wt-school"
|
||||||
|
|
||||||
|
IMAGE_FRONT = "${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/frontend"
|
||||||
|
IMAGE_BACK = "${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/backend"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
|
||||||
|
stage("Check Tag") {
|
||||||
|
when {
|
||||||
|
not {
|
||||||
|
buildingTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
currentBuild.result = 'NOT_BUILT'
|
||||||
|
error("⚠️ Pipeline uniquement déclenchée sur les tags. Aucun tag détecté.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage("Build Docker Images") {
|
||||||
|
when {
|
||||||
|
buildingTag()
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
chmod +x ./ci/scripts/makeDocker.sh
|
||||||
|
./ci/scripts/makeDocker.sh ${TAG_NAME}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage("Push Images to Registry") {
|
||||||
|
when {
|
||||||
|
buildingTag()
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: "gitea-jenkins",
|
||||||
|
usernameVariable: "REGISTRY_USER",
|
||||||
|
passwordVariable: "REGISTRY_PASS"
|
||||||
|
)]) {
|
||||||
|
|
||||||
|
sh """
|
||||||
|
echo "Login registry..."
|
||||||
|
docker login ${DOCKER_REGISTRY} \
|
||||||
|
-u ${REGISTRY_USER} \
|
||||||
|
-p ${REGISTRY_PASS}
|
||||||
|
|
||||||
|
echo "Push version images..."
|
||||||
|
docker push ${IMAGE_FRONT}:${TAG_NAME}
|
||||||
|
docker push ${IMAGE_BACK}:${TAG_NAME}
|
||||||
|
|
||||||
|
echo "Tag latest..."
|
||||||
|
docker tag ${IMAGE_FRONT}:${TAG_NAME} ${IMAGE_FRONT}:latest
|
||||||
|
docker tag ${IMAGE_BACK}:${TAG_NAME} ${IMAGE_BACK}:latest
|
||||||
|
|
||||||
|
docker push ${IMAGE_FRONT}:latest
|
||||||
|
docker push ${IMAGE_BACK}:latest
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
sh """
|
||||||
|
docker builder prune -f
|
||||||
|
docker image prune -f
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
ci/deploy.Jenkinsfile
Normal file
42
ci/deploy.Jenkinsfile
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label "SLAVE-N3WT" }
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(name: 'ENVIRONMENT', choices: ['demo', 'prod'], description: 'Choisir environnement')
|
||||||
|
string(name: 'VERSION', defaultValue: 'v1.0.0', description: 'Version Docker à déployer')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
PLATEFORME_DEMO = 'demo.n3wtschool.com'
|
||||||
|
PLATEFORME_PROD = 'vps.n3wtschool.com'
|
||||||
|
DEPLOY_DIR = '~/n3wtschool'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Deploy') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def targetHost = params.ENVIRONMENT == 'prod' ? env.PLATEFORME_PROD : env.PLATEFORME_DEMO
|
||||||
|
def deployDir = env.DEPLOY_DIR
|
||||||
|
|
||||||
|
// Le credential id Jenkins qui contient la clé SSH
|
||||||
|
def sshCredentialId = params.ENVIRONMENT == 'prod' ? 'vps_n3wt_prod' : 'demo_n3wt'
|
||||||
|
|
||||||
|
// Le user SSH que tu passes dans la commande ssh
|
||||||
|
def sshUser = params.ENVIRONMENT == 'prod' ? 'root' : 'demo'
|
||||||
|
|
||||||
|
sshagent([sshCredentialId]) {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${sshUser}@${targetHost} <<EOF
|
||||||
|
cd ${deployDir}
|
||||||
|
docker compose down
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
EOF
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
|
||||||
# Récupération de la version depuis les arguments
|
# Récupération de la version depuis les arguments
|
||||||
VERSION=$1
|
VERSION=$1
|
||||||
|
|
||||||
@ -8,21 +11,22 @@ if [ -z "$VERSION" ]; then
|
|||||||
echo "Usage: ./makeDocker.sh <version>"
|
echo "Usage: ./makeDocker.sh <version>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
SCRIPT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
cd $SCRIPT_PATH
|
|
||||||
# Configuration
|
# Configuration
|
||||||
DOCKER_REGISTRY="git.v0id.ovh"
|
DOCKER_REGISTRY="git.v0id.ovh"
|
||||||
|
ORG_NAME="n3wt-innov"
|
||||||
APP_NAME="n3wt-school"
|
APP_NAME="n3wt-school"
|
||||||
|
|
||||||
echo "Début de la construction des images Docker pour la version ${VERSION}"
|
echo "Début de la construction des images Docker pour la version ${VERSION}"
|
||||||
|
|
||||||
# Construction de l'image Frontend
|
# Construction de l'image Frontend
|
||||||
echo "Construction de l'image Frontend..."
|
echo "Construction de l'image Frontend..."
|
||||||
cd ../Front-End
|
cd $SCRIPT_PATH/../../Front-End
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg BUILD_MODE=production \
|
--build-arg BUILD_MODE=production \
|
||||||
-t ${DOCKER_REGISTRY}/n3wt-innov/${APP_NAME}/frontend:${VERSION} \
|
-t ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/frontend:${VERSION} \
|
||||||
-t ${DOCKER_REGISTRY}/n3wt-innov/${APP_NAME}/frontend:latest \
|
-t ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/frontend:latest \
|
||||||
.
|
.
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -32,10 +36,10 @@ fi
|
|||||||
|
|
||||||
# Construction de l'image Backend
|
# Construction de l'image Backend
|
||||||
echo "Construction de l'image Backend..."
|
echo "Construction de l'image Backend..."
|
||||||
cd ../Back-End
|
cd $SCRIPT_PATH/../../Back-End
|
||||||
docker build \
|
docker build \
|
||||||
-t ${DOCKER_REGISTRY}/n3wt-innov/${APP_NAME}/backend:${VERSION} \
|
-t ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/backend:${VERSION} \
|
||||||
-t ${DOCKER_REGISTRY}/n3wt-innov/${APP_NAME}/backend:latest \
|
-t ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/backend:latest \
|
||||||
.
|
.
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -45,9 +49,9 @@ fi
|
|||||||
|
|
||||||
echo "Construction des images Docker terminée avec succès"
|
echo "Construction des images Docker terminée avec succès"
|
||||||
echo "Images créées :"
|
echo "Images créées :"
|
||||||
echo "- ${DOCKER_REGISTRY}/${APP_NAME}/frontend:${VERSION}"
|
echo "- ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/frontend:${VERSION}"
|
||||||
echo "- ${DOCKER_REGISTRY}/${APP_NAME}/frontend:latest"
|
echo "- ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/frontend:latest"
|
||||||
echo "- ${DOCKER_REGISTRY}/${APP_NAME}/backend:${VERSION}"
|
echo "- ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/backend:${VERSION}"
|
||||||
echo "- ${DOCKER_REGISTRY}/${APP_NAME}/backend:latest"
|
echo "- ${DOCKER_REGISTRY}/${ORG_NAME}/${APP_NAME}/backend:latest"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
38
docs/manuels/MEP/MO_PRE_MEP.md
Normal file
38
docs/manuels/MEP/MO_PRE_MEP.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Préparation de la RELEASE et du CHANGELOG
|
||||||
|
|
||||||
|
- Vérifier que l'ensemble des tickets sont mergé dans develop
|
||||||
|
- Fusionner develop dans main via une [nouvelle demande d'ajout](https://git.v0id.ovh/n3wt-innov/n3wt-school/compare/main...develop)
|
||||||
|
- Faire une release avec la commande `npm run release` sur la branch main
|
||||||
|
\*\* NB: si vous souhaité avoir une release particulier (cf. Utilisation de standart-version)
|
||||||
|
- Pousser le commit de changement de version/Changelog et le tag sur main
|
||||||
|
- Depuis jenkins lancer le build sur le nouveau tag créé : https://jenkins.v0id.ovh/job/N3WT/job/Newt-Innov/job/n3wt-school/view/tags/
|
||||||
|
|
||||||
|
# Faire une Mise en Production
|
||||||
|
|
||||||
|
- Depuis jenkins deployer la nouvelle version tagué.
|
||||||
|
|
||||||
|
# Utilisation de standart-version
|
||||||
|
|
||||||
|
L'utilisation de la norme conventionnal commit permet la génération automatique d'un CHANGELOG
|
||||||
|
via l'outil [standard-version](https://github.com/conventional-changelog/standard-version)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Faire la première release (1.0.0)
|
||||||
|
npm run release -- --first-release
|
||||||
|
|
||||||
|
# Faire une prerelease (RC,alpha,beta)
|
||||||
|
npm run release -- --prerelease <name>
|
||||||
|
|
||||||
|
|
||||||
|
# Faire une release
|
||||||
|
npm run release
|
||||||
|
|
||||||
|
# Forcer la release sur un mode particulier (majeur, mineur ou patch)
|
||||||
|
# npm run script
|
||||||
|
npm run release -- --release-as minor
|
||||||
|
# Or
|
||||||
|
npm run release -- --release-as 1.1.0
|
||||||
|
|
||||||
|
# ignorer les hooks de commit lors de la release
|
||||||
|
npm run release -- --no-verify
|
||||||
|
```
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school",
|
"name": "n3wt-school",
|
||||||
"version": "0.0.3",
|
"version": "0.0.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
@ -20,4 +20,4 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"standard-version": "^9.5.0"
|
"standard-version": "^9.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user