mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
Compare commits
16 Commits
0.0.2
...
7486f6c5ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 7486f6c5ce | |||
| 1e5bc6ccba | |||
| 0fb668b212 | |||
| 5e62ee5100 | |||
| e89d2fc4c3 | |||
| 9481a0132d | |||
| 482e8c1357 | |||
| 0e0141d155 | |||
| 7f002e2e6a | |||
| 0064b8d35a | |||
| ec2c1daebc | |||
| 67cea2f1c6 | |||
| 5785bfae46 | |||
| a17078709b | |||
| d58155da06 | |||
| 043d93dcc4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
.env
|
.env
|
||||||
node_modules/
|
node_modules/
|
||||||
hardcoded-strings-report.md
|
hardcoded-strings-report.md
|
||||||
|
backend.env
|
||||||
@ -1 +1 @@
|
|||||||
node scripts/prepare-commit-msg.js "$1" "$2"
|
#node scripts/prepare-commit-msg.js "$1" "$2"
|
||||||
@ -236,7 +236,6 @@ def makeToken(user):
|
|||||||
"establishment__name": role.establishment.name,
|
"establishment__name": role.establishment.name,
|
||||||
"establishment__evaluation_frequency": role.establishment.evaluation_frequency,
|
"establishment__evaluation_frequency": role.establishment.evaluation_frequency,
|
||||||
"establishment__total_capacity": role.establishment.total_capacity,
|
"establishment__total_capacity": role.establishment.total_capacity,
|
||||||
"establishment__api_docuseal": role.establishment.api_docuseal,
|
|
||||||
"establishment__logo": logo_url,
|
"establishment__logo": logo_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
# This file is intentionally left blank to make this directory a Python package.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from django.urls import path, re_path
|
|
||||||
from .views import generate_jwt_token, clone_template, remove_template, download_template
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'generateToken$', generate_jwt_token, name='generate_jwt_token'),
|
|
||||||
re_path(r'cloneTemplate$', clone_template, name='clone_template'),
|
|
||||||
re_path(r'removeTemplate/(?P<id>[0-9]+)$', remove_template, name='remove_template'),
|
|
||||||
re_path(r'downloadTemplate/(?P<slug>[\w-]+)$', download_template, name='download_template')
|
|
||||||
]
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from rest_framework.decorators import api_view
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
import jwt
|
|
||||||
import datetime
|
|
||||||
import requests
|
|
||||||
from Establishment.models import Establishment
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(['POST'])
|
|
||||||
def generate_jwt_token(request):
|
|
||||||
# Récupérer l'établissement concerné (par ID ou autre info transmise)
|
|
||||||
establishment_id = request.data.get('establishment_id')
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
establishment = Establishment.objects.get(id=establishment_id)
|
|
||||||
except Establishment.DoesNotExist:
|
|
||||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
# Vérifier la clé API reçue dans le header
|
|
||||||
api_key = request.headers.get('X-Auth-Token')
|
|
||||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
|
||||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
# Récupérer les données de la requête
|
|
||||||
user_email = request.data.get('user_email')
|
|
||||||
documents_urls = request.data.get('documents_urls', [])
|
|
||||||
template_id = request.data.get('id')
|
|
||||||
|
|
||||||
if not user_email:
|
|
||||||
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# Utiliser la clé API de l'établissement comme secret JWT
|
|
||||||
jwt_secret = establishment.api_docuseal
|
|
||||||
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
|
|
||||||
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
'user_email': user_email,
|
|
||||||
'documents_urls': documents_urls,
|
|
||||||
'template_id': template_id,
|
|
||||||
'exp': datetime.datetime.utcnow() + expiration_delta
|
|
||||||
}
|
|
||||||
|
|
||||||
token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
|
|
||||||
return Response({'token': token}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(['POST'])
|
|
||||||
def clone_template(request):
|
|
||||||
# Récupérer l'établissement concerné
|
|
||||||
establishment_id = request.data.get('establishment_id')
|
|
||||||
print(f"establishment_id : {establishment_id}")
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
establishment = Establishment.objects.get(id=establishment_id)
|
|
||||||
except Establishment.DoesNotExist:
|
|
||||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
# Vérifier la clé API reçue dans le header
|
|
||||||
api_key = request.headers.get('X-Auth-Token')
|
|
||||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
|
||||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
# Récupérer les données de la requête
|
|
||||||
document_id = request.data.get('templateId')
|
|
||||||
email = request.data.get('email')
|
|
||||||
is_required = request.data.get('is_required')
|
|
||||||
|
|
||||||
# Vérifier les données requises
|
|
||||||
if not document_id:
|
|
||||||
return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# URL de l'API de DocuSeal pour cloner le template
|
|
||||||
clone_url = f'https://docuseal.com/api/templates/{document_id}/clone'
|
|
||||||
|
|
||||||
# Faire la requête pour cloner le template
|
|
||||||
try:
|
|
||||||
response = requests.post(clone_url, headers={
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': establishment.api_docuseal
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.status_code != status.HTTP_200_OK:
|
|
||||||
return Response({'error': 'Failed to clone template'}, status=response.status_code)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if is_required:
|
|
||||||
# URL de l'API de DocuSeal pour créer une submission
|
|
||||||
submission_url = f'https://docuseal.com/api/submissions'
|
|
||||||
|
|
||||||
try:
|
|
||||||
clone_id = data['id']
|
|
||||||
response = requests.post(submission_url, json={
|
|
||||||
'template_id': clone_id,
|
|
||||||
'send_email': False,
|
|
||||||
'submitters': [{'email': email}]
|
|
||||||
}, headers={
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': establishment.api_docuseal
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.status_code != status.HTTP_200_OK:
|
|
||||||
return Response({'error': 'Failed to create submission'}, status=response.status_code)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
data[0]['id'] = clone_id
|
|
||||||
return Response(data[0], status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
else:
|
|
||||||
print(f'NOT REQUIRED -> on ne crée pas de submission')
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(['DELETE'])
|
|
||||||
def remove_template(request, id):
|
|
||||||
# Récupérer l'établissement concerné
|
|
||||||
establishment_id = request.GET.get('establishment_id')
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
establishment = Establishment.objects.get(id=establishment_id)
|
|
||||||
except Establishment.DoesNotExist:
|
|
||||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
# Vérifier la clé API reçue dans le header
|
|
||||||
api_key = request.headers.get('X-Auth-Token')
|
|
||||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
|
||||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
# URL de l'API de DocuSeal pour supprimer le template
|
|
||||||
|
|
||||||
clone_url = f'https://docuseal.com/api/templates/{id}'
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.delete(clone_url, headers={
|
|
||||||
'X-Auth-Token': establishment.api_docuseal
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.status_code != status.HTTP_200_OK:
|
|
||||||
return Response({'error': 'Failed to remove template'}, status=response.status_code)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(['GET'])
|
|
||||||
def download_template(request, slug):
|
|
||||||
# Récupérer l'établissement concerné
|
|
||||||
establishment_id = request.GET.get('establishment_id')
|
|
||||||
if not establishment_id:
|
|
||||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
try:
|
|
||||||
establishment = Establishment.objects.get(id=establishment_id)
|
|
||||||
except Establishment.DoesNotExist:
|
|
||||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
# Vérifier la clé API reçue dans le header
|
|
||||||
api_key = request.headers.get('X-Auth-Token')
|
|
||||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
|
||||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
# Vérifier les données requises
|
|
||||||
if not slug:
|
|
||||||
return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# URL de l'API de DocuSeal pour télécharger le template
|
|
||||||
download_url = f'https://docuseal.com/submitters/{slug}/download'
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(download_url, headers={
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': establishment.api_docuseal
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.status_code != status.HTTP_200_OK:
|
|
||||||
return Response({'error': 'Failed to download template'}, status=response.status_code)
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
@ -27,7 +27,6 @@ class Establishment(models.Model):
|
|||||||
licence_code = models.CharField(max_length=100, blank=True)
|
licence_code = models.CharField(max_length=100, blank=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
|
|
||||||
logo = models.FileField(
|
logo = models.FileField(
|
||||||
upload_to=registration_logo_upload_to,
|
upload_to=registration_logo_upload_to,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@ -349,13 +349,6 @@ SIMPLE_JWT = {
|
|||||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration for DocuSeal JWT
|
|
||||||
DOCUSEAL_JWT = {
|
|
||||||
'ALGORITHM': 'HS256',
|
|
||||||
'SIGNING_KEY': SECRET_KEY,
|
|
||||||
'EXPIRATION_DELTA': timedelta(hours=1)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Django Channels Configuration
|
# Django Channels Configuration
|
||||||
ASGI_APPLICATION = 'N3wtSchool.asgi.application'
|
ASGI_APPLICATION = 'N3wtSchool.asgi.application'
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,6 @@ urlpatterns = [
|
|||||||
path("GestionEmail/", include(("GestionEmail.urls", 'GestionEmail'), namespace='GestionEmail')),
|
path("GestionEmail/", include(("GestionEmail.urls", 'GestionEmail'), namespace='GestionEmail')),
|
||||||
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
|
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
|
||||||
path("School/", include(("School.urls", 'School'), namespace='School')),
|
path("School/", include(("School.urls", 'School'), namespace='School')),
|
||||||
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
|
|
||||||
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
|
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
|
||||||
path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')),
|
path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')),
|
||||||
path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')),
|
path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')),
|
||||||
|
|||||||
@ -277,6 +277,16 @@ class RegistrationForm(models.Model):
|
|||||||
return "RF_" + self.student.last_name + "_" + self.student.first_name
|
return "RF_" + self.student.last_name + "_" + self.student.first_name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
# Préparer le flag de création / changement de fileGroup
|
||||||
|
was_new = self.pk is None
|
||||||
|
old_fileGroup = None
|
||||||
|
if not was_new:
|
||||||
|
try:
|
||||||
|
old_instance = RegistrationForm.objects.get(pk=self.pk)
|
||||||
|
old_fileGroup = old_instance.fileGroup
|
||||||
|
except RegistrationForm.DoesNotExist:
|
||||||
|
old_fileGroup = None
|
||||||
|
|
||||||
# Vérifier si un fichier existant doit être remplacé
|
# Vérifier si un fichier existant doit être remplacé
|
||||||
if self.pk: # Si l'objet existe déjà dans la base de données
|
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||||
try:
|
try:
|
||||||
@ -290,16 +300,27 @@ class RegistrationForm(models.Model):
|
|||||||
# Appeler la méthode save originale
|
# Appeler la méthode save originale
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Après save : si nouveau ou changement de fileGroup -> créer les templates
|
||||||
|
fileGroup_changed = (self.fileGroup is not None) and (old_fileGroup is None or (old_fileGroup and old_fileGroup.id != self.fileGroup.id))
|
||||||
|
if was_new or fileGroup_changed:
|
||||||
|
try:
|
||||||
|
import Subscriptions.util as util
|
||||||
|
created = util.create_templates_for_registration_form(self)
|
||||||
|
if created:
|
||||||
|
logger.info("Created %d templates for RegistrationForm %s", len(created), self.pk)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error creating templates for RegistrationForm %s: %s", self.pk, e)
|
||||||
|
|
||||||
#############################################################
|
#############################################################
|
||||||
####################### MASTER FILES ########################
|
####################### MASTER FILES ########################
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
####### DocuSeal masters (documents école, à signer ou pas) #######
|
####### Formulaires masters (documents école, à signer ou pas) #######
|
||||||
class RegistrationSchoolFileMaster(models.Model):
|
class RegistrationSchoolFileMaster(models.Model):
|
||||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||||
id = models.IntegerField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
is_required = models.BooleanField(default=False)
|
is_required = models.BooleanField(default=False)
|
||||||
|
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.group.name} - {self.id}'
|
return f'{self.group.name} - {self.id}'
|
||||||
@ -321,14 +342,14 @@ def registration_school_file_upload_to(instance, filename):
|
|||||||
def registration_parent_file_upload_to(instance, filename):
|
def registration_parent_file_upload_to(instance, filename):
|
||||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||||
|
|
||||||
####### DocuSeal templates (par dossier d'inscription) #######
|
####### Formulaires templates (par dossier d'inscription) #######
|
||||||
class RegistrationSchoolFileTemplate(models.Model):
|
class RegistrationSchoolFileTemplate(models.Model):
|
||||||
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||||
id = models.IntegerField(primary_key=True)
|
|
||||||
slug = models.CharField(max_length=255, default="")
|
slug = models.CharField(max_length=255, default="")
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||||
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
||||||
|
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
<img src="{{URL_DJANGO}}/static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||||
<h1>Confirmation de souscription</h1>
|
<h1>Confirmation de souscription</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
@ -24,7 +24,12 @@ from .views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||||
from .views import registration_file_views, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
|
from .views import (
|
||||||
|
registration_school_file_masters_views,
|
||||||
|
registration_school_file_templates_views,
|
||||||
|
get_school_file_templates_by_rf,
|
||||||
|
get_parent_file_templates_by_rf
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
|
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
|
||||||
|
|||||||
@ -21,8 +21,161 @@ from PyPDF2 import PdfMerger
|
|||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import json
|
||||||
|
from django.http import QueryDict
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def build_payload_from_request(request):
|
||||||
|
"""
|
||||||
|
Normalise la request en payload prêt à être donné au serializer.
|
||||||
|
- supporte multipart/form-data où le front envoie 'data' (JSON string) ou un fichier JSON + fichiers
|
||||||
|
- supporte application/json ou form-data simple
|
||||||
|
Retour: (payload_dict, None) ou (None, Response erreur)
|
||||||
|
"""
|
||||||
|
data_field = request.data.get('data') if hasattr(request.data, 'get') else None
|
||||||
|
if data_field:
|
||||||
|
try:
|
||||||
|
# Si 'data' est un fichier (InMemoryUploadedFile ou fichier similaire), lire et décoder
|
||||||
|
if hasattr(data_field, 'read'):
|
||||||
|
raw = data_field.read()
|
||||||
|
if isinstance(raw, (bytes, bytearray)):
|
||||||
|
text = raw.decode('utf-8')
|
||||||
|
else:
|
||||||
|
text = raw
|
||||||
|
payload = json.loads(text)
|
||||||
|
# Si 'data' est bytes déjà
|
||||||
|
elif isinstance(data_field, (bytes, bytearray)):
|
||||||
|
payload = json.loads(data_field.decode('utf-8'))
|
||||||
|
# Si 'data' est une string JSON
|
||||||
|
elif isinstance(data_field, str):
|
||||||
|
payload = json.loads(data_field)
|
||||||
|
else:
|
||||||
|
# type inattendu
|
||||||
|
raise ValueError(f"Unsupported 'data' type: {type(data_field)}")
|
||||||
|
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
||||||
|
logger.error(f'Invalid JSON in "data": {e}')
|
||||||
|
return None, Response({'error': "Invalid JSON in 'data'", 'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
payload = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data)
|
||||||
|
if isinstance(payload, QueryDict):
|
||||||
|
payload = payload.dict()
|
||||||
|
|
||||||
|
# Attacher les fichiers présents (ex: photo, files.*, etc.), sauf 'data' (déjà traité)
|
||||||
|
for f_key, f_val in request.FILES.items():
|
||||||
|
if f_key == 'data':
|
||||||
|
# remettre le pointeur au début si besoin (déjà lu) — non indispensable ici mais sûr
|
||||||
|
try:
|
||||||
|
f_val.seek(0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# ne pas mettre le fichier 'data' dans le payload (c'est le JSON)
|
||||||
|
continue
|
||||||
|
payload[f_key] = f_val
|
||||||
|
|
||||||
|
return payload, None
|
||||||
|
|
||||||
|
def create_templates_for_registration_form(register_form):
|
||||||
|
"""
|
||||||
|
Idempotent:
|
||||||
|
- supprime les templates existants qui ne correspondent pas
|
||||||
|
aux masters du fileGroup courant du register_form (et supprime leurs fichiers).
|
||||||
|
- crée les templates manquants pour les masters du fileGroup courant.
|
||||||
|
Retourne la liste des templates créés.
|
||||||
|
"""
|
||||||
|
from Subscriptions.models import (
|
||||||
|
RegistrationSchoolFileMaster,
|
||||||
|
RegistrationSchoolFileTemplate,
|
||||||
|
# RegistrationParentFileMaster,
|
||||||
|
# RegistrationParentFileTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
|
created = []
|
||||||
|
|
||||||
|
# Récupérer les masters du fileGroup courant
|
||||||
|
current_group = getattr(register_form, "fileGroup", None)
|
||||||
|
if not current_group:
|
||||||
|
# Si plus de fileGroup, supprimer tous les templates existants pour ce RF
|
||||||
|
school_existing = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form)
|
||||||
|
for t in school_existing:
|
||||||
|
try:
|
||||||
|
if getattr(t, "file", None):
|
||||||
|
t.file.delete(save=False)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
|
||||||
|
t.delete()
|
||||||
|
# parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
||||||
|
# for t in parent_existing:
|
||||||
|
# try:
|
||||||
|
# if getattr(t, "file", None):
|
||||||
|
# t.file.delete(save=False)
|
||||||
|
# except Exception:
|
||||||
|
# logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
||||||
|
# t.delete()
|
||||||
|
return created
|
||||||
|
|
||||||
|
school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct()
|
||||||
|
# parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||||
|
|
||||||
|
school_master_ids = {m.pk for m in school_masters}
|
||||||
|
#parent_master_ids = {m.pk for m in parent_masters}
|
||||||
|
|
||||||
|
# Supprimer les school templates obsolètes
|
||||||
|
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
|
||||||
|
if not tmpl.master_id or tmpl.master_id not in school_master_ids:
|
||||||
|
try:
|
||||||
|
if getattr(tmpl, "file", None):
|
||||||
|
tmpl.file.delete(save=False)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Erreur suppression fichier school template obsolète %s", getattr(tmpl, "pk", None))
|
||||||
|
tmpl.delete()
|
||||||
|
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||||
|
|
||||||
|
# Supprimer les parent templates obsolètes
|
||||||
|
# for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
||||||
|
# if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
||||||
|
# try:
|
||||||
|
# if getattr(tmpl, "file", None):
|
||||||
|
# tmpl.file.delete(save=False)
|
||||||
|
# except Exception:
|
||||||
|
# logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
||||||
|
# tmpl.delete()
|
||||||
|
# logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||||
|
|
||||||
|
# Créer les school templates manquants
|
||||||
|
for m in school_masters:
|
||||||
|
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||||
|
if exists:
|
||||||
|
continue
|
||||||
|
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||||||
|
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||||||
|
tmpl = RegistrationSchoolFileTemplate.objects.create(
|
||||||
|
master=m,
|
||||||
|
registration_form=register_form,
|
||||||
|
name=m.name or "",
|
||||||
|
formTemplateData=m.formMasterData or [],
|
||||||
|
slug=slug,
|
||||||
|
)
|
||||||
|
created.append(tmpl)
|
||||||
|
logger.info("Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||||
|
|
||||||
|
# Créer les parent templates manquants
|
||||||
|
# for m in parent_masters:
|
||||||
|
# exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||||
|
# if exists:
|
||||||
|
# continue
|
||||||
|
# tmpl = RegistrationParentFileTemplate.objects.create(
|
||||||
|
# master=m,
|
||||||
|
# registration_form=register_form,
|
||||||
|
# file=None,
|
||||||
|
# )
|
||||||
|
# created.append(tmpl)
|
||||||
|
# logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
def recupereListeFichesInscription():
|
def recupereListeFichesInscription():
|
||||||
"""
|
"""
|
||||||
Retourne la liste complète des fiches d’inscription.
|
Retourne la liste complète des fiches d’inscription.
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
|
from .register_form_views import (
|
||||||
from .registration_file_views import (
|
RegisterFormView,
|
||||||
|
RegisterFormWithIdView,
|
||||||
|
send,
|
||||||
|
resend,
|
||||||
|
archive,
|
||||||
|
get_school_file_templates_by_rf,
|
||||||
|
get_parent_file_templates_by_rf
|
||||||
|
)
|
||||||
|
from .registration_school_file_masters_views import (
|
||||||
RegistrationSchoolFileMasterView,
|
RegistrationSchoolFileMasterView,
|
||||||
RegistrationSchoolFileMasterSimpleView,
|
RegistrationSchoolFileMasterSimpleView,
|
||||||
RegistrationSchoolFileTemplateView,
|
|
||||||
RegistrationSchoolFileTemplateSimpleView,
|
|
||||||
RegistrationParentFileMasterView,
|
RegistrationParentFileMasterView,
|
||||||
RegistrationParentFileMasterSimpleView,
|
RegistrationParentFileMasterSimpleView,
|
||||||
RegistrationParentFileTemplateSimpleView,
|
RegistrationParentFileTemplateSimpleView,
|
||||||
RegistrationParentFileTemplateView
|
RegistrationParentFileTemplateView
|
||||||
)
|
)
|
||||||
|
from .registration_school_file_templates_views import (
|
||||||
|
RegistrationSchoolFileTemplateView,
|
||||||
|
RegistrationSchoolFileTemplateSimpleView,
|
||||||
|
)
|
||||||
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||||
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
||||||
from .guardian_views import GuardianView, DissociateGuardianView
|
from .guardian_views import GuardianView, DissociateGuardianView
|
||||||
@ -33,7 +43,7 @@ __all__ = [
|
|||||||
'RegistrationFileGroupSimpleView',
|
'RegistrationFileGroupSimpleView',
|
||||||
'get_registration_files_by_group',
|
'get_registration_files_by_group',
|
||||||
'get_school_file_templates_by_rf',
|
'get_school_file_templates_by_rf',
|
||||||
'get_parent_file_templates_by_rf'
|
'get_parent_file_templates_by_rf',
|
||||||
'StudentView',
|
'StudentView',
|
||||||
'StudentListView',
|
'StudentListView',
|
||||||
'ChildrenListView',
|
'ChildrenListView',
|
||||||
|
|||||||
@ -0,0 +1,363 @@
|
|||||||
|
from django.http.response import JsonResponse
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import status
|
||||||
|
import json
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
|
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||||
|
from Subscriptions.models import (
|
||||||
|
RegistrationForm,
|
||||||
|
RegistrationSchoolFileMaster,
|
||||||
|
RegistrationSchoolFileTemplate,
|
||||||
|
RegistrationParentFileMaster,
|
||||||
|
RegistrationParentFileTemplate
|
||||||
|
)
|
||||||
|
from N3wtSchool import bdd
|
||||||
|
import logging
|
||||||
|
import Subscriptions.util as util
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RegistrationSchoolFileMasterView(APIView):
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationSchoolFileMasterSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les masters liés à l'établissement via groups.establishment
|
||||||
|
masters = RegistrationSchoolFileMaster.objects.filter(
|
||||||
|
groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau master de template d'inscription",
|
||||||
|
request_body=RegistrationSchoolFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationSchoolFileMasterSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
logger.info(f"raw request.data: {request.data}")
|
||||||
|
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for serializer: {payload}")
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
obj = serializer.save()
|
||||||
|
|
||||||
|
# Propager la création des templates côté serveur pour les RegistrationForm
|
||||||
|
try:
|
||||||
|
groups_qs = obj.groups.all()
|
||||||
|
if groups_qs.exists():
|
||||||
|
# Tous les RegistrationForm dont fileGroup est dans les groups du master
|
||||||
|
rfs = RegistrationForm.objects.filter(fileGroup__in=groups_qs).distinct()
|
||||||
|
for rf in rfs:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error creating templates for RF %s from master %s: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while propagating templates after master creation %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
|
||||||
|
return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
logger.error(f"serializer errors: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un master de template d'inscription spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationSchoolFileMasterSerializer,
|
||||||
|
404: "Master non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
|
if master is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(master)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un master de template d'inscription existant",
|
||||||
|
request_body=RegistrationSchoolFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationSchoolFileMasterSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Master non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
|
if master is None:
|
||||||
|
return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# snapshot des groups avant update
|
||||||
|
old_group_ids = set(master.groups.values_list('id', flat=True))
|
||||||
|
|
||||||
|
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for update serializer: {payload}")
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
obj = serializer.save()
|
||||||
|
|
||||||
|
# groups après update
|
||||||
|
new_group_ids = set(obj.groups.values_list('id', flat=True))
|
||||||
|
|
||||||
|
removed_group_ids = old_group_ids - new_group_ids
|
||||||
|
added_group_ids = new_group_ids - old_group_ids
|
||||||
|
|
||||||
|
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||||
|
if removed_group_ids:
|
||||||
|
try:
|
||||||
|
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||||
|
for rf in rfs_removed:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf) # supprimera les templates obsolètes
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error cleaning templates for RF %s after master %s group removal: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while processing RFs for removed groups after master update %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||||
|
if added_group_ids:
|
||||||
|
try:
|
||||||
|
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||||
|
for rf in rfs_added:
|
||||||
|
try:
|
||||||
|
util.create_templates_for_registration_form(rf) # créera les templates manquants
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error creating templates for RF %s after master %s group addition: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error while processing RFs for added groups after master update %s", getattr(obj, 'pk', None))
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un master de template d'inscription",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Master non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
|
if master is not None:
|
||||||
|
master.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
class RegistrationParentFileMasterView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les fichiers parents liés à l'établissement
|
||||||
|
templates = RegistrationParentFileMaster.objects.filter(
|
||||||
|
groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau fichier parent",
|
||||||
|
request_body=RegistrationParentFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationParentFileMasterSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationParentFileMasterSimpleView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un fichier parent spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileMasterSerializer,
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(template)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un fichier parent existant",
|
||||||
|
request_body=RegistrationParentFileMasterSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileMasterSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileMasterSerializer(template, data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un fichier parent",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Fichier parent non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||||
|
if template is not None:
|
||||||
|
template.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
'establishment_id',
|
||||||
|
openapi.IN_QUERY,
|
||||||
|
description="ID de l'établissement",
|
||||||
|
type=openapi.TYPE_INTEGER,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
establishment_id = request.GET.get('establishment_id')
|
||||||
|
if not establishment_id:
|
||||||
|
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||||
|
templates = RegistrationParentFileTemplate.objects.filter(
|
||||||
|
master__groups__establishment__id=establishment_id
|
||||||
|
).distinct()
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Crée un nouveau template d'inscription",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
201: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
class RegistrationParentFileTemplateSimpleView(APIView):
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère un template d'inscription spécifique",
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template)
|
||||||
|
return JsonResponse(serializer.data, safe=False)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Met à jour un template d'inscription existant",
|
||||||
|
request_body=RegistrationParentFileTemplateSerializer,
|
||||||
|
responses={
|
||||||
|
200: RegistrationParentFileTemplateSerializer,
|
||||||
|
400: "Données invalides",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def put(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is None:
|
||||||
|
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Supprime un template d'inscription",
|
||||||
|
responses={
|
||||||
|
204: "Suppression réussie",
|
||||||
|
404: "Template non trouvé"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def delete(self, request, id):
|
||||||
|
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||||
|
if template is not None:
|
||||||
|
template.delete()
|
||||||
|
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
else:
|
||||||
|
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
@ -5,12 +5,19 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
import json
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||||
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
import logging
|
||||||
|
import Subscriptions.util as util
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RegistrationSchoolFileMasterView(APIView):
|
class RegistrationSchoolFileMasterView(APIView):
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@ -45,10 +52,19 @@ class RegistrationSchoolFileMasterView(APIView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = RegistrationSchoolFileMasterSerializer(data=request.data)
|
logger.info(f"raw request.data: {request.data}")
|
||||||
|
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for serializer: {payload}")
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
obj = serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
logger.error(f"serializer errors: {serializer.errors}")
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class RegistrationSchoolFileMasterSimpleView(APIView):
|
class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||||
@ -78,11 +94,19 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
|||||||
def put(self, request, id):
|
def put(self, request, id):
|
||||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
if master is None:
|
if master is None:
|
||||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
serializer = RegistrationSchoolFileMasterSerializer(master, data=request.data)
|
|
||||||
|
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||||
|
payload, resp = util.build_payload_from_request(request)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
logger.info(f"payload for update serializer: {payload}")
|
||||||
|
serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
@ -96,7 +120,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
|||||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||||
if master is not None:
|
if master is not None:
|
||||||
master.delete()
|
master.delete()
|
||||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
@ -1 +1 @@
|
|||||||
__version__ = "0.0.1"
|
__version__ = "0.0.3"
|
||||||
|
|||||||
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
from watchfiles import run_process
|
||||||
|
|
||||||
def run_command(command):
|
def run_command(command):
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
@ -11,6 +12,7 @@ def run_command(command):
|
|||||||
return process.returncode
|
return process.returncode
|
||||||
|
|
||||||
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
|
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
|
||||||
|
watch_mode = os.getenv('DJANGO_WATCH', 'false').lower() == 'true'
|
||||||
|
|
||||||
commands = [
|
commands = [
|
||||||
["python", "manage.py", "collectstatic", "--noinput"],
|
["python", "manage.py", "collectstatic", "--noinput"],
|
||||||
@ -32,23 +34,55 @@ test_commands = [
|
|||||||
["python", "manage.py", "init_mock_datas"]
|
["python", "manage.py", "init_mock_datas"]
|
||||||
]
|
]
|
||||||
|
|
||||||
for command in commands:
|
def run_daphne():
|
||||||
if run_command(command) != 0:
|
try:
|
||||||
exit(1)
|
result = subprocess.run([
|
||||||
|
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
|
||||||
|
])
|
||||||
|
return result.returncode
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Arrêt de Daphne (KeyboardInterrupt)")
|
||||||
|
return 0
|
||||||
|
|
||||||
#if test_mode:
|
if __name__ == "__main__":
|
||||||
# for test_command in test_commands:
|
for command in commands:
|
||||||
# if run_command(test_command) != 0:
|
if run_command(command) != 0:
|
||||||
# exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Lancer les processus en parallèle
|
#if test_mode:
|
||||||
|
# for test_command in test_commands:
|
||||||
|
# if run_command(test_command) != 0:
|
||||||
|
# exit(1)
|
||||||
|
|
||||||
processes = [
|
if watch_mode:
|
||||||
subprocess.Popen(["daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"]),
|
celery_worker = subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"])
|
||||||
subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]),
|
celery_beat = subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
|
||||||
subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
|
try:
|
||||||
]
|
run_process(
|
||||||
|
'.',
|
||||||
# Attendre la fin des processus
|
target=run_daphne
|
||||||
for process in processes:
|
)
|
||||||
process.wait()
|
except KeyboardInterrupt:
|
||||||
|
print("Arrêt demandé (KeyboardInterrupt)")
|
||||||
|
finally:
|
||||||
|
celery_worker.terminate()
|
||||||
|
celery_beat.terminate()
|
||||||
|
celery_worker.wait()
|
||||||
|
celery_beat.wait()
|
||||||
|
else:
|
||||||
|
processes = [
|
||||||
|
subprocess.Popen([
|
||||||
|
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
|
||||||
|
]),
|
||||||
|
subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]),
|
||||||
|
subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
for process in processes:
|
||||||
|
process.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Arrêt demandé (KeyboardInterrupt)")
|
||||||
|
for process in processes:
|
||||||
|
process.terminate()
|
||||||
|
for process in processes:
|
||||||
|
process.wait()
|
||||||
267
CHANGELOG.md
267
CHANGELOG.md
@ -0,0 +1,267 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Corrections de bugs
|
||||||
|
|
||||||
|
* Ajout d'un '/' en fin d'URL ([67cea2f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67cea2f1c6edae8eed5e024c79b1e19d08788d4c))
|
||||||
|
|
||||||
|
### 0.0.2 (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* mise à jour de la doc swagger ([11fc446](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/11fc446b904cc64d63154ad5c6711a8296a7fc51))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorisations
|
||||||
|
|
||||||
|
* "registerFilesTemplates" -> "registrerFileTemplate" ([83f4d67](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/83f4d67a6fc3f786803343957b276f8419f3058d))
|
||||||
|
* adaptation mobile ([4b8f85e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4b8f85e68dc95585d96a4cbad219ad068cbc8acf))
|
||||||
|
* Affichage des notifications dans la partie "Users" ([af30ae3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/af30ae33b5660c55fa6824498f4325aab3de3c5a))
|
||||||
|
* Affichage des notifications dans la partie "Users" ([e509625](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e5096258110faac37b9457705dd1b51bc231983f))
|
||||||
|
* Augmentation du nombre de données ([95c154a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95c154a4a2d746c4350887bb697af142152ed8d7))
|
||||||
|
* changement de la philosophie de logging ([c7723ec](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c7723eceee650de86eea3263d44d374ad9844282))
|
||||||
|
* Changement des IconTextInput en TextInput, modification du composant step ([a248898](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a248898203286213c3447333611e1a9981dff64a))
|
||||||
|
* Composant *InscriptionForm* ([56e2762](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56e27628f897920659a6ce186539ddec7e94a05a))
|
||||||
|
* Creation d'un provider et d'un systeme de middleware ([5088479](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/508847940c8c35fd982ab935f4d69371869eed5a))
|
||||||
|
* Création de composants et uniformisation des modales ([#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)) ([d51778b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d51778ba54e95283aa6ad7821fda673813c7c7a0))
|
||||||
|
* Création de nouveaux composants / update formulaire de ([7acae47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7acae479da658707fb3e073ebcdfee023d18500b)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||||
|
* Deplacement du JWT dans le back ([eb89a32](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb89a324abbdf69091e5c78530ec62f2c2ccbcd1))
|
||||||
|
* Document Ecole/Parent ([7564865](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7564865d8f414fbefa0731c4ca472a100efb6036))
|
||||||
|
* gestion des erreurs ([f3490a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3490a4e9584b959450ca45c8e74e430396425b3))
|
||||||
|
* Injection des env var dans le frontend ([aae5d27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aae5d27d8c556c5687951f3a04e01d42f69f3085))
|
||||||
|
* je suis une merde ([c4d4542](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c4d45426b520a498f409be8617c7936224195290))
|
||||||
|
* Mise à jour de la doc swagger / URL ([4c95b6a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c95b6a83f15a9989fac5f69a9386664d25ec9f6))
|
||||||
|
* Modification de l'url de l'api Auth ([9bf9c5f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9bf9c5f62df1a6482ba27b897da498592b57e04f))
|
||||||
|
* Modification de la construction docker ([2d128aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2d128aaf30e60813c0c5caa244a93ff46e3985f3))
|
||||||
|
* Partie "School" ([58fe509](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58fe509734a3b5dc6e0b5c6aa3fd713fd4dc821e))
|
||||||
|
* Partie FRONT / School ([24352ef](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24352efad304dee7418dc846681a4b38047431f6))
|
||||||
|
* Refactoring de la section ClassSection ([1a8ef26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1a8ef26f5883abe4855949a54aa50defb98c852d))
|
||||||
|
* refactoring du FRONT page subscribe ([427b6c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/427b6c758892469d07579159511e7ce1ceed20d0))
|
||||||
|
* Refactorisation du login et de admin/subscription ([41aa9d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/41aa9d55d388c0ddf189c7b9ab6057487f86484b))
|
||||||
|
* Remplacement de quelques popup par les notifications ([ce83e02](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ce83e02f7b3e53ef2b859436432784d6eb69200d))
|
||||||
|
* Renommage du menu "Eleves" en "Inscriptions" ([692e845](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/692e8454bf9840ada3f8e052d7ef13cbf1b0d9c0))
|
||||||
|
* Revue de la modale permettant de créer un dossier ([cb3f909](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb3f909fa4e7a53148cd13cf190c13b0670d35de))
|
||||||
|
* Revue de la modale permettant de créer un dossier ([665625e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/665625e0280683fef056e9c950fc6555d889643e))
|
||||||
|
* SpecialitySection + TeacherSection (en cours) ([72dd769](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/72dd7699d6bd61e17b4c3dc0098ca0989a94b2c8))
|
||||||
|
* Suppression des paramètres mail mot de passes des settings ([ec2630a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ec2630a6e40dedaaa8f41a04b44e5ec1f6b2a1e0))
|
||||||
|
* Traduction en anglais des modules "GestionInscription" et ([2b414b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2b414b83913c2f0f81cf226b78577ad522443d7b))
|
||||||
|
* Transformation des requetes vers le back en action ajout des ([147a701](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/147a70135d2f10ac16961c098d85da0a1bcafb38))
|
||||||
|
* Utilisation d'une application "Common" pour tous les modèles ([e65e310](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e65e31014d2b89afe1e5f077e8d4109f07d40d0b))
|
||||||
|
|
||||||
|
|
||||||
|
### Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
* A la signature d'un document, on récupère l'URL du PDF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([2ac4832](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ac48329851f91f6bb02a44e02ad5a90b4ae504c))
|
||||||
|
* Affichage d'icones dans le tableau des inscriptions dans la ([9559db5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9559db59eb418d233682217ef72f315bccc6fe1d))
|
||||||
|
* Ajout d'un composant permettant de visualiser les fichiers signés ([7f442b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f442b9cae008dab4f18438f9ee46be21ed037b0))
|
||||||
|
* Ajout d'un nouveau status avec envoi de mandat SEPA + envoi de ([4c2e2f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c2e2f87565dc6e2be501839c274a5aa6969a9ec))
|
||||||
|
* Ajout d'un nouvel état dans l'automatique lorsqu'un mandat SEPA ([545349c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/545349c7db7d0f653f3ae06b10d441ef975b0cc0))
|
||||||
|
* Ajout d'une colonne dans le tableau des pièces jointes indiquant ([3c0806e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c0806e26c116dbccd808bd8c8b170c5c4d9bc5b))
|
||||||
|
* Ajout d'une fonction de dissociation entre un responsable et un ([3bcc620](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3bcc620ee103690a2ee5f79e6203aba880bda9b7))
|
||||||
|
* Ajout d'une fonction de logout ([c2bba1a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c2bba1abbfafbb7aca1bb07e8019d7fa244a808e))
|
||||||
|
* Ajout d'une fonction de logout ([0ef6a2b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0ef6a2b1192dbd3ecc59ce0e8cbba233ccc9c821))
|
||||||
|
* Ajout de l'emploi du temps sur la page parent ([78d96f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/78d96f82f91ed777073250b960eee8f326cccb43))
|
||||||
|
* Ajout de l'envoie de mail [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([99a882a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/99a882a64acfd9340d6849edd1766de5173a2341))
|
||||||
|
* Ajout de l'option d'envoi automatique [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([a77dd8e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a77dd8ec64bd78ab9c42aad3f93a181e64719d06))
|
||||||
|
* Ajout de la configuration des tarifs de l'école [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([5a0e65b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a0e65bb752a80781517394d7b2a673788f7595e))
|
||||||
|
* Ajout de la fratrie [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([4a382d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a382d523ccfd4cf8fa7e672e9315b86dbdbbb14))
|
||||||
|
* Ajout de la fratrie / Gestion des index de fratrie / Gestion des ([2ab1684](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ab1684791377804dd03c8467a94dbc1244e102f))
|
||||||
|
* Ajout de la gestion des fichier d'inscription [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([3c27133](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c27133cdb9943c5e20b81c03f9e2fa47077dbbb))
|
||||||
|
* Ajout de la photo pour le dossier de l'élève + correction ([5851341](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5851341235998647a4142bdf1996ddc9db21762d))
|
||||||
|
* Ajout de la possibilité de supprimer une association ([c9350a7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9350a796b65ea4eef0e38390ab9fb1d88196210))
|
||||||
|
* Ajout de la sélection des modes de paiements / refactoring de ([5a7661d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a7661db93454b9a73b9f6bd46646c6135a0f203))
|
||||||
|
* Ajout des Bundles de fichiers [[#24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/24)] ([ffc6ce8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ffc6ce8de835e9caf547b6c4a893436aa93513ba))
|
||||||
|
* ajout des documents d'inscription [[#20](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/20)] ([b8ef34a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8ef34a04b14c8a8fb980fcd9255296ceb699ec6))
|
||||||
|
* Ajout des évenements à venir ([c03fa0b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c03fa0ba42d69918501beb5bb98637a449eb2da0))
|
||||||
|
* Ajout des frais d'inscription lors de la création d'un RF [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([ece23de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ece23deb19483c50d9999541a482e3378db19d23))
|
||||||
|
* Ajout des frais de scolarité dans le dossier d'inscription [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([0c2e0b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c2e0b92f43f223adc22db36ecad7fd864737a98))
|
||||||
|
* Ajout des modes de paiements + création d'une commande dans le ([0c5e3aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c5e3aa0988a16b6f9f8c0b411c2c1b443c972a7))
|
||||||
|
* Ajout des payementPlans dans le formulaire / ajout de la photo ([d37aed5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37aed5f6496b8c8ca5519689dfc811d9626e09e))
|
||||||
|
* Ajout du logo de l'école ([6a0b90e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6a0b90e98fbcc707756ae7fbbff921e480f2c695))
|
||||||
|
* Ajout du logo N3wt dans les mails ([8a71fa1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8a71fa1830d0c0fb11467208bc98dc4f71598199))
|
||||||
|
* Ajout du suivi de version dans le footer du Front ([fb7fbaf](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb7fbaf8394ebf41e6f3f31897e6d009c537a481))
|
||||||
|
* Amélioration de la fiche élève pour y ajouter la fratrie et les ([256f995](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/256f995698e79572eb3d51ea60b96b6fad47d553))
|
||||||
|
* Amélioration du dashboard ([eb48523](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb48523f7d466698faa268b8b25e6f1ed90bdfd7))
|
||||||
|
* Amorçage de la gestion des absences [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([cb4fe74](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb4fe74a9e316a92c6b5e1d2550aaf2b1036a744))
|
||||||
|
* Aussi pour la table des parents tant qu'à faire ([a3182c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a3182c0ba7c6ea9f99a4fe34a4a00079b4676d59))
|
||||||
|
* **backend:** Ajout du logger django [[#7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/7)] ([b8511f9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8511f94b633b9bf5bd764b3706c53b74b3a6648))
|
||||||
|
* Bilan de compétence d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([5760c89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5760c89105f38f4481e2cc6fa788bb0c39e8caa8))
|
||||||
|
* Champ de recherche de l'élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([eb7805e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb7805e54e41f6eaefad81fea1616f0613365e8c))
|
||||||
|
* Configuration des compétences par cycle [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([4e5aab6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4e5aab6db74a8d1dfdfb4928f60ad47da52c89e8))
|
||||||
|
* Configuration et gestion du planning [[#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)] ([830d9a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/830d9a48c003e1cca469b1cf4082305e16685181))
|
||||||
|
* Création d'un annuaire / mise à jour du subscribe ([6bd5704](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bd5704983282264bc50c73677495740f7d7e8a9))
|
||||||
|
* Création d'un profile selector [[#37](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/37),[#38](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/38)] ([89b01b7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/89b01b79db884c393db29332b95f570e47d20ed1))
|
||||||
|
* création d'une tooltip pour les informations supplémentaires de ([9197615](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/91976157e44e319b23fd35fa89859164bab71202))
|
||||||
|
* création de 4 JSON de compétences en attendant de les mettre en ([69405c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/69405c577e7af3d07654fca96015d21f475e700d))
|
||||||
|
* Création de clones lors de la création de RF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([d1a0067](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1a0067f7b7125e453ff6fc75efead881a7af37d))
|
||||||
|
* Création nouveau style / pagination profils annuaires ([760ee00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/760ee0009e983776dfd500df7465ae66593dc85d))
|
||||||
|
* Dockerisation d'un serveur docuseal + initialisation d'un compte ([8897d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8897d523dc23fd2d89a0ec66b5cc7fa15b69db5b))
|
||||||
|
* Envoie d'un mail de bienvue au directeur ([5be5f9f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5be5f9f70d4fcf56da29afb19187806ff2e6e428))
|
||||||
|
* Evolution des modèles pour intégrer un planning et du m2m ([85d4c00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/85d4c007cb2091ae1911ca1998f1b830470b8310))
|
||||||
|
* Formulaire de création RF sur une seule pag ([76f9a7d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/76f9a7dd14d7065f4add01718fda499fbb9183c7))
|
||||||
|
* Génération d'une page de suivi pédagogique + fix utilisation ([2a6b3bd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2a6b3bdf63ddc13509b66690ea5d76eac77d1090))
|
||||||
|
* Génération du bilan de compétence en PDF [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0fe6c76](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0fe6c761892097d043902f4f051b9fdb5fef29d0))
|
||||||
|
* Gestion de la création d'un nouveau guardian, de l'association ([fb73f9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb73f9e9a86430d7498aa8a10e5abc46325b7b2c))
|
||||||
|
* Gestion de la mise à jour des profiles / roles / lors de l'édition ([dfd707d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dfd707d7a0c7f4514f5583f07803d20e3c2d6bd7))
|
||||||
|
* Gestion de la sauvegarde du fichier d'inscription / affichage du ([d6edf25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d6edf250bbc1cc1a9862e26174bc24ca4f9ee4c1))
|
||||||
|
* Gestion de la validation du dossier d'inscription ([b23264c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b23264c0d4008a4317c009a73ae11f57ee6917e2))
|
||||||
|
* Gestion des absences du jour [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([030d19d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/030d19d411af8f0d87a3cb72cb401d9dd5fa96ce))
|
||||||
|
* Gestion des documents nécessitant des signatures électroniques et ([e3879f5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e3879f516b81b7e4b784049668b2507f12e8155f)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||||
|
* Gestion des documents parent ([59aee80](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59aee80c2e7592a7cdb119d1d30a5ad2c8bb20b0))
|
||||||
|
* Gestion des documents signés durant l'inscription / possibilité de ([905b95f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/905b95f3a364f0d1ce8348d086870045d942bf92))
|
||||||
|
* gestion des no data dans les table [[#33](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/33)] ([2888f8d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2888f8dcce8d593df8f81a635eaac94af4603829))
|
||||||
|
* Gestion des pièces à fournir par les parents (configuration école) ([a65bd47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a65bd47905cc33c44416c1def0413579b96d820d))
|
||||||
|
* Gestion des profils ADMIN/ECOLE (création des enseignants) ([e0bfd3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e0bfd3e11579c512aa6ad63c73e00e40be4eaf06))
|
||||||
|
* Gestion des profils des enseignants / Visualisation d'une classe [[#4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/4)] ([81d1dfa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/81d1dfa9a70d0cd8d80e7d951a74c9355bba5238))
|
||||||
|
* Gestion des rattachements de Guardian à des RF déjà existants ([7d1b9c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7d1b9c5657439d2fff287f60b9aba79a5dfdf089))
|
||||||
|
* Gestion du planning [3] ([58144ba](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58144ba0d0f1b53e9313f4cd4d3fbc3e6bfdd274))
|
||||||
|
* Gestion multi-profil multi-école ([1617829](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/16178296ec4dd4843be26b6e09b9c0f080df7ee4))
|
||||||
|
* Harmonisation des fees / ajout de type de réduction / mise à jour ([5462306](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5462306a6020493cf747ea3bb8edb3240c36286f)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||||
|
* Merge remote-tracking branch 'origin/WIP_style' into develop ([f887ae1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f887ae18862b740fa904d8ca04a3932eec455908))
|
||||||
|
* Messagerie WIP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([23a593d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23a593dbc77b6f544a17de5a451ff60316f50292))
|
||||||
|
* Mise à jour des Dockerfile préparation d'un environnement de démo [[#12](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/12)] ([32a77c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/32a77c780abe8c0aa9846843ac81d13e4b8cf73a))
|
||||||
|
* Mise à jour des Teacher ([173ac47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/173ac47fb26ba2f101802571621fc4112adb1a9f))
|
||||||
|
* Mise à jour du modèle (possibilité d'associer une réduciton à un ([8d1a41e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8d1a41e2693c3704b68e8d75bd32c4a89a6389e5)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||||
|
* mise en place de la messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([d37145b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37145b73e2012f21a329ee97a565189233ca0f8))
|
||||||
|
* Mise en place des actions pour chaque state du RF, possibilité ([8fc9478](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8fc947878665ca04e1697fa6df140e0d80c5a672))
|
||||||
|
* Mise en place des paiements en plusieurs fois - partie BACK [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([274db24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/274db249aa25f2a0281638c318a68cf88a721a45))
|
||||||
|
* Mise en place des paiements en plusieurs fois (partie BACK) [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([23203c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23203c0397f6247d32462cceca33d964898223a9))
|
||||||
|
* Mise en place du Backend-messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([c6bc0d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6bc0d0b515a5be7b0bf930ff628a5e9b5ebbb33))
|
||||||
|
* Nommage des templates / Intégration dans formulaire d'inscription ([eb81bbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb81bbba9265b9f3a71e500737436ee5301b7a5e)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||||
|
* Ordonnancement de l'inscription sur plusieurs pages + contrôle des ([daad12c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/daad12cf40ce3b628581892f9a894a0841baa5e3))
|
||||||
|
* Oubli fichier [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([d7fca9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d7fca9e942412a8d2fe379f38052f3b41ed9c0f9))
|
||||||
|
* passage des mail au format HTML ([b97cf6e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b97cf6e02b92ba6750662bbf9e9c3af6ad19ab38))
|
||||||
|
* Passage par une variable d'environnement pour les CORS et CSRF ([f9e870e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f9e870e11fe3041be53f4c357427d8060f50199f))
|
||||||
|
* Peuplement de la BDD avec les JSON d'entrée [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([c6d7528](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6d75281a1fab0d9dc27d4da80f91c6fffb1bc0e))
|
||||||
|
* planning events ([c9b0f0d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9b0f0d77a5ec61a239deb71959738f3b0e82d37))
|
||||||
|
* Pre cablage du dashboard [#] ([1911f79](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1911f79f4578f8bc3b455308182c46d2d59e5580))
|
||||||
|
* Préparation de la gestion des compétences en énumérant les élèves ([1c75927](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1c75927bbab497cfc86fc3a9aea11d436318be69)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||||
|
* Preparation des modèles Settings pour l'enregistrement SMTP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([eda6f58](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eda6f587fb21bf041784209228518c8a6f03b1b5))
|
||||||
|
* preparation du dockerfile pour le frontend [[#13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/13)] ([9716373](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9716373fa2d754177d4e71082b9079b71daab971))
|
||||||
|
* Rattachement d'un dossier de compétences à une période scolaire ([7de839e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7de839ee5c9b09f7874575bdaf57436ec11b293f)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||||
|
* Refactoring de la fonction de création de profil sur guardian côté ([753a8d6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/753a8d647ec3e45c8aabecba6d38b1a19741e0c0))
|
||||||
|
* Sauvegarde des compétences d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0513603](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05136035ab1d811c904e35d99ddb884c68b7fd74))
|
||||||
|
* Sauvegarde des fichiers migration ([017c029](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/017c0290dd1fab8afa3a05541e57a321733ff5c9))
|
||||||
|
* Signatures électroniques docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([c8c8941](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c8c8941ec875b541cfb55c3504a0e951f36163ef))
|
||||||
|
* Sortie des calculs des montants totaux de la partie configuration + revue du rendu [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([799e1c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/799e1c6717fceec4b29edfbdd0af52268b7e8fce))
|
||||||
|
* Suite de la gestion des sessions ([8ea68bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8ea68bbad0646d99209d1821a2b71364630005b3))
|
||||||
|
* Suppression de l'ancienne POPUP de RF ([5927e48](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5927e48e6544e8819a29766562107834d44e7a5d))
|
||||||
|
* Suppression des localStorage ([023b46e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/023b46e16e8da56971a8c55c0930e6ab4fbf53ec))
|
||||||
|
* Suppression des templates docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([081dc06](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/081dc060014ee15ca6881fc83b779679a271326d))
|
||||||
|
* Upload du SEPA par les parents / Création d'un composant header ([8417d3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8417d3eb141b116e2e9f8c6038831ce1bbe30e2a))
|
||||||
|
* Utilisation d'une clef API Docuseal par établissement ([23ab7d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23ab7d04ef0c940b7008e8bc7d4b43b373d16d40))
|
||||||
|
* Utilisation de l'établissement en variable de session / gestion de ([f2ad1de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2ad1de5a4215395f3aa7a0e04ac2eb3edc5ec51))
|
||||||
|
* Utilisation des nouvelles alertes dans la page admin de la gestion ([67193a8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67193a8b3601784f37563094f3fdede943523b53))
|
||||||
|
* Validation du dossier d'inscription en affectant l'élève à une ([0f49236](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0f49236965f575f6af17837b9860fa4481227785))
|
||||||
|
|
||||||
|
|
||||||
|
### Corrections de bugs
|
||||||
|
|
||||||
|
* correction des redirections vers la login page ([2e0fe86](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2e0fe86c71e9f02e8ee77ccbd80533a63a31ef63))
|
||||||
|
* Ajout d'un champ is_required pour les documents parents facultatifs ([5866427](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5866427544e24c8e79cb773d38bda683f63f4531))
|
||||||
|
* Ajout d'un message de confirmation lors de la suppression d'un ([9248480](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92484804f6483eab401612315b5513cc78e6a726))
|
||||||
|
* ajout de credential include dans get CSRF ([c161fa7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c161fa7e7568437ba501a565ad53192b9cb3b6f3))
|
||||||
|
* Ajout de l'établissement dans la requête KPI récupérant les ([ada2a44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ada2a44c3ec9ba45462bd7e78984dfa38008e231))
|
||||||
|
* Ajout des niveaux scolaires dans le back [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([05542df](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05542dfc40649fd194ee551f0298f1535753f219))
|
||||||
|
* ajout des urls prod et demo ([043d93d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/043d93dcc476e5eb3962fdbe0f6a81b937122647))
|
||||||
|
* Ajout du % ou € en mode édition de réduction ([f2628bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2628bb45a14da42d014e42b1521820ffeedfb33))
|
||||||
|
* Ajout du controle sur le format des dates ([e538ac3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e538ac3d56294d4e647a38d730168ea567c76f04))
|
||||||
|
* Ajout du mode Visu ([e1c6073](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1c607308c12cf75695e9d4593dc27ebe74e6a4f))
|
||||||
|
* ajustement du handlePhoneChange [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([31fdc61](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/31fdc612b10843ce694b55696f67bd2a80d56769))
|
||||||
|
* Application des périodes à un studentCompetency lors de la création ([d65b171](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d65b171da8a310acca15936a39e44239763c88b9))
|
||||||
|
* application des recommandations linter es pour générer un build de prod ([d1aa8b5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1aa8b54fb71bb946e95a19105f51f7f29c75fda))
|
||||||
|
* Application du formattage sur les fichiers modifiés ([001a5bc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/001a5bc83c0bf54061b2b04967da3fc11e2cd8dc))
|
||||||
|
* boucle inifinie dans UseEffect ([f3c4284](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3c428477879729d36760bb61dac015311c84fec))
|
||||||
|
* Bug lorsqu'on déselectionne un paiementPlan ([d64500f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d64500f4022710423c77d023476065816ecd061d))
|
||||||
|
* build error ([65d5b8c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/65d5b8c424bf0e0a9da1b39500c8252f683725c7))
|
||||||
|
* Calcul du montant total des tarif par RF + affichage des tarifs ([c269b89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c269b89d3d58cc65f254b75f6d713c4fd15f6320)), closes [#26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/26)
|
||||||
|
* calcul nombre de pages dans chaque tab ([5440f5c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5440f5cbdbca8b9435a17914c7e7c4ecc34e6bb3))
|
||||||
|
* Champs requis sur les teachers and classes ([42b4c99](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/42b4c99be86f050ccd76302caf725af5df413d17))
|
||||||
|
* Changement d'icone associé aux documents soumis à validation ([500b6e9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/500b6e9af7ac76dafa35bd830cd0767cece47d27))
|
||||||
|
* code mort ([4fc061f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fc061fc255b4174f794ac58da1b6849419e9f1a))
|
||||||
|
* Condition de validation d'ajout d'un nouveau document parent / ([9e69790](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9e69790683fd83b0e48a9f70150661cb06a7b556))
|
||||||
|
* conflits + closeModal lors de la création d'un RF ([1617b13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1617b132c4f67fdbaf261808a0a9596b7a72a4dc))
|
||||||
|
* coquille ([c9c7e77](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9c7e7715efde8766c3b2ad2c355dc9a9960b19f))
|
||||||
|
* coquille dans les imports ([4ecf25a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4ecf25a6ab90a57da0013f6ed603d6cd5bd4eeeb))
|
||||||
|
* Correction de l'affichage des numéros de téléphone [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([4f774c1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f774c18e47fc57e081022a03ea352638e7211d2))
|
||||||
|
* correction de l'ouverture du dashbord [[#39](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/39)] ([a157d53](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a157d53932d5576fc9768f5c063cf9aafa214d43))
|
||||||
|
* Correction de la désactivation des spécialités lorsqu'on ([afc1632](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/afc1632797c0d35df7da03432eba9ab0f1875f55)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||||
|
* Correction dépendances circulaires ([fc9a1ed](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc9a1ed252e1e115e4a2f7c4a3a04ee6757be683))
|
||||||
|
* Correction des Protected Routes avec multi role ([dd0884b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dd0884bbce6b6549f0f3fca991045f7170889710))
|
||||||
|
* correction des refresh des protected routes [[#36](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/36)] ([839a262](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/839a26257b659a86903d3f982548884cc87366b9))
|
||||||
|
* Correction du Establishment context au refresh ([43e301e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43e301ed641a742323a98c430e30e134babc4aa4))
|
||||||
|
* correction fileGroup lors de l'enregistrement d'un nouveau responsable ([dce2114](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dce2114a7940310e2c4241c2cdbd7e3fd060fb60))
|
||||||
|
* Correction option fusion ([e61cd51](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e61cd51ce2b0665c18f9497e6d2b1f7b8196723e))
|
||||||
|
* Correction sur le calcul du nombre total de pages [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([5946cbd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5946cbdee661527317dac66f99f0abce021c835a))
|
||||||
|
* correction titre mail reset mdp ([cac1519](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cac1519bf311b660831222d76d4d5165ee4f4d7e))
|
||||||
|
* Correction URL ([170f7c4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/170f7c4fa80e1cd40079ac861e7e633c62f143df))
|
||||||
|
* Corrrection typo dans description des tableaux frais/réduction ([175932f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/175932ffa3fb2747cafd158b8142df9b7010a3d4))
|
||||||
|
* csrf ([59a0d40](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59a0d401301fe77226fd5f294a3cd7e589d46fad))
|
||||||
|
* Division par 0 ([a42cf34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a42cf348a0a3cc43c6c6b643b1da158690d67cb8))
|
||||||
|
* double confirmation sur les popup ([677cec1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/677cec1ec2f7a3582327f4747d088c6bccbd2560))
|
||||||
|
* entrypoint access right ([a041ffa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a041ffaee75b74e0d559fb14bc79fbcfae98da14))
|
||||||
|
* faire plaisir à LSO ([9374b00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9374b001c9944cb6af1e10451f0e5f236a7890e8))
|
||||||
|
* formulaire sur toute la larguer + initiation à un autre style de bg ([4fd40ac](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fd40ac5fc90ea1ddda9d73ea290b588074c6e2f))
|
||||||
|
* Fusion documents ([857b8b2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/857b8b26c3722171007399dc66cd9980b33151c5))
|
||||||
|
* Generation d'une fiche d'élève avec le nouveau modèle PayementMode ([4f40d1f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f40d1f29d7abd1e0c6bf889b10f811f184ff10d))
|
||||||
|
* Génération uniquement des compétences évaluées dans le PDF ([eca8d7a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eca8d7a8d59f39313123166859f4c4bf548d150e))
|
||||||
|
* gestion des codes retours ([7f35527](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f3552764979e098ca2e8c3547354c8ae6feaa23))
|
||||||
|
* Gestion des listes d'inscription "vides" [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([edc9724](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/edc97242f2219f48233441b8c7ec97ef9551c60c))
|
||||||
|
* gestion du jour d'échéance ([2576d21](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2576d2173460267664d927bd093580a21c18725b))
|
||||||
|
* import du Loader ([e2a39ff](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e2a39ff74dd9671bb1d00de2b6cec1cd3e4ff614))
|
||||||
|
* inject env var ([fc337b1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc337b1e0b4605f3490435f4819b01d38f921156))
|
||||||
|
* Limite du nombre de responsables légaux à 2 [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([1ced4a1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1ced4a10696057b8df114dc95adf9868e8d7aa43))
|
||||||
|
* Link documents with establishments ([2f6d30b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2f6d30b85b90508cae49081a82eadea5039f60b2))
|
||||||
|
* load the school image eorrectly ([6bc2405](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bc24055cd5a79d59d2b56c7e767ac1b30d99fff))
|
||||||
|
* Lors de la création d'un clone, on prend le nom de l'élève et pas ([db8e1d8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/db8e1d8ab320222370c64d7b7fde3e43c59921e8))
|
||||||
|
* Messages de retour reset/new password ([4a6b7ce](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a6b7ce379747565c77205728e7b0d9c8a7c9585))
|
||||||
|
* Mise à jour correcte du fichier après avoir été signé ([5ea3cbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5ea3cbb0790840d823e799cc64766a99ef5591a9))
|
||||||
|
* Mise à jour des upcomming events ([f93c428](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f93c42825964d91d11af26541eecb9ba5f01e801))
|
||||||
|
* mise à jour settings pour la prod / correction CORS ([25e2799](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/25e2799c0f0e46b1a6d78bcc849cc777e67a01f1))
|
||||||
|
* Mise en page des inscriptions (boutons ajout / barre de recherche) ([cf14431](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cf144310a13fa4cbd01a292002d8a9963acc4598))
|
||||||
|
* Modèle créé 2 fois par erreur ([49907d7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/49907d7ec8847017115191e937ba9f68350c92bd))
|
||||||
|
* Modification d'un guardian sans changer d'adresse mail (même ([95b449d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95b449ddfde4160a55237e0c50e6bed604dcdfe5))
|
||||||
|
* Ne pas dissocier de responsable s'il n'y en a pas d'autre rattaché ([ac0672f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ac0672f3349aa2ac62db0e3927658a3f2d66cebf))
|
||||||
|
* Ne pas retourner d'erreur si pas de dossier d'inscription ([be27fe1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be27fe1232c3808b0a65d5f1b265ef454eb35e74))
|
||||||
|
* Nouvelle amélioration ([8b3f963](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8b3f9637a91fe817c87427015a89ba3e469d525d))
|
||||||
|
* On attend que la session soit mise à jour pour intiialiser le ([ccecd78](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ccecd78704c3e6db58724401b92dd065a7e733ab))
|
||||||
|
* On commence à la page 1 ([3c62cc9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c62cc9ad2cfa691ee798d27ee6b377676e50bb7))
|
||||||
|
* On empêche la sauvegarde d'un document à signer tant qu'aucun ([be013f0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be013f07864345024320114bd734508a033fd5db))
|
||||||
|
* On ne peut sélectionner que les élèves inscrits [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([56c223f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56c223f3cc0498b4a6619d68f0185c36482c4ec9))
|
||||||
|
* Ordre des guardians lors de leur création / déselection correcte si ([3b667d3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3b667d3b150684a685d7d76cf06d050049ee07cd))
|
||||||
|
* pagination annuaire ([980f169](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/980f169c1d1a46f0d47f4b9ff65fa940ac610023))
|
||||||
|
* PieChart ([fe2d4d4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fe2d4d45137df3b1ead4d21c29722fec0bd0fbab))
|
||||||
|
* Positionnement de la variable isSepa ([82573f1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/82573f1b2333a01675cebf575f33ab77e70e138b))
|
||||||
|
* Possibilité d'ajouter un 2ème guardian, même si son mail est ([8cf2290](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8cf22905e533a23ee679107cc0bcae1198badb4a))
|
||||||
|
* Récupération d'un template donné ([9b13d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9b13d52e8d8926bcc6f756ff4d2c9d278a0cc387))
|
||||||
|
* Refresh par profil role ([24069b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24069b894ef2009d9fe0ad884e7a39c29a5a9504))
|
||||||
|
* refresh token ([053d524](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/053d524a513adfec8bd9b3467fc358c257776a85))
|
||||||
|
* régression CORS_ALLOWED_ORIGINS ([a69498d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a69498dd06649b601a16a509c7a80c9f67c7872e))
|
||||||
|
* régression lors de l'uniformisation des modales ([00f7bfd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/00f7bfde4abd10d080dc2035d3607d6c35e7db14))
|
||||||
|
* Remise du message de confirmation supprimé par erreur ([efcc5e6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/efcc5e66722c829dcdf522a6903c616901a14604))
|
||||||
|
* Remise en état du bouton Submit ([e9650c9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e9650c992e6c8339d7acde4000bf4f3dd8e98bac))
|
||||||
|
* Remise en place de l'API_KEY docuseal dans le back ([6d80594](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6d805940fe5524cae1864c0beebcd136bda84eda))
|
||||||
|
* remove lint error ([aef6c19](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aef6c193b1eaaedbb0642ce7929b2cfe8f47d682))
|
||||||
|
* Remplacement des enum par des modèles pour les payementModes et les ([7fe5346](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7fe53465acc2df53351f713ccacd12223d6eff1a))
|
||||||
|
* restore du start.py suite à des tests ([de5f7cd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/de5f7cd41e52b27ee3d8f47cf47fbfdad78216ac))
|
||||||
|
* right ([05f1f16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05f1f16727c2510385425b23fa6ab98fa62d07be))
|
||||||
|
* Scroll de l'emploi du temps élève ([f38a441](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f38a4414c28ec52981201c15b7eda0dccc1f932f))
|
||||||
|
* searchTerm inscription ([8f0cf16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8f0cf16f707ac1cd51f0d619fa1c5ea0ba023f68))
|
||||||
|
* Session storage selectedEstablishmentEvaluationFrequency et selectedEstablishmentTotalCapacity ([e30753f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e30753f1d6d2911d51bb9dfbf32fbae6f2b62b5d))
|
||||||
|
* Suite du commit précédent ([cd9c10a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cd9c10a88af2a05350570c424fb284280c0f65ee))
|
||||||
|
* Suppression d'un profil uniquement s'il ne contient aucun guardian ([330018e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/330018edfdbdc071c15838bc22b8a4e726773204))
|
||||||
|
* Suppression de la top bar admin [[#34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/34)] ([3990d75](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3990d75e521bea007a8f479924507498d9586a71))
|
||||||
|
* Suppression de print inutiles ([43874f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43874f8b9e76b3e9f131f240ee895d958cd73fab))
|
||||||
|
* Suppression event planning ([c117f96](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c117f96e528244ee68ce69b7685880e171976e32))
|
||||||
|
* Unicité des fees + utilisation de l'establishmentID [[#44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/44)] ([d37e6c3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37e6c384d0ef16053ed9fcc1e979f7f902cc8d8))
|
||||||
|
* Uniformisation des Modales et Popup [[#35](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/35)] ([f252efd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f252efdef4fce1f2d19ac7ca1eb9c049706c0d9f))
|
||||||
|
* Utilisation des bonnes colonnes pour les fees et discounts selon si ([9f1f97e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9f1f97e0c56771208305b28a740504c220287053))
|
||||||
|
* Utilisation du signal "post-migrate" pour créer la spécialité par ([e1202c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1202c6e6d4061552fa7d530e3e09b11384843c3))
|
||||||
|
* Variables booléennes par défaut ([6bedf71](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bedf715ccf9bb9bae4f92d735e3d7b714c96849))
|
||||||
|
* variables csrf ([789816e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/789816e9868685e2ae08b536b6b6ada1a6a64595))
|
||||||
|
* warning sur ouverture modale de fichiers ([889a3a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/889a3a48c5c2a3f6cb65de8ede0efbe639408011))
|
||||||
|
|||||||
@ -5,9 +5,6 @@ const pkg = require('./package.json');
|
|||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
|
||||||
const apiUrlObj = new URL(apiUrl);
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
@ -22,9 +19,17 @@ const nextConfig = {
|
|||||||
hostname: 'www.gravatar.com',
|
hostname: 'www.gravatar.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: apiUrlObj.protocol.replace(':', ''),
|
protocol: 'https',
|
||||||
hostname: apiUrlObj.hostname,
|
hostname: 'api.demo.n3wtschool.com',
|
||||||
...(apiUrlObj.port ? { port: apiUrlObj.port } : {}),
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'api.prod.n3wtschool.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: '8080',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -37,14 +42,9 @@ const nextConfig = {
|
|||||||
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
|
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
|
||||||
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
|
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
|
||||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
|
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
|
||||||
DOCUSEAL_API_KEY: process.env.DOCUSEAL_API_KEY,
|
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
source: '/api/documents/:path*',
|
|
||||||
destination: 'https://api.docuseal.com/v1/documents/:path*',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: '/api/auth/:path*',
|
source: '/api/auth/:path*',
|
||||||
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
|
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
|
||||||
|
|||||||
37
Front-End/package-lock.json
generated
37
Front-End/package-lock.json
generated
@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docuseal/react": "^1.0.56",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@ -29,6 +28,7 @@
|
|||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"react-international-phone": "^4.5.0",
|
"react-international-phone": "^4.5.0",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-tooltip": "^5.28.0"
|
"react-tooltip": "^5.28.0"
|
||||||
@ -536,11 +536,6 @@
|
|||||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@docuseal/react": {
|
|
||||||
"version": "1.0.66",
|
|
||||||
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
|
|
||||||
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
|
|
||||||
},
|
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@ -8834,6 +8829,21 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.62.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
|
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-international-phone": {
|
"node_modules/react-international-phone": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
||||||
@ -11253,11 +11263,6 @@
|
|||||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@docuseal/react": {
|
|
||||||
"version": "1.0.66",
|
|
||||||
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
|
|
||||||
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
|
|
||||||
},
|
|
||||||
"@eslint-community/eslint-utils": {
|
"@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@ -17160,6 +17165,12 @@
|
|||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-hook-form": {
|
||||||
|
"version": "7.62.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
|
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-international-phone": {
|
"react-international-phone": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@ -14,7 +14,6 @@
|
|||||||
"test:coverage": "jest --coverage"
|
"test:coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docuseal/react": "^1.0.56",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@ -35,19 +34,20 @@
|
|||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"react-international-phone": "^4.5.0",
|
"react-international-phone": "^4.5.0",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-tooltip": "^5.28.0"
|
"react-tooltip": "^5.28.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.11",
|
"eslint-config-next": "14.2.11",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.14"
|
"tailwindcss": "^3.4.14"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import AcademicResults from '@/components/Grades/AcademicResults';
|
import AcademicResults from '@/components/Grades/AcademicResults';
|
||||||
import Attendance from '@/components/Grades/Attendance';
|
import Attendance from '@/components/Grades/Attendance';
|
||||||
import Remarks from '@/components/Grades/Remarks';
|
import Remarks from '@/components/Grades/Remarks';
|
||||||
@ -9,7 +9,7 @@ import Homeworks from '@/components/Grades/Homeworks';
|
|||||||
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
|
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
|
||||||
import Orientation from '@/components/Grades/Orientation';
|
import Orientation from '@/components/Grades/Orientation';
|
||||||
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||||
@ -29,7 +29,7 @@ import { useClasses } from '@/context/ClassesContext';
|
|||||||
import { Award, FileText } from 'lucide-react';
|
import { Award, FileText } from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import GradeView from '@/components/Grades/GradeView';
|
import GradeView from '@/components/Grades/GradeView';
|
||||||
import {
|
import {
|
||||||
fetchStudentCompetencies,
|
fetchStudentCompetencies,
|
||||||
|
|||||||
@ -36,7 +36,6 @@ export default function DashboardPage() {
|
|||||||
const {
|
const {
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
selectedEstablishmentTotalCapacity,
|
selectedEstablishmentTotalCapacity,
|
||||||
apiDocuseal,
|
|
||||||
} = useEstablishment();
|
} = useEstablishment();
|
||||||
|
|
||||||
const [statusDistribution, setStatusDistribution] = useState([
|
const [statusDistribution, setStatusDistribution] = useState([
|
||||||
@ -165,25 +164,6 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={selectedEstablishmentId} className="p-6">
|
<div key={selectedEstablishmentId} className="p-6">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
|
||||||
apiDocuseal
|
|
||||||
? 'bg-green-100 text-green-700 border border-green-300'
|
|
||||||
: 'bg-red-100 text-red-700 border border-red-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{apiDocuseal ? (
|
|
||||||
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
|
|
||||||
)}
|
|
||||||
{apiDocuseal
|
|
||||||
? 'Clé API Docuseal renseignée'
|
|
||||||
: 'Clé API Docuseal manquante'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Statistiques principales */}
|
{/* Statistiques principales */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Tab from '@/components/Tab';
|
import Tab from '@/components/Tab';
|
||||||
import TabContent from '@/components/TabContent';
|
import TabContent from '@/components/TabContent';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
|
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
fetchSmtpSettings,
|
fetchSmtpSettings,
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import { fetchClasse } from '@/app/actions/schoolAction';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import {
|
import {
|
||||||
fetchAbsences,
|
fetchAbsences,
|
||||||
createAbsences,
|
createAbsences,
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
@ -353,7 +353,6 @@ export default function Page() {
|
|||||||
<FilesGroupsManagement
|
<FilesGroupsManagement
|
||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
selectedEstablishmentId={selectedEstablishmentId}
|
selectedEstablishmentId={selectedEstablishmentId}
|
||||||
apiDocuseal={apiDocuseal}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { User, Mail } from 'lucide-react';
|
import { User, Mail } from 'lucide-react';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
||||||
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
||||||
import SectionTitle from '@/components/SectionTitle';
|
import SectionTitle from '@/components/SectionTitle';
|
||||||
import InputPhone from '@/components/InputPhone';
|
import InputPhone from '@/components/Form/InputPhone';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import RadioList from '@/components/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
@ -35,7 +35,6 @@ import {
|
|||||||
fetchRegistrationFileGroups,
|
fetchRegistrationFileGroups,
|
||||||
fetchRegistrationSchoolFileMasters,
|
fetchRegistrationSchoolFileMasters,
|
||||||
fetchRegistrationParentFileMasters,
|
fetchRegistrationParentFileMasters,
|
||||||
cloneTemplate,
|
|
||||||
createRegistrationSchoolFileTemplate,
|
createRegistrationSchoolFileTemplate,
|
||||||
createRegistrationParentFileTemplate,
|
createRegistrationParentFileTemplate,
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
@ -96,7 +95,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
const { getNiveauLabel } = useClasses();
|
const { getNiveauLabel } = useClasses();
|
||||||
|
|
||||||
const formDataRef = useRef(formData);
|
const formDataRef = useRef(formData);
|
||||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -522,128 +521,23 @@ export default function CreateSubscriptionPage() {
|
|||||||
} else {
|
} else {
|
||||||
// Création du dossier d'inscription
|
// Création du dossier d'inscription
|
||||||
createRegisterForm(data, csrfToken)
|
createRegisterForm(data, csrfToken)
|
||||||
.then((data) => {
|
.then((response) => {
|
||||||
// Clonage des schoolFileTemplates
|
showNotification(
|
||||||
const masters = schoolFileMasters.filter((file) =>
|
"Dossier d'inscription créé avec succès",
|
||||||
file.groups.includes(selectedFileGroup)
|
'success',
|
||||||
|
'Succès'
|
||||||
);
|
);
|
||||||
const parentMasters = parentFileMasters.filter((file) =>
|
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||||
file.groups.includes(selectedFileGroup)
|
})
|
||||||
);
|
|
||||||
|
|
||||||
const clonePromises = masters.map((templateMaster) =>
|
|
||||||
cloneTemplate(
|
|
||||||
templateMaster.id,
|
|
||||||
formDataRef.current.guardianEmail,
|
|
||||||
templateMaster.is_required,
|
|
||||||
selectedEstablishmentId,
|
|
||||||
apiDocuseal
|
|
||||||
)
|
|
||||||
.then((clonedDocument) => {
|
|
||||||
const cloneData = {
|
|
||||||
name: `${templateMaster.name}_${formDataRef.current.studentFirstName}_${formDataRef.current.studentLastName}`,
|
|
||||||
slug: clonedDocument.slug,
|
|
||||||
id: clonedDocument.id,
|
|
||||||
master: templateMaster.id,
|
|
||||||
registration_form: data.student.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return createRegistrationSchoolFileTemplate(
|
|
||||||
cloneData,
|
|
||||||
csrfToken
|
|
||||||
)
|
|
||||||
.then((response) =>
|
|
||||||
logger.debug('Template enregistré avec succès:', response)
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
setIsLoading(false);
|
|
||||||
logger.error(
|
|
||||||
"Erreur lors de l'enregistrement du template:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
showNotification(
|
|
||||||
"Erreur lors de la création du dossier d'inscription",
|
|
||||||
'error',
|
|
||||||
'Erreur',
|
|
||||||
'ERR_ADM_SUB_03'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setIsLoading(false);
|
|
||||||
logger.error('Error during cloning or sending:', error);
|
|
||||||
showNotification(
|
|
||||||
"Erreur lors de la création du dossier d'inscription",
|
|
||||||
'error',
|
|
||||||
'Erreur',
|
|
||||||
'ERR_ADM_SUB_05'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clonage des parentFileTemplates
|
|
||||||
const parentClonePromises = parentMasters.map((parentMaster) => {
|
|
||||||
const parentTemplateData = {
|
|
||||||
master: parentMaster.id,
|
|
||||||
registration_form: data.student.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return createRegistrationParentFileTemplate(
|
|
||||||
parentTemplateData,
|
|
||||||
csrfToken
|
|
||||||
)
|
|
||||||
.then((response) =>
|
|
||||||
logger.debug(
|
|
||||||
'Parent template enregistré avec succès:',
|
|
||||||
response
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
setIsLoading(false);
|
|
||||||
logger.error(
|
|
||||||
"Erreur lors de l'enregistrement du parent template:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
showNotification(
|
|
||||||
"Erreur lors de la création du dossier d'inscription",
|
|
||||||
'error',
|
|
||||||
'Erreur',
|
|
||||||
'ERR_ADM_SUB_02'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attendre que tous les clones soient créés
|
|
||||||
Promise.all([...clonePromises, ...parentClonePromises])
|
|
||||||
.then(() => {
|
|
||||||
// Redirection après succès
|
|
||||||
showNotification(
|
|
||||||
"Dossier d'inscription créé avec succès",
|
|
||||||
'success',
|
|
||||||
'Succès'
|
|
||||||
);
|
|
||||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setIsLoading(false);
|
|
||||||
showNotification(
|
|
||||||
"Erreur lors de la création du dossier d'inscription",
|
|
||||||
'error',
|
|
||||||
'Erreur',
|
|
||||||
'ERR_ADM_SUB_04'
|
|
||||||
);
|
|
||||||
logger.error('Error during cloning or sending:', error);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
logger.error('Erreur lors de la mise à jour du dossier:', error);
|
||||||
showNotification(
|
showNotification(
|
||||||
"Erreur lors de la création du dossier d'inscription",
|
"Erreur lors de la création du dossier d'inscription",
|
||||||
'error',
|
'error',
|
||||||
'Erreur',
|
'Erreur',
|
||||||
'ERR_ADM_SUB_01'
|
'ERR_ADM_SUB_01'
|
||||||
);
|
);
|
||||||
logger.error('Error during register form creation:', error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const [formErrors, setFormErrors] = useState({});
|
const [formErrors, setFormErrors] = useState({});
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (data) => {
|
const handleSubmit = (data) => {
|
||||||
@ -59,7 +59,6 @@ export default function Page() {
|
|||||||
studentId={studentId}
|
studentId={studentId}
|
||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
selectedEstablishmentId={selectedEstablishmentId}
|
selectedEstablishmentId={selectedEstablishmentId}
|
||||||
apiDocuseal = {apiDocuseal}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
|
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
|
||||||
errors={formErrors}
|
errors={formErrors}
|
||||||
|
|||||||
@ -40,8 +40,8 @@ import {
|
|||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { PhoneLabel } from '@/components/PhoneLabel';
|
import { PhoneLabel } from '@/components/Form/PhoneLabel';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import FilesModal from '@/components/Inscription/FilesModal';
|
import FilesModal from '@/components/Inscription/FilesModal';
|
||||||
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
||||||
|
|
||||||
@ -250,7 +250,12 @@ export default function Page({ params: { locale } }) {
|
|||||||
}, 500); // Debounce la recherche
|
}, 500); // Debounce la recherche
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [searchTerm, selectedEstablishmentId, currentSchoolYearPage, itemsPerPage]);
|
}, [
|
||||||
|
searchTerm,
|
||||||
|
selectedEstablishmentId,
|
||||||
|
currentSchoolYearPage,
|
||||||
|
itemsPerPage,
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UseEffect to update page count of tab
|
* UseEffect to update page count of tab
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import Logo from '@/components/Logo'; // Import du composant Logo
|
import Logo from '@/components/Logo'; // Import du composant Logo
|
||||||
|
import FormRenderer from '@/components/Form/FormRenderer';
|
||||||
|
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const t = useTranslations('homePage');
|
const t = useTranslations('homePage');
|
||||||
@ -13,6 +15,7 @@ export default function Home() {
|
|||||||
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
||||||
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
|
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
|
||||||
<Button text={t('loginButton')} primary href="/users/login" />
|
<Button text={t('loginButton')} primary href="/users/login" />
|
||||||
|
<FormTemplateBuilder />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export default function Page() {
|
|||||||
const enable = searchParams.get('enabled') === 'true';
|
const enable = searchParams.get('enabled') === 'true';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (data) => {
|
const handleSubmit = (data) => {
|
||||||
@ -53,7 +53,6 @@ export default function Page() {
|
|||||||
studentId={studentId}
|
studentId={studentId}
|
||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
selectedEstablishmentId={selectedEstablishmentId}
|
selectedEstablishmentId={selectedEstablishmentId}
|
||||||
apiDocuseal = {apiDocuseal}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
cancelUrl={FE_PARENTS_HOME_URL}
|
cancelUrl={FE_PARENTS_HOME_URL}
|
||||||
enable={enable}
|
enable={enable}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
CalendarDays,
|
CalendarDays,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import StatusLabel from '@/components/StatusLabel';
|
import StatusLabel from '@/components/StatusLabel';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||||
import {
|
import {
|
||||||
fetchChildren,
|
fetchChildren,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import React, { useState } from 'react';
|
|||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||||
import Button from '@/components/Button'; // Importez le composant Button
|
import Button from '@/components/Form/Button'; // Importez le composant Button
|
||||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||||
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
||||||
import { login } from '@/app/actions/authAction';
|
import { login } from '@/app/actions/authAction';
|
||||||
@ -35,11 +35,7 @@ export default function Page() {
|
|||||||
logger.debug('Sign In Result', result);
|
logger.debug('Sign In Result', result);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
showNotification(
|
showNotification(result.error, 'error', 'Erreur');
|
||||||
result.error,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
} else {
|
||||||
// On initialise le contexte establishement avec la session
|
// On initialise le contexte establishement avec la session
|
||||||
@ -50,11 +46,7 @@ export default function Page() {
|
|||||||
if (url) {
|
if (url) {
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
showNotification(
|
showNotification('Type de rôle non géré', 'error', 'Erreur');
|
||||||
'Type de rôle non géré',
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { User } from 'lucide-react';
|
import { User } from 'lucide-react';
|
||||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
@ -25,25 +25,13 @@ export default function Page() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
logger.debug('Success:', data);
|
logger.debug('Success:', data);
|
||||||
if (data.message !== '') {
|
if (data.message !== '') {
|
||||||
showNotification(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
data.message,
|
|
||||||
'success',
|
|
||||||
'Succès'
|
|
||||||
);
|
|
||||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||||
} else {
|
} else {
|
||||||
if (data.errorMessage) {
|
if (data.errorMessage) {
|
||||||
showNotification(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
data.errorMessage,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
} else if (data.errorFields) {
|
} else if (data.errorFields) {
|
||||||
showNotification(
|
showNotification(data.errorFields.email, 'error', 'Erreur');
|
||||||
data.errorFields.email,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||||
import { KeySquare } from 'lucide-react';
|
import { KeySquare } from 'lucide-react';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
@ -33,21 +33,12 @@ export default function Page() {
|
|||||||
resetPassword(uuid, data, csrfToken)
|
resetPassword(uuid, data, csrfToken)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.message !== '') {
|
if (data.message !== '') {
|
||||||
|
|
||||||
logger.debug('Success:', data);
|
logger.debug('Success:', data);
|
||||||
showNotification(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
data.message,
|
|
||||||
'success',
|
|
||||||
'Succès'
|
|
||||||
);
|
|
||||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||||
} else {
|
} else {
|
||||||
if (data.errorMessage) {
|
if (data.errorMessage) {
|
||||||
showNotification(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
data.errorMessage,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
} else if (data.errorFields) {
|
} else if (data.errorFields) {
|
||||||
showNotification(
|
showNotification(
|
||||||
data.errorFields.password1 || data.errorFields.password2,
|
data.errorFields.password1 || data.errorFields.password2,
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import InputTextIcon from '@/components/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { User, KeySquare } from 'lucide-react';
|
import { User, KeySquare } from 'lucide-react';
|
||||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
@ -36,22 +36,16 @@ export default function Page() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
logger.debug('Success:', data);
|
logger.debug('Success:', data);
|
||||||
if (data.message !== '') {
|
if (data.message !== '') {
|
||||||
showNotification(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
data.message,
|
|
||||||
'success',
|
|
||||||
'Succès'
|
|
||||||
);
|
|
||||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||||
} else {
|
} else {
|
||||||
if (data.errorMessage) {
|
if (data.errorMessage) {
|
||||||
showNotification(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
data.errorMessage,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
} else if (data.errorFields) {
|
} else if (data.errorFields) {
|
||||||
showNotification(
|
showNotification(
|
||||||
data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
|
data.errorFields.email ||
|
||||||
|
data.errorFields.password1 ||
|
||||||
|
data.errorFields.password2,
|
||||||
'error',
|
'error',
|
||||||
'Erreur'
|
'Erreur'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,11 +3,7 @@ import {
|
|||||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
|
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
|
||||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
|
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
|
||||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
||||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL,
|
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
|
||||||
FE_API_DOCUSEAL_CLONE_URL,
|
|
||||||
FE_API_DOCUSEAL_DOWNLOAD_URL,
|
|
||||||
FE_API_DOCUSEAL_GENERATE_TOKEN,
|
|
||||||
FE_API_DOCUSEAL_DELETE_URL
|
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||||
|
|
||||||
@ -327,62 +323,3 @@ export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// API requests
|
|
||||||
export const removeTemplate = (templateId, selectedEstablishmentId, apiDocuseal) => {
|
|
||||||
return fetch(`${FE_API_DOCUSEAL_DELETE_URL}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
templateId,
|
|
||||||
establishment_id :selectedEstablishmentId,
|
|
||||||
apiDocuseal
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneTemplate = (templateId, email, is_required, selectedEstablishmentId, apiDocuseal) => {
|
|
||||||
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
templateId,
|
|
||||||
email,
|
|
||||||
is_required,
|
|
||||||
establishment_id :selectedEstablishmentId,
|
|
||||||
apiDocuseal
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => {
|
|
||||||
const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`;
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => {
|
|
||||||
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }),
|
|
||||||
})
|
|
||||||
.then(requestResponseHandler)
|
|
||||||
.catch(errorHandler);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
|||||||
import AlertMessage from '@/components/AlertMessage';
|
import AlertMessage from '@/components/AlertMessage';
|
||||||
import RecipientInput from '@/components/RecipientInput';
|
import RecipientInput from '@/components/RecipientInput';
|
||||||
import { useRouter } from 'next/navigation'; // Ajoute cette ligne
|
import { useRouter } from 'next/navigation'; // Ajoute cette ligne
|
||||||
import WisiwigTextArea from '@/components/WisiwigTextArea';
|
import WisiwigTextArea from '@/components/Form/WisiwigTextArea';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
export default function EmailSender({ csrfToken }) {
|
export default function EmailSender({ csrfToken }) {
|
||||||
const [recipients, setRecipients] = useState([]);
|
const [recipients, setRecipients] = useState([]);
|
||||||
|
|||||||
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import InputTextIcon from './InputTextIcon';
|
||||||
|
import SelectChoice from './SelectChoice';
|
||||||
|
import Button from './Button';
|
||||||
|
import IconSelector from './IconSelector';
|
||||||
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
import { FIELD_TYPES } from './FormTypes';
|
||||||
|
|
||||||
|
export default function AddFieldModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
editingField = null,
|
||||||
|
editingIndex = -1,
|
||||||
|
}) {
|
||||||
|
const isEditing = editingIndex >= 0;
|
||||||
|
|
||||||
|
const [currentField, setCurrentField] = useState({
|
||||||
|
id: '',
|
||||||
|
label: '',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
icon: '',
|
||||||
|
options: [],
|
||||||
|
text: '',
|
||||||
|
placeholder: '',
|
||||||
|
acceptTypes: '',
|
||||||
|
maxSize: 5, // 5MB par défaut
|
||||||
|
checked: false,
|
||||||
|
validation: {
|
||||||
|
pattern: '',
|
||||||
|
minLength: '',
|
||||||
|
maxLength: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||||
|
const [newOption, setNewOption] = useState('');
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset, setValue } = useForm();
|
||||||
|
|
||||||
|
// Mettre à jour l'état et les valeurs du formulaire lorsque editingField change
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const defaultValues = editingField || {
|
||||||
|
id: '',
|
||||||
|
label: '',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
icon: '',
|
||||||
|
options: [],
|
||||||
|
text: '',
|
||||||
|
placeholder: '',
|
||||||
|
acceptTypes: '',
|
||||||
|
maxSize: 5,
|
||||||
|
checked: false,
|
||||||
|
validation: {
|
||||||
|
pattern: '',
|
||||||
|
minLength: '',
|
||||||
|
maxLength: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentField(defaultValues);
|
||||||
|
|
||||||
|
// Réinitialiser le formulaire avec les valeurs de l'élément à éditer
|
||||||
|
reset({
|
||||||
|
type: defaultValues.type,
|
||||||
|
label: defaultValues.label,
|
||||||
|
placeholder: defaultValues.placeholder,
|
||||||
|
required: defaultValues.required,
|
||||||
|
icon: defaultValues.icon,
|
||||||
|
text: defaultValues.text,
|
||||||
|
acceptTypes: defaultValues.acceptTypes,
|
||||||
|
maxSize: defaultValues.maxSize,
|
||||||
|
checked: defaultValues.checked,
|
||||||
|
validation: defaultValues.validation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen, editingField, reset]);
|
||||||
|
|
||||||
|
// Ajouter une option au select
|
||||||
|
const addOption = () => {
|
||||||
|
if (newOption.trim()) {
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
options: [...currentField.options, newOption.trim()],
|
||||||
|
});
|
||||||
|
setNewOption('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supprimer une option du select
|
||||||
|
const removeOption = (index) => {
|
||||||
|
const newOptions = currentField.options.filter((_, i) => i !== index);
|
||||||
|
setCurrentField({ ...currentField, options: newOptions });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sélectionner une icône
|
||||||
|
const selectIcon = (iconName) => {
|
||||||
|
setCurrentField({ ...currentField, icon: iconName });
|
||||||
|
|
||||||
|
// Mettre à jour la valeur dans le formulaire
|
||||||
|
const iconField = control._fields.icon;
|
||||||
|
if (iconField && iconField.onChange) {
|
||||||
|
iconField.onChange(iconName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldSubmit = (data) => {
|
||||||
|
onSubmit(data, currentField, editingIndex);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-semibold">
|
||||||
|
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700 text-xl"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(handleFieldSubmit)} className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.type}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<SelectChoice
|
||||||
|
label="Type de champ"
|
||||||
|
name="type"
|
||||||
|
selected={value}
|
||||||
|
callback={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
type: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
choices={FIELD_TYPES}
|
||||||
|
placeHolder="Sélectionner un type"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{![
|
||||||
|
'paragraph',
|
||||||
|
'heading1',
|
||||||
|
'heading2',
|
||||||
|
'heading3',
|
||||||
|
'heading4',
|
||||||
|
'heading5',
|
||||||
|
'heading6',
|
||||||
|
].includes(currentField.type) && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="label"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.label}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label="Label du champ"
|
||||||
|
name="label"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
label: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="placeholder"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.placeholder}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label="Placeholder (optionnel)"
|
||||||
|
name="placeholder"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
placeholder: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Controller
|
||||||
|
name="required"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.required}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="required"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
required: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="required">Champ obligatoire</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(currentField.type === 'text' ||
|
||||||
|
currentField.type === 'email' ||
|
||||||
|
currentField.type === 'date') && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Icône (optionnel)
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="icon"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.icon}
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 flex items-center gap-2 p-3 border border-gray-300 rounded-md bg-gray-50">
|
||||||
|
{currentField.icon &&
|
||||||
|
LucideIcons[currentField.icon] ? (
|
||||||
|
<>
|
||||||
|
{React.createElement(
|
||||||
|
LucideIcons[currentField.icon],
|
||||||
|
{
|
||||||
|
size: 20,
|
||||||
|
className: 'text-gray-600',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
{currentField.icon}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Aucune icône sélectionnée
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
text="Choisir"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowIconPicker(true);
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||||
|
/>
|
||||||
|
{currentField.icon && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
text="✕"
|
||||||
|
onClick={() => {
|
||||||
|
onChange('');
|
||||||
|
setCurrentField({ ...currentField, icon: '' });
|
||||||
|
}}
|
||||||
|
className="px-2 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{[
|
||||||
|
'paragraph',
|
||||||
|
'heading1',
|
||||||
|
'heading2',
|
||||||
|
'heading3',
|
||||||
|
'heading4',
|
||||||
|
'heading5',
|
||||||
|
'heading6',
|
||||||
|
].includes(currentField.type) && (
|
||||||
|
<Controller
|
||||||
|
name="text"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.text}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{currentField.type === 'paragraph'
|
||||||
|
? 'Texte du paragraphe'
|
||||||
|
: 'Texte du titre'}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
text: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'select' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Options de la liste
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newOption}
|
||||||
|
onChange={(e) => setNewOption(e.target.value)}
|
||||||
|
placeholder="Nouvelle option"
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
onKeyPress={(e) =>
|
||||||
|
e.key === 'Enter' && (e.preventDefault(), addOption())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
text="Ajouter"
|
||||||
|
onClick={addOption}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{currentField.options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
||||||
|
>
|
||||||
|
<span>{option}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeOption(index)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'radio' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Options des boutons radio
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newOption}
|
||||||
|
onChange={(e) => setNewOption(e.target.value)}
|
||||||
|
placeholder="Nouvelle option"
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
onKeyPress={(e) =>
|
||||||
|
e.key === 'Enter' && (e.preventDefault(), addOption())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
text="Ajouter"
|
||||||
|
onClick={addOption}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{currentField.options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
||||||
|
>
|
||||||
|
<span>{option}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeOption(index)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'phone' && (
|
||||||
|
<Controller
|
||||||
|
name="validation.pattern"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.validation?.pattern || ''}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label="Format de téléphone (optionnel, exemple: ^\\+?[0-9]{10,15}$)"
|
||||||
|
name="phonePattern"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
validation: {
|
||||||
|
...currentField.validation,
|
||||||
|
pattern: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'file' && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="acceptTypes"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.acceptTypes || ''}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label="Types de fichiers acceptés (ex: .pdf,.jpg,.png)"
|
||||||
|
name="acceptTypes"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
acceptTypes: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="maxSize"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.maxSize || 5}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label="Taille maximale (MB)"
|
||||||
|
name="maxSize"
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
maxSize: parseInt(e.target.value) || 5,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'checkbox' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
<Controller
|
||||||
|
name="checked"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.checked || false}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="defaultChecked"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
checked: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="defaultChecked">Coché par défaut</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
<Controller
|
||||||
|
name="horizontal"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.horizontal || false}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="horizontal"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
horizontal: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="horizontal">Label au-dessus (horizontal)</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentField.type === 'toggle' && (
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
<Controller
|
||||||
|
name="checked"
|
||||||
|
control={control}
|
||||||
|
defaultValue={currentField.checked || false}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="defaultToggled"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
setCurrentField({
|
||||||
|
...currentField,
|
||||||
|
checked: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="defaultToggled">Activé par défaut</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-6">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
text={isEditing ? 'Modifier' : 'Ajouter'}
|
||||||
|
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
text="Annuler"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Sélecteur d'icônes - déplacé en dehors du formulaire */}
|
||||||
|
<IconSelector
|
||||||
|
isOpen={showIconPicker}
|
||||||
|
onClose={() => setShowIconPicker(false)}
|
||||||
|
onSelect={selectIcon}
|
||||||
|
selectedIcon={currentField.icon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ const CheckBox = ({
|
|||||||
handleChange,
|
handleChange,
|
||||||
fieldName,
|
fieldName,
|
||||||
itemLabelFunc = () => null,
|
itemLabelFunc = () => null,
|
||||||
horizontal,
|
horizontal = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
||||||
const isChecked = Array.isArray(formData[fieldName])
|
const isChecked = Array.isArray(formData[fieldName])
|
||||||
@ -22,7 +22,7 @@ const CheckBox = ({
|
|||||||
{horizontal && (
|
{horizontal && (
|
||||||
<label
|
<label
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
className="block text-sm text-center mb-1 font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{itemLabelFunc(item)}
|
{itemLabelFunc(item)}
|
||||||
</label>
|
</label>
|
||||||
@ -40,7 +40,7 @@ const CheckBox = ({
|
|||||||
{!horizontal && (
|
{!horizontal && (
|
||||||
<label
|
<label
|
||||||
htmlFor={`${fieldName}-${item.id}`}
|
htmlFor={`${fieldName}-${item.id}`}
|
||||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{itemLabelFunc(item)}
|
{itemLabelFunc(item)}
|
||||||
</label>
|
</label>
|
||||||
443
Front-End/src/components/Form/FormRenderer.js
Normal file
443
Front-End/src/components/Form/FormRenderer.js
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import logger from '@/utils/logger';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import SelectChoice from './SelectChoice';
|
||||||
|
import InputTextIcon from './InputTextIcon';
|
||||||
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
import Button from './Button';
|
||||||
|
import DjangoCSRFToken from '../DjangoCSRFToken';
|
||||||
|
import WisiwigTextArea from './WisiwigTextArea';
|
||||||
|
import RadioList from './RadioList';
|
||||||
|
import CheckBox from './CheckBox';
|
||||||
|
import ToggleSwitch from './ToggleSwitch';
|
||||||
|
import InputPhone from './InputPhone';
|
||||||
|
import FileUpload from './FileUpload';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Récupère une icône Lucide par son nom.
|
||||||
|
*/
|
||||||
|
export function getIcon(name) {
|
||||||
|
if (Object.keys(LucideIcons).includes(name)) {
|
||||||
|
const Icon = LucideIcons[name];
|
||||||
|
return Icon ?? null;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formConfigTest = {
|
||||||
|
id: 0,
|
||||||
|
title: 'Mon formulaire dynamique',
|
||||||
|
submitLabel: 'Envoyer',
|
||||||
|
fields: [
|
||||||
|
{ id: 'name', label: 'Nom', type: 'text', required: true },
|
||||||
|
{ id: 'email', label: 'Email', type: 'email' },
|
||||||
|
{
|
||||||
|
id: 'email2',
|
||||||
|
label: 'Email',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'Mail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
label: 'Rôle',
|
||||||
|
type: 'select',
|
||||||
|
options: ['Admin', 'Utilisateur', 'Invité'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
text: "Bonjour, Bienvenue dans ce formulaire d'inscription haha",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'birthdate',
|
||||||
|
label: 'Date de naissance',
|
||||||
|
type: 'date',
|
||||||
|
icon: 'Calendar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'textarea',
|
||||||
|
label: 'toto',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormRenderer({
|
||||||
|
formConfig = formConfigTest,
|
||||||
|
csrfToken,
|
||||||
|
onFormSubmit = (data) => {
|
||||||
|
alert(JSON.stringify(data, null, 2));
|
||||||
|
}, // Callback de soumission personnalisé (optionnel)
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
reset,
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
// Fonction utilitaire pour envoyer les données au backend
|
||||||
|
const sendFormDataToBackend = async (formData) => {
|
||||||
|
try {
|
||||||
|
// Cette fonction peut être remplacée par votre propre implémentation
|
||||||
|
// Exemple avec fetch:
|
||||||
|
const response = await fetch('/api/submit-form', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
// Les en-têtes sont automatiquement définis pour FormData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
logger.debug('Envoi réussi:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Erreur lors de l'envoi:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
logger.debug('=== DÉBUT onSubmit ===');
|
||||||
|
logger.debug('Réponses :', data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier si nous avons des fichiers dans les données
|
||||||
|
const hasFiles = Object.keys(data).some((key) => {
|
||||||
|
return (
|
||||||
|
data[key] instanceof FileList ||
|
||||||
|
(data[key] && data[key][0] instanceof File)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasFiles) {
|
||||||
|
// Utiliser FormData pour l'envoi de fichiers
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Ajouter l'ID du formulaire
|
||||||
|
formData.append('formId', formConfig.id.toString());
|
||||||
|
|
||||||
|
// Traiter chaque champ et ses valeurs
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const value = data[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
value instanceof FileList ||
|
||||||
|
(value && value[0] instanceof File)
|
||||||
|
) {
|
||||||
|
// Gérer les champs de type fichier
|
||||||
|
if (value.length > 0) {
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
formData.append(`files.${key}`, value[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Gérer les autres types de champs
|
||||||
|
formData.append(
|
||||||
|
`data.${key}`,
|
||||||
|
value !== undefined ? value.toString() : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onFormSubmit) {
|
||||||
|
// Utiliser le callback personnalisé si fourni
|
||||||
|
await onFormSubmit(formData, true);
|
||||||
|
} else {
|
||||||
|
// Sinon, utiliser la fonction par défaut
|
||||||
|
await sendFormDataToBackend(formData);
|
||||||
|
alert('Formulaire avec fichier(s) envoyé avec succès');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pas de fichier, on peut utiliser JSON
|
||||||
|
const formattedData = {
|
||||||
|
formId: formConfig.id,
|
||||||
|
responses: { ...data },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (onFormSubmit) {
|
||||||
|
// Utiliser le callback personnalisé si fourni
|
||||||
|
await onFormSubmit(formattedData, false);
|
||||||
|
} else {
|
||||||
|
// Afficher un message pour démonstration
|
||||||
|
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(); // Réinitialiser le formulaire après soumission
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la soumission du formulaire:', error);
|
||||||
|
alert(`Erreur lors de l'envoi du formulaire: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('=== FIN onSubmit ===');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (errors) => {
|
||||||
|
logger.error('=== ERREURS DE VALIDATION ===');
|
||||||
|
logger.error('Erreurs :', errors);
|
||||||
|
alert('Erreurs de validation : ' + JSON.stringify(errors, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit, onError)}
|
||||||
|
className="max-w-md mx-auto"
|
||||||
|
>
|
||||||
|
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-4">
|
||||||
|
{formConfig.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{formConfig.fields.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
||||||
|
className="flex flex-col mt-4"
|
||||||
|
>
|
||||||
|
{field.type === 'heading1' && (
|
||||||
|
<h1 className="text-3xl font-bold mb-3">{field.text}</h1>
|
||||||
|
)}
|
||||||
|
{field.type === 'heading2' && (
|
||||||
|
<h2 className="text-2xl font-bold mb-3">{field.text}</h2>
|
||||||
|
)}
|
||||||
|
{field.type === 'heading3' && (
|
||||||
|
<h3 className="text-xl font-bold mb-2">{field.text}</h3>
|
||||||
|
)}
|
||||||
|
{field.type === 'heading4' && (
|
||||||
|
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
|
||||||
|
)}
|
||||||
|
{field.type === 'heading5' && (
|
||||||
|
<h5 className="text-base font-bold mb-1">{field.text}</h5>
|
||||||
|
)}
|
||||||
|
{field.type === 'heading6' && (
|
||||||
|
<h6 className="text-sm font-bold mb-1">{field.text}</h6>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.type === 'paragraph' && <p className="mb-4">{field.text}</p>}
|
||||||
|
|
||||||
|
{(field.type === 'text' ||
|
||||||
|
field.type === 'email' ||
|
||||||
|
field.type === 'date') && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: field.required,
|
||||||
|
pattern: field.validation?.pattern
|
||||||
|
? new RegExp(field.validation.pattern)
|
||||||
|
: undefined,
|
||||||
|
minLength: field.validation?.minLength,
|
||||||
|
maxLength: field.validation?.maxLength,
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<InputTextIcon
|
||||||
|
label={field.label}
|
||||||
|
required={field.required}
|
||||||
|
IconItem={field.icon ? getIcon(field.icon) : null}
|
||||||
|
type={field.type}
|
||||||
|
name={name}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'phone' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<InputPhone
|
||||||
|
label={field.label}
|
||||||
|
required={field.required}
|
||||||
|
name={name}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'select' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<SelectChoice
|
||||||
|
label={field.label}
|
||||||
|
required={field.required}
|
||||||
|
name={name}
|
||||||
|
selected={value || ''}
|
||||||
|
callback={onChange}
|
||||||
|
choices={field.options.map((e) => ({ label: e, value: e }))}
|
||||||
|
placeHolder={`Sélectionner ${field.label.toLowerCase()}`}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'radio' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<RadioList
|
||||||
|
items={field.options.map((option, idx) => ({
|
||||||
|
id: idx,
|
||||||
|
label: option,
|
||||||
|
}))}
|
||||||
|
formData={{
|
||||||
|
[field.id]: value
|
||||||
|
? field.options.findIndex((o) => o === value)
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
handleChange={(e) =>
|
||||||
|
onChange(field.options[parseInt(e.target.value)])
|
||||||
|
}
|
||||||
|
fieldName={field.id}
|
||||||
|
sectionLabel={field.label}
|
||||||
|
required={field.required}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'checkbox' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
defaultValue={field.checked || false}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<div>
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: field.id, label: field.label }}
|
||||||
|
formData={{ [field.id]: value || false }}
|
||||||
|
handleChange={(e) => onChange(e.target.checked)}
|
||||||
|
fieldName={field.id}
|
||||||
|
itemLabelFunc={(item) => item.label}
|
||||||
|
horizontal={field.horizontal || false}
|
||||||
|
/>
|
||||||
|
{errors[field.id] && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'toggle' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
defaultValue={field.checked || false}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<div>
|
||||||
|
<ToggleSwitch
|
||||||
|
name={field.id}
|
||||||
|
label={field.label + (field.required ? ' *' : '')}
|
||||||
|
checked={value || false}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
{errors[field.id] && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'file' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FileUpload
|
||||||
|
selectionMessage={field.label}
|
||||||
|
required={field.required}
|
||||||
|
uploadedFileName={value ? value[0]?.name : null}
|
||||||
|
onFileSelect={(file) => {
|
||||||
|
// Créer un objet de type FileList similaire pour la compatibilité
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
onChange(dataTransfer.files);
|
||||||
|
}}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === 'textarea' && (
|
||||||
|
<Controller
|
||||||
|
name={field.id}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: field.required }}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<WisiwigTextArea
|
||||||
|
label={field.label}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
required={field.required}
|
||||||
|
errorMsg={
|
||||||
|
errors[field.id]
|
||||||
|
? field.required
|
||||||
|
? `${field.label} est requis`
|
||||||
|
: 'Champ invalide'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="form-group-submit mt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
primary
|
||||||
|
text={formConfig.submitLabel ? formConfig.submitLabel : 'Envoyer'}
|
||||||
|
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import InputTextIcon from './InputTextIcon';
|
||||||
|
import FormRenderer from './FormRenderer';
|
||||||
|
import AddFieldModal from './AddFieldModal';
|
||||||
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import {
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
PlusCircle,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
GripVertical,
|
||||||
|
TextCursorInput,
|
||||||
|
AtSign,
|
||||||
|
Calendar,
|
||||||
|
ChevronDown,
|
||||||
|
Type,
|
||||||
|
AlignLeft,
|
||||||
|
Save,
|
||||||
|
ChevronUp,
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
Heading4,
|
||||||
|
Heading5,
|
||||||
|
Heading6,
|
||||||
|
Code,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Phone,
|
||||||
|
Radio,
|
||||||
|
ToggleLeft,
|
||||||
|
CheckSquare,
|
||||||
|
FileUp,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const FIELD_TYPES_ICON = {
|
||||||
|
text: { icon: TextCursorInput },
|
||||||
|
email: { icon: AtSign },
|
||||||
|
phone: { icon: Phone },
|
||||||
|
date: { icon: Calendar },
|
||||||
|
select: { icon: ChevronDown },
|
||||||
|
radio: { icon: Radio },
|
||||||
|
checkbox: { icon: CheckSquare },
|
||||||
|
toggle: { icon: ToggleLeft },
|
||||||
|
file: { icon: FileUp },
|
||||||
|
textarea: { icon: Type },
|
||||||
|
paragraph: { icon: AlignLeft },
|
||||||
|
heading1: { icon: Heading1 },
|
||||||
|
heading2: { icon: Heading2 },
|
||||||
|
heading3: { icon: Heading3 },
|
||||||
|
heading4: { icon: Heading4 },
|
||||||
|
heading5: { icon: Heading5 },
|
||||||
|
heading6: { icon: Heading6 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type d'item pour le drag and drop
|
||||||
|
const ItemTypes = {
|
||||||
|
FIELD: 'field',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Composant pour un champ draggable
|
||||||
|
const DraggableFieldItem = ({
|
||||||
|
field,
|
||||||
|
index,
|
||||||
|
moveField,
|
||||||
|
editField,
|
||||||
|
deleteField,
|
||||||
|
}) => {
|
||||||
|
const ref = React.useRef(null);
|
||||||
|
|
||||||
|
// Configuration du drag (ce qu'on peut déplacer)
|
||||||
|
const [{ isDragging }, drag] = useDrag({
|
||||||
|
type: ItemTypes.FIELD,
|
||||||
|
item: { index },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration du drop (où on peut déposer)
|
||||||
|
const [, drop] = useDrop({
|
||||||
|
accept: ItemTypes.FIELD,
|
||||||
|
hover: (item, monitor) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const hoverIndex = index;
|
||||||
|
|
||||||
|
// Ne rien faire si on survole le même élément
|
||||||
|
if (dragIndex === hoverIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déterminer la position de la souris par rapport à l'élément survolé
|
||||||
|
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||||
|
const hoverMiddleY =
|
||||||
|
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// Ne pas remplacer si on n'a pas dépassé la moitié de l'élément
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effectuer le déplacement
|
||||||
|
moveField(dragIndex, hoverIndex);
|
||||||
|
|
||||||
|
// Mettre à jour l'index de l'élément déplacé
|
||||||
|
item.index = hoverIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combiner drag et drop sur le même élément de référence
|
||||||
|
drag(drop(ref));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`flex items-center justify-between bg-gray-50 p-3 rounded border border-gray-200 ${
|
||||||
|
isDragging ? 'opacity-50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="cursor-move text-gray-400 hover:text-gray-600">
|
||||||
|
<GripVertical size={18} />
|
||||||
|
</div>
|
||||||
|
{FIELD_TYPES_ICON[field.type] &&
|
||||||
|
React.createElement(FIELD_TYPES_ICON[field.type].icon, {
|
||||||
|
size: 18,
|
||||||
|
className: 'text-gray-600',
|
||||||
|
})}
|
||||||
|
<span className="font-medium">
|
||||||
|
{field.type === 'paragraph'
|
||||||
|
? 'Paragraphe'
|
||||||
|
: field.type.startsWith('heading')
|
||||||
|
? `Titre ${field.type.replace('heading', '')}`
|
||||||
|
: field.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
({field.type}){field.required && ' *'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => editField(index)}
|
||||||
|
className="p-1 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Modifier"
|
||||||
|
>
|
||||||
|
<Edit2 size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteField(index)}
|
||||||
|
className="p-1 text-red-500 hover:text-red-700"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormTemplateBuilder() {
|
||||||
|
const [formConfig, setFormConfig] = useState({
|
||||||
|
id: 0,
|
||||||
|
title: 'Nouveau formulaire',
|
||||||
|
submitLabel: 'Envoyer',
|
||||||
|
fields: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveMessage, setSaveMessage] = useState({ type: '', text: '' });
|
||||||
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
|
const [showJsonSection, setShowJsonSection] = useState(false);
|
||||||
|
|
||||||
|
const { reset: resetField } = useForm();
|
||||||
|
|
||||||
|
// Gérer l'affichage du bouton de défilement
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
// Afficher le bouton quand on descend d'au moins 300px
|
||||||
|
setShowScrollButton(window.scrollY > 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter l'écouteur d'événement
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
// Nettoyage de l'écouteur lors du démontage du composant
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fonction pour remonter en haut de la page
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Générer un ID unique pour les champs
|
||||||
|
const generateFieldId = (label) => {
|
||||||
|
return label
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[àáâãäå]/g, 'a')
|
||||||
|
.replace(/[èéêë]/g, 'e')
|
||||||
|
.replace(/[ìíîï]/g, 'i')
|
||||||
|
.replace(/[òóôõö]/g, 'o')
|
||||||
|
.replace(/[ùúûü]/g, 'u')
|
||||||
|
.replace(/[ç]/g, 'c')
|
||||||
|
.replace(/[^a-z0-9]/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter ou modifier un champ
|
||||||
|
const handleFieldSubmit = (data, currentField, editIndex) => {
|
||||||
|
const isHeadingType = data.type.startsWith('heading');
|
||||||
|
const isContentTypeOnly = data.type === 'paragraph' || isHeadingType;
|
||||||
|
|
||||||
|
const fieldData = {
|
||||||
|
...data,
|
||||||
|
id: isContentTypeOnly
|
||||||
|
? undefined
|
||||||
|
: generateFieldId(data.label || 'field'),
|
||||||
|
options: ['select', 'radio'].includes(data.type)
|
||||||
|
? currentField.options
|
||||||
|
: undefined,
|
||||||
|
icon: data.icon || currentField.icon || undefined,
|
||||||
|
placeholder: data.placeholder || undefined,
|
||||||
|
text: isContentTypeOnly ? data.text : undefined,
|
||||||
|
checked: ['checkbox', 'toggle'].includes(data.type)
|
||||||
|
? currentField.checked
|
||||||
|
: undefined,
|
||||||
|
horizontal:
|
||||||
|
data.type === 'checkbox' ? currentField.horizontal : undefined,
|
||||||
|
acceptTypes: data.type === 'file' ? currentField.acceptTypes : undefined,
|
||||||
|
maxSize: data.type === 'file' ? currentField.maxSize : undefined,
|
||||||
|
validation: ['phone', 'email', 'text'].includes(data.type)
|
||||||
|
? currentField.validation
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nettoyer les propriétés undefined
|
||||||
|
Object.keys(fieldData).forEach((key) => {
|
||||||
|
if (fieldData[key] === undefined || fieldData[key] === '') {
|
||||||
|
delete fieldData[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFields = [...formConfig.fields];
|
||||||
|
|
||||||
|
if (editIndex >= 0) {
|
||||||
|
newFields[editIndex] = fieldData;
|
||||||
|
} else {
|
||||||
|
newFields.push(fieldData);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormConfig({ ...formConfig, fields: newFields });
|
||||||
|
setEditingIndex(-1);
|
||||||
|
}; // Modifier un champ existant
|
||||||
|
const editField = (index) => {
|
||||||
|
setEditingIndex(index);
|
||||||
|
setShowAddFieldModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supprimer un champ
|
||||||
|
const deleteField = (index) => {
|
||||||
|
const newFields = formConfig.fields.filter((_, i) => i !== index);
|
||||||
|
setFormConfig({ ...formConfig, fields: newFields });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Déplacer un champ
|
||||||
|
const moveField = (dragIndex, hoverIndex) => {
|
||||||
|
const newFields = [...formConfig.fields];
|
||||||
|
const draggedField = newFields[dragIndex];
|
||||||
|
|
||||||
|
// Supprimer l'élément déplacé
|
||||||
|
newFields.splice(dragIndex, 1);
|
||||||
|
|
||||||
|
// Insérer l'élément à sa nouvelle position
|
||||||
|
newFields.splice(hoverIndex, 0, draggedField);
|
||||||
|
|
||||||
|
setFormConfig({ ...formConfig, fields: newFields });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exporter le JSON
|
||||||
|
const exportJson = () => {
|
||||||
|
const jsonString = JSON.stringify(formConfig, null, 2);
|
||||||
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `formulaire_${formConfig.title.replace(/\s+/g, '_').toLowerCase()}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Importer un JSON
|
||||||
|
const importJson = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const imported = JSON.parse(e.target.result);
|
||||||
|
setFormConfig(imported);
|
||||||
|
} catch (error) {
|
||||||
|
alert('Erreur lors de l'importation du fichier JSON');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sauvegarder le formulaire (pour le backend)
|
||||||
|
const saveFormTemplate = async () => {
|
||||||
|
// Validation basique
|
||||||
|
if (!formConfig.title.trim()) {
|
||||||
|
setSaveMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Le titre du formulaire est requis',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formConfig.fields.length === 0) {
|
||||||
|
setSaveMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Ajoutez au moins un champ au formulaire',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
setSaveMessage({ type: '', text: '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulation d'envoi au backend (à remplacer par l'appel API réel)
|
||||||
|
// const response = await fetch('/api/form-templates', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify(formConfig),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error('Erreur lors de l\'enregistrement du formulaire');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const data = await response.json();
|
||||||
|
|
||||||
|
// Simulation d'une réponse du backend
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
setSaveMessage({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Formulaire enregistré avec succès',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si le backend renvoie un ID, on peut mettre à jour l'ID du formulaire
|
||||||
|
// setFormConfig({ ...formConfig, id: data.id });
|
||||||
|
} catch (error) {
|
||||||
|
setSaveMessage({
|
||||||
|
type: 'error',
|
||||||
|
text:
|
||||||
|
error.message || "Une erreur est survenue lors de l'enregistrement",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<div className="max-w-6xl mx-auto p-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||||
|
{/* Panel de configuration */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
showJsonSection
|
||||||
|
? 'lg:col-span-3 space-y-6'
|
||||||
|
: 'lg:col-span-5 space-y-6'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h2 className="text-xl font-bold mb-6">
|
||||||
|
Configuration du formulaire
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Configuration générale */}
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold mr-4">
|
||||||
|
Paramètres généraux
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowJsonSection(!showJsonSection)}
|
||||||
|
className="px-4 py-2 rounded-md inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-600 text-white"
|
||||||
|
title={
|
||||||
|
showJsonSection ? 'Masquer le JSON' : 'Afficher le JSON'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showJsonSection ? (
|
||||||
|
<EyeOff size={18} />
|
||||||
|
) : (
|
||||||
|
<Eye size={18} />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{showJsonSection ? 'Masquer JSON' : 'Afficher JSON'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveFormTemplate}
|
||||||
|
disabled={saving}
|
||||||
|
className={`px-4 py-2 rounded-md inline-flex items-center gap-2 ${
|
||||||
|
saving
|
||||||
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
|
}`}
|
||||||
|
title="Enregistrer le formulaire"
|
||||||
|
>
|
||||||
|
<Save size={18} />
|
||||||
|
<span>
|
||||||
|
{saving ? 'Enregistrement...' : 'Enregistrer'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{saveMessage.text && (
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded ${
|
||||||
|
saveMessage.type === 'error'
|
||||||
|
? 'bg-red-100 text-red-700'
|
||||||
|
: 'bg-green-100 text-green-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{saveMessage.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InputTextIcon
|
||||||
|
label="Titre du formulaire"
|
||||||
|
name="title"
|
||||||
|
value={formConfig.title}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormConfig({ ...formConfig, title: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputTextIcon
|
||||||
|
label="Texte du bouton de soumission du formulaire"
|
||||||
|
name="submitLabel"
|
||||||
|
value={formConfig.submitLabel}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormConfig({
|
||||||
|
...formConfig,
|
||||||
|
submitLabel: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des champs */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold mr-4">
|
||||||
|
Champs du formulaire ({formConfig.fields.length})
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingIndex(-1);
|
||||||
|
setShowAddFieldModal(true);
|
||||||
|
}}
|
||||||
|
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
|
||||||
|
title="Ajouter un champ"
|
||||||
|
>
|
||||||
|
<PlusCircle size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formConfig.fields.length === 0 ? (
|
||||||
|
<div className="text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
||||||
|
<p className="text-gray-500 italic mb-4">
|
||||||
|
Aucun champ ajouté
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingIndex(-1);
|
||||||
|
setShowAddFieldModal(true);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<PlusCircle size={18} />
|
||||||
|
<span>Ajouter mon premier champ</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formConfig.fields.map((field, index) => (
|
||||||
|
<DraggableFieldItem
|
||||||
|
key={index}
|
||||||
|
field={field}
|
||||||
|
index={index}
|
||||||
|
moveField={moveField}
|
||||||
|
editField={editField}
|
||||||
|
deleteField={deleteField}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="mt-6">
|
||||||
|
{/* Les actions ont été déplacées dans la section JSON généré */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JSON généré */}
|
||||||
|
{showJsonSection && (
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow h-full">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold mr-4">JSON généré</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={exportJson}
|
||||||
|
className="p-2 bg-purple-500 text-white rounded-md hover:bg-purple-600 transition-colors"
|
||||||
|
title="Exporter JSON"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
</button>
|
||||||
|
<label
|
||||||
|
className="p-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 cursor-pointer transition-colors"
|
||||||
|
title="Importer JSON"
|
||||||
|
>
|
||||||
|
<Upload size={18} />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={importJson}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">
|
||||||
|
{JSON.stringify(formConfig, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aperçu */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Aperçu du formulaire</h3>
|
||||||
|
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
|
||||||
|
{formConfig.fields.length > 0 ? (
|
||||||
|
<FormRenderer formConfig={formConfig} />
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 italic text-center">
|
||||||
|
Ajoutez des champs pour voir l'aperçu
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal d'ajout/modification de champ */}
|
||||||
|
<AddFieldModal
|
||||||
|
isOpen={showAddFieldModal}
|
||||||
|
onClose={() => setShowAddFieldModal(false)}
|
||||||
|
onSubmit={handleFieldSubmit}
|
||||||
|
editingField={
|
||||||
|
editingIndex >= 0 ? formConfig.fields[editingIndex] : null
|
||||||
|
}
|
||||||
|
editingIndex={editingIndex}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bouton flottant pour remonter en haut */}
|
||||||
|
{showScrollButton && (
|
||||||
|
<div className="fixed bottom-6 right-6 z-10">
|
||||||
|
<button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="p-4 rounded-full shadow-lg flex items-center justify-center bg-gray-500 hover:bg-gray-600 text-white transition-all duration-300"
|
||||||
|
title="Remonter en haut de la page"
|
||||||
|
>
|
||||||
|
<ChevronUp size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
Front-End/src/components/Form/FormTypes.js
Normal file
19
Front-End/src/components/Form/FormTypes.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export const FIELD_TYPES = [
|
||||||
|
{ value: 'text', label: 'Texte' },
|
||||||
|
{ value: 'email', label: 'Email' },
|
||||||
|
{ value: 'phone', label: 'Téléphone' },
|
||||||
|
{ value: 'date', label: 'Date' },
|
||||||
|
{ value: 'select', label: 'Liste déroulante' },
|
||||||
|
{ value: 'radio', label: 'Boutons radio' },
|
||||||
|
{ value: 'checkbox', label: 'Case à cocher' },
|
||||||
|
{ value: 'toggle', label: 'Interrupteur' },
|
||||||
|
{ value: 'file', label: 'Upload de fichier' },
|
||||||
|
{ value: 'textarea', label: 'Zone de texte riche' },
|
||||||
|
{ value: 'paragraph', label: 'Paragraphe' },
|
||||||
|
{ value: 'heading1', label: 'Titre 1' },
|
||||||
|
{ value: 'heading2', label: 'Titre 2' },
|
||||||
|
{ value: 'heading3', label: 'Titre 3' },
|
||||||
|
{ value: 'heading4', label: 'Titre 4' },
|
||||||
|
{ value: 'heading5', label: 'Titre 5' },
|
||||||
|
{ value: 'heading6', label: 'Titre 6' },
|
||||||
|
];
|
||||||
145
Front-End/src/components/Form/IconSelector.js
Normal file
145
Front-End/src/components/Form/IconSelector.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
|
export default function IconSelector({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
selectedIcon = '',
|
||||||
|
}) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const excludedKeys = new Set([
|
||||||
|
'Icon',
|
||||||
|
'DynamicIcon',
|
||||||
|
'createLucideIcon',
|
||||||
|
'default',
|
||||||
|
'icons',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allIcons = Object.keys(LucideIcons).filter((key) => {
|
||||||
|
// Exclure les utilitaires
|
||||||
|
if (excludedKeys.has(key)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredIcons = useMemo(() => {
|
||||||
|
if (!searchTerm) return allIcons;
|
||||||
|
return allIcons.filter((iconName) =>
|
||||||
|
iconName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [searchTerm, allIcons]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const selectIcon = (iconName) => {
|
||||||
|
onSelect(iconName);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
Choisir une icône ({filteredIcons.length} / {allIcons.length}{' '}
|
||||||
|
icônes)
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700 text-xl"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barre de recherche */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher une icône..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<LucideIcons.Search
|
||||||
|
className="absolute left-3 top-3.5 text-gray-400"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchTerm('')}
|
||||||
|
className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<LucideIcons.X size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||||
|
{filteredIcons.map((iconName) => {
|
||||||
|
try {
|
||||||
|
const IconComponent = LucideIcons[iconName];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={iconName}
|
||||||
|
onClick={() => selectIcon(iconName)}
|
||||||
|
className={`
|
||||||
|
p-5 rounded-lg border-2 transition-all duration-200
|
||||||
|
hover:bg-blue-50 hover:border-blue-300 hover:shadow-md hover:scale-105
|
||||||
|
flex flex-col items-center justify-center gap-4 min-h-[140px] w-full
|
||||||
|
${
|
||||||
|
selectedIcon === iconName
|
||||||
|
? 'bg-blue-100 border-blue-500 shadow-md scale-105'
|
||||||
|
: 'bg-gray-50 border-gray-200'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={iconName}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
size={32}
|
||||||
|
className="text-gray-700 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-600 text-center leading-tight break-words px-1 overflow-hidden max-w-full">
|
||||||
|
{iconName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// En cas d'erreur avec une icône spécifique, ne pas la rendre
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-between items-center">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{searchTerm ? (
|
||||||
|
<>
|
||||||
|
{filteredIcons.length} icône(s) trouvée(s) sur {allIcons.length}{' '}
|
||||||
|
disponibles
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Total : {allIcons.length} icônes disponibles</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
text="Aucune icône"
|
||||||
|
onClick={() => selectIcon('')}
|
||||||
|
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Annuler"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default function InputTextIcon({
|
export default function InputTextIcon({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@ -31,9 +33,11 @@ export default function InputTextIcon({
|
|||||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
{IconItem ? (
|
||||||
{IconItem && <IconItem />}
|
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||||
</span>
|
<IconItem />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
id={name}
|
id={name}
|
||||||
@ -4,10 +4,10 @@ import 'react-quill/dist/quill.snow.css';
|
|||||||
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
|
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
|
||||||
|
|
||||||
export default function WisiwigTextArea({
|
export default function WisiwigTextArea({
|
||||||
label = 'Mail',
|
label = 'Zone de Texte',
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = 'Ecrivez votre mail ici...',
|
placeholder = 'Ecrivez votre texte ici...',
|
||||||
className = 'h-64',
|
className = 'h-64',
|
||||||
required = false,
|
required = false,
|
||||||
errorMsg,
|
errorMsg,
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react';
|
import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react';
|
||||||
import RadioList from '@/components/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
|
|
||||||
const LEVELS = [
|
const LEVELS = [
|
||||||
{ value: 0, label: 'Non évalué' },
|
{ value: 0, label: 'Non évalué' },
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import { Upload, Eye, Trash2, FileText } from 'lucide-react';
|
import { Upload, Eye, Trash2, FileText } from 'lucide-react';
|
||||||
import { BASE_URL } from '@/utils/Url';
|
import { BASE_URL } from '@/utils/Url';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Import des dépendances nécessaires
|
// Import des dépendances nécessaires
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import {
|
import {
|
||||||
fetchSchoolFileTemplatesFromRegistrationFiles,
|
fetchSchoolFileTemplatesFromRegistrationFiles,
|
||||||
@ -220,9 +220,7 @@ export default function InscriptionFormShared({
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setProfiles(data);
|
setProfiles(data);
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) => logger.error('Error fetching profiles : ', error));
|
||||||
logger.error('Error fetching profiles : ', error)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
// Fetch data for registration payment modes
|
// Fetch data for registration payment modes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import RadioList from '@/components/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
export default function PaymentMethodSelector({
|
export default function PaymentMethodSelector({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import InputPhone from '@/components/InputPhone';
|
import InputPhone from '@/components/Form/InputPhone';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Trash2, Plus, Users } from 'lucide-react';
|
import { Trash2, Plus, Users } from 'lucide-react';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Trash2, Plus, Users } from 'lucide-react';
|
import { Trash2, Plus, Users } from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import { fetchRegisterForm } from '@/app/actions/subscriptionAction';
|
import { fetchRegisterForm } from '@/app/actions/subscriptionAction';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { User } from 'lucide-react';
|
import { User } from 'lucide-react';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import { BASE_URL } from '@/utils/Url';
|
import { BASE_URL } from '@/utils/Url';
|
||||||
import { levels, genders } from '@/utils/constants';
|
import { levels, genders } from '@/utils/constants';
|
||||||
|
|
||||||
@ -112,13 +112,10 @@ export default function StudentInfoForm({
|
|||||||
(field === 'birth_place' &&
|
(field === 'birth_place' &&
|
||||||
(!formData.birth_place || formData.birth_place.trim() === '')) ||
|
(!formData.birth_place || formData.birth_place.trim() === '')) ||
|
||||||
(field === 'birth_postal_code' &&
|
(field === 'birth_postal_code' &&
|
||||||
(
|
(!formData.birth_postal_code ||
|
||||||
!formData.birth_postal_code ||
|
|
||||||
String(formData.birth_postal_code).trim() === '' ||
|
String(formData.birth_postal_code).trim() === '' ||
|
||||||
isNaN(Number(formData.birth_postal_code)) ||
|
isNaN(Number(formData.birth_postal_code)) ||
|
||||||
!Number.isInteger(Number(formData.birth_postal_code))
|
!Number.isInteger(Number(formData.birth_postal_code)))) ||
|
||||||
)
|
|
||||||
) ||
|
|
||||||
(field === 'address' &&
|
(field === 'address' &&
|
||||||
(!formData.address || formData.address.trim() === '')) ||
|
(!formData.address || formData.address.trim() === '')) ||
|
||||||
(field === 'attending_physician' &&
|
(field === 'attending_physician' &&
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import { BASE_URL } from '@/utils/Url';
|
import { BASE_URL } from '@/utils/Url';
|
||||||
import {
|
import {
|
||||||
fetchSchoolFileTemplatesFromRegistrationFiles,
|
fetchSchoolFileTemplatesFromRegistrationFiles,
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { School, CheckCircle, Hourglass, FileText } from 'lucide-react';
|
import { School, CheckCircle, Hourglass, FileText } from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
export default function ValidateSubscription({
|
export default function ValidateSubscription({
|
||||||
studentId,
|
studentId,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Table from '@/components/Table';
|
|||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
|
|
||||||
const paymentPlansOptions = [
|
const paymentPlansOptions = [
|
||||||
{ id: 1, name: '1 fois', frequency: 1 },
|
{ id: 1, name: '1 fois', frequency: 1 },
|
||||||
|
|||||||
@ -24,8 +24,7 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
|||||||
setSelectedEstablishmentEvaluationFrequency,
|
setSelectedEstablishmentEvaluationFrequency,
|
||||||
setSelectedEstablishmentTotalCapacity,
|
setSelectedEstablishmentTotalCapacity,
|
||||||
selectedEstablishmentLogo,
|
selectedEstablishmentLogo,
|
||||||
setSelectedEstablishmentLogo,
|
setSelectedEstablishmentLogo
|
||||||
setApiDocuseal
|
|
||||||
} = useEstablishment();
|
} = useEstablishment();
|
||||||
const { isConnected, connectionStatus } = useChatConnection();
|
const { isConnected, connectionStatus } = useChatConnection();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@ -41,8 +40,6 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
|||||||
user.roles[roleId].establishment__total_capacity;
|
user.roles[roleId].establishment__total_capacity;
|
||||||
const establishmentLogo =
|
const establishmentLogo =
|
||||||
user.roles[roleId].establishment__logo;
|
user.roles[roleId].establishment__logo;
|
||||||
const establishmentApiDocuseal =
|
|
||||||
user.roles[roleId].establishment__api_docuseal;
|
|
||||||
setProfileRole(role);
|
setProfileRole(role);
|
||||||
setSelectedEstablishmentId(establishmentId);
|
setSelectedEstablishmentId(establishmentId);
|
||||||
setSelectedEstablishmentEvaluationFrequency(
|
setSelectedEstablishmentEvaluationFrequency(
|
||||||
@ -50,7 +47,6 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
|||||||
);
|
);
|
||||||
setSelectedEstablishmentTotalCapacity(establishmentTotalCapacity);
|
setSelectedEstablishmentTotalCapacity(establishmentTotalCapacity);
|
||||||
setSelectedEstablishmentLogo(establishmentLogo);
|
setSelectedEstablishmentLogo(establishmentLogo);
|
||||||
setApiDocuseal(establishmentApiDocuseal);
|
|
||||||
setSelectedRoleId(roleId);
|
setSelectedRoleId(roleId);
|
||||||
if (onRoleChange) {
|
if (onRoleChange) {
|
||||||
onRoleChange(roleId);
|
onRoleChange(roleId);
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import React, { useState, useRef, useCallback } from 'react';
|
|||||||
import TreeView from '@/components/Structure/Competencies/TreeView';
|
import TreeView from '@/components/Structure/Competencies/TreeView';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { Award, CheckCircle } from 'lucide-react';
|
import { Award, CheckCircle } from 'lucide-react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import {
|
import {
|
||||||
fetchEstablishmentCompetencies,
|
fetchEstablishmentCompetencies,
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
|
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
|
||||||
import MultiSelect from '@/components/MultiSelect';
|
import MultiSelect from '@/components/Form/MultiSelect';
|
||||||
import LevelLabel from '@/components/CustomLabels/LevelLabel';
|
import LevelLabel from '@/components/CustomLabels/LevelLabel';
|
||||||
import { DndProvider, useDrop } from 'react-dnd';
|
import { DndProvider, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
|
import InputTextWithColorIcon from '@/components/Form/InputTextWithColorIcon';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
|
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
import TeacherItem from './TeacherItem';
|
import TeacherItem from './TeacherItem';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|||||||
@ -2,12 +2,9 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
fetchRegistrationFileGroups,
|
fetchRegistrationFileGroups,
|
||||||
createRegistrationSchoolFileTemplate,
|
createRegistrationSchoolFileTemplate,
|
||||||
cloneTemplate,
|
|
||||||
generateToken,
|
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
import { DocusealBuilder } from '@docuseal/react';
|
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
|
import MultiSelect from '@/components/Form/MultiSelect'; // Import du composant MultiSelect
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
@ -19,18 +16,13 @@ export default function FileUploadDocuSeal({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
}) {
|
}) {
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
const [token, setToken] = useState(null);
|
|
||||||
const [templateMaster, setTemplateMaster] = useState(null);
|
const [templateMaster, setTemplateMaster] = useState(null);
|
||||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||||
const [selectedGroups, setSelectedGroups] = useState([]);
|
const [selectedGroups, setSelectedGroups] = useState([]);
|
||||||
const [guardianDetails, setGuardianDetails] = useState([]);
|
|
||||||
|
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [popupMessage, setPopupMessage] = useState('');
|
const [popupMessage, setPopupMessage] = useState('');
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const { selectedEstablishmentId, user } = useEstablishment();
|
||||||
|
|
||||||
const { selectedEstablishmentId, user, apiDocuseal } = useEstablishment();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
|
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
|
||||||
@ -47,31 +39,10 @@ export default function FileUploadDocuSeal({
|
|||||||
if (!user && !user?.email) {
|
if (!user && !user?.email) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = fileToEdit ? fileToEdit.id : null;
|
|
||||||
|
|
||||||
generateToken(user?.email, id, selectedEstablishmentId, apiDocuseal)
|
|
||||||
.then((data) => {
|
|
||||||
setToken(data.token);
|
|
||||||
})
|
|
||||||
.catch((error) =>
|
|
||||||
logger.error('Erreur lors de la génération du token:', error)
|
|
||||||
);
|
|
||||||
}, [fileToEdit]);
|
}, [fileToEdit]);
|
||||||
|
|
||||||
const handleGroupChange = (selectedGroups) => {
|
const handleGroupChange = (selectedGroups) => {
|
||||||
setSelectedGroups(selectedGroups);
|
setSelectedGroups(selectedGroups);
|
||||||
|
|
||||||
const details = selectedGroups.flatMap((group) =>
|
|
||||||
group.registration_forms.flatMap((form) =>
|
|
||||||
form.guardians.map((guardian) => ({
|
|
||||||
email: guardian.associated_profile_email,
|
|
||||||
last_name: form.last_name,
|
|
||||||
first_name: form.first_name,
|
|
||||||
registration_form: form.student_id,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setGuardianDetails(details); // Mettre à jour la variable d'état avec les détails des guardians
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoad = (detail) => {
|
const handleLoad = (detail) => {
|
||||||
@ -118,39 +89,6 @@ export default function FileUploadDocuSeal({
|
|||||||
id: templateMaster?.id,
|
id: templateMaster?.id,
|
||||||
is_required: is_required,
|
is_required: is_required,
|
||||||
});
|
});
|
||||||
|
|
||||||
guardianDetails.forEach((guardian, index) => {
|
|
||||||
logger.debug('creation du clone avec required : ', is_required);
|
|
||||||
cloneTemplate(templateMaster?.id, guardian.email, is_required, selectedEstablishmentId, apiDocuseal)
|
|
||||||
.then((clonedDocument) => {
|
|
||||||
// Sauvegarde des schoolFileTemplates clonés dans la base de données
|
|
||||||
const data = {
|
|
||||||
name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`,
|
|
||||||
slug: clonedDocument.slug,
|
|
||||||
id: clonedDocument.id,
|
|
||||||
master: templateMaster?.id,
|
|
||||||
registration_form: guardian.registration_form,
|
|
||||||
};
|
|
||||||
logger.debug('creation : ', data);
|
|
||||||
createRegistrationSchoolFileTemplate(data, csrfToken)
|
|
||||||
.then((response) => {
|
|
||||||
logger.debug('Template enregistré avec succès:', response);
|
|
||||||
onSuccess();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
"Erreur lors de l'enregistrement du template:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logique pour envoyer chaque template au submitter
|
|
||||||
logger.debug('Sending template to:', guardian.email);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error during cloning or sending:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -206,32 +144,7 @@ export default function FileUploadDocuSeal({
|
|||||||
|
|
||||||
{/* Zone de configuration des documents */}
|
{/* Zone de configuration des documents */}
|
||||||
<div className="col-span-8 bg-white p-6 rounded-lg shadow-md border border-gray-200">
|
<div className="col-span-8 bg-white p-6 rounded-lg shadow-md border border-gray-200">
|
||||||
{token && (
|
|
||||||
<div className="h-full overflow-auto">
|
|
||||||
{/* Description de l'étape */}
|
|
||||||
<p className="text-gray-700 text-base font-medium mb-4">
|
|
||||||
Étape 2 - Sélectionnez un document
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<DocusealBuilder
|
|
||||||
token={token}
|
|
||||||
headers={{
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
}}
|
|
||||||
withSendButton={false}
|
|
||||||
withSignYourselfButton={false}
|
|
||||||
autosave={false}
|
|
||||||
withDocumentsList={false}
|
|
||||||
language={'fr'}
|
|
||||||
onLoad={handleLoad}
|
|
||||||
onUpload={handleUpload}
|
|
||||||
onChange={handleChange}
|
|
||||||
onSave={handleSubmit}
|
|
||||||
className="h-full overflow-auto"
|
|
||||||
style={{ maxHeight: '65vh' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Download, Edit3, Trash2, FolderPlus, Signature, AlertTriangle } from 'lucide-react';
|
import { Download, Edit3, Trash2, FolderPlus, Signature, AlertTriangle } from 'lucide-react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
|
|
||||||
import { BASE_URL } from '@/utils/Url';
|
import { BASE_URL } from '@/utils/Url';
|
||||||
import {
|
import {
|
||||||
// GET
|
// GET
|
||||||
@ -21,9 +20,7 @@ import {
|
|||||||
// DELETE
|
// DELETE
|
||||||
deleteRegistrationFileGroup,
|
deleteRegistrationFileGroup,
|
||||||
deleteRegistrationSchoolFileMaster,
|
deleteRegistrationSchoolFileMaster,
|
||||||
deleteRegistrationParentFileMaster,
|
deleteRegistrationParentFileMaster
|
||||||
|
|
||||||
removeTemplate
|
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
|
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
@ -33,11 +30,11 @@ import Popup from '@/components/Popup';
|
|||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
import AlertMessage from '@/components/AlertMessage';
|
||||||
|
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
|
||||||
|
|
||||||
export default function FilesGroupsManagement({
|
export default function FilesGroupsManagement({
|
||||||
csrfToken,
|
csrfToken,
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId
|
||||||
apiDocuseal
|
|
||||||
}) {
|
}) {
|
||||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||||
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
||||||
@ -116,72 +113,24 @@ export default function FilesGroupsManagement({
|
|||||||
);
|
);
|
||||||
setRemovePopupOnConfirm(() => () => {
|
setRemovePopupOnConfirm(() => () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Supprimer les clones associés via l'API DocuSeal
|
// Supprimer le template master de la base de données
|
||||||
const removeClonesPromises = [
|
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
|
||||||
...schoolFileTemplates
|
.then((response) => {
|
||||||
.filter((template) => template.master === templateMaster.id)
|
if (response.ok) {
|
||||||
.map((template) =>
|
setSchoolFileMasters(
|
||||||
removeTemplate(template.id, selectedEstablishmentId, apiDocuseal)
|
schoolFileMasters.filter(
|
||||||
),
|
(fichier) => fichier.id !== templateMaster.id
|
||||||
removeTemplate(templateMaster.id, selectedEstablishmentId, apiDocuseal),
|
)
|
||||||
];
|
);
|
||||||
|
showNotification(
|
||||||
|
`Le document "${templateMaster.name}" a été correctement supprimé.`,
|
||||||
|
'success',
|
||||||
|
'Succès'
|
||||||
|
);
|
||||||
|
|
||||||
// Attendre que toutes les suppressions dans DocuSeal soient terminées
|
setRemovePopupVisible(false);
|
||||||
Promise.all(removeClonesPromises)
|
setIsLoading(false);
|
||||||
.then((responses) => {
|
} else {
|
||||||
const allSuccessful = responses.every((response) => response && response.id);
|
|
||||||
if (allSuccessful) {
|
|
||||||
logger.debug('Master et clones supprimés avec succès de DocuSeal.');
|
|
||||||
|
|
||||||
// Supprimer le template master de la base de données
|
|
||||||
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setSchoolFileMasters(
|
|
||||||
schoolFileMasters.filter(
|
|
||||||
(fichier) => fichier.id !== templateMaster.id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
showNotification(
|
|
||||||
`Le document "${templateMaster.name}" a été correctement supprimé.`,
|
|
||||||
'success',
|
|
||||||
'Succès'
|
|
||||||
);
|
|
||||||
|
|
||||||
setRemovePopupVisible(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
} else {
|
|
||||||
showNotification(
|
|
||||||
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
setRemovePopupVisible(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error deleting file from database:', error);
|
|
||||||
showNotification(
|
|
||||||
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
setRemovePopupVisible(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showNotification(
|
|
||||||
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
setRemovePopupVisible(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error removing template from DocuSeal:', error);
|
|
||||||
showNotification(
|
showNotification(
|
||||||
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
||||||
'error',
|
'error',
|
||||||
@ -189,7 +138,18 @@ export default function FilesGroupsManagement({
|
|||||||
);
|
);
|
||||||
setRemovePopupVisible(false);
|
setRemovePopupVisible(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Error deleting file from database:', error);
|
||||||
|
showNotification(
|
||||||
|
`Erreur lors de la suppression du document "${templateMaster.name}".`,
|
||||||
|
'error',
|
||||||
|
'Erreur'
|
||||||
|
);
|
||||||
|
setRemovePopupVisible(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -542,25 +502,13 @@ export default function FilesGroupsManagement({
|
|||||||
icon={Signature}
|
icon={Signature}
|
||||||
title="Formulaires à remplir"
|
title="Formulaires à remplir"
|
||||||
description="Gérez les formulaires nécessitant une signature électronique."
|
description="Gérez les formulaires nécessitant une signature électronique."
|
||||||
button={apiDocuseal}
|
button={true}
|
||||||
buttonOpeningModal={true}
|
buttonOpeningModal={true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mb-4">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
|
||||||
!apiDocuseal && 'bg-red-100 text-red-700 border border-red-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{!apiDocuseal && (
|
|
||||||
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
|
|
||||||
)}
|
|
||||||
{!apiDocuseal && 'Clé API Docuseal manquante'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Table
|
<Table
|
||||||
data={filteredFiles}
|
data={filteredFiles}
|
||||||
columns={columnsFiles}
|
columns={columnsFiles}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
|
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import MultiSelect from '@/components/MultiSelect';
|
import MultiSelect from '@/components/Form/MultiSelect';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
|
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
import AlertMessage from '@/components/AlertMessage';
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
|||||||
@ -46,10 +46,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
const storedUser = sessionStorage.getItem('user');
|
const storedUser = sessionStorage.getItem('user');
|
||||||
return storedUser ? JSON.parse(storedUser) : null;
|
return storedUser ? JSON.parse(storedUser) : null;
|
||||||
});
|
});
|
||||||
const [apiDocuseal, setApiDocusealState] = useState(() => {
|
|
||||||
const storedApiDocuseal = sessionStorage.getItem('apiDocuseal');
|
|
||||||
return storedApiDocuseal ? JSON.parse(storedApiDocuseal) : null;
|
|
||||||
});
|
|
||||||
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
|
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
|
||||||
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
|
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
|
||||||
return storedLogo ? JSON.parse(storedLogo) : null;
|
return storedLogo ? JSON.parse(storedLogo) : null;
|
||||||
@ -94,11 +90,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
sessionStorage.setItem('user', JSON.stringify(user));
|
sessionStorage.setItem('user', JSON.stringify(user));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setApiDocuseal = (api) => {
|
|
||||||
setApiDocusealState(api);
|
|
||||||
sessionStorage.setItem('apiDocuseal', JSON.stringify(api));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSelectedEstablishmentLogo = (logo) => {
|
const setSelectedEstablishmentLogo = (logo) => {
|
||||||
setSelectedEstablishmentLogoState(logo);
|
setSelectedEstablishmentLogoState(logo);
|
||||||
sessionStorage.setItem('selectedEstablishmentLogo', JSON.stringify(logo));
|
sessionStorage.setItem('selectedEstablishmentLogo', JSON.stringify(logo));
|
||||||
@ -122,7 +113,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
name: role.establishment__name,
|
name: role.establishment__name,
|
||||||
evaluation_frequency: role.establishment__evaluation_frequency,
|
evaluation_frequency: role.establishment__evaluation_frequency,
|
||||||
total_capacity: role.establishment__total_capacity,
|
total_capacity: role.establishment__total_capacity,
|
||||||
api_docuseal: role.establishment__api_docuseal,
|
|
||||||
logo: role.establishment__logo,
|
logo: role.establishment__logo,
|
||||||
role_id: i,
|
role_id: i,
|
||||||
role_type: role.role_type,
|
role_type: role.role_type,
|
||||||
@ -143,9 +133,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
setSelectedEstablishmentTotalCapacity(
|
setSelectedEstablishmentTotalCapacity(
|
||||||
userEstablishments[roleIndexDefault].total_capacity
|
userEstablishments[roleIndexDefault].total_capacity
|
||||||
);
|
);
|
||||||
setApiDocuseal(
|
|
||||||
userEstablishments[roleIndexDefault].api_docuseal
|
|
||||||
);
|
|
||||||
setSelectedEstablishmentLogo(
|
setSelectedEstablishmentLogo(
|
||||||
userEstablishments[roleIndexDefault].logo
|
userEstablishments[roleIndexDefault].logo
|
||||||
);
|
);
|
||||||
@ -168,7 +155,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
setUserState(null);
|
setUserState(null);
|
||||||
setSelectedEstablishmentEvaluationFrequencyState(null);
|
setSelectedEstablishmentEvaluationFrequencyState(null);
|
||||||
setSelectedEstablishmentTotalCapacityState(null);
|
setSelectedEstablishmentTotalCapacityState(null);
|
||||||
setApiDocusealState(null);
|
|
||||||
setSelectedEstablishmentLogoState(null);
|
setSelectedEstablishmentLogoState(null);
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
};
|
};
|
||||||
@ -184,8 +170,6 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
setSelectedEstablishmentEvaluationFrequency,
|
setSelectedEstablishmentEvaluationFrequency,
|
||||||
selectedEstablishmentTotalCapacity,
|
selectedEstablishmentTotalCapacity,
|
||||||
setSelectedEstablishmentTotalCapacity,
|
setSelectedEstablishmentTotalCapacity,
|
||||||
apiDocuseal,
|
|
||||||
setApiDocuseal,
|
|
||||||
selectedEstablishmentLogo,
|
selectedEstablishmentLogo,
|
||||||
setSelectedEstablishmentLogo,
|
setSelectedEstablishmentLogo,
|
||||||
selectedRoleId,
|
selectedRoleId,
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
import logger from '@/utils/logger';
|
|
||||||
import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url';
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const { templateId, email, is_required, establishment_id, apiDocuseal } = req.body;
|
|
||||||
|
|
||||||
fetch(BE_DOCUSEAL_CLONE_TEMPLATE, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': apiDocuseal,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
templateId,
|
|
||||||
email,
|
|
||||||
is_required,
|
|
||||||
establishment_id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.json().then((err) => {
|
|
||||||
throw new Error(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
logger.debug('Template cloned successfully:', data);
|
|
||||||
res.status(200).json(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error cloning template:', error);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.setHeader('Allow', ['POST']);
|
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import logger from '@/utils/logger';
|
|
||||||
import { BE_DOCUSEAL_DOWNLOAD_TEMPLATE } from '@/utils/Url';
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const { slug, establishment_id, apiDocuseal } = req.query;
|
|
||||||
logger.debug('slug : ', slug);
|
|
||||||
|
|
||||||
fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}?establishment_id=${establishment_id}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'X-Auth-Token': apiDocuseal,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.json().then((err) => {
|
|
||||||
throw new Error(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
logger.debug('Template downloaded successfully:', data);
|
|
||||||
res.status(200).json(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error downloading template:', error);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.setHeader('Allow', ['GET']);
|
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import logger from '@/utils/logger';
|
|
||||||
import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url';
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const { apiDocuseal, ...rest } = req.body;
|
|
||||||
|
|
||||||
fetch(BE_DOCUSEAL_GET_JWT, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': apiDocuseal,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(rest),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
logger.debug('Response status:', response.status);
|
|
||||||
return response
|
|
||||||
.json()
|
|
||||||
.then((data) => ({ status: response.status, data }));
|
|
||||||
})
|
|
||||||
.then(({ status, data }) => {
|
|
||||||
logger.debug('Response data:', data);
|
|
||||||
res.status(status).json(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error:', error);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.setHeader('Allow', ['POST']);
|
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import logger from '@/utils/logger';
|
|
||||||
import { BE_DOCUSEAL_REMOVE_TEMPLATE } from '@/utils/Url';
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (req.method === 'DELETE') {
|
|
||||||
const { templateId, establishment_id, apiDocuseal } = req.body;
|
|
||||||
|
|
||||||
fetch(`${BE_DOCUSEAL_REMOVE_TEMPLATE}/${templateId}?establishment_id=${establishment_id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'X-Auth-Token': apiDocuseal,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return response.json().then((err) => {
|
|
||||||
throw new Error(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
logger.debug('Template removed successfully:', data);
|
|
||||||
res.status(200).json(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Error removing template:', error);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.setHeader('Allow', ['DELETE']);
|
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,12 +4,6 @@ export const WS_BASE_URL = process.env.NEXT_PUBLIC_WSAPI_URL;
|
|||||||
|
|
||||||
//URL-Back-End
|
//URL-Back-End
|
||||||
|
|
||||||
// GESTION DocuSeal
|
|
||||||
export const BE_DOCUSEAL_GET_JWT = `${BASE_URL}/DocuSeal/generateToken`;
|
|
||||||
export const BE_DOCUSEAL_CLONE_TEMPLATE = `${BASE_URL}/DocuSeal/cloneTemplate`;
|
|
||||||
export const BE_DOCUSEAL_REMOVE_TEMPLATE = `${BASE_URL}/DocuSeal/removeTemplate`;
|
|
||||||
export const BE_DOCUSEAL_DOWNLOAD_TEMPLATE = `${BASE_URL}/DocuSeal/downloadTemplate`;
|
|
||||||
|
|
||||||
// GESTION LOGIN
|
// GESTION LOGIN
|
||||||
export const BE_AUTH_NEW_PASSWORD_URL = `${BASE_URL}/Auth/newPassword`;
|
export const BE_AUTH_NEW_PASSWORD_URL = `${BASE_URL}/Auth/newPassword`;
|
||||||
export const BE_AUTH_REGISTER_URL = `${BASE_URL}/Auth/subscribe`;
|
export const BE_AUTH_REGISTER_URL = `${BASE_URL}/Auth/subscribe`;
|
||||||
@ -131,12 +125,6 @@ export const FE_PARENTS_HOME_URL = '/parents';
|
|||||||
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
|
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
|
||||||
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
|
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
|
||||||
|
|
||||||
// API DOCUSEAL
|
|
||||||
export const FE_API_DOCUSEAL_GENERATE_TOKEN = '/api/docuseal/generateToken';
|
|
||||||
export const FE_API_DOCUSEAL_CLONE_URL = '/api/docuseal/cloneTemplate';
|
|
||||||
export const FE_API_DOCUSEAL_DOWNLOAD_URL = '/api/docuseal/downloadTemplate';
|
|
||||||
export const FE_API_DOCUSEAL_DELETE_URL = '/api/docuseal/removeTemplate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fonction pour obtenir l'URL de redirection en fonction du rôle
|
* Fonction pour obtenir l'URL de redirection en fonction du rôle
|
||||||
* @param {RIGHTS} role
|
* @param {RIGHTS} role
|
||||||
|
|||||||
22
conf/backend.env.default
Normal file
22
conf/backend.env.default
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
TZ="Europe/Paris"
|
||||||
|
TEST_MODE=true
|
||||||
|
CSRF_COOKIE_SECURE=true
|
||||||
|
CSRF_COOKIE_DOMAIN=".localhost"
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||||
|
CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||||
|
BASE_URL=http://localhost:3000
|
||||||
|
DEBUG=false
|
||||||
|
EMAIL_HOST="smtp.hostinger.com"
|
||||||
|
EMAIL_PORT="587"
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=''
|
||||||
|
EMAIL_USE_TLS=true
|
||||||
|
EMAIL_USE_SSL=false
|
||||||
|
DB_NAME="school"
|
||||||
|
DB_USER="postgres"
|
||||||
|
DB_PASSWORD="postgres"
|
||||||
|
DB_HOST="database"
|
||||||
|
DB_PORT="5432"
|
||||||
|
URL_DJANGO="http://localhost:8080"
|
||||||
|
SECRET_KEY="<SIGNINGKEY>"
|
||||||
@ -1,15 +1,19 @@
|
|||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: 'redis:latest'
|
image: "redis:latest"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
expose:
|
expose:
|
||||||
- 6379
|
- 6379
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: 'postgres:latest'
|
image: "postgres:latest"
|
||||||
expose:
|
expose:
|
||||||
- 5432
|
- 5432
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
@ -20,17 +24,13 @@ services:
|
|||||||
image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest
|
image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
environment:
|
env_file: "./conf/backend.env"
|
||||||
- TZ=Europe/Paris
|
|
||||||
- TEST_MODE=True
|
|
||||||
links:
|
links:
|
||||||
- "database:database"
|
- "database:database"
|
||||||
- "redis:redis"
|
- "redis:redis"
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
volumes:
|
|
||||||
- ./conf/application.json:/Back-End/Subscriptions/Configuration/application.json
|
|
||||||
command: python start.py
|
command: python start.py
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
@ -40,6 +40,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- NEXT_PUBLIC_API_URL=http://toto:8080
|
volumes:
|
||||||
depends_on:
|
- ./conf/env:/app/.env
|
||||||
- backend
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
redis-data:
|
||||||
|
|||||||
@ -1,55 +1,24 @@
|
|||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: "redis:latest"
|
image: "redis:latest"
|
||||||
ports:
|
volumes:
|
||||||
- 6379:6379
|
- redis-data:/data
|
||||||
|
expose:
|
||||||
|
- 6379
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: "postgres:latest"
|
image: "postgres:latest"
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: school
|
POSTGRES_DB: school
|
||||||
TZ: Europe/Paris
|
TZ: Europe/Paris
|
||||||
# docuseal_db:
|
|
||||||
# image: postgres:latest
|
|
||||||
# environment:
|
|
||||||
# POSTGRES_USER: postgres
|
|
||||||
# POSTGRES_PASSWORD: postgres
|
|
||||||
# DOCUSEAL_DB_HOST: docuseal_db
|
|
||||||
# POSTGRES_DB: docuseal
|
|
||||||
# ports:
|
|
||||||
# - 5433:5432 # port différent si besoin d'accès direct depuis l'hôte
|
|
||||||
|
|
||||||
# docuseal:
|
|
||||||
# image: docuseal/docuseal:latest
|
|
||||||
# container_name: docuseal_app
|
|
||||||
# depends_on:
|
|
||||||
# - docuseal_db
|
|
||||||
# ports:
|
|
||||||
# - "3001:3000"
|
|
||||||
# environment:
|
|
||||||
# DATABASE_URL: postgresql://postgres:postgres@docuseal_db:5432/docuseal
|
|
||||||
# volumes:
|
|
||||||
# - ./docuseal:/data/docuseal
|
|
||||||
|
|
||||||
# caddy:
|
|
||||||
# image: caddy:2
|
|
||||||
# container_name: caddy
|
|
||||||
# restart: unless-stopped
|
|
||||||
# ports:
|
|
||||||
# - "4000:4443"
|
|
||||||
# volumes:
|
|
||||||
# - ./Caddyfile:/etc/caddy/Caddyfile
|
|
||||||
# - caddy_data:/data
|
|
||||||
# - caddy_config:/config
|
|
||||||
# depends_on:
|
|
||||||
# - docuseal
|
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@ -58,54 +27,15 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- ./Back-End:/Back-End
|
- ./Back-End:/Back-End
|
||||||
environment:
|
env_file: "./conf/backend.env"
|
||||||
- TZ=Europe/Paris
|
|
||||||
- TEST_MODE=True
|
|
||||||
- CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
|
||||||
- CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
|
||||||
- BASE_URL=http://localhost:3000
|
|
||||||
links:
|
links:
|
||||||
- "database:database"
|
- "database:database"
|
||||||
- "redis:redis"
|
- "redis:redis"
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
#- docuseal
|
|
||||||
command: python start.py
|
command: python start.py
|
||||||
|
|
||||||
# init_docuseal_users:
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# depends_on:
|
|
||||||
# - docuseal
|
|
||||||
# environment:
|
|
||||||
# DOCUSEAL_DB_HOST: docuseal_db
|
|
||||||
# POSTGRES_USER: postgres
|
|
||||||
# POSTGRES_PASSWORD: postgres
|
|
||||||
# USER_FIRST_NAME: n3wt
|
|
||||||
# USER_LAST_NAME: school
|
|
||||||
# USER_COMPANY: n3wt.innov
|
|
||||||
# USER_EMAIL: n3wt.school@gmail.com
|
|
||||||
# USER_PASSWORD: n3wt1234
|
|
||||||
# volumes:
|
|
||||||
# - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh
|
|
||||||
|
|
||||||
# frontend:
|
|
||||||
# build:
|
|
||||||
# context: ./Front-End
|
|
||||||
# args:
|
|
||||||
# - BUILD_MODE=development
|
|
||||||
# ports:
|
|
||||||
# - 3000:3000
|
|
||||||
# volumes:
|
|
||||||
# - ./Front-End:/app
|
|
||||||
# env_file:
|
|
||||||
# - .env
|
|
||||||
# environment:
|
|
||||||
# - TZ=Europe/Paris
|
|
||||||
# depends_on:
|
|
||||||
# - backend
|
|
||||||
volumes:
|
volumes:
|
||||||
caddy_data:
|
postgres-data:
|
||||||
caddy_config:
|
redis-data:
|
||||||
|
|||||||
94
docs/manuels/installation/premier-pas.md
Normal file
94
docs/manuels/installation/premier-pas.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 🧭 Premiers Pas avec N3WT-SCHOOL
|
||||||
|
|
||||||
|
Bienvenue dans **N3WT-SCHOOL** !
|
||||||
|
Ce guide rapide vous accompagnera dans les premières étapes de configuration de votre instance afin de la rendre pleinement opérationnelle pour votre établissement.
|
||||||
|
|
||||||
|
## ✅ Étapes à suivre :
|
||||||
|
|
||||||
|
1. **Configurer la signature électronique des documents via Docuseal**
|
||||||
|
2. **Activer l'envoi d'e-mails depuis la plateforme**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✍️ 1. Configuration de la signature électronique (Docuseal)
|
||||||
|
|
||||||
|
Afin de permettre la signature électronique des documents administratifs (inscriptions, conventions, etc.), N3WT-SCHOOL s'appuie sur [**Docuseal**](https://docuseal.com), un service sécurisé de signature électronique.
|
||||||
|
|
||||||
|
### Étapes :
|
||||||
|
|
||||||
|
1. Connectez-vous ou créez un compte sur Docuseal :
|
||||||
|
👉 [https://docuseal.com/sign_in](https://docuseal.com/sign_in)
|
||||||
|
|
||||||
|
2. Une fois connecté, accédez à la section API :
|
||||||
|
👉 [https://console.docuseal.com/api](https://console.docuseal.com/api)
|
||||||
|
|
||||||
|
3. Copiez votre **X-Auth-Token** personnel.
|
||||||
|
Ce jeton permettra à N3WT-SCHOOL de se connecter à votre compte Docuseal.
|
||||||
|
|
||||||
|
4. **Envoyez votre X-Auth-Token à l'équipe N3WT-SCHOOL** pour qu'un administrateur puisse finaliser la configuration :
|
||||||
|
✉️ Contact : [contact@n3wtschool.com](mailto:contact@n3wtschool.com)
|
||||||
|
|
||||||
|
> ⚠️ Cette opération doit impérativement être réalisée par un administrateur N3WT-SCHOOL.
|
||||||
|
> Ne partagez pas ce token en dehors de ce cadre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 2. Configuration de l'envoi d’e-mails
|
||||||
|
|
||||||
|
L’envoi de mails depuis N3WT-SCHOOL est requis pour :
|
||||||
|
|
||||||
|
- Notifications aux étudiants
|
||||||
|
- Accusés de réception
|
||||||
|
- Envoi de documents (factures, conventions…)
|
||||||
|
|
||||||
|
Vous devrez renseigner les informations de votre fournisseur SMTP dans **Paramètres > E-mail** de l’application.
|
||||||
|
|
||||||
|
### Informations requises :
|
||||||
|
|
||||||
|
- Hôte SMTP
|
||||||
|
- Port SMTP
|
||||||
|
- Type de sécurité (TLS / SSL)
|
||||||
|
- Adresse e-mail (utilisateur SMTP)
|
||||||
|
- Mot de passe ou **mot de passe applicatif**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Mot de passe applicatif (Gmail, Outlook, etc.)
|
||||||
|
|
||||||
|
Certains fournisseurs (notamment **Gmail**, **Yahoo**, **iCloud**) ne permettent pas d’utiliser directement votre mot de passe personnel pour des applications tierces.
|
||||||
|
Vous devez créer un **mot de passe applicatif**.
|
||||||
|
|
||||||
|
### Exemple : Créer un mot de passe applicatif avec Gmail
|
||||||
|
|
||||||
|
1. Connectez-vous à [votre compte Google](https://myaccount.google.com)
|
||||||
|
2. Allez dans **Sécurité > Validation en 2 étapes**
|
||||||
|
3. Activez la validation en 2 étapes si ce n’est pas déjà fait
|
||||||
|
4. Ensuite, allez dans **Mots de passe des applications**
|
||||||
|
5. Sélectionnez une application (ex. : "Autre (personnalisée)") et nommez-la "N3WT-SCHOOL"
|
||||||
|
6. Copiez le mot de passe généré et utilisez-le comme **mot de passe SMTP**
|
||||||
|
|
||||||
|
> 📎 Vous pouvez consulter l’aide officielle de Google ici :
|
||||||
|
> [Créer un mot de passe d’application – Google](https://support.google.com/accounts/answer/185833)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Configuration SMTP — Fournisseurs courants
|
||||||
|
|
||||||
|
| Fournisseur | SMTP Host | Port TLS | Port SSL | Sécurité | Lien aide SMTP |
|
||||||
|
| ----------------- | ------------------- | -------- | -------- | -------- | ---------------------------------------------------------------------------- |
|
||||||
|
| Gmail | smtp.gmail.com | 587 | 465 | TLS/SSL | [Aide SMTP Gmail](https://support.google.com/mail/answer/7126229?hl=fr) |
|
||||||
|
| Outlook / Hotmail | smtp.office365.com | 587 | — | TLS | [Aide SMTP Outlook](https://support.microsoft.com/fr-fr/office) |
|
||||||
|
| Yahoo Mail | smtp.mail.yahoo.com | 587 | 465 | TLS/SSL | [Aide SMTP Yahoo](https://help.yahoo.com/kb/SLN4724.html) |
|
||||||
|
| iCloud Mail | smtp.mail.me.com | 587 | 465 | TLS/SSL | [Aide iCloud SMTP](https://support.apple.com/fr-fr/HT202304) |
|
||||||
|
| OVH | ssl0.ovh.net | 587 | 465 | TLS/SSL | [Aide OVH SMTP](https://help.ovhcloud.com/csm/fr-email-general-settings) |
|
||||||
|
| Infomaniak | mail.infomaniak.com | 587 | 465 | TLS/SSL | [Aide SMTP Infomaniak](https://www.infomaniak.com/fr/support/faq/1817) |
|
||||||
|
| Gandi | mail.gandi.net | 587 | 465 | TLS/SSL | [Aide SMTP Gandi](https://docs.gandi.net/fr/mail/faq/envoyer_des_mails.html) |
|
||||||
|
|
||||||
|
> 📝 Si votre fournisseur ne figure pas dans cette liste, n'hésitez pas à contacter votre fournisseur de mail pour obtenir ces informations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Vous êtes prêt·e !
|
||||||
|
|
||||||
|
Une fois ces deux configurations effectuées, votre instance N3WT-SCHOOL est prête à fonctionner pleinement.
|
||||||
|
Vous pourrez ensuite ajouter vos formations, étudiants, documents et automatiser toute votre gestion scolaire.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school",
|
"name": "n3wt-school",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
|
|||||||
Reference in New Issue
Block a user