feat: Signatures électroniques docuseal [#22]

This commit is contained in:
N3WT DE COMPET
2025-02-28 18:30:18 +01:00
parent 8897d523dc
commit c8c8941ec8
41 changed files with 984 additions and 549 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg"

View File

@ -0,0 +1 @@
# This file is intentionally left blank to make this directory a Python package.

View File

@ -0,0 +1,7 @@
from django.urls import path, re_path
from .views import generate_jwt_token, clone_template
urlpatterns = [
re_path(r'generateToken$', generate_jwt_token, name='generate_jwt_token'),
re_path(r'cloneTemplate$', clone_template, name='clone_template'),
]

View File

@ -0,0 +1,81 @@
from django.conf import settings
from django.http import JsonResponse
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
@csrf_exempt
@api_view(['POST'])
def generate_jwt_token(request):
# Vérifier la clé API
api_key = request.headers.get('X-Auth-Token')
print(f'api_key : {api_key}')
print(f'settings.DOCUSEAL_JWT["API_KEY"] : {settings.DOCUSEAL_JWT["API_KEY"]}')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]:
return Response({'error': 'Invalid API key'}, 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('template_id') # Récupérer le template_id
# Vérifier les données requises
if not user_email:
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
# Utiliser la configuration JWT de DocuSeal depuis les settings
jwt_secret = settings.DOCUSEAL_JWT['API_KEY']
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
# Définir le payload
payload = {
'user_email': user_email,
'documents_urls': documents_urls,
'template_id': template_id, # Ajouter le template_id au payload
'exp': datetime.datetime.utcnow() + expiration_delta # Temps d'expiration du token
}
# Générer le token JWT
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):
# Vérifier la clé API
api_key = request.headers.get('X-Auth-Token')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]:
return Response({'error': 'Invalid API key'}, 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')
# Vérifier les données requises
if not document_id or not email :
return Response({'error': 'template ID, email are 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, json={'submitters': [{'email': email}]}, headers={
'Content-Type': 'application/json',
'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY']
})
if response.status_code != 200:
return Response({'error': 'Failed to clone 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)

View File

@ -0,0 +1,8 @@
class ContentSecurityPolicyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['Content-Security-Policy'] = "frame-ancestors 'self' http://localhost:3000"
return response

View File

@ -251,6 +251,12 @@ DOCUMENT_DIR = 'documents'
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_ALL_HEADERS = True CORS_ALLOW_ALL_HEADERS = True
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'content-type',
'authorization',
'X-Auth-Token',
'x-csrftoken'
]
CORS_ALLOWED_ORIGINS = [ CORS_ALLOWED_ORIGINS = [
os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000') os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
@ -333,5 +339,5 @@ DOCUSEAL_JWT = {
'ALGORITHM': 'HS256', 'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY, 'SIGNING_KEY': SECRET_KEY,
'EXPIRATION_DELTA': timedelta(hours=1), 'EXPIRATION_DELTA': timedelta(hours=1),
'API_KEY': '1kzHsXqN8P2ezUGT7TjVuBwM1hqtLsztrVSsQ87T7Mz' 'API_KEY': os.getenv('DOCUSEAL_API_KEY')
} }

View File

@ -7,7 +7,7 @@ from Subscriptions.models import (
Fee, Fee,
Discount, Discount,
RegistrationFileGroup, RegistrationFileGroup,
RegistrationFileTemplate # RegistrationFileTemplate
) )
from Auth.models import Profile from Auth.models import Profile
from School.models import ( from School.models import (
@ -43,7 +43,7 @@ class Command(BaseCommand):
self.create_or_update_teachers() self.create_or_update_teachers()
self.create_or_update_school_classes() self.create_or_update_school_classes()
self.create_or_update_registration_file_group() self.create_or_update_registration_file_group()
self.create_or_update_registration_file_template() # self.create_or_update_registration_file_template()
self.create_register_form() self.create_register_form()
def create_or_update_establishment(self): def create_or_update_establishment(self):
@ -376,13 +376,21 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS('SchoolClasses initialized or updated successfully')) self.stdout.write(self.style.SUCCESS('SchoolClasses initialized or updated successfully'))
def create_or_update_registration_file_group(self): def create_or_update_registration_file_group(self):
group_data = { group_data_1 = {
"name": "LMDE", "name": "LMDE",
"description": "Fichiers d'inscription de l'école LMDE" "description": "Fichiers d'inscription de l'école LMDE"
} }
self.registration_file_group, created = RegistrationFileGroup.objects.get_or_create(name=group_data["name"], defaults=group_data) self.registration_file_group_1, created = RegistrationFileGroup.objects.get_or_create(name=group_data_1["name"], defaults=group_data_1)
self.stdout.write(self.style.SUCCESS('RegistrationFileGroup initialized or updated successfully')) self.stdout.write(self.style.SUCCESS('RegistrationFileGroup 1 initialized or updated successfully'))
group_data_2 = {
"name": "LMDE 2",
"description": "Fichiers d'inscription de l'école LMDE 2"
}
self.registration_file_group_2, created = RegistrationFileGroup.objects.get_or_create(name=group_data_2["name"], defaults=group_data_2)
self.stdout.write(self.style.SUCCESS('RegistrationFileGroup 2 initialized or updated successfully'))
def create_or_update_registration_file_template(self): def create_or_update_registration_file_template(self):
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
@ -394,28 +402,28 @@ class Command(BaseCommand):
"file": "RIB LA MAISON DES ENFANTS.pdf", "file": "RIB LA MAISON DES ENFANTS.pdf",
"order": 0, "order": 0,
"is_required": False, "is_required": False,
"group": self.registration_file_group "group": self.registration_file_group_2 # Associer ce fichier au deuxième groupe
}, },
{ {
"name": "Contrat d'engagement 2024 2025", "name": "Contrat d'engagement 2024 2025",
"file": "Contrat d'engagement 2024 2025.pdf", "file": "Contrat d'engagement 2024 2025.pdf",
"order": 0, "order": 0,
"is_required": True, "is_required": True,
"group": self.registration_file_group "group": self.registration_file_group_1
}, },
{ {
"name": "Bulletin d'adhésion familiale scolaire", "name": "Bulletin d'adhésion familiale scolaire",
"file": "Bulletin d'adhésion familiale scolaire.pdf", "file": "Bulletin d'adhésion familiale scolaire.pdf",
"order": 0, "order": 0,
"is_required": True, "is_required": True,
"group": self.registration_file_group "group": self.registration_file_group_1
}, },
{ {
"name": "Fiche sanitaire de liaison", "name": "Fiche sanitaire de liaison",
"file": "Fiche sanitaire de liaison.pdf", "file": "Fiche sanitaire de liaison.pdf",
"order": 0, "order": 0,
"is_required": True, "is_required": True,
"group": self.registration_file_group "group": self.registration_file_group_1
} }
] ]
@ -442,14 +450,24 @@ class Command(BaseCommand):
def create_register_form(self): def create_register_form(self):
# Créer ou mettre à jour le profil associé au guardian # Créer ou mettre à jour le profil associé au guardian
profile_data = { profiles_data = [
{
"email": "anthony.casini.30@gmail.com", "email": "anthony.casini.30@gmail.com",
"droit": 2, "droit": 2,
"username": "anthony.casini.30@gmail.com", "username": "anthony.casini.30@gmail.com",
"is_active": True, "is_active": True,
"password": "Provisoire01!" "password": "Provisoire01!"
},
{
"email": "anthony.audrey.34@gmail.com",
"droit": 2,
"username": "anthony.audrey.34@gmail.com",
"is_active": True,
"password": "Provisoire01!"
} }
]
for profile_data in profiles_data:
user, created = Profile.objects.update_or_create( user, created = Profile.objects.update_or_create(
email=profile_data["email"], email=profile_data["email"],
defaults={ defaults={
@ -466,13 +484,13 @@ class Command(BaseCommand):
# Créer les données du guardian # Créer les données du guardian
guardian_data = { guardian_data = {
"associated_profile_id": user.id, "associated_profile_id": user.id,
"email": "anthony.casini.30@gmail.com", "email": profile_data["email"],
} }
# Créer les données de l'étudiant # Créer les données de l'étudiant
student_data = { student_data = {
"last_name": "CASINI", "last_name": f'lastname_{user.id}',
"first_name": "Giulia", "first_name": f'firstname_{user.id}',
} }
# Créer ou mettre à jour l'étudiant et le guardian # Créer ou mettre à jour l'étudiant et le guardian
@ -494,7 +512,7 @@ class Command(BaseCommand):
# Créer les données du formulaire d'inscription # Créer les données du formulaire d'inscription
register_form_data = { register_form_data = {
"student": student, "student": student,
"fileGroup": self.registration_file_group, "fileGroup": self.registration_file_group_1,
"establishment": Establishment.objects.get(id=1), "establishment": Establishment.objects.get(id=1),
"status": 1 "status": 1
} }
@ -504,7 +522,7 @@ class Command(BaseCommand):
register_form.fees.set(fees) register_form.fees.set(fees)
register_form.discounts.set(discounts) register_form.discounts.set(discounts)
if not created: if not created:
register_form.fileGroup = self.registration_file_group register_form.fileGroup = self.registration_file_group_1
register_form.save() register_form.save()
self.stdout.write(self.style.SUCCESS('RegistrationForm initialized or updated successfully')) self.stdout.write(self.style.SUCCESS('RegistrationForm initialized or updated successfully'))

View File

@ -89,7 +89,7 @@ class Student(models.Model):
siblings = models.ManyToManyField(Sibling, blank=True) siblings = models.ManyToManyField(Sibling, blank=True)
# Many-to-Many Relationship # Many-to-Many Relationship
registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students') registration_files = models.ManyToManyField('RegistrationTemplate', blank=True, related_name='students')
# Many-to-Many Relationship # Many-to-Many Relationship
spoken_languages = models.ManyToManyField(Language, blank=True) spoken_languages = models.ManyToManyField(Language, blank=True)
@ -162,7 +162,7 @@ class Student(models.Model):
return None return None
class RegistrationFileGroup(models.Model): class RegistrationFileGroup(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255, default="")
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
@ -172,10 +172,35 @@ def registration_file_path(instance, filename):
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename # Génère le chemin : registration_files/dossier_rf_{student_id}/filename
return f'registration_files/dossier_rf_{instance.student_id}/{filename}' return f'registration_files/dossier_rf_{instance.student_id}/{filename}'
class RegistrationTemplateMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='template_masters')
template_id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
def __str__(self):
return f'{self.group.name} - {self.template_id}'
class RegistrationTemplate(models.Model):
master = models.ForeignKey(RegistrationTemplateMaster, on_delete=models.CASCADE, related_name='templates')
template_id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='templates')
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationTemplate.objects.filter(register_form_id=register_form_id).order_by('template__order')
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
class RegistrationForm(models.Model): class RegistrationForm(models.Model):
"""
Gère le dossier dinscription lié à un élève donné.
"""
class RegistrationFormStatus(models.IntegerChoices): class RegistrationFormStatus(models.IntegerChoices):
RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_ABSENT = 0, _('Pas de dossier d\'inscription')
RF_CREATED = 1, _('Dossier d\'inscription créé') RF_CREATED = 1, _('Dossier d\'inscription créé')
@ -205,7 +230,7 @@ class RegistrationForm(models.Model):
discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms') discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms')
fileGroup = models.ForeignKey(RegistrationFileGroup, fileGroup = models.ForeignKey(RegistrationFileGroup,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='file_group', related_name='register_forms',
null=True, null=True,
blank=True) blank=True)
@ -213,56 +238,3 @@ class RegistrationForm(models.Model):
def __str__(self): def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name return "RF_" + self.student.last_name + "_" + self.student.first_name
class RegistrationFileTemplate(models.Model):
"""
Modèle pour stocker les fichiers "templates" dinscription.
"""
name = models.CharField(max_length=255)
file = models.FileField(upload_to='templates_files/', blank=True, null=True)
order = models.PositiveIntegerField(default=0) # Ajout du champ order
date_added = models.DateTimeField(auto_now_add=True)
is_required = models.BooleanField(default=False)
group = models.ForeignKey(RegistrationFileGroup, on_delete=models.CASCADE, related_name='file_templates', null=True, blank=True)
@property
def formatted_date_added(self):
if self.date_added:
return self.date_added.strftime('%d-%m-%Y')
return None
def __str__(self):
return self.name
def registration_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.register_form.pk}/{filename}"
class RegistrationFile(models.Model):
"""
Fichier lié à un dossier dinscription particulier.
"""
name = models.CharField(max_length=255)
file = models.FileField(upload_to=registration_file_upload_to)
date_added = models.DateTimeField(auto_now_add=True)
template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE)
register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files')
@property
def formatted_date_added(self):
if self.date_added:
return self.date_added.strftime('%d-%m-%Y')
return None
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order')
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames

View File

@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import RegistrationFileTemplate, RegistrationFile, RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationTemplateMaster, RegistrationTemplate
from School.models import SchoolClass, Fee, Discount, FeeType from School.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import Profile from Auth.models import Profile
@ -11,20 +11,38 @@ from django.utils import timezone
import pytz import pytz
from datetime import datetime from datetime import datetime
class RegistrationTemplateMasterSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationTemplateMaster
fields = '__all__'
class RegistrationTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationTemplate
fields = '__all__'
class GuardianSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Guardian
fields = ['id', 'email']
class RegistrationFormSimpleSerializer(serializers.ModelSerializer):
guardians = GuardianSimpleSerializer(many=True, source='student.guardians')
class Meta:
model = RegistrationForm
fields = ['student_id', 'guardians']
class RegistrationFileGroupSerializer(serializers.ModelSerializer): class RegistrationFileGroupSerializer(serializers.ModelSerializer):
registration_forms = serializers.SerializerMethodField()
class Meta: class Meta:
model = RegistrationFileGroup model = RegistrationFileGroup
fields = '__all__' fields = '__all__'
class RegistrationFileSerializer(serializers.ModelSerializer): def get_registration_forms(self, obj):
class Meta: forms = RegistrationForm.objects.filter(fileGroup=obj)
model = RegistrationFile return RegistrationFormSimpleSerializer(forms, many=True).data
fields = '__all__'
class RegistrationFileTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationFileTemplate
fields = '__all__'
class LanguageSerializer(serializers.ModelSerializer): class LanguageSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
@ -132,7 +150,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
registration_file = serializers.FileField(required=False) registration_file = serializers.FileField(required=False)
status_label = serializers.SerializerMethodField() status_label = serializers.SerializerMethodField()
formatted_last_update = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField()
registration_files = RegistrationFileSerializer(many=True, required=False) registration_files = RegistrationTemplateSerializer(many=True, required=False)
fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False) fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False)
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False) discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False)
totalRegistrationFees = serializers.SerializerMethodField() totalRegistrationFees = serializers.SerializerMethodField()

View File

@ -7,8 +7,9 @@ from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archi
# SubClasses # SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView from .views import StudentView, GuardianView, ChildrenListView, StudentListView
# Files # Files
from .views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
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
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"),
@ -27,15 +28,13 @@ urlpatterns = [
# Page de formulaire d'inscription - RESPONSABLE # Page de formulaire d'inscription - RESPONSABLE
re_path(r'^lastGuardianId$', GuardianView.as_view(), name="lastGuardianId"), re_path(r'^lastGuardianId$', GuardianView.as_view(), name="lastGuardianId"),
# modèles de fichiers d'inscription
re_path(r'^registrationFileTemplates/(?P<id>[0-9]+)$', RegistrationFileTemplateSimpleView.as_view(), name="registrationFileTemplate"),
re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'),
# fichiers d'inscription
re_path(r'^registrationFiles/(?P<id>[0-9]+)$', RegistrationFileSimpleView.as_view(), name='registrationFiles'),
re_path(r'^registrationFiles$', RegistrationFileView.as_view(), name="registrationFiles"),
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'), re_path(r'^registrationFileGroups/(?P<id>[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'),
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)/registrationFiles$', get_registration_files_by_group, name="get_registration_files_by_group"), re_path(r'^registrationFileGroups/(?P<id>[0-9]+)/registrationFiles$', get_registration_files_by_group, name="get_registration_files_by_group"),
re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'), re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'),
re_path(r'^registrationTemplateMasters/(?P<id>[0-9]+)$', RegistrationTemplateMasterSimpleView.as_view(), name='registrationTemplateMasters'),
re_path(r'^registrationTemplateMasters$', RegistrationTemplateMasterView.as_view(), name='registrationTemplateMasters'),
re_path(r'^registrationTemplates/(?P<id>[0-9]+)$', RegistrationTemplateSimpleView.as_view(), name='registrationTemplates'),
re_path(r'^registrationTemplates$', RegistrationTemplateView.as_view(), name="registrationTemplates"),
] ]

View File

@ -1,5 +1,5 @@
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive
from .registration_file_views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
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 from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView from .guardian_views import GuardianView
@ -10,10 +10,10 @@ __all__ = [
'send', 'send',
'resend', 'resend',
'archive', 'archive',
'RegistrationFileView', 'RegistrationTemplateView',
'RegistrationFileSimpleView', 'RegistrationTemplateSimpleView',
'RegistrationFileTemplateView', 'RegistrationTemplateMasterView',
'RegistrationFileTemplateSimpleView', 'RegistrationTemplateMasterSimpleView',
'RegistrationFileGroupView', 'RegistrationFileGroupView',
'RegistrationFileGroupSimpleView', 'RegistrationFileGroupSimpleView',
'get_registration_files_by_group', 'get_registration_files_by_group',

View File

@ -19,7 +19,7 @@ import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer from Subscriptions.serializers import RegistrationFormSerializer
from Subscriptions.pagination import CustomPagination from Subscriptions.pagination import CustomPagination
from Subscriptions.signals import clear_cache from Subscriptions.signals import clear_cache
from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationFile, RegistrationFileGroup from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationTemplate, RegistrationFileGroup
from Subscriptions.automate import updateStateMachine from Subscriptions.automate import updateStateMachine
from N3wtSchool import settings, bdd from N3wtSchool import settings, bdd
@ -252,7 +252,7 @@ class RegisterFormWithIdView(APIView):
registerForm.save() registerForm.save()
# Récupération des fichiers d'inscription # Récupération des fichiers d'inscription
fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) fileNames = RegistrationTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file: if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path) fileNames.insert(0, registerForm.registration_file.path)

View File

@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from Subscriptions.serializers import RegistrationFileGroupSerializer from Subscriptions.serializers import RegistrationFileGroupSerializer
from Subscriptions.models import RegistrationFileGroup, RegistrationFileTemplate from Subscriptions.models import RegistrationFileGroup, RegistrationTemplateMaster
from N3wtSchool import bdd from N3wtSchool import bdd
class RegistrationFileGroupView(APIView): class RegistrationFileGroupView(APIView):
@ -118,7 +118,7 @@ class RegistrationFileGroupSimpleView(APIView):
def get_registration_files_by_group(request, id): def get_registration_files_by_group(request, id):
try: try:
group = RegistrationFileGroup.objects.get(id=id) group = RegistrationFileGroup.objects.get(id=id)
templates = RegistrationFileTemplate.objects.filter(group=group) templates = RegistrationTemplateMaster.objects.filter(group=group)
templates_data = list(templates.values()) templates_data = list(templates.values())
return JsonResponse(templates_data, safe=False) return JsonResponse(templates_data, safe=False)
except RegistrationFileGroup.DoesNotExist: except RegistrationFileGroup.DoesNotExist:

View File

@ -1,5 +1,4 @@
from django.http.response import JsonResponse from django.http.response import JsonResponse
from django.core.files import File
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
@ -7,205 +6,154 @@ 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 os from Subscriptions.serializers import RegistrationTemplateMasterSerializer, RegistrationTemplateSerializer
from Subscriptions.models import RegistrationTemplateMaster, RegistrationTemplate
from Subscriptions.serializers import RegistrationFileTemplateSerializer, RegistrationFileSerializer
from Subscriptions.models import RegistrationFileTemplate, RegistrationFile
from N3wtSchool import bdd from N3wtSchool import bdd
class RegistrationTemplateMasterView(APIView):
class RegistrationFileTemplateView(APIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Récupère tous les fichiers templates pour les dossiers d'inscription", operation_description="Récupère tous les masters de templates d'inscription",
responses={200: RegistrationFileTemplateSerializer(many=True)} responses={200: RegistrationTemplateMasterSerializer(many=True)}
) )
def get(self, request): def get(self, request):
""" masters = RegistrationTemplateMaster.objects.all()
Récupère les fichiers templates pour les dossiers dinscription. serializer = RegistrationTemplateMasterSerializer(masters, many=True)
"""
files = RegistrationFileTemplate.objects.all()
serializer = RegistrationFileTemplateSerializer(files, many=True)
return Response(serializer.data) return Response(serializer.data)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Crée un nouveau fichier template pour les dossiers d'inscription", operation_description="Crée un nouveau master de template d'inscription",
request_body=RegistrationFileTemplateSerializer, request_body=RegistrationTemplateMasterSerializer,
responses={ responses={
201: RegistrationFileTemplateSerializer, 201: RegistrationTemplateMasterSerializer,
400: "Données invalides" 400: "Données invalides"
} }
) )
def post(self, request): def post(self, request):
""" serializer = RegistrationTemplateMasterSerializer(data=request.data)
Crée un fichier template pour les dossiers dinscription.
"""
serializer = RegistrationFileTemplateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationTemplateMasterSimpleView(APIView):
class RegistrationFileTemplateSimpleView(APIView):
"""
Gère les fichiers templates pour les dossiers dinscription.
"""
parser_classes = (MultiPartParser, FormParser)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Récupère un fichier template spécifique", operation_description="Récupère un master de template d'inscription spécifique",
responses={ responses={
200: RegistrationFileTemplateSerializer, 200: RegistrationTemplateMasterSerializer,
404: "Fichier template non trouvé" 404: "Master non trouvé"
} }
) )
def get(self, request, id): def get(self, request, id):
""" master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
Récupère les fichiers templates pour les dossiers dinscription. if master is None:
""" return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) serializer = RegistrationTemplateMasterSerializer(master)
if registationFileTemplate is None:
return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileTemplateSerializer(registationFileTemplate)
return JsonResponse(serializer.data, safe=False) return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Met à jour un fichier template existant", operation_description="Met à jour un master de template d'inscription existant",
request_body=RegistrationFileTemplateSerializer, request_body=RegistrationTemplateMasterSerializer,
responses={ responses={
201: RegistrationFileTemplateSerializer, 200: RegistrationTemplateMasterSerializer,
400: "Données invalides", 400: "Données invalides",
404: "Fichier template non trouvé" 404: "Master non trouvé"
} }
) )
def put(self, request, id): def put(self, request, id):
""" master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
Met à jour un fichier template existant. if master is None:
""" return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) serializer = RegistrationTemplateMasterSerializer(master, data=request.data)
if registationFileTemplate is None:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileTemplateSerializer(registationFileTemplate,data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_200_OK)
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(
operation_description="Supprime un fichier template", operation_description="Supprime un master de template d'inscription",
responses={ responses={
204: "Suppression réussie", 204: "Suppression réussie",
404: "Fichier template non trouvé" 404: "Master non trouvé"
} }
) )
def delete(self, request, id): def delete(self, request, id):
""" master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
Supprime un fichier template existant. if master is not None:
""" master.delete()
registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
if registrationFileTemplate is not None:
registrationFileTemplate.file.delete() # Supprimer le fichier uploadé
registrationFileTemplate.delete()
return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else: else:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationFileView(APIView): class RegistrationTemplateView(APIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Récupère tous les fichiers d'inscription", operation_description="Récupère tous les templates d'inscription",
responses={200: RegistrationFileSerializer(many=True)} responses={200: RegistrationTemplateSerializer(many=True)}
) )
def get(self, request): def get(self, request):
""" templates = RegistrationTemplate.objects.all()
Récupère les fichiers liés à un dossier dinscription donné. serializer = RegistrationTemplateSerializer(templates, many=True)
"""
files = RegistrationFile.objects.all()
serializer = RegistrationFileSerializer(files, many=True)
return Response(serializer.data) return Response(serializer.data)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Crée un nouveau fichier d'inscription", operation_description="Crée un nouveau template d'inscription",
request_body=RegistrationFileSerializer, request_body=RegistrationTemplateSerializer,
responses={ responses={
201: RegistrationFileSerializer, 201: RegistrationTemplateSerializer,
400: "Données invalides" 400: "Données invalides"
} }
) )
def post(self, request): def post(self, request):
""" serializer = RegistrationTemplateSerializer(data=request.data)
Crée un RegistrationFile pour le RegistrationForm associé.
"""
serializer = RegistrationFileSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationTemplateSimpleView(APIView):
class RegistrationFileSimpleView(APIView):
"""
Gère la création, mise à jour et suppression de fichiers liés à un dossier dinscription.
"""
parser_classes = (MultiPartParser, FormParser)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Récupère un fichier d'inscription spécifique", operation_description="Récupère un template d'inscription spécifique",
responses={ responses={
200: RegistrationFileSerializer, 200: RegistrationTemplateSerializer,
404: "Fichier non trouvé" 404: "Template non trouvé"
} }
) )
def get(self, request, id): def get(self, request, id):
""" template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
Récupère les fichiers liés à un dossier dinscription donné. if template is None:
""" return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) serializer = RegistrationTemplateSerializer(template)
if registationFile is None:
return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileSerializer(registationFile)
return JsonResponse(serializer.data, safe=False) return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Met à jour un fichier d'inscription existant", operation_description="Met à jour un template d'inscription existant",
request_body=RegistrationFileSerializer, request_body=RegistrationTemplateSerializer,
responses={ responses={
200: openapi.Response( 200: RegistrationTemplateSerializer,
description="Fichier mis à jour avec succès",
schema=RegistrationFileSerializer
),
400: "Données invalides", 400: "Données invalides",
404: "Fichier non trouvé" 404: "Template non trouvé"
} }
) )
def put(self, request, id): def put(self, request, id):
""" template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
Met à jour un RegistrationFile existant. if template is None:
""" return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) serializer = RegistrationTemplateSerializer(template, data=request.data)
if registrationFile is None:
return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationFileSerializer(registrationFile, data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) 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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Supprime un fichier d'inscription", operation_description="Supprime un template d'inscription",
responses={ responses={
200: "Suppression réussie", 204: "Suppression réussie",
404: "Fichier non trouvé" 404: "Template non trouvé"
} }
) )
def delete(self, request, id): def delete(self, request, id):
""" template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
Supprime un RegistrationFile existant. if template is not None:
""" template.delete()
registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
if registrationFile is not None:
registrationFile.file.delete() # Supprimer le fichier uploadé
registrationFile.delete()
return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False)
else: else:
return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)

View File

@ -2,3 +2,4 @@ NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_USE_FAKE_DATA='false' NEXT_PUBLIC_USE_FAKE_DATA='false'
AUTH_SECRET='false' AUTH_SECRET='false'
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg"

View File

@ -1,6 +0,0 @@
{
"i18n-ally.localesPaths": [
"messages"
],
"i18n-ally.keystyle": "nested"
}

View File

@ -22,8 +22,8 @@ import { createDatas,
fetchRegistrationPaymentModes, fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction'; fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
import SidebarTabs from '@/components/SidebarTabs'; import SidebarTabs from '@/components/SidebarTabs';
import FilesManagement from '@/components/Structure/Files/FilesManagement'; import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
import { fetchRegisterFormFileTemplate } from '@/app/actions/subscriptionAction'; import { fetchRegistrationTemplateMaster } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
@ -70,7 +70,7 @@ export default function Page() {
handleTuitionFees(); handleTuitionFees();
// Fetch data for registration file templates // Fetch data for registration file templates
fetchRegisterFormFileTemplate() fetchRegistrationTemplateMaster()
.then((data)=> { .then((data)=> {
setFichiers(data) setFichiers(data)
}) })
@ -301,7 +301,7 @@ export default function Page() {
{ {
id: 'Files', id: 'Files',
label: 'Documents d\'inscription', label: 'Documents d\'inscription',
content: <FilesManagement csrfToken={csrfToken} /> content: <FilesGroupsManagement csrfToken={csrfToken} />
} }
]; ];

View File

@ -24,7 +24,7 @@ import {
createRegisterForm, createRegisterForm,
sendRegisterForm, sendRegisterForm,
archiveRegisterForm, archiveRegisterForm,
fetchRegisterFormFileTemplate, fetchRegistrationTemplateMaster,
fetchStudents, fetchStudents,
editRegisterForm } from "@/app/actions/subscriptionAction" editRegisterForm } from "@/app/actions/subscriptionAction"
@ -195,7 +195,7 @@ useEffect(() => {
fetchRegisterForms(ARCHIVED) fetchRegisterForms(ARCHIVED)
.then(registerFormArchivedDataHandler) .then(registerFormArchivedDataHandler)
.catch(requestErrorHandler) .catch(requestErrorHandler)
fetchRegisterFormFileTemplate() fetchRegistrationTemplateMaster()
.then((data)=> { .then((data)=> {
logger.debug(data); logger.debug(data);
@ -254,7 +254,7 @@ useEffect(() => {
fetchRegisterForms(ARCHIVED) fetchRegisterForms(ARCHIVED)
.then(registerFormArchivedDataHandler) .then(registerFormArchivedDataHandler)
.catch(requestErrorHandler) .catch(requestErrorHandler)
fetchRegisterFormFileTemplate() fetchRegistrationTemplateMaster()
.then((data)=> {setFichiers(data)}) .then((data)=> {setFichiers(data)})
.catch((err)=>{ err = err.message; logger.debug(err);}); .catch((err)=>{ err = err.message; logger.debug(err);});
} else { } else {

View File

@ -6,12 +6,13 @@ import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import useLocalStorage from '@/hooks/useLocalStorage';
import { fetchMessages } from '@/app/actions/messagerieAction'; import { fetchMessages } from '@/app/actions/messagerieAction';
import ProtectedRoute from '@/components/ProtectedRoute'; import ProtectedRoute from '@/components/ProtectedRoute';
import { disconnect } from '@/app/actions/authAction'; import { disconnect } from '@/app/actions/authAction';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
export default function Layout({ export default function Layout({
children, children,
@ -19,7 +20,8 @@ export default function Layout({
const router = useRouter(); // Définition de router const router = useRouter(); // Définition de router
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ; const { data: session, status } = useSession();
const [userId, setUserId] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isPopupVisible, setIsPopupVisible] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false);
@ -32,27 +34,33 @@ export default function Layout({
disconnect(); disconnect();
}; };
useEffect(() => { // useEffect(() => {
setIsLoading(true); // if (status === 'loading') return;
setUserId(userId) // if (!session) {
fetchMessages(userId) // router.push(`${FE_USERS_LOGIN_URL}`);
.then(data => { // }
if (data) {
setMessages(data);
}
logger.debug('Success :', data);
})
.catch(error => {
logger.error('Error fetching data:', error);
})
.finally(() => {
setIsLoading(false);
});
}, [userId]);
if (isLoading) { // const userIdFromSession = session.user.id;
return <div>Loading...</div>; // setUserId(userIdFromSession);
} // setIsLoading(true);
// fetchMessages(userId)
// .then(data => {
// if (data) {
// setMessages(data);
// }
// logger.debug('Success :', data);
// })
// .catch(error => {
// logger.error('Error fetching data:', error);
// })
// .finally(() => {
// setIsLoading(false);
// });
// }, [userId]);
// if (isLoading) {
// return <div>Loading...</div>;
// }
return ( return (
<ProtectedRoute> <ProtectedRoute>

View File

@ -4,22 +4,31 @@ import { useRouter } from 'next/navigation';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { Edit } from 'lucide-react'; import { Edit } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel'; import StatusLabel from '@/components/StatusLabel';
import useLocalStorage from '@/hooks/useLocalStorage';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url'; import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
import { fetchChildren } from '@/app/actions/subscriptionAction'; import { fetchChildren } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
export default function ParentHomePage() { export default function ParentHomePage() {
const [actions, setActions] = useState([]); const [actions, setActions] = useState([]);
const [children, setChildren] = useState([]); const [children, setChildren] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ; const { data: session, status } = useSession();
const [userId, setUserId] = useState(null);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (!userId) return; if (status === 'loading') return;
fetchChildren(userId).then(data => { if (!session) {
router.push(`${FE_USERS_LOGIN_URL}`);
}
console.log(session);
const userIdFromSession = session.user.user_id;
setUserId(userIdFromSession);
fetchChildren(userIdFromSession).then(data => {
setChildren(data); setChildren(data);
}); });
}, [userId]); }, [userId]);

View File

@ -13,10 +13,10 @@ import {
FE_PARENTS_HOME_URL FE_PARENTS_HOME_URL
} from '@/utils/Url'; } from '@/utils/Url';
import { login } from '@/app/actions/authAction'; import { login } from '@/app/actions/authAction';
import useLocalStorage from '@/hooks/useLocalStorage';
import { getSession } from 'next-auth/react'; import { getSession } from 'next-auth/react';
import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
@ -28,8 +28,6 @@ export default function Page() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [userId, setUserId] = useLocalStorage("userId", '') ;
const router = useRouter(); const router = useRouter();
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
@ -55,7 +53,6 @@ export default function Page() {
} }
const user = session.user; const user = session.user;
logger.debug('User Session:', user); logger.debug('User Session:', user);
localStorage.setItem('userId', user.id); // Stocker l'identifiant de l'utilisateur
if (user.droit === 0) { if (user.droit === 0) {
// Vue ECOLE // Vue ECOLE
} else if (user.droit === 1) { } else if (user.droit === 1) {

View File

@ -2,9 +2,9 @@ import {
BE_SUBSCRIPTION_STUDENTS_URL, BE_SUBSCRIPTION_STUDENTS_URL,
BE_SUBSCRIPTION_CHILDRENS_URL, BE_SUBSCRIPTION_CHILDRENS_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL
} from '@/utils/Url'; } from '@/utils/Url';
export const PENDING = 'pending'; export const PENDING = 'pending';
@ -101,10 +101,10 @@ export const archiveRegisterForm = (id) => {
}).then(requestResponseHandler) }).then(requestResponseHandler)
} }
export const fetchRegisterFormFile = (id = null) => { export const fetchRegistrationTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}` let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`
if (id) { if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${id}`; url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${id}`;
} }
const request = new Request( const request = new Request(
`${url}`, `${url}`,
@ -118,8 +118,8 @@ export const fetchRegisterFormFile = (id = null) => {
return fetch(request).then(requestResponseHandler) return fetch(request).then(requestResponseHandler)
}; };
export const editRegistrationFormFile= (fileId, data, csrfToken) => { export const editRegistrationTemplates= (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${fileId}`, {
method: 'PUT', method: 'PUT',
body: data, body: data,
headers: { headers: {
@ -130,21 +130,22 @@ export const editRegistrationFormFile= (fileId, data, csrfToken) => {
.then(requestResponseHandler) .then(requestResponseHandler)
} }
export const createRegistrationFormFile = (data,csrfToken) => { export const createRegistrationTemplates = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`, {
method: 'POST', method: 'POST',
body: data, body: JSON.stringify(data),
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
}, },
credentials: 'include', credentials: 'include',
}) })
.then(requestResponseHandler) .then(requestResponseHandler)
} }
export const deleteRegisterFormFile= (fileId,csrfToken) => { export const deleteRegistrationTemplates= (fileId,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${fileId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
@ -153,10 +154,10 @@ export const deleteRegisterFormFile= (fileId,csrfToken) => {
}) })
} }
export const fetchRegisterFormFileTemplate = (id = null) => { export const fetchRegistrationTemplateMaster = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`; let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`;
if(id){ if(id){
url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${id}`; url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${id}`;
} }
const request = new Request( const request = new Request(
`${url}`, `${url}`,
@ -170,21 +171,22 @@ export const fetchRegisterFormFileTemplate = (id = null) => {
return fetch(request).then(requestResponseHandler) return fetch(request).then(requestResponseHandler)
}; };
export const createRegistrationFormFileTemplate = (data,csrfToken) => { export const createRegistrationTemplateMaster = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`, {
method: 'POST', method: 'POST',
body: data, body: JSON.stringify(data),
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
'Content-Type':'application/json'
}, },
credentials: 'include', credentials: 'include',
}) })
.then(requestResponseHandler) .then(requestResponseHandler)
} }
export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => { export const deleteRegistrationTemplateMaster = (fileId,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
@ -193,8 +195,8 @@ export const deleteRegisterFormFileTemplate = (fileId,csrfToken) => {
}) })
} }
export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => { export const editRegistrationTemplateMaster = (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`, {
method: 'PUT', method: 'PUT',
body: data, body: data,
headers: { headers: {

View File

@ -0,0 +1,17 @@
import React, { useEffect } from 'react';
import { DocusealBuilder as OriginalDocusealBuilder } from '@docuseal/react';
const DocusealBuilder = ({ onSave, onSend, ...props }) => {
useEffect(() => {
if (onSave) {
props.save = onSave;
}
if (onSend) {
props.send = onSend;
}
}, [onSave, onSend, props]);
return <OriginalDocusealBuilder {...props} />;
};
export default DocusealBuilder;

View File

@ -1,50 +0,0 @@
import React, { useState } from 'react';
import { Upload } from 'lucide-react';
export default function DraggableFileUpload({ fileName, onFileSelect }) {
const [dragActive, setDragActive] = useState(false);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
};
const handleDragLeave = () => {
setDragActive(false);
};
const handleFileChosen = (selectedFile) => {
onFileSelect && onFileSelect(selectedFile);
};
const handleDrop = (event) => {
event.preventDefault();
setDragActive(false);
const droppedFile = event.dataTransfer.files[0];
handleFileChosen(droppedFile);
};
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
handleFileChosen(selectedFile);
};
return (
<div>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import Table from '@/components/Table';
export default function FilesToSign({ fileTemplates, columns }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à remplir</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import Table from '@/components/Table';
export default function FilesToUpload({ fileTemplates, columns }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à uploader</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}

View File

@ -7,7 +7,7 @@ import Loader from '@/components/Loader';
import Button from '@/components/Button'; import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { fetchRegisterFormFileTemplate, createRegistrationFormFile, fetchRegisterForm, deleteRegisterFormFile } from '@/app/actions/subscriptionAction'; import { fetchRegistrationTemplateMaster, createRegistrationTemplates, fetchRegisterForm, deleteRegistrationTemplates } from '@/app/actions/subscriptionAction';
import { fetchRegistrationFileFromGroup } from '@/app/actions/registerFileGroupAction'; import { fetchRegistrationFileFromGroup } from '@/app/actions/registerFileGroupAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
@ -117,7 +117,7 @@ export default function InscriptionFormShared({
data.append('register_form', formData.id); data.append('register_form', formData.id);
try { try {
const response = await createRegistrationFormFile(data, csrfToken); const response = await createRegistrationTemplates(data, csrfToken);
if (response) { if (response) {
setUploadedFiles(prev => { setUploadedFiles(prev => {
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId); const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
@ -158,7 +158,7 @@ export default function InscriptionFormShared({
if (!fileToDelete) return; if (!fileToDelete) return;
try { try {
await deleteRegisterFormFile(fileToDelete.id, csrfToken); await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId)); setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
} catch (error) { } catch (error) {
logger.error('Error deleting file:', error); logger.error('Error deleting file:', error);
@ -276,7 +276,7 @@ export default function InscriptionFormShared({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2> <h2 className="text-xl font-bold mb-4 text-gray-800">{requiredFileTemplates[currentPage - 2].name}</h2>
<iframe <iframe
src={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}?signature=true`} src={`${BASE_URL}/data/${requiredFileTemplates[currentPage - 2].file}`}
width="100%" width="100%"
height="800px" height="800px"
className="w-full" // Utiliser la classe CSS pour la largeur className="w-full" // Utiliser la classe CSS pour la largeur

View File

@ -0,0 +1,126 @@
import React from 'react';
import InputText from '@/components/InputText';
import SelectChoice from '@/components/SelectChoice';
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
const levels = [
{ value:'1', label: 'TPS - Très Petite Section'},
{ value:'2', label: 'PS - Petite Section'},
{ value:'3', label: 'MS - Moyenne Section'},
{ value:'4', label: 'GS - Grande Section'},
];
export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, errors }) {
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
return (
<>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Informations de l&apos;élève</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputText
name="last_name"
label="Nom"
value={formData.last_name}
onChange={(e) => updateFormField('last_name', e.target.value)}
required
errorMsg={getError('last_name')}
/>
<InputText
name="first_name"
label="Prénom"
value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required
/>
<InputText
name="nationality"
label="Nationalité"
value={formData.nationality}
required
onChange={(e) => updateFormField('nationality', e.target.value)}
/>
<InputText
name="birth_date"
type="date"
label="Date de Naissance"
value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)}
required
errorMsg={getError('birth_date')}
/>
<InputText
name="birth_place"
label="Lieu de Naissance"
value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)}
required
errorMsg={getError('birth_place')}
/>
<InputText
name="birth_postal_code"
label="Code Postal de Naissance"
value={formData.birth_postal_code}
onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
required
errorMsg={getError('birth_postal_code')}
/>
<div className="md:col-span-2">
<InputText
name="address"
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
required
errorMsg={getError('address')}
/>
</div>
<InputText
name="attending_physician"
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)}
required
errorMsg={getError('attending_physician')}
/>
<SelectChoice
name="level"
label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)}
choices={levels}
required
errorMsg={getError('level')}
/>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
<ResponsableInputFields
guardians={guardians}
onGuardiansChange={(id, field, value) => {
const updatedGuardians = guardians.map(resp =>
resp.id === id ? { ...resp, [field]: value } : resp
);
setGuardians(updatedGuardians);
}}
addGuardian={(e) => {
e.preventDefault();
setGuardians([...guardians, { id: Date.now() }]);
}}
deleteGuardian={(index) => {
const newArray = [...guardians];
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/>
</div>
</>
);
}

View File

@ -1,12 +1,12 @@
import * as Dialog from '@radix-ui/react-dialog'; import * as Dialog from '@radix-ui/react-dialog';
const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => { const Modal = ({ isOpen, setIsOpen, title, ContentComponent, modalClassName }) => {
return ( return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}> <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<Dialog.Content className="fixed inset-0 flex items-center justify-center p-4"> <Dialog.Content className="fixed inset-0 flex items-center justify-center p-4">
<div className="inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 min-w-[500px] w-max m-12 h-max"> <div className={`inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 ${modalClassName ? modalClassName : 'min-w-[500px] m-12 w-max max-h-[80vh] h-full'}`}>
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<Dialog.Title className="text-xl font-medium text-gray-900"> <Dialog.Title className="text-xl font-medium text-gray-900">
{title} {title}
@ -23,7 +23,7 @@ const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => {
</button> </button>
</Dialog.Close> </Dialog.Close>
</div> </div>
<div className="w-full"> <div className="w-full h-full">
<ContentComponent /> <ContentComponent />
</div> </div>
</div> </div>

View File

@ -31,13 +31,10 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
</label>
<div className="relative mt-1"> <div className="relative mt-1">
<button <button
type="button" type="button"
className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm" className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm hover:border-emerald-500 focus:border-emerald-500"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
> >
{selectedOptions.length > 0 ? ( {selectedOptions.length > 0 ? (
@ -49,7 +46,7 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
))} ))}
</div> </div>
) : ( ) : (
'Sélectionnez les niveaux' <span>{label}</span>
)} )}
<ChevronDown className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none h-full w-5" /> <ChevronDown className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none h-full w-5" />
</button> </button>

View File

@ -291,6 +291,7 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
return ( return (
<MultiSelect <MultiSelect
name="levels" name="levels"
label="Sélection de niveaux"
options={allNiveaux} options={allNiveaux}
selectedOptions={currentData.levels ? currentData.levels.map(levelId => allNiveaux.find(level => level.id === levelId)) : []} selectedOptions={currentData.levels ? currentData.levels.map(levelId => allNiveaux.find(level => level.id === levelId)) : []}
onChange={handleMultiSelectChange} onChange={handleMultiSelectChange}

View File

@ -0,0 +1,200 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import { fetchRegistrationFileGroups } from '@/app/actions/registerFileGroupAction';
import DocusealBuilder from '@/components/DocusealBuilder'; // Import du composant wrapper
import logger from '@/utils/logger';
import { BE_DOCUSEAL_GET_JWT, BASE_URL } from '@/utils/Url';
import Button from '@/components/Button'; // Import du composant Button
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { createRegistrationTemplates } from '@/app/actions/subscriptionAction'; // Import de la fonction createRegistrationTemplates
import { useCsrfToken } from '@/context/CsrfContext';
export default function FileUpload({ handleCreateTemplateMaster, fileToEdit = null }) {
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);
const [token, setToken] = useState(null);
const [templateMaster, setTemplateMaster] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedGroups, setSelectedGroups] = useState([]);
const [guardianEmails, setGuardianEmails] = useState([]);
const [registrationFormIds, setRegistrationFormIds] = useState([]);
const csrfToken = useCsrfToken();
useEffect(() => {
fetchRegistrationFileGroups().then(data => setGroups(data));
if (fileToEdit) {
setUploadedFileName(fileToEdit.name || '');
setSelectedGroups(fileToEdit.groups || []);
}
}, [fileToEdit]);
useEffect(() => {
const body = fileToEdit
? JSON.stringify({
user_email: 'n3wt.school@gmail.com',
template_id: fileToEdit.template_id
})
: JSON.stringify({
user_email: 'n3wt.school@gmail.com'
});
fetch('/api/docuseal/generateToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: body,
})
.then((response) => response.json())
.then((data) => {
setToken(data.token);
})
.catch((error) => console.error(error));
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setUploadedFileName(event.target.value);
};
const handleGroupChange = (selectedGroups) => {
setSelectedGroups(selectedGroups);
const emails = selectedGroups.flatMap(group => group.registration_forms.flatMap(form => form.guardians.map(guardian => guardian.email)));
setGuardianEmails(emails); // Mettre à jour la variable d'état avec les emails des guardians
const registrationFormIds = selectedGroups.flatMap(group => group.registration_forms.map(form => form.student_id));
setRegistrationFormIds(registrationFormIds); // Mettre à jour la variable d'état avec les IDs des dossiers d'inscription
logger.debug('Emails des Guardians associés aux groupes sélectionnés:', emails);
logger.debug('IDs des dossiers d\'inscription associés aux groupes sélectionnés:', registrationFormIds);
};
const handleLoad = (detail) => {
const templateId = detail?.id;
setTemplateMaster(detail);
logger.debug('Master template created with ID:', templateId);
}
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
const handleSubmit = () => {
logger.debug('Création du template master:', templateMaster?.id);
handleCreateTemplateMaster({
name: uploadedFileName,
group_ids: selectedGroups.map(group => group.id),
template_id: templateMaster?.id
});
guardianEmails.forEach((email, index) => {
cloneTemplate(templateMaster?.id, email)
.then(clonedDocument => {
// Sauvegarde des templates clonés dans la base de données
const data = {
name: `clone_${clonedDocument.id}`,
template_id: clonedDocument.id,
master: templateMaster?.id,
registration_form: registrationFormIds[index]
};
createRegistrationTemplates(data, csrfToken)
.then(response => {
logger.debug('Template enregistré avec succès:', response);
})
.catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error);
});
// Logique pour envoyer chaque template au submitter
logger.debug('Sending template to:', email);
})
.catch(error => {
logger.error('Error during cloning or sending:', error);
});
});
};
const cloneTemplate = (templateId, email) => {
return fetch('/api/docuseal/cloneTemplate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId,
email
})
})
.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);
return data;
})
.catch(error => {
console.error('Error cloning template:', error);
throw error;
});
};
return (
<div className="h-full flex flex-col mt-4 space-y-4">
<div className="grid grid-cols-10 gap-4 items-start">
<div className="col-span-2">
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
options={groups}
selectedOptions={selectedGroups}
onChange={handleGroupChange}
errorMsg={null}
/>
</div>
<div className="col-span-7">
{token && (
<DocusealBuilder
token={token}
headers={{
'Authorization': `Bearer ${token}`
}}
withSendButton={false}
withSignYourselfButton={false}
autosave={true}
language={'fr'}
onLoad={handleLoad}
onUpload={handleUpload}
className="h-full overflow-auto" // Ajouter overflow-auto pour permettre le défilement
style={{ maxHeight: '70vh' }} // Limiter la hauteur maximale du composant
// Il faut auter l'host correspondant (une fois passé en HTTPS)
//host="docuseal:3001"
/>
)}
</div>
<div className="col-span-1 flex justify-end">
<Button
text="Valider"
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(uploadedFileName === '' || selectedGroups.length === 0)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
primary
disabled={uploadedFileName === '' || selectedGroups.length === 0}
/>
</div>
</div>
</div>
);
}

View File

@ -2,14 +2,15 @@ import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react'; import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload'; import FileUpload from '@/components/Structure/Files/FileUpload';
import { formatDate } from '@/utils/Date'; import { formatDate } from '@/utils/Date';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import { import {
fetchRegisterFormFileTemplate, fetchRegistrationTemplateMaster,
createRegistrationFormFileTemplate, createRegistrationTemplateMaster,
editRegistrationFormFileTemplate, editRegistrationTemplateMaster,
deleteRegisterFormFileTemplate deleteRegistrationTemplateMaster,
getRegisterFormFileTemplate
} from '@/app/actions/subscriptionAction'; } from '@/app/actions/subscriptionAction';
import { import {
fetchRegistrationFileGroups, fetchRegistrationFileGroups,
@ -17,10 +18,9 @@ import {
deleteRegistrationFileGroup, deleteRegistrationFileGroup,
editRegistrationFileGroup editRegistrationFileGroup
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/RegistrationFileGroupForm'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
export default function FilesManagement({ csrfToken }) { export default function FilesGroupsManagement({ csrfToken }) {
const [fichiers, setFichiers] = useState([]); const [fichiers, setFichiers] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null);
@ -29,23 +29,18 @@ export default function FilesManagement({ csrfToken }) {
const [fileToEdit, setFileToEdit] = useState(null); const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null); const [groupToEdit, setGroupToEdit] = useState(null);
const [token, setToken] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
// Fonction pour transformer les données des fichiers avec les informations complètes du groupe
const transformFileData = (file, groups) => { const transformFileData = (file, groups) => {
if (!file.group) return file; const groupInfos = file.groups.map(groupId => groups.find(g => g.id === groupId) || { id: groupId, name: 'Groupe inconnu' });
const groupInfo = groups.find(g => g.id === file.group);
return { return {
...file, ...file,
group: groupInfo || { id: file.group, name: 'Groupe inconnu' } groups: groupInfos
}; };
}; };
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
fetchRegisterFormFileTemplate(), fetchRegistrationTemplateMaster(),
fetchRegistrationFileGroups() fetchRegistrationFileGroups()
]).then(([filesData, groupsData]) => { ]).then(([filesData, groupsData]) => {
setGroups(groupsData); setGroups(groupsData);
@ -57,12 +52,12 @@ export default function FilesManagement({ csrfToken }) {
const transformedFiles = filesData.map(file => transformFileData(file, groupsData)); const transformedFiles = filesData.map(file => transformFileData(file, groupsData));
setFichiers(transformedFiles); setFichiers(transformedFiles);
}).catch(err => { }).catch(err => {
logger.debug(err.message); console.log(err.message);
}); });
}, []); }, []);
const handleFileDelete = (fileId) => { const handleFileDelete = (fileId) => {
deleteRegisterFormFileTemplate(fileId, csrfToken) deleteRegistrationTemplateMaster(fileId, csrfToken)
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
setFichiers(fichiers.filter(fichier => fichier.id !== fileId)); setFichiers(fichiers.filter(fichier => fichier.id !== fileId));
@ -72,7 +67,7 @@ export default function FilesManagement({ csrfToken }) {
} }
}) })
.catch(error => { .catch(error => {
logger.error('Error deleting file:', error); console.error('Error deleting file:', error);
alert('Erreur lors de la suppression du fichier.'); alert('Erreur lors de la suppression du fichier.');
}); });
}; };
@ -83,7 +78,45 @@ export default function FilesManagement({ csrfToken }) {
setIsModalOpen(true); setIsModalOpen(true);
}; };
const handleFileUpload = ({file, name, is_required, order, groupId}) => { const handleCreateTemplateMaster = ({name, group_ids, template_id}) => {
const data = {
name: name,
template_id: template_id,
groups: group_ids
};
console.log(data);
if (isEditing && fileToEdit) {
editRegistrationTemplateMaster(fileToEdit.id, data, csrfToken)
.then(data => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? transformedFile : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
});
} else {
createRegistrationTemplateMaster(data, csrfToken)
.then(data => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFiles => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
const handleTemplate = ({name, is_required, order, groupId, document_id}) => {
if (!name) { if (!name) {
alert('Veuillez entrer un nom de fichier.'); alert('Veuillez entrer un nom de fichier.');
return; return;
@ -96,14 +129,16 @@ export default function FilesManagement({ csrfToken }) {
formData.append('name', name); formData.append('name', name);
formData.append('is_required', is_required); formData.append('is_required', is_required);
formData.append('order', order); formData.append('order', order);
formData.append('document_id', document_id);
// Modification ici : vérifier si groupId existe et n'est pas vide // Modification ici : vérifier si groupId existe et n'est pas vide
if (groupId && groupId !== '') { if (groupId && groupId !== '') {
formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id' formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id'
} }
if (isEditing && fileToEdit) { if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) editRegistrationTemplateMaster(fileToEdit.id, formData, csrfToken)
.then(data => { .then(data => {
// Transformer le fichier mis à jour avec les informations du groupe // Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups); const transformedFile = transformFileData(data, groups);
@ -115,11 +150,11 @@ export default function FilesManagement({ csrfToken }) {
setIsEditing(false); setIsEditing(false);
}) })
.catch(error => { .catch(error => {
logger.error('Error editing file:', error); console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier'); alert('Erreur lors de la modification du fichier');
}); });
} else { } else {
createRegistrationFormFileTemplate(formData, csrfToken) createRegistrationTemplateMaster(formData, csrfToken)
.then(data => { .then(data => {
// Transformer le nouveau fichier avec les informations du groupe // Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups); const transformedFile = transformFileData(data, groups);
@ -127,25 +162,33 @@ export default function FilesManagement({ csrfToken }) {
setIsModalOpen(false); setIsModalOpen(false);
}) })
.catch(error => { .catch(error => {
logger.error('Error uploading file:', error); console.error('Error uploading file:', error);
}); });
} }
}; };
const handleGroupSubmit = async (groupData) => { const handleGroupSubmit = (groupData) => {
try {
if (groupToEdit) { if (groupToEdit) {
const updatedGroup = await editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken); editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken)
.then(updatedGroup => {
setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group)); setGroups(groups.map(group => group.id === groupToEdit.id ? updatedGroup : group));
setGroupToEdit(null); setGroupToEdit(null);
} else {
const newGroup = await createRegistrationFileGroup(groupData, csrfToken);
setGroups([...groups, newGroup]);
}
setIsGroupModalOpen(false); setIsGroupModalOpen(false);
} catch (error) { })
logger.error('Error handling group:', error); .catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe'); alert('Erreur lors de l\'opération sur le groupe');
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
.then(newGroup => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
} }
}; };
@ -175,24 +218,20 @@ export default function FilesManagement({ csrfToken }) {
alert('Groupe supprimé avec succès.'); alert('Groupe supprimé avec succès.');
}) })
.catch(error => { .catch(error => {
logger.error('Error deleting group:', error); console.error('Error deleting group:', error);
alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.'); alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.');
}); });
} }
}; };
// Ajouter cette fonction de filtrage
const filteredFiles = fichiers.filter(file => { const filteredFiles = fichiers.filter(file => {
if (!selectedGroup) return true; if (!selectedGroup) return true;
return file.group && file.group.id === parseInt(selectedGroup); return file.groups && file.groups.some(group => group.id === parseInt(selectedGroup));
}); });
const columnsFiles = [ const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name }, { name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Groupe', transform: (row) => row.group ? row.group.name : 'Aucun' }, { name: 'Groupes', transform: (row) => row.groups && row.groups.length > 0 ? row.groups.map(group => group.name).join(', ') : 'Aucun' },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Actions', transform: (row) => ( { name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
{row.file && ( {row.file && (
@ -206,9 +245,6 @@ export default function FilesManagement({ csrfToken }) {
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700"> <button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
<button onClick={() => handleSignatureRequest(row)} className="text-green-500 hover:text-green-700">
<Signature size={16} />
</button>
</div> </div>
)} )}
]; ];
@ -228,40 +264,24 @@ export default function FilesManagement({ csrfToken }) {
)} )}
]; ];
// Fonction pour gérer la demande de signature
const handleSignatureRequest = (file) => {
fetch('http://localhost:8080/DocuSeal/generateToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
document_id: file.id,
user_email: 'anthony.casini.30@gmail.com',
url: file.file
}),
})
.then((response) => response.json())
.then((data) => {
console.log("token received : ", data.token);
setToken(data.token);
setSelectedFile(file);
})
.catch((error) => console.error(error));
};
return ( return (
<div> <div>
<Modal <Modal
isOpen={isModalOpen} isOpen={isModalOpen}
setIsOpen={setIsModalOpen} setIsOpen={(isOpen) => {
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'} setIsModalOpen(isOpen);
if (!isOpen) {
setFileToEdit(null);
}
}}
title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => ( ContentComponent={() => (
<FileUpload <FileUpload
onFileUpload={handleFileUpload} handleCreateTemplateMaster={handleCreateTemplateMaster}
fileToEdit={fileToEdit} fileToEdit={fileToEdit}
/> />
)} )}
modalClassName='w-4/5 h-4/5'
/> />
<Modal <Modal
isOpen={isGroupModalOpen} isOpen={isGroupModalOpen}
@ -324,14 +344,6 @@ export default function FilesManagement({ csrfToken }) {
/> />
</div> </div>
)} )}
{token && selectedFile && (
<DocusealBuilder
token={token}
headers={{
'Authorization': `Bearer Rh2CC75ZMZqirmtBGA5NRjUzj8hr9eDYTBeZxv3jgzb`
}}
/>
)}
</div> </div>
); );
} }

View File

@ -1,48 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
if (typeof window !== 'undefined') {
const item = window.localStorage.getItem(key);
// Vérifier si l'item existe et n'est pas undefined
return item !== null && item !== 'undefined'
? JSON.parse(item)
: initialValue;
}
return initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
useEffect(() => {
try {
// Vérifier si la valeur n'est pas undefined avant de la stocker
if (typeof storedValue !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} else {
window.localStorage.removeItem(key);
}
} catch (error) {
console.error('Error writing to localStorage:', error);
}
}, [key, storedValue]);
const setValue = (value) => {
try {
// Permettre à la valeur d'être une fonction
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
} catch (error) {
console.error('Error updating localStorage value:', error);
}
};
return [storedValue, setValue];
};
export default useLocalStorage;

View File

@ -0,0 +1,36 @@
import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'POST') {
const { templateId, email } = req.body;
fetch(BE_DOCUSEAL_CLONE_TEMPLATE, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': process.env.DOCUSEAL_API_KEY
},
body: JSON.stringify({
templateId,
email
})
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message); });
}
return response.json();
})
.then(data => {
console.log('Template cloned successfully:', data);
res.status(200).json(data);
})
.catch(error => {
console.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`);
}
}

View File

@ -0,0 +1,31 @@
import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'POST') {
console.log('DOCUSEAL_API_KEY:', process.env.DOCUSEAL_API_KEY);
fetch(BE_DOCUSEAL_GET_JWT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': process.env.DOCUSEAL_API_KEY
},
body: JSON.stringify(req.body),
})
.then(response => {
console.log('Response status:', response.status);
return response.json().then(data => ({ status: response.status, data }));
})
.then(({ status, data }) => {
console.log('Response data:', data);
res.status(status).json(data);
})
.catch(error => {
console.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`);
}
}

View File

@ -4,6 +4,9 @@ export const BASE_URL = process.env.NEXT_PUBLIC_API_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`
// 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`
@ -20,8 +23,8 @@ export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession`
export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students` // Récupère la liste des élèves inscrits ou en cours d'inscriptions export const BE_SUBSCRIPTION_STUDENTS_URL = `${BASE_URL}/Subscriptions/students` // Récupère la liste des élèves inscrits ou en cours d'inscriptions
export const BE_SUBSCRIPTION_CHILDRENS_URL = `${BASE_URL}/Subscriptions/children` // Récupère la liste des élèves d'un profil export const BE_SUBSCRIPTION_CHILDRENS_URL = `${BASE_URL}/Subscriptions/children` // Récupère la liste des élèves d'un profil
export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms` export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/registerForms`
export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL = `${BASE_URL}/Subscriptions/registrationFileTemplates` export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL = `${BASE_URL}/Subscriptions/registrationTemplateMasters`
export const BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL = `${BASE_URL}/Subscriptions/registrationFiles` export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationTemplates`
export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups` export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`
export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId` export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`

View File

@ -35,11 +35,13 @@ services:
- 8080:8080 - 8080:8080
volumes: volumes:
- ./Back-End:/Back-End - ./Back-End:/Back-End
env_file:
- .env
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
- TEST_MODE=True - TEST_MODE=True
- CORS_ALLOWED_ORIGINS=http://localhost:3000 - CORS_ALLOWED_ORIGINS=http://localhost:3000
- CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:8080 - CSRF_TRUSTED_ORIGINS=http://localhost:3000
links: links:
- "database:database" - "database:database"
- "redis:redis" - "redis:redis"
@ -66,17 +68,19 @@ services:
volumes: volumes:
- ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh
frontend: # frontend:
build: # build:
context: ./Front-End # context: ./Front-End
args: # args:
- BUILD_MODE=development # - BUILD_MODE=development
ports: # ports:
- 3000:3000 # - 3000:3000
volumes: # volumes:
- ./Front-End:/app # - ./Front-End:/app
environment: # env_file:
- TZ=Europe/Paris # - .env
depends_on: # environment:
- backend # - TZ=Europe/Paris
# depends_on:
# - backend