mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
Compare commits
46 Commits
WIP_Docker
...
7486f6c5ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 7486f6c5ce | |||
| 1e5bc6ccba | |||
| 0fb668b212 | |||
| 5e62ee5100 | |||
| e89d2fc4c3 | |||
| 9481a0132d | |||
| 482e8c1357 | |||
| 0e0141d155 | |||
| 7f002e2e6a | |||
| 0064b8d35a | |||
| ec2c1daebc | |||
| 67cea2f1c6 | |||
| 5785bfae46 | |||
| a17078709b | |||
| d58155da06 | |||
| 043d93dcc4 | |||
| 6bc24055cd | |||
| 2f6d30b85b | |||
| c161fa7e75 | |||
| 789816e986 | |||
| 6bedf715cc | |||
| 59a0d40130 | |||
| 25e2799c0f | |||
| 017c0290dd | |||
| fe2d4d4513 | |||
| f93c428259 | |||
| e61cd51ce2 | |||
| 6a0b90e98f | |||
| 8a71fa1830 | |||
| f265540da2 | |||
| 5be5f9f70d | |||
| 68a6a63c4f | |||
| af30ae33b5 | |||
| e509625811 | |||
| 3a2455f918 | |||
| e74f9c98a2 | |||
| 8f0cf16f70 | |||
| 78d96f82f9 | |||
| c117f96e52 | |||
| e4668ef1e5 | |||
| ec2630a6e4 | |||
| d65b171da8 | |||
| 4a6b7ce379 | |||
| 170f7c4fa8 | |||
| ce83e02f7b | |||
| a69498dd06 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
.env
|
||||
node_modules/
|
||||
hardcoded-strings-report.md
|
||||
backend.env
|
||||
@ -1 +1 @@
|
||||
node scripts/prepare-commit-msg.js "$1" "$2"
|
||||
#node scripts/prepare-commit-msg.js "$1" "$2"
|
||||
75
Back-End/Auth/migrations/0001_initial.py
Normal file
75
Back-End/Auth/migrations/0001_initial.py
Normal file
@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProfileRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role_type', models.IntegerField(choices=[(-1, 'NON DEFINI'), (0, 'ECOLE'), (1, 'ADMIN'), (2, 'PARENT')], default=-1)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile_roles', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Directeur',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('profile_role', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='directeur_profile', to='Auth.profilerole')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('email', models.EmailField(default='', max_length=255, unique=True, validators=[django.core.validators.EmailValidator()])),
|
||||
('roleIndexLoginDefault', models.IntegerField(default=0)),
|
||||
('code', models.CharField(blank=True, default='', max_length=200)),
|
||||
('datePeremption', models.CharField(blank=True, default='', max_length=200)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profilerole',
|
||||
name='profile',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
0
Back-End/Auth/migrations/__init__.py
Normal file
0
Back-End/Auth/migrations/__init__.py
Normal file
@ -223,14 +223,28 @@ def makeToken(user):
|
||||
"""
|
||||
try:
|
||||
# Récupérer tous les rôles de l'utilisateur actifs
|
||||
roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency', 'establishment__total_capacity', 'establishment__api_docuseal')
|
||||
roles_qs = ProfileRole.objects.filter(profile=user, is_active=True).select_related('establishment')
|
||||
roles = []
|
||||
for role in roles_qs:
|
||||
logo_url = ""
|
||||
if role.establishment.logo:
|
||||
# Construit l'URL complète pour le logo
|
||||
logo_url = f"{role.establishment.logo.url}"
|
||||
roles.append({
|
||||
"role_type": role.role_type,
|
||||
"establishment__id": role.establishment.id,
|
||||
"establishment__name": role.establishment.name,
|
||||
"establishment__evaluation_frequency": role.establishment.evaluation_frequency,
|
||||
"establishment__total_capacity": role.establishment.total_capacity,
|
||||
"establishment__logo": logo_url,
|
||||
})
|
||||
|
||||
# Générer le JWT avec la bonne syntaxe datetime
|
||||
access_payload = {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||
'roles': list(roles),
|
||||
'roles': roles,
|
||||
'type': 'access',
|
||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
||||
'iat': datetime.utcnow(),
|
||||
@ -361,7 +375,7 @@ class SubscribeView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
establishment_id = newProfilConnection['establishment_id']
|
||||
@ -437,7 +451,7 @@ class NewPasswordView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
@ -487,7 +501,7 @@ class ResetPasswordView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request, code):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
@ -498,7 +512,7 @@ class ResetPasswordView(APIView):
|
||||
if profil:
|
||||
|
||||
if datetime.strptime(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'), '%d-%m-%Y %H:%M') > datetime.strptime(profil.datePeremption, '%d-%m-%Y %H:%M'):
|
||||
retourErreur = error.returnMessage[error.EXPIRED_URL] % (_uuid)
|
||||
retourErreur = error.returnMessage[error.EXPIRED_URL]
|
||||
elif validationOk:
|
||||
retour = error.returnMessage[error.PASSWORD_CHANGED]
|
||||
|
||||
|
||||
63
Back-End/Common/migrations/0001_initial.py
Normal file
63
Back-End/Common/migrations/0001_initial.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Cycle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('number', models.IntegerField(unique=True)),
|
||||
('label', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('cycle', models.IntegerField(choices=[(1, 'Cycle 1'), (2, 'Cycle 2'), (3, 'Cycle 3'), (4, 'Cycle 4')])),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentModeType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentPlanType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='Common.domain')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Level',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='Common.cycle')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Common/migrations/__init__.py
Normal file
0
Back-End/Common/migrations/__init__.py
Normal file
@ -1 +0,0 @@
|
||||
# This file is intentionally left blank to make this directory a Python package.
|
||||
@ -1,9 +0,0 @@
|
||||
from django.urls import path, re_path
|
||||
from .views import generate_jwt_token, clone_template, remove_template, download_template
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'generateToken$', generate_jwt_token, name='generate_jwt_token'),
|
||||
re_path(r'cloneTemplate$', clone_template, name='clone_template'),
|
||||
re_path(r'removeTemplate/(?P<id>[0-9]+)$', remove_template, name='remove_template'),
|
||||
re_path(r'downloadTemplate/(?P<slug>[\w-]+)$', download_template, name='download_template')
|
||||
]
|
||||
@ -1,200 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
import jwt
|
||||
import datetime
|
||||
import requests
|
||||
from Establishment.models import Establishment
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def generate_jwt_token(request):
|
||||
# Récupérer l'établissement concerné (par ID ou autre info transmise)
|
||||
establishment_id = request.data.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Récupérer les données de la requête
|
||||
user_email = request.data.get('user_email')
|
||||
documents_urls = request.data.get('documents_urls', [])
|
||||
template_id = request.data.get('id')
|
||||
|
||||
if not user_email:
|
||||
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Utiliser la clé API de l'établissement comme secret JWT
|
||||
jwt_secret = establishment.api_docuseal
|
||||
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
|
||||
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
|
||||
|
||||
payload = {
|
||||
'user_email': user_email,
|
||||
'documents_urls': documents_urls,
|
||||
'template_id': template_id,
|
||||
'exp': datetime.datetime.utcnow() + expiration_delta
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
|
||||
return Response({'token': token}, status=status.HTTP_200_OK)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def clone_template(request):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.data.get('establishment_id')
|
||||
print(f"establishment_id : {establishment_id}")
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Récupérer les données de la requête
|
||||
document_id = request.data.get('templateId')
|
||||
email = request.data.get('email')
|
||||
is_required = request.data.get('is_required')
|
||||
|
||||
# Vérifier les données requises
|
||||
if not document_id:
|
||||
return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# URL de l'API de DocuSeal pour cloner le template
|
||||
clone_url = f'https://docuseal.com/api/templates/{document_id}/clone'
|
||||
|
||||
# Faire la requête pour cloner le template
|
||||
try:
|
||||
response = requests.post(clone_url, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to clone template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if is_required:
|
||||
# URL de l'API de DocuSeal pour créer une submission
|
||||
submission_url = f'https://docuseal.com/api/submissions'
|
||||
|
||||
try:
|
||||
clone_id = data['id']
|
||||
response = requests.post(submission_url, json={
|
||||
'template_id': clone_id,
|
||||
'send_email': False,
|
||||
'submitters': [{'email': email}]
|
||||
}, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to create submission'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
data[0]['id'] = clone_id
|
||||
return Response(data[0], status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
else:
|
||||
print(f'NOT REQUIRED -> on ne crée pas de submission')
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['DELETE'])
|
||||
def remove_template(request, id):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# URL de l'API de DocuSeal pour supprimer le template
|
||||
|
||||
clone_url = f'https://docuseal.com/api/templates/{id}'
|
||||
|
||||
try:
|
||||
response = requests.delete(clone_url, headers={
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to remove template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
def download_template(request, slug):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Vérifier les données requises
|
||||
if not slug:
|
||||
return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# URL de l'API de DocuSeal pour télécharger le template
|
||||
download_url = f'https://docuseal.com/submitters/{slug}/download'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to download template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
29
Back-End/Establishment/migrations/0001_initial.py
Normal file
29
Back-End/Establishment/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Establishment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('address', models.CharField(max_length=255)),
|
||||
('total_capacity', models.IntegerField()),
|
||||
('establishment_type', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Maternelle'), (2, 'Primaire'), (3, 'Secondaire')]), size=None)),
|
||||
('evaluation_frequency', models.IntegerField(choices=[(1, 'Trimestre'), (2, 'Semestre'), (3, 'Année')], default=1)),
|
||||
('licence_code', models.CharField(blank=True, max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='api_docuseal',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
19
Back-End/Establishment/migrations/0003_establishment_logo.py
Normal file
19
Back-End/Establishment/migrations/0003_establishment_logo.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-31 09:56
|
||||
|
||||
import Establishment.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0002_establishment_api_docuseal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='logo',
|
||||
field=models.FileField(blank=True, null=True, upload_to=Establishment.models.registration_logo_upload_to),
|
||||
),
|
||||
]
|
||||
0
Back-End/Establishment/migrations/__init__.py
Normal file
0
Back-End/Establishment/migrations/__init__.py
Normal file
@ -2,6 +2,12 @@ from django.db import models
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import os
|
||||
|
||||
def registration_logo_upload_to(instance, filename):
|
||||
ext = os.path.splitext(filename)[1]
|
||||
return f"logos/school_{instance.pk}/logo{ext}"
|
||||
|
||||
class StructureType(models.IntegerChoices):
|
||||
MATERNELLE = 1, _('Maternelle')
|
||||
PRIMAIRE = 2, _('Primaire')
|
||||
@ -21,7 +27,11 @@ class Establishment(models.Model):
|
||||
licence_code = models.CharField(max_length=100, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
|
||||
logo = models.FileField(
|
||||
upload_to=registration_logo_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -1,16 +1,19 @@
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
|
||||
from django.utils.decorators import method_decorator
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from .models import Establishment
|
||||
from .serializers import EstablishmentSerializer
|
||||
from N3wtSchool.bdd import delete_object, getAllObjects
|
||||
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
||||
from School.models import EstablishmentCompetency, Competency
|
||||
from django.db.models import Q
|
||||
from Auth.models import Profile, ProfileRole, Directeur
|
||||
from Settings.models import SMTPSettings
|
||||
import N3wtSchool.mailManager as mailer
|
||||
import os
|
||||
from N3wtSchool import settings
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
@ -41,6 +44,8 @@ class EstablishmentListCreateView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class EstablishmentDetailView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def get(self, request, id=None):
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=id)
|
||||
@ -50,15 +55,20 @@ class EstablishmentDetailView(APIView):
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, id):
|
||||
establishment_data = JSONParser().parse(request)
|
||||
"""
|
||||
Met à jour un établissement existant.
|
||||
Accepte les données en multipart/form-data pour permettre l'upload de fichiers (ex : logo).
|
||||
"""
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=id)
|
||||
except Establishment.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
establishment_serializer = EstablishmentSerializer(establishment, data=establishment_data, partial=True)
|
||||
|
||||
# Utilise request.data pour supporter multipart/form-data (fichiers et champs classiques)
|
||||
establishment_serializer = EstablishmentSerializer(establishment, data=request.data, partial=True)
|
||||
if establishment_serializer.is_valid():
|
||||
establishment_serializer.save()
|
||||
return JsonResponse(establishment_serializer.data, safe=False)
|
||||
return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, id):
|
||||
@ -66,6 +76,7 @@ class EstablishmentDetailView(APIView):
|
||||
|
||||
def create_establishment_with_directeur(establishment_data):
|
||||
# Extraction des sous-objets
|
||||
# school_name = establishment_data.get("name")
|
||||
directeur_data = establishment_data.pop("directeur", None)
|
||||
smtp_settings_data = establishment_data.pop("smtp_settings", {})
|
||||
|
||||
@ -90,6 +101,8 @@ def create_establishment_with_directeur(establishment_data):
|
||||
# Création de l'établissement
|
||||
establishment_serializer = EstablishmentSerializer(data=establishment_data)
|
||||
establishment_serializer.is_valid(raise_exception=True)
|
||||
# base_dir = os.path.join(settings.MEDIA_ROOT, f"logo/school_{school_name}")
|
||||
# os.makedirs(base_dir, exist_ok=True)
|
||||
establishment = establishment_serializer.save()
|
||||
|
||||
# Création ou récupération du ProfileRole ADMIN pour ce profil et cet établissement
|
||||
@ -97,7 +110,7 @@ def create_establishment_with_directeur(establishment_data):
|
||||
profile=profile,
|
||||
establishment=establishment,
|
||||
role_type=ProfileRole.RoleType.PROFIL_ADMIN,
|
||||
defaults={"is_active": True}
|
||||
defaults={"is_active": False}
|
||||
)
|
||||
|
||||
# Création ou mise à jour du Directeur lié à ce ProfileRole
|
||||
@ -114,4 +127,6 @@ def create_establishment_with_directeur(establishment_data):
|
||||
smtp_settings_data["establishment"] = establishment
|
||||
SMTPSettings.objects.create(**smtp_settings_data)
|
||||
|
||||
# Envoi du mail
|
||||
mailer.sendRegistrationDirector(directeur_email, establishment.pk)
|
||||
return establishment, establishment_serializer.data
|
||||
30
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
30
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Messagerie',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('objet', models.CharField(blank=True, default='', max_length=200)),
|
||||
('corpus', models.CharField(blank=True, default='', max_length=200)),
|
||||
('date_envoi', models.DateTimeField(auto_now_add=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('conversation_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('destinataire', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='messages_recus', to=settings.AUTH_USER_MODEL)),
|
||||
('emetteur', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='messages_envoyes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,87 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:40
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('GestionMessagerie', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Conversation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('conversation_type', models.CharField(choices=[('private', 'Privée'), ('group', 'Groupe')], default='private', max_length=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('last_activity', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('content', models.TextField()),
|
||||
('message_type', models.CharField(choices=[('text', 'Texte'), ('file', 'Fichier'), ('image', 'Image'), ('system', 'Système')], default='text', max_length=10)),
|
||||
('file_url', models.URLField(blank=True, null=True)),
|
||||
('file_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('file_size', models.BigIntegerField(blank=True, null=True)),
|
||||
('file_type', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_edited', models.BooleanField(default=False)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='GestionMessagerie.conversation')),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPresence',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('online', 'En ligne'), ('away', 'Absent'), ('busy', 'Occupé'), ('offline', 'Hors ligne')], default='offline', max_length=10)),
|
||||
('last_seen', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_typing_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='typing_users', to='GestionMessagerie.conversation')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='presence', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConversationParticipant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('last_read_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='GestionMessagerie.conversation')),
|
||||
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_participants', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('conversation', 'participant')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageRead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('read_at', models.DateTimeField(auto_now_add=True)),
|
||||
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_by', to='GestionMessagerie.message')),
|
||||
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('message', 'participant')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
Back-End/GestionMessagerie/migrations/__init__.py
Normal file
0
Back-End/GestionMessagerie/migrations/__init__.py
Normal file
28
Back-End/GestionNotification/migrations/0001_initial.py
Normal file
28
Back-End/GestionNotification/migrations/0001_initial.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.CharField(max_length=255)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('typeNotification', models.IntegerField(choices=[(0, 'Aucune notification'), (1, 'Un message a été reçu'), (2, "Le dossier d'inscription a été mis à jour")], default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/GestionNotification/migrations/__init__.py
Normal file
0
Back-End/GestionNotification/migrations/__init__.py
Normal file
@ -1,4 +1,5 @@
|
||||
from typing import Final
|
||||
from N3wtSchool import settings
|
||||
|
||||
WRONG_ID: Final = 1
|
||||
INCOMPLETE: Final = 2
|
||||
@ -8,11 +9,14 @@ DIFFERENT_PASWWORD: Final = 5
|
||||
PROFIL_NOT_EXISTS: Final = 6
|
||||
MESSAGE_REINIT_PASSWORD: Final = 7
|
||||
EXPIRED_URL: Final = 8
|
||||
PASSWORD_CHANGED: Final = 8
|
||||
WRONG_MAIL_FORMAT: Final = 9
|
||||
PROFIL_INACTIVE: Final = 10
|
||||
MESSAGE_ACTIVATION_PROFILE: Final = 11
|
||||
PROFIL_ACTIVE: Final = 12
|
||||
PASSWORD_CHANGED: Final = 9
|
||||
WRONG_MAIL_FORMAT: Final = 10
|
||||
PROFIL_INACTIVE: Final = 11
|
||||
MESSAGE_ACTIVATION_PROFILE: Final = 12
|
||||
PROFIL_ACTIVE: Final = 13
|
||||
|
||||
def get_expired_url_message():
|
||||
return f"L'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : {settings.BASE_URL}/password/new"
|
||||
|
||||
returnMessage = {
|
||||
WRONG_ID:'Identifiants invalides',
|
||||
@ -22,7 +26,7 @@ returnMessage = {
|
||||
DIFFERENT_PASWWORD: 'Les mots de passe ne correspondent pas',
|
||||
PROFIL_NOT_EXISTS: 'Aucun profil associé à cet utilisateur',
|
||||
MESSAGE_REINIT_PASSWORD: 'Un mail a été envoyé à l\'adresse \'%s\'',
|
||||
EXPIRED_URL:'L\'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : http://localhost:3000/password/reset?uuid=%s',
|
||||
EXPIRED_URL: get_expired_url_message(),
|
||||
PASSWORD_CHANGED: 'Le mot de passe a été réinitialisé',
|
||||
WRONG_MAIL_FORMAT: 'L\'adresse mail est mal formatée',
|
||||
PROFIL_INACTIVE: 'Le profil n\'est pas actif',
|
||||
|
||||
@ -101,6 +101,27 @@ def envoieReinitMotDePasse(recipients, code):
|
||||
|
||||
return errorMessage
|
||||
|
||||
def sendRegistrationDirector(recipients, establishment_id):
|
||||
errorMessage = ''
|
||||
try:
|
||||
# Préparation du contexte pour le template
|
||||
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Bienvenue dans la communauté !'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'URL_DJANGO': settings.URL_DJANGO,
|
||||
'email': recipients,
|
||||
'establishment': establishment_id
|
||||
}
|
||||
subject = EMAIL_INSCRIPTION_SUBJECT
|
||||
html_message = render_to_string('emails/subscribeDirector.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
return errorMessage
|
||||
|
||||
|
||||
def sendRegisterForm(recipients, establishment_id):
|
||||
errorMessage = ''
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
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"
|
||||
response['Content-Security-Policy'] = f"frame-ancestors 'self' {settings.BASE_URL}"
|
||||
return response
|
||||
|
||||
@ -24,18 +24,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
MEDIA_URL = '/data/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
|
||||
|
||||
BASE_URL = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
|
||||
BASE_URL = os.getenv('BASE_URL', 'http://localhost:3000')
|
||||
|
||||
|
||||
LOGIN_REDIRECT_URL = '/Subscriptions/registerForms'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-afjm6kvigncxzx6jjjf(qb0n(*qvi#je79r=gqflcn007d_ve9'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
DEBUG = os.getenv('DJANGO_DEBUG', True)
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
@ -211,8 +209,6 @@ USE_I18N = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
DEBUG = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
@ -232,33 +228,18 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
########################################################################
|
||||
|
||||
|
||||
|
||||
DJANGO_SUPERUSER_PASSWORD='admin'
|
||||
DJANGO_SUPERUSER_USERNAME='admin'
|
||||
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
|
||||
# Configuration de l'email de l'application
|
||||
smtp_config_file = 'N3wtSchool/Configuration/application.json'
|
||||
|
||||
if os.path.exists(smtp_config_file):
|
||||
try:
|
||||
with open(smtp_config_file, 'r') as f:
|
||||
smtpSettings = json.load(f)
|
||||
EMAIL_HOST = smtpSettings.get('hostSMTP', '')
|
||||
EMAIL_PORT = smtpSettings.get('portSMTP', 587)
|
||||
EMAIL_HOST_USER = smtpSettings.get('username', '')
|
||||
EMAIL_HOST_PASSWORD = smtpSettings.get('password', '')
|
||||
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.com')
|
||||
EMAIL_PORT = os.getenv('EMAIL_PORT', 587)
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_USE_TLS = smtpSettings.get('useTLS', True)
|
||||
EMAIL_USE_SSL = smtpSettings.get('useSSL', False)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la lecture du fichier de configuration SMTP : {e}")
|
||||
else:
|
||||
logger.error(f"Fichier de configuration SMTP introuvable : {smtp_config_file}")
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'true').lower() == 'true'
|
||||
EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'false').lower() == 'true'
|
||||
|
||||
DOCUMENT_DIR = 'documents'
|
||||
|
||||
# Configuration CORS temporaire pour debug
|
||||
CORS_ALLOW_ALL_HEADERS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Configuration CORS spécifique pour la production
|
||||
@ -291,20 +272,26 @@ CORS_ALLOWED_METHODS = [
|
||||
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',')
|
||||
|
||||
CSRF_COOKIE_HTTPONLY = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
CSRF_COOKIE_NAME = 'csrftoken'
|
||||
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
||||
|
||||
USE_TZ = True
|
||||
TZ_APPLI = 'Europe/Paris'
|
||||
|
||||
DB_NAME = os.getenv('DB_NAME', 'school')
|
||||
DB_USER = os.getenv('DB_USER', 'postgres')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', 'postgres')
|
||||
DB_HOST = os.getenv('DB_HOST', 'database')
|
||||
DB_PORT = os.getenv('DB_PORT', '5432')
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
"NAME": "school",
|
||||
"USER": "postgres",
|
||||
"PASSWORD": "postgres",
|
||||
"HOST": "database",
|
||||
"PORT": "5432",
|
||||
"NAME": DB_NAME,
|
||||
"USER": DB_USER,
|
||||
"PASSWORD": DB_PASSWORD,
|
||||
"HOST": DB_HOST,
|
||||
"PORT": DB_PORT,
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,14 +326,14 @@ CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_TIMEZONE = 'Europe/Paris'
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||
|
||||
URL_DJANGO = 'http://localhost:8080/'
|
||||
URL_DJANGO = os.getenv('URL_DJANGO', 'http://localhost:8080/')
|
||||
|
||||
REDIS_HOST = 'redis'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
REDIS_PASSWORD = None
|
||||
|
||||
SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3'
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3')
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
@ -362,13 +349,6 @@ SIMPLE_JWT = {
|
||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||
}
|
||||
|
||||
# Configuration for DocuSeal JWT
|
||||
DOCUSEAL_JWT = {
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'EXPIRATION_DELTA': timedelta(hours=1)
|
||||
}
|
||||
|
||||
# Django Channels Configuration
|
||||
ASGI_APPLICATION = 'N3wtSchool.asgi.application'
|
||||
|
||||
|
||||
@ -46,7 +46,6 @@ urlpatterns = [
|
||||
path("GestionEmail/", include(("GestionEmail.urls", 'GestionEmail'), namespace='GestionEmail')),
|
||||
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
|
||||
path("School/", include(("School.urls", 'School'), namespace='School')),
|
||||
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
|
||||
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
|
||||
path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')),
|
||||
path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')),
|
||||
|
||||
44
Back-End/Planning/migrations/0001_initial.py
Normal file
44
Back-End/Planning/migrations/0001_initial.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
('School', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Planning',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, default='', null=True)),
|
||||
('color', models.CharField(default='#000000', max_length=255)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
('school_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='planning', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Events',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, default='', null=True)),
|
||||
('start', models.DateTimeField()),
|
||||
('end', models.DateTimeField()),
|
||||
('recursionType', models.IntegerField(choices=[(0, 'Aucune'), (1, 'Quotidienne'), (2, 'Hebdomadaire'), (3, 'Mensuel'), (4, 'Personnalisé')], default=0)),
|
||||
('recursionEnd', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('color', models.CharField(max_length=255)),
|
||||
('location', models.CharField(blank=True, default='', max_length=255, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('planning', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Planning.planning')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Planning/migrations/__init__.py
Normal file
0
Back-End/Planning/migrations/__init__.py
Normal file
@ -147,7 +147,7 @@ class EventsWithIdView(APIView):
|
||||
return JsonResponse({'error': 'Event not found'}, status=404)
|
||||
|
||||
event.delete()
|
||||
return JsonResponse({'message': 'Event deleted'}, status=204)
|
||||
return JsonResponse({'message': 'Event deleted'}, status=200)
|
||||
|
||||
class UpcomingEventsView(APIView):
|
||||
def get(self, request):
|
||||
|
||||
145
Back-End/School/migrations/0001_initial.py
Normal file
145
Back-End/School/migrations/0001_initial.py
Normal file
@ -0,0 +1,145 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Auth', '0001_initial'),
|
||||
('Common', '0001_initial'),
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Competency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.TextField()),
|
||||
('end_of_cycle', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('level', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competencies', to='Common.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Discount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('discount_type', models.IntegerField(choices=[(0, 'Currency'), (1, 'Percent')], default=0)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='discounts', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EstablishmentCompetency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('custom_name', models.TextField(blank=True, help_text='Nom de la compétence custom', null=True)),
|
||||
('is_required', models.BooleanField(default=True)),
|
||||
('competency', models.ForeignKey(blank=True, help_text='Compétence de référence (optionnelle si custom)', null=True, on_delete=django.db.models.deletion.CASCADE, to='School.competency')),
|
||||
('custom_category', models.ForeignKey(blank=True, help_text='Catégorie de la compétence custom', null=True, on_delete=django.db.models.deletion.CASCADE, to='Common.category')),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('establishment', 'competency', 'custom_name', 'custom_category')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='competency',
|
||||
name='establishments',
|
||||
field=models.ManyToManyField(blank=True, related_name='competencies', through='School.EstablishmentCompetency', to='Establishment.establishment'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Fee',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('base_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fees', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentMode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_modes', to='Establishment.establishment')),
|
||||
('mode', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_modes', to='Common.paymentmodetype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentPlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('due_dates', django.contrib.postgres.fields.ArrayField(base_field=models.DateField(), blank=True, null=True, size=None)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_plans', to='Establishment.establishment')),
|
||||
('plan_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_plans', to='Common.paymentplantype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SchoolClass',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('atmosphere_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('age_range', models.JSONField(blank=True, null=True)),
|
||||
('number_of_students', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('teaching_language', models.CharField(blank=True, max_length=255)),
|
||||
('school_year', models.CharField(blank=True, max_length=9)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('type', models.IntegerField(choices=[(1, 'Annuel'), (2, 'Semestriel'), (3, 'Trimestriel')], default=1)),
|
||||
('time_range', models.JSONField(default=list)),
|
||||
('opening_days', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), default=list, size=None)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='school_classes', to='Establishment.establishment')),
|
||||
('levels', models.ManyToManyField(blank=True, related_name='school_classes', to='Common.level')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Planning',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('level', models.IntegerField(blank=True, choices=[(1, 'Très Petite Section (TPS)'), (2, 'Petite Section (PS)'), (3, 'Moyenne Section (MS)'), (4, 'Grande Section (GS)'), (5, 'Cours Préparatoire (CP)'), (6, 'Cours Élémentaire 1 (CE1)'), (7, 'Cours Élémentaire 2 (CE2)'), (8, 'Cours Moyen 1 (CM1)'), (9, 'Cours Moyen 2 (CM2)')], null=True)),
|
||||
('schedule', models.JSONField(default=dict)),
|
||||
('school_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plannings', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Speciality',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('color_code', models.CharField(default='#FFFFFF', max_length=7)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specialities', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Teacher',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='teacher_profile', to='Auth.profilerole')),
|
||||
('specialities', models.ManyToManyField(blank=True, to='School.speciality')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='schoolclass',
|
||||
name='teachers',
|
||||
field=models.ManyToManyField(blank=True, to='School.teacher'),
|
||||
),
|
||||
]
|
||||
0
Back-End/School/migrations/__init__.py
Normal file
0
Back-End/School/migrations/__init__.py
Normal file
@ -33,6 +33,10 @@ from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
||||
from django.db.models import Q
|
||||
from collections import defaultdict
|
||||
from Subscriptions.models import Student, StudentCompetency
|
||||
from Subscriptions.util import getCurrentSchoolYear
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
@ -581,7 +585,6 @@ class EstablishmentCompetencyListCreateView(APIView):
|
||||
|
||||
try:
|
||||
category = Category.objects.get(id=category_id)
|
||||
# Vérifier si une compétence custom du même nom existe déjà pour cet établissement et cette catégorie
|
||||
ec_exists = EstablishmentCompetency.objects.filter(
|
||||
establishment_id=establishment_id,
|
||||
competency__isnull=True,
|
||||
@ -598,12 +601,31 @@ class EstablishmentCompetencyListCreateView(APIView):
|
||||
custom_category=category,
|
||||
is_required=False
|
||||
)
|
||||
# Associer à tous les élèves de l'établissement
|
||||
|
||||
# Récupérer l'établissement et sa fréquence d'évaluation
|
||||
establishment = ec.establishment
|
||||
evaluation_frequency = establishment.evaluation_frequency # 1=Trimestre, 2=Semestre, 3=Année
|
||||
|
||||
# Déterminer l'année scolaire courante
|
||||
school_year = getCurrentSchoolYear()
|
||||
|
||||
# Générer les périodes selon la fréquence
|
||||
periods = []
|
||||
if evaluation_frequency == 1: # Trimestre
|
||||
periods = [f"T{i+1}_{school_year}" for i in range(3)]
|
||||
elif evaluation_frequency == 2: # Semestre
|
||||
periods = [f"S{i+1}_{school_year}" for i in range(2)]
|
||||
elif evaluation_frequency == 3: # Année
|
||||
periods = [f"A_{school_year}"]
|
||||
|
||||
# Associer à tous les élèves de l'établissement pour chaque période
|
||||
students = Student.objects.filter(associated_class__establishment_id=establishment_id)
|
||||
for student in students:
|
||||
for period in periods:
|
||||
StudentCompetency.objects.get_or_create(
|
||||
student=student,
|
||||
establishment_competency=ec
|
||||
establishment_competency=ec,
|
||||
period=period
|
||||
)
|
||||
|
||||
created.append({
|
||||
|
||||
29
Back-End/Settings/migrations/0001_initial.py
Normal file
29
Back-End/Settings/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SMTPSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('smtp_server', models.CharField(max_length=255)),
|
||||
('smtp_port', models.PositiveIntegerField()),
|
||||
('smtp_user', models.CharField(max_length=255)),
|
||||
('smtp_password', models.CharField(max_length=255)),
|
||||
('use_tls', models.BooleanField(default=True)),
|
||||
('use_ssl', models.BooleanField(default=False)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Settings/migrations/__init__.py
Normal file
0
Back-End/Settings/migrations/__init__.py
Normal file
213
Back-End/Subscriptions/migrations/0001_initial.py
Normal file
213
Back-End/Subscriptions/migrations/0001_initial.py
Normal file
@ -0,0 +1,213 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import Subscriptions.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Auth', '__first__'),
|
||||
('Common', '0001_initial'),
|
||||
('Establishment', '0001_initial'),
|
||||
('School', '__first__'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Language',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('label', models.CharField(default='', max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Student',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('photo', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_photo_upload_to)),
|
||||
('last_name', models.CharField(default='', max_length=200)),
|
||||
('first_name', models.CharField(default='', max_length=200)),
|
||||
('gender', models.IntegerField(blank=True, choices=[(0, 'Sélection du genre'), (1, 'Garçon'), (2, 'Fille')], default=0)),
|
||||
('nationality', models.CharField(blank=True, default='', max_length=200)),
|
||||
('address', models.CharField(blank=True, default='', max_length=200)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
('birth_place', models.CharField(blank=True, default='', max_length=200)),
|
||||
('birth_postal_code', models.IntegerField(blank=True, default=0)),
|
||||
('attending_physician', models.CharField(blank=True, default='', max_length=200)),
|
||||
('associated_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationSchoolFileTemplate',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('slug', models.CharField(default='', max_length=255)),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sibling',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Guardian',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
('address', models.CharField(blank=True, default='', max_length=200)),
|
||||
('phone', models.CharField(blank=True, default='', max_length=200)),
|
||||
('profession', models.CharField(blank=True, default='', max_length=200)),
|
||||
('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='guardian_profile', to='Auth.profilerole')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationFileGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='file_group', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationForm',
|
||||
fields=[
|
||||
('student', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='Subscriptions.student')),
|
||||
('status', models.IntegerField(choices=[(0, "Pas de dossier d'inscription"), (1, "Dossier d'inscription initialisé"), (2, "Dossier d'inscription envoyé"), (3, "Dossier d'inscription en cours de validation"), (4, "Dossier d'inscription à relancer"), (5, "Dossier d'inscription validé"), (6, "Dossier d'inscription archivé"), (7, 'Mandat SEPA envoyé'), (8, 'Mandat SEPA à envoyer')], default=0)),
|
||||
('last_update', models.DateTimeField(auto_now=True)),
|
||||
('school_year', models.CharField(blank=True, default='', max_length=9)),
|
||||
('notes', models.CharField(blank=True, max_length=200)),
|
||||
('registration_link_code', models.CharField(blank=True, default='', max_length=200)),
|
||||
('registration_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('sepa_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('fusion_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('associated_rf', models.CharField(blank=True, default='', max_length=200)),
|
||||
('discounts', models.ManyToManyField(blank=True, related_name='register_forms', to='School.discount')),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='register_forms', to='Establishment.establishment')),
|
||||
('fees', models.ManyToManyField(blank=True, related_name='register_forms', to='School.fee')),
|
||||
('fileGroup', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='register_forms', to='Subscriptions.registrationfilegroup')),
|
||||
('registration_payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registration_payment_modes_forms', to='School.paymentmode')),
|
||||
('registration_payment_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registration_payment_plans_forms', to='School.paymentplan')),
|
||||
('tuition_payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tuition_payment_modes_forms', to='School.paymentmode')),
|
||||
('tuition_payment_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tuition_payment_plans_forms', to='School.paymentplan')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='guardians',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.guardian'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='level',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='Common.level'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='profiles',
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='spoken_languages',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.language'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BilanCompetence',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_bilan_form_upload_to)),
|
||||
('period', models.CharField(help_text='Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bilans', to='Subscriptions.student')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AbsenceManagement',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('day', models.DateField(blank=True, null=True)),
|
||||
('moment', models.IntegerField(choices=[(1, 'Morning'), (2, 'Afternoon'), (3, 'Total')], default=3)),
|
||||
('reason', models.IntegerField(choices=[(1, 'Justified Absence'), (2, 'Unjustified Absence'), (3, 'Justified Late'), (4, 'Unjustified Late')], default=2)),
|
||||
('commentaire', models.TextField(blank=True, null=True)),
|
||||
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='Establishment.establishment')),
|
||||
('student', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='Subscriptions.student')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationParentFileMaster',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('description', models.CharField(blank=True, null=True)),
|
||||
('is_required', models.BooleanField(default=False)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='parent_file_masters', to='Subscriptions.registrationfilegroup')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationSchoolFileMaster',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('is_required', models.BooleanField(default=False)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='school_file_masters', to='Subscriptions.registrationfilegroup')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='registration_files',
|
||||
field=models.ManyToManyField(blank=True, related_name='students', to='Subscriptions.registrationschoolfiletemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='registrationschoolfiletemplate',
|
||||
name='master',
|
||||
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_templates', to='Subscriptions.registrationschoolfilemaster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='siblings',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.sibling'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='registrationschoolfiletemplate',
|
||||
name='registration_form',
|
||||
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_templates', to='Subscriptions.registrationform'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationParentFileTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
||||
('master', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationparentfilemaster')),
|
||||
('registration_form', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationform')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StudentCompetency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('score', models.IntegerField(blank=True, null=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('period', models.CharField(blank=True, default='', help_text="Période d'évaluation ex: T1-2024_2025, S1-2024_2025, A-2024_2025", max_length=20)),
|
||||
('establishment_competency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_scores', to='School.establishmentcompetency')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competency_scores', to='Subscriptions.student')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('student', 'establishment_competency', 'period')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Subscriptions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='registrationparentfilemaster',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
]
|
||||
0
Back-End/Subscriptions/migrations/__init__.py
Normal file
0
Back-End/Subscriptions/migrations/__init__.py
Normal file
@ -6,6 +6,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("SubscriptionModels")
|
||||
|
||||
class Language(models.Model):
|
||||
"""
|
||||
@ -274,6 +277,16 @@ class RegistrationForm(models.Model):
|
||||
return "RF_" + self.student.last_name + "_" + self.student.first_name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Préparer le flag de création / changement de fileGroup
|
||||
was_new = self.pk is None
|
||||
old_fileGroup = None
|
||||
if not was_new:
|
||||
try:
|
||||
old_instance = RegistrationForm.objects.get(pk=self.pk)
|
||||
old_fileGroup = old_instance.fileGroup
|
||||
except RegistrationForm.DoesNotExist:
|
||||
old_fileGroup = None
|
||||
|
||||
# Vérifier si un fichier existant doit être remplacé
|
||||
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||
try:
|
||||
@ -287,16 +300,27 @@ class RegistrationForm(models.Model):
|
||||
# Appeler la méthode save originale
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Après save : si nouveau ou changement de fileGroup -> créer les templates
|
||||
fileGroup_changed = (self.fileGroup is not None) and (old_fileGroup is None or (old_fileGroup and old_fileGroup.id != self.fileGroup.id))
|
||||
if was_new or fileGroup_changed:
|
||||
try:
|
||||
import Subscriptions.util as util
|
||||
created = util.create_templates_for_registration_form(self)
|
||||
if created:
|
||||
logger.info("Created %d templates for RegistrationForm %s", len(created), self.pk)
|
||||
except Exception as e:
|
||||
logger.exception("Error creating templates for RegistrationForm %s: %s", self.pk, e)
|
||||
|
||||
#############################################################
|
||||
####################### MASTER FILES ########################
|
||||
#############################################################
|
||||
|
||||
####### DocuSeal masters (documents école, à signer ou pas) #######
|
||||
####### Formulaires masters (documents école, à signer ou pas) #######
|
||||
class RegistrationSchoolFileMaster(models.Model):
|
||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||
id = models.IntegerField(primary_key=True)
|
||||
name = models.CharField(max_length=255, default="")
|
||||
is_required = models.BooleanField(default=False)
|
||||
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.group.name} - {self.id}'
|
||||
@ -318,14 +342,14 @@ def registration_school_file_upload_to(instance, filename):
|
||||
def registration_parent_file_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||
|
||||
####### DocuSeal templates (par dossier d'inscription) #######
|
||||
####### Formulaires templates (par dossier d'inscription) #######
|
||||
class RegistrationSchoolFileTemplate(models.Model):
|
||||
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||
id = models.IntegerField(primary_key=True)
|
||||
slug = models.CharField(max_length=255, default="")
|
||||
name = models.CharField(max_length=255, default="")
|
||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
||||
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -394,6 +418,7 @@ class RegistrationParentFileTemplate(models.Model):
|
||||
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
|
||||
filenames = []
|
||||
for reg_file in registration_files:
|
||||
if reg_file.file and hasattr(reg_file.file, 'path'):
|
||||
filenames.append(reg_file.file.path)
|
||||
return filenames
|
||||
|
||||
|
||||
@ -381,16 +381,20 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
||||
|
||||
class StudentByParentSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
associated_class_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
fields = ['id', 'last_name', 'first_name', 'level', 'photo']
|
||||
fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StudentByParentSerializer, self).__init__(*args, **kwargs)
|
||||
for field in self.fields:
|
||||
self.fields[field].required = False
|
||||
|
||||
def get_associated_class_name(self, obj):
|
||||
return obj.associated_class.atmosphere_name if obj.associated_class else None
|
||||
|
||||
class RegistrationFormByParentSerializer(serializers.ModelSerializer):
|
||||
student = StudentByParentSerializer(many=False, required=True)
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Confirmation d'inscription</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Confirmation de souscription</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
width: 120px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}/static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Confirmation de souscription</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
<p>Nous sommes ravis de vous compter parmi les utilisateurs de N3wt School et vous remercions pour votre confiance</p>
|
||||
<p>Vous trouverez ci-joint le lien vers la page d'authentification : <a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a></p>
|
||||
<p>S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : <a href="{{BASE_URL}}/users/subscribe?establishment_id={{establishment}}">{{BASE_URL}}/users/subscribe</a></p>
|
||||
<p>votre identifiant est : {{ email }}</p>
|
||||
<p>Notre équipe est à votre disposition pour vous aider à tirer pleinement parti des fonctionnalités offertes par Newt School.</p>
|
||||
<p>N'hésitez pas à nous contacter pour toute question ou besoin d'assistance.</p>
|
||||
<p>Cordialement,</p>
|
||||
<p>L'équipe N3wt School</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@ -11,28 +11,25 @@
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.6;
|
||||
color: #222;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 3px solid #4CAF50;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
font-size: 20pt;
|
||||
font-size: 22pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin: 0;
|
||||
@ -41,51 +38,47 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #4CAF50;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #fefefe;
|
||||
margin-bottom: 32px; /* Espacement augmenté entre les sections */
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18pt;
|
||||
font-size: 15pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 18px; /* Espacement sous le titre de section */
|
||||
border-bottom: 1px solid #4CAF50;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
td {
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
th, td {
|
||||
border: 1px solid #bbb;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f3f3f3;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.label-cell {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: 30%;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
.value-cell {
|
||||
color: #333;
|
||||
width: 65%;
|
||||
text-align: left;
|
||||
}
|
||||
.phone {
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
}
|
||||
.signature {
|
||||
margin-top: 30px;
|
||||
@ -97,6 +90,12 @@
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.subsection-title {
|
||||
font-size: 12pt;
|
||||
color: #333;
|
||||
margin: 8px 0 4px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -106,125 +105,121 @@
|
||||
<div class="header">
|
||||
<h1 class="title">Fiche élève de {{ student.last_name }} {{ student.first_name }}</h1>
|
||||
{% if student.photo %}
|
||||
<img
|
||||
src="{{ student.get_photo_url }}"
|
||||
alt="Photo de l'élève"
|
||||
class="photo"
|
||||
/>
|
||||
<img src="{{ student.get_photo_url }}" alt="Photo de l'élève" class="photo" />
|
||||
{% else %}
|
||||
<img
|
||||
src="/static/img/default-photo.jpg"
|
||||
alt="Photo par défaut"
|
||||
class="photo"
|
||||
/>
|
||||
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Student Section -->
|
||||
<!-- Élève -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">ÉLÈVE</h2>
|
||||
<div class="section-title">ÉLÈVE</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ student.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ student.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse :</td>
|
||||
<td colspan="3" class="value-cell">{{ student.address }}</td>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ student.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Genre :</td>
|
||||
<td class="label-cell">Genre</td>
|
||||
<td class="value-cell">{{ student|getStudentGender }}</td>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ student.birth_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">À :</td>
|
||||
<td colspan="3" class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
||||
<td class="label-cell">À</td>
|
||||
<td class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
||||
<td class="label-cell">Nationalité</td>
|
||||
<td class="value-cell">{{ student.nationality }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Nationalité :</td>
|
||||
<td class="value-cell">{{ student.nationality }}</td>
|
||||
<td class="label-cell">Niveau :</td>
|
||||
<td class="label-cell">Niveau</td>
|
||||
<td class="value-cell">{{ student|getStudentLevel }}</td>
|
||||
<td class="label-cell"></td>
|
||||
<td class="value-cell"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Guardians Section -->
|
||||
<!-- Responsables -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">RESPONSABLES</h2>
|
||||
<div class="section-title">RESPONSABLES</div>
|
||||
{% for guardian in student.getGuardians %}
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Responsable {{ forloop.counter }}</h3>
|
||||
<div>
|
||||
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ guardian.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ guardian.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse :</td>
|
||||
<td colspan="3" class="value-cell">{{ guardian.address }}</td>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td class="label-cell">Email</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ guardian.birth_date }}</td>
|
||||
<td class="label-cell">Email :</td>
|
||||
<td class="value-cell">{{ guardian.email }}</td>
|
||||
<td class="label-cell">Téléphone</td>
|
||||
<td class="value-cell">{{ guardian.phone|phone_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Téléphone :</td>
|
||||
<td class="value-cell phone">{{ guardian.phone|phone_format }}</td>
|
||||
<td class="label-cell">Profession :</td>
|
||||
<td class="value-cell">{{ guardian.profession }}</td>
|
||||
<td class="label-cell">Profession</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.profession }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Siblings Section -->
|
||||
<!-- Fratrie -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">FRATRIE</h2>
|
||||
<div class="section-title">FRATRIE</div>
|
||||
{% for sibling in student.getSiblings %}
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Frère/Soeur {{ forloop.counter }}</h3>
|
||||
<div>
|
||||
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ sibling.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ sibling.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td colspan="3" class="value-cell">{{ sibling.birth_date }}</td>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<!-- Paiement -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">MODALITÉS DE PAIEMENT</h2>
|
||||
<div class="section-title">MODALITÉS DE PAIEMENT</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Frais d'inscription :</td>
|
||||
<td class="label-cell">Frais d'inscription</td>
|
||||
<td class="value-cell">{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Frais de scolarité :</td>
|
||||
<td class="label-cell">Frais de scolarité</td>
|
||||
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<!-- Signature -->
|
||||
<div class="signature">
|
||||
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,12 @@ from .views import (
|
||||
)
|
||||
|
||||
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||
from .views import registration_file_views, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
|
||||
from .views import (
|
||||
registration_school_file_masters_views,
|
||||
registration_school_file_templates_views,
|
||||
get_school_file_templates_by_rf,
|
||||
get_parent_file_templates_by_rf
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
|
||||
|
||||
@ -21,8 +21,161 @@ from PyPDF2 import PdfMerger
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import json
|
||||
from django.http import QueryDict
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def build_payload_from_request(request):
|
||||
"""
|
||||
Normalise la request en payload prêt à être donné au serializer.
|
||||
- supporte multipart/form-data où le front envoie 'data' (JSON string) ou un fichier JSON + fichiers
|
||||
- supporte application/json ou form-data simple
|
||||
Retour: (payload_dict, None) ou (None, Response erreur)
|
||||
"""
|
||||
data_field = request.data.get('data') if hasattr(request.data, 'get') else None
|
||||
if data_field:
|
||||
try:
|
||||
# Si 'data' est un fichier (InMemoryUploadedFile ou fichier similaire), lire et décoder
|
||||
if hasattr(data_field, 'read'):
|
||||
raw = data_field.read()
|
||||
if isinstance(raw, (bytes, bytearray)):
|
||||
text = raw.decode('utf-8')
|
||||
else:
|
||||
text = raw
|
||||
payload = json.loads(text)
|
||||
# Si 'data' est bytes déjà
|
||||
elif isinstance(data_field, (bytes, bytearray)):
|
||||
payload = json.loads(data_field.decode('utf-8'))
|
||||
# Si 'data' est une string JSON
|
||||
elif isinstance(data_field, str):
|
||||
payload = json.loads(data_field)
|
||||
else:
|
||||
# type inattendu
|
||||
raise ValueError(f"Unsupported 'data' type: {type(data_field)}")
|
||||
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
||||
logger.error(f'Invalid JSON in "data": {e}')
|
||||
return None, Response({'error': "Invalid JSON in 'data'", 'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
payload = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data)
|
||||
if isinstance(payload, QueryDict):
|
||||
payload = payload.dict()
|
||||
|
||||
# Attacher les fichiers présents (ex: photo, files.*, etc.), sauf 'data' (déjà traité)
|
||||
for f_key, f_val in request.FILES.items():
|
||||
if f_key == 'data':
|
||||
# remettre le pointeur au début si besoin (déjà lu) — non indispensable ici mais sûr
|
||||
try:
|
||||
f_val.seek(0)
|
||||
except Exception:
|
||||
pass
|
||||
# ne pas mettre le fichier 'data' dans le payload (c'est le JSON)
|
||||
continue
|
||||
payload[f_key] = f_val
|
||||
|
||||
return payload, None
|
||||
|
||||
def create_templates_for_registration_form(register_form):
|
||||
"""
|
||||
Idempotent:
|
||||
- supprime les templates existants qui ne correspondent pas
|
||||
aux masters du fileGroup courant du register_form (et supprime leurs fichiers).
|
||||
- crée les templates manquants pour les masters du fileGroup courant.
|
||||
Retourne la liste des templates créés.
|
||||
"""
|
||||
from Subscriptions.models import (
|
||||
RegistrationSchoolFileMaster,
|
||||
RegistrationSchoolFileTemplate,
|
||||
# RegistrationParentFileMaster,
|
||||
# RegistrationParentFileTemplate,
|
||||
)
|
||||
|
||||
created = []
|
||||
|
||||
# Récupérer les masters du fileGroup courant
|
||||
current_group = getattr(register_form, "fileGroup", None)
|
||||
if not current_group:
|
||||
# Si plus de fileGroup, supprimer tous les templates existants pour ce RF
|
||||
school_existing = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form)
|
||||
for t in school_existing:
|
||||
try:
|
||||
if getattr(t, "file", None):
|
||||
t.file.delete(save=False)
|
||||
except Exception:
|
||||
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
|
||||
t.delete()
|
||||
# parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
|
||||
# for t in parent_existing:
|
||||
# try:
|
||||
# if getattr(t, "file", None):
|
||||
# t.file.delete(save=False)
|
||||
# except Exception:
|
||||
# logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
|
||||
# t.delete()
|
||||
return created
|
||||
|
||||
school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct()
|
||||
# parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||
|
||||
school_master_ids = {m.pk for m in school_masters}
|
||||
#parent_master_ids = {m.pk for m in parent_masters}
|
||||
|
||||
# Supprimer les school templates obsolètes
|
||||
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
|
||||
if not tmpl.master_id or tmpl.master_id not in school_master_ids:
|
||||
try:
|
||||
if getattr(tmpl, "file", None):
|
||||
tmpl.file.delete(save=False)
|
||||
except Exception:
|
||||
logger.exception("Erreur suppression fichier school template obsolète %s", getattr(tmpl, "pk", None))
|
||||
tmpl.delete()
|
||||
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
|
||||
# Supprimer les parent templates obsolètes
|
||||
# for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
|
||||
# if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
|
||||
# try:
|
||||
# if getattr(tmpl, "file", None):
|
||||
# tmpl.file.delete(save=False)
|
||||
# except Exception:
|
||||
# logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
|
||||
# tmpl.delete()
|
||||
# logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
|
||||
# Créer les school templates manquants
|
||||
for m in school_masters:
|
||||
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||
if exists:
|
||||
continue
|
||||
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||||
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||||
tmpl = RegistrationSchoolFileTemplate.objects.create(
|
||||
master=m,
|
||||
registration_form=register_form,
|
||||
name=m.name or "",
|
||||
formTemplateData=m.formMasterData or [],
|
||||
slug=slug,
|
||||
)
|
||||
created.append(tmpl)
|
||||
logger.info("Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
|
||||
# Créer les parent templates manquants
|
||||
# for m in parent_masters:
|
||||
# exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||
# if exists:
|
||||
# continue
|
||||
# tmpl = RegistrationParentFileTemplate.objects.create(
|
||||
# master=m,
|
||||
# registration_form=register_form,
|
||||
# file=None,
|
||||
# )
|
||||
# created.append(tmpl)
|
||||
# logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
|
||||
return created
|
||||
|
||||
def recupereListeFichesInscription():
|
||||
"""
|
||||
Retourne la liste complète des fiches d’inscription.
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_school_file_templates_by_rf, get_parent_file_templates_by_rf
|
||||
from .registration_file_views import (
|
||||
from .register_form_views import (
|
||||
RegisterFormView,
|
||||
RegisterFormWithIdView,
|
||||
send,
|
||||
resend,
|
||||
archive,
|
||||
get_school_file_templates_by_rf,
|
||||
get_parent_file_templates_by_rf
|
||||
)
|
||||
from .registration_school_file_masters_views import (
|
||||
RegistrationSchoolFileMasterView,
|
||||
RegistrationSchoolFileMasterSimpleView,
|
||||
RegistrationSchoolFileTemplateView,
|
||||
RegistrationSchoolFileTemplateSimpleView,
|
||||
RegistrationParentFileMasterView,
|
||||
RegistrationParentFileMasterSimpleView,
|
||||
RegistrationParentFileTemplateSimpleView,
|
||||
RegistrationParentFileTemplateView
|
||||
)
|
||||
from .registration_school_file_templates_views import (
|
||||
RegistrationSchoolFileTemplateView,
|
||||
RegistrationSchoolFileTemplateSimpleView,
|
||||
)
|
||||
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
|
||||
from .student_views import StudentView, StudentListView, ChildrenListView, search_students
|
||||
from .guardian_views import GuardianView, DissociateGuardianView
|
||||
@ -33,7 +43,7 @@ __all__ = [
|
||||
'RegistrationFileGroupSimpleView',
|
||||
'get_registration_files_by_group',
|
||||
'get_school_file_templates_by_rf',
|
||||
'get_parent_file_templates_by_rf'
|
||||
'get_parent_file_templates_by_rf',
|
||||
'StudentView',
|
||||
'StudentListView',
|
||||
'ChildrenListView',
|
||||
|
||||
@ -88,6 +88,7 @@ class RegisterFormView(APIView):
|
||||
filter = request.GET.get('filter', '').strip()
|
||||
page_size = request.GET.get('page_size', None)
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
search = request.GET.get('search', '').strip() # <-- Ajout du paramètre search
|
||||
|
||||
# Gestion du page_size
|
||||
if page_size is not None:
|
||||
@ -113,7 +114,14 @@ class RegisterFormView(APIView):
|
||||
registerForms_List = None
|
||||
|
||||
if registerForms_List:
|
||||
registerForms_List = registerForms_List.filter(establishment=establishment_id).order_by('-last_update')
|
||||
registerForms_List = registerForms_List.filter(establishment=establishment_id)
|
||||
# Ajout du filtre search sur le nom et prénom de l'élève
|
||||
if search:
|
||||
registerForms_List = registerForms_List.filter(
|
||||
Q(student__first_name__icontains=search) |
|
||||
Q(student__last_name__icontains=search)
|
||||
)
|
||||
registerForms_List = registerForms_List.order_by('-last_update')
|
||||
|
||||
if not registerForms_List:
|
||||
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
|
||||
|
||||
@ -0,0 +1,363 @@
|
||||
from django.http.response import JsonResponse
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
import json
|
||||
from django.http import QueryDict
|
||||
|
||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.models import (
|
||||
RegistrationForm,
|
||||
RegistrationSchoolFileMaster,
|
||||
RegistrationSchoolFileTemplate,
|
||||
RegistrationParentFileMaster,
|
||||
RegistrationParentFileTemplate
|
||||
)
|
||||
from N3wtSchool import bdd
|
||||
import logging
|
||||
import Subscriptions.util as util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RegistrationSchoolFileMasterView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationSchoolFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les masters liés à l'établissement via groups.establishment
|
||||
masters = RegistrationSchoolFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Crée un nouveau master de template d'inscription",
|
||||
request_body=RegistrationSchoolFileMasterSerializer,
|
||||
responses={
|
||||
201: RegistrationSchoolFileMasterSerializer,
|
||||
400: "Données invalides"
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
logger.info(f"raw request.data: {request.data}")
|
||||
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for serializer: {payload}")
|
||||
serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
obj = serializer.save()
|
||||
|
||||
# Propager la création des templates côté serveur pour les RegistrationForm
|
||||
try:
|
||||
groups_qs = obj.groups.all()
|
||||
if groups_qs.exists():
|
||||
# Tous les RegistrationForm dont fileGroup est dans les groups du master
|
||||
rfs = RegistrationForm.objects.filter(fileGroup__in=groups_qs).distinct()
|
||||
for rf in rfs:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf)
|
||||
except Exception as e:
|
||||
logger.exception("Error creating templates for RF %s from master %s: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while propagating templates after master creation %s", getattr(obj, 'pk', None))
|
||||
|
||||
|
||||
return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
logger.error(f"serializer errors: {serializer.errors}")
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère un master de template d'inscription spécifique",
|
||||
responses={
|
||||
200: RegistrationSchoolFileMasterSerializer,
|
||||
404: "Master non trouvé"
|
||||
}
|
||||
)
|
||||
def get(self, request, id):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is None:
|
||||
return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationSchoolFileMasterSerializer(master)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Met à jour un master de template d'inscription existant",
|
||||
request_body=RegistrationSchoolFileMasterSerializer,
|
||||
responses={
|
||||
200: RegistrationSchoolFileMasterSerializer,
|
||||
400: "Données invalides",
|
||||
404: "Master non trouvé"
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is None:
|
||||
return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# snapshot des groups avant update
|
||||
old_group_ids = set(master.groups.values_list('id', flat=True))
|
||||
|
||||
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for update serializer: {payload}")
|
||||
serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
obj = serializer.save()
|
||||
|
||||
# groups après update
|
||||
new_group_ids = set(obj.groups.values_list('id', flat=True))
|
||||
|
||||
removed_group_ids = old_group_ids - new_group_ids
|
||||
added_group_ids = new_group_ids - old_group_ids
|
||||
|
||||
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||
if removed_group_ids:
|
||||
try:
|
||||
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||
for rf in rfs_removed:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf) # supprimera les templates obsolètes
|
||||
except Exception as e:
|
||||
logger.exception("Error cleaning templates for RF %s after master %s group removal: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while processing RFs for removed groups after master update %s", getattr(obj, 'pk', None))
|
||||
|
||||
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||
if added_group_ids:
|
||||
try:
|
||||
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||
for rf in rfs_added:
|
||||
try:
|
||||
util.create_templates_for_registration_form(rf) # créera les templates manquants
|
||||
except Exception as e:
|
||||
logger.exception("Error creating templates for RF %s after master %s group addition: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e)
|
||||
except Exception:
|
||||
logger.exception("Error while processing RFs for added groups after master update %s", getattr(obj, 'pk', None))
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprime un master de template d'inscription",
|
||||
responses={
|
||||
204: "Suppression réussie",
|
||||
404: "Master non trouvé"
|
||||
}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is not None:
|
||||
master.delete()
|
||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
class RegistrationParentFileMasterView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les fichiers parents liés à l'établissement
|
||||
templates = RegistrationParentFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Crée un nouveau fichier parent",
|
||||
request_body=RegistrationParentFileMasterSerializer,
|
||||
responses={
|
||||
201: RegistrationParentFileMasterSerializer,
|
||||
400: "Données invalides"
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = RegistrationParentFileMasterSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationParentFileMasterSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère un fichier parent spécifique",
|
||||
responses={
|
||||
200: RegistrationParentFileMasterSerializer,
|
||||
404: "Fichier parent non trouvé"
|
||||
}
|
||||
)
|
||||
def get(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationParentFileMasterSerializer(template)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Met à jour un fichier parent existant",
|
||||
request_body=RegistrationParentFileMasterSerializer,
|
||||
responses={
|
||||
200: RegistrationParentFileMasterSerializer,
|
||||
400: "Données invalides",
|
||||
404: "Fichier parent non trouvé"
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationParentFileMasterSerializer(template, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprime un fichier parent",
|
||||
responses={
|
||||
204: "Suppression réussie",
|
||||
404: "Fichier parent non trouvé"
|
||||
}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id)
|
||||
if template is not None:
|
||||
template.delete()
|
||||
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
class RegistrationParentFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Crée un nouveau template d'inscription",
|
||||
request_body=RegistrationParentFileTemplateSerializer,
|
||||
responses={
|
||||
201: RegistrationParentFileTemplateSerializer,
|
||||
400: "Données invalides"
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = RegistrationParentFileTemplateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationParentFileTemplateSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère un template d'inscription spécifique",
|
||||
responses={
|
||||
200: RegistrationParentFileTemplateSerializer,
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def get(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationParentFileTemplateSerializer(template)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Met à jour un template d'inscription existant",
|
||||
request_body=RegistrationParentFileTemplateSerializer,
|
||||
responses={
|
||||
200: RegistrationParentFileTemplateSerializer,
|
||||
400: "Données invalides",
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprime un template d'inscription",
|
||||
responses={
|
||||
204: "Suppression réussie",
|
||||
404: "Template non trouvé"
|
||||
}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is not None:
|
||||
template.delete()
|
||||
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
@ -5,18 +5,41 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
import json
|
||||
from django.http import QueryDict
|
||||
|
||||
from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer
|
||||
from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate
|
||||
from N3wtSchool import bdd
|
||||
import logging
|
||||
import Subscriptions.util as util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RegistrationSchoolFileMasterView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les masters de templates d'inscription",
|
||||
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationSchoolFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
masters = RegistrationSchoolFileMaster.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les masters liés à l'établissement via groups.establishment
|
||||
masters = RegistrationSchoolFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -29,10 +52,19 @@ class RegistrationSchoolFileMasterView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = RegistrationSchoolFileMasterSerializer(data=request.data)
|
||||
logger.info(f"raw request.data: {request.data}")
|
||||
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for serializer: {payload}")
|
||||
serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
obj = serializer.save()
|
||||
return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
logger.error(f"serializer errors: {serializer.errors}")
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
@ -62,11 +94,19 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
def put(self, request, id):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is None:
|
||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationSchoolFileMasterSerializer(master, data=request.data)
|
||||
return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON)
|
||||
payload, resp = util.build_payload_from_request(request)
|
||||
if resp:
|
||||
return resp
|
||||
|
||||
logger.info(f"payload for update serializer: {payload}")
|
||||
serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
logger.error(f"serializer errors on put: {serializer.errors}")
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
@ -80,17 +120,33 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id)
|
||||
if master is not None:
|
||||
master.delete()
|
||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
|
||||
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
class RegistrationSchoolFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates d'inscription",
|
||||
operation_description="Récupère tous les templates d'inscription pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationSchoolFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationSchoolFileTemplate.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationSchoolFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationSchoolFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -160,11 +216,27 @@ class RegistrationSchoolFileTemplateSimpleView(APIView):
|
||||
|
||||
class RegistrationParentFileMasterView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les fichiers parents",
|
||||
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationParentFileMaster.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les fichiers parents liés à l'établissement
|
||||
templates = RegistrationParentFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -234,11 +306,27 @@ class RegistrationParentFileMasterSimpleView(APIView):
|
||||
|
||||
class RegistrationParentFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates d'inscription",
|
||||
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationParentFileTemplate.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -1 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.0.3"
|
||||
|
||||
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
import subprocess
|
||||
import os
|
||||
from watchfiles import run_process
|
||||
|
||||
def run_command(command):
|
||||
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@ -10,21 +11,22 @@ def run_command(command):
|
||||
print(f"stderr: {stderr.decode()}")
|
||||
return process.returncode
|
||||
|
||||
test_mode = os.getenv('TEST_MODE', 'False') == 'True'
|
||||
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
|
||||
watch_mode = os.getenv('DJANGO_WATCH', 'false').lower() == 'true'
|
||||
|
||||
commands = [
|
||||
["python", "manage.py", "collectstatic", "--noinput"],
|
||||
["python", "manage.py", "flush", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Common", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Settings", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Planning", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Auth", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "School", "--noinput"],
|
||||
#["python", "manage.py", "flush", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Common", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Settings", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Planning", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Auth", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "School", "--noinput"],
|
||||
["python", "manage.py", "migrate", "--noinput"]
|
||||
]
|
||||
|
||||
@ -32,6 +34,17 @@ test_commands = [
|
||||
["python", "manage.py", "init_mock_datas"]
|
||||
]
|
||||
|
||||
def run_daphne():
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
|
||||
])
|
||||
return result.returncode
|
||||
except KeyboardInterrupt:
|
||||
print("Arrêt de Daphne (KeyboardInterrupt)")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
for command in commands:
|
||||
if run_command(command) != 0:
|
||||
exit(1)
|
||||
@ -41,14 +54,35 @@ for command in commands:
|
||||
# if run_command(test_command) != 0:
|
||||
# exit(1)
|
||||
|
||||
# Lancer les processus en parallèle
|
||||
|
||||
if watch_mode:
|
||||
celery_worker = subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"])
|
||||
celery_beat = subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
|
||||
try:
|
||||
run_process(
|
||||
'.',
|
||||
target=run_daphne
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("Arrêt demandé (KeyboardInterrupt)")
|
||||
finally:
|
||||
celery_worker.terminate()
|
||||
celery_beat.terminate()
|
||||
celery_worker.wait()
|
||||
celery_beat.wait()
|
||||
else:
|
||||
processes = [
|
||||
subprocess.Popen(["daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"]),
|
||||
subprocess.Popen([
|
||||
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
|
||||
]),
|
||||
subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]),
|
||||
subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
|
||||
]
|
||||
|
||||
# Attendre la fin des processus
|
||||
try:
|
||||
for process in processes:
|
||||
process.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Arrêt demandé (KeyboardInterrupt)")
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
for process in processes:
|
||||
process.wait()
|
||||
@ -1,8 +1,42 @@
|
||||
<svg width="38" height="120" viewBox="0 0 38 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38 79.0995V40L29.9458 49.0478V72.3137L1.27073 99.134C-2.59496 103.012 3.20408 109.151 7.39397 106.243L38 79.0995Z" fill="#D9006D" fill-opacity="0.8"/>
|
||||
<path d="M0 79.0995V40L8.05422 49.0478V72.3137L36.7293 99.134C40.595 103.012 34.7959 109.151 30.606 106.243L0 79.0995Z" fill="#038ECE" fill-opacity="0.8"/>
|
||||
<path d="M19 83L12 89.6585L19 96L26 89.6585L19 83Z" fill="#011922"/>
|
||||
<path d="M14 28.8736H24V72.1264H14V28.8736Z" fill="#001821"/>
|
||||
<path d="M24 28.8736C24 31.5652 21.7614 33.7471 19 33.7471C16.2386 33.7471 14 31.5652 14 28.8736C14 26.182 16.2386 24 19 24C21.7614 24 24 26.182 24 28.8736Z" fill="#001821"/>
|
||||
<path d="M24 72.1264C24 74.818 21.7614 77 19 77C16.2386 77 14 74.818 14 72.1264C14 69.4348 16.2386 67.2529 19 67.2529C21.7614 67.2529 24 69.4348 24 72.1264Z" fill="#001821"/>
|
||||
<svg width="565" height="609" viewBox="0 0 565 609" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M170.999 374.501C167.799 383.301 172.332 402.834 174.999 411.501C189.998 452.002 218.216 463.317 223 464C237 466 238.5 464 246.5 459.5C248.1 451.9 238.333 445 234 443C224 439.8 213.833 431 210 427C246.4 422.6 255.833 416.167 256 413.5C246.8 395.9 213.5 396.834 198 399.501C181.2 372.301 172.999 371.501 170.999 374.501Z" fill="#003625"/>
|
||||
<path d="M166 84.5L208 94.5L206 112.5C203 121 196.9 138.5 196.5 140.5C196.1 142.5 174 145.333 163 146.5L112 166.5C96.3333 177.833 64.7 200.8 63.5 202C62.3 203.2 42.3333 237.5 32.5 254.5L23 320L21 389.5C29.8333 416.333 47.7 470.6 48.5 473C49.5 476 66.9995 505.5 65.9995 505.5C65.1995 505.5 92.6662 529.5 106.5 541.5C116.666 549.667 137.4 566.2 139 567C140.6 567.8 156.333 574.667 164 578L221 584.5H247.5L291.5 572.5L341 546L388 490.5L397 475L411.5 447.5L414.5 425L406.5 400L386.5 377.5L366 371L367.5 381L375 402L377 419L367.5 440.5L360.5 456.5L337 475C327.166 480.167 307.5 489.9 307.5 487.5C307.5 484.5 269 494 264 494.5C259 495 231 493 227.5 490.5C224.7 488.5 183 471.667 162.5 463.5L104 282L199 314C219.5 315.5 261.2 318.5 264 318.5C266.8 318.5 275.5 315.5 279.5 314L276 307.5L272.5 299L264 289.5C266.833 288.167 273.9 285.5 279.5 285.5C285.1 285.5 281.833 279.5 279.5 276.5L272.5 266.5L236 261L227.5 248.5V227L264 185L279.5 157.5L288.5 142.5L295 130L307.5 120.5L328 112.5L392.5 103.5L457.5 87.5H476L481 84.5V73L476 60.5L470.5 50C467 47.6667 459.5 42.4 457.5 40C455.5 37.6 449.666 33 447 31H435.5L425.5 19.5L416 16L406 11H392.5L377 16H366.5L314 19.5L264 25.5L199 55L166 84.5Z" fill="#10B981"/>
|
||||
<path d="M215 304C195.4 294 147.167 248.5 125.5 227L119.5 230.5C116.5 235.333 110.5 245.3 110.5 246.5C110.5 247.7 109.833 257 109.5 261.5C112 266.167 117.5 276.4 119.5 280C122 284.5 131.5 284.5 142 290C152.5 295.5 171 297 173.5 298C176 299 192 311 194.5 313C196.5 314.6 200.667 313.667 202.5 313L215 304Z" fill="#059669"/>
|
||||
<path d="M33.5 381.5C32 386.167 29 396.9 29 402.5V415V427.5C41.1667 449.167 65.9 493 67.5 495C69.5 497.5 84 521 87.5 524.5C91 528 105 542.5 107.5 546C109.5 548.8 124.667 560.833 132 566.5L184.5 575C281.7 595 343 545 361.5 517.5L359.5 511C275.1 571.8 199.333 562.333 172 550C178.4 550 184.333 546.333 186.5 544.5C83.3 509.3 41.5 421.167 33.5 381.5Z" fill="#059669"/>
|
||||
<path d="M346 184C315.2 203.2 291.5 245.333 283.5 264C283.5 259.599 239.5 254.166 217.5 252C232.064 267.745 261.219 267.834 273.898 267.992C273.587 267.653 273.741 267.591 274.5 268C274.304 267.997 274.103 267.995 273.898 267.992C274.473 268.621 276.642 270.201 279.5 271.5C283.9 273.5 279.5 278 274.5 276C272.1 276 265.5 279.333 262.5 281H258C250 283.8 229 272.167 219.5 266C167.1 228.8 137.333 222.5 129 224L145.5 214C149.5 210.4 184.5 230.5 201.5 241C205.9 203.4 251 177.333 273 169C251.4 135 283 116.167 301.5 111C308.7 107.4 404.5 96.1665 451.5 90.9997C464.3 86.5997 465.5 92.833 464.5 96.4997C444.9 129.7 377.334 168.666 346 184Z" fill="white"/>
|
||||
<path d="M299 33.9991C400.2 9.99913 455.167 48.3325 470 70.4991C471 68.3328 473.1 63.7 473.5 62.5C474 61 467 48.5 466 47C465 45.5 459 43 457.5 42C456 41 449.5 37.5 448 36C446.5 34.5 437 32.4987 436 32.4987C435 32.4987 430 28.4987 428.5 28.4987C427 28.4987 424 22.5 422.5 20.9987C421 19.4974 411.5 15 410.5 14C409.5 13 391.5 13 389 13C387 13 381.167 15.9991 378.5 17.4987C374.833 17.9987 366.9 18.6987 364.5 17.4987C361.5 15.9987 334 17.4987 331.5 17.4987C329 17.4987 300 20.9987 299 20.9987C298 20.9987 273 27.4987 270 28.4987C267.6 29.2987 249.667 31.4987 241 32.4987L188.5 65.9987V70.4991C223.3 46.4995 254 40.499 265 40.4987C250.2 48.8987 242.833 65.332 241 72.4987C255 53.2987 285.5 38.8323 299 33.9991Z" fill="white"/>
|
||||
<path d="M273 295.001C268.2 296.601 269.667 300.667 271 302.501C259.001 296.9 259 286.168 260.5 281.502C273.5 276.502 274.5 275.501 275 272.502C275.4 270.102 257.833 270.502 249 271.002C239.8 271.802 224.5 259.335 218 253.002C224 253.002 245.833 256.001 256 257.5C264.8 257.5 278.333 260.833 284 262.5C303.2 218.9 334.667 191 348 182.5C447.6 126.099 466.833 98.9995 464 92.5C466 88.5 454.833 90.8333 449 92.5C379 113.7 318.167 118 296.5 117.5L318.5 107C338.899 107 428.333 82.3333 470.5 70C469.7 44.8 439.834 32.5 425.001 29.5C407.001 11.5 385.834 16 377.501 20.5C341.901 19.3 323.334 21 318.5 22C226.1 32.8 185.333 67.1667 176.5 83C206.1 80.2 220.167 85.8333 223.5 89C213.5 90.6 209.667 99.3333 209 103.5C206.2 108.3 201.5 129.5 199.5 139.5L188.501 145.5C96.9026 141.9 49.001 215.668 36.5 253.002C15.7 329.003 27.8333 401.334 36.5 428C74.9 542.799 158.167 574.5 195 576C276.6 586.8 339.333 541.167 360.5 517L358.5 510C389.7 488.4 403.167 450 406 433.5C410.799 391.9 382.666 377.833 368 376C397.999 417.999 367.834 457.833 349.001 472.5C289.226 524.9 200.428 504 163.5 487C43.5 421 68.1667 314.834 95.5 270.002C95.9 203.203 141 199.834 163.5 206.5C140.7 210.9 125 227 120 234.5C97.2 262.5 125.833 278.5 143 283.001C145 281.8 167.5 290.5 178.5 295.001C179.7 293.001 193 304.501 199.5 310.501L202 309.501C207.6 295.503 218.667 301.334 223.5 306L246 348.5C248.8 360.1 242.167 362 238.5 361.5C225.3 361.9 210.667 337.667 205 325.5L206.5 350C207.7 360 202.333 363.167 199.5 363.5C185.9 363.5 180.833 340.167 180 328.5C182 313.7 173.167 311.667 168.5 312.5C156.1 322.899 153 357.5 153 373.5C157 445.899 206.667 474.333 231 479.5C311.8 500.7 354.334 451.333 365.501 424C369.101 377.2 332.334 364.167 313.5 363.5C329.899 351.1 348.667 350.667 356.001 352C422.401 360 433.001 425.667 430.001 457.5C409.201 575.899 293.667 607.833 238.5 609C96.1 601.4 31.1676 490.5 16.5015 436C-33.0985 282.8 46.1681 182.833 92.0015 152C124.001 130.4 159.668 126.333 173.501 127C185.501 127.8 188.501 122 188.501 119C192.901 106.2 165.668 105.667 151.501 107C134.701 109 134.501 98.1667 136.501 92.5C202.101 14.1 314.501 5.49998 362.501 11C404.501 -15 432.667 12.5 441.501 29.5C481.101 33.1 489.667 60 489.001 73C486.201 106.2 465.834 129.5 456.001 137C449.201 145 419.167 165.333 405.001 174.5L344.001 209L344.501 211C356.501 219 371.501 242.334 377.501 253.002C386.301 251.402 402.167 254.335 409.001 256.002C419.001 260.002 427.167 267.002 430.001 270.002V275.002C429.601 276.202 426.501 277.168 425.001 277.502C422.601 278.702 407.334 273.668 400.001 271.002H398.001V272.502L409.001 281.502C415.801 287.102 420.501 295.835 422.001 299.501C424.001 305.102 422.167 308.501 421.001 309.501C418.201 312.702 414.501 311.501 413.001 310.501L386.001 284.502L385.001 284.002L392.001 303.001C394.001 307.001 394.167 315.335 394.001 319.001C391.201 331.001 384.167 327.001 381.001 323.501L365.501 286.501L365.001 310.501C363.801 318.901 358.501 322.335 356.001 323.001C351.201 323.001 349.334 320.335 349.001 319.001C347.401 316.202 346.667 301.168 346.501 294.001C344.101 286.801 334.834 281.001 330.501 279.001C322.501 274.201 302.167 274.667 293.001 275.501C293.001 279.901 292.334 282.334 292.001 283.001L273 295.001Z" fill="#003625"/>
|
||||
<path d="M255 296.5C239.8 285.3 233.667 289.5 232.5 293C226.1 301.8 235.167 312.667 240.5 317C251.3 327 272.667 334.167 282 336.5C295.6 337.3 296 327.833 294.5 323C290.1 316.2 266.333 302.5 255 296.5Z" fill="#003625"/>
|
||||
<path d="M283.499 263.5C278.299 258.7 255.999 254.5 245.499 253C241.899 249.401 294.666 208.834 321.499 189L303.499 185.5C331.899 183.5 362.333 153.334 373.999 138.5C417.499 142 460.999 87.5004 462.999 90.5004C464.599 92.9004 464.333 95.1671 463.999 96.0004C448.799 127.2 382.666 165 351.499 180C321.499 194.8 293.666 241.834 283.499 263.5Z" fill="#EEEDE8"/>
|
||||
<path d="M201.5 147L198.5 139C191.7 143.8 184.5 143.833 182 144.5C141.6 139.3 104.5 164.5 91 176.5C37.8 218.1 21.6667 289.833 21 320.5C36.6 234.1 88.8333 186.5 112 173.5C135.2 156.3 171.667 153 187 153.5C189.8 153.5 197.833 149.167 201.5 147Z" fill="white"/>
|
||||
<path d="M331.248 39C354.848 39.4 364.414 59.5 366.248 69.5C364.081 68.8333 359.848 67.6 360.248 68C360.648 68.4 359.415 72.1667 358.748 74C352.748 89.2 336.582 93 329.248 93C311.248 92.2 303.081 77.3333 301.248 70C298.448 46 320.081 39.3333 331.248 39Z" fill="#003625"/>
|
||||
<path d="M310.749 63.4997C316.749 48.2997 331.582 50.1664 338.249 52.9997C325.849 55.7994 323.748 58.4998 324.248 59.5C329.448 70.3 323.748 73.3333 320.249 73.5C311.849 73.5 310.415 66.8331 310.749 63.4997Z" fill="white"/>
|
||||
<circle cx="226" cy="107" r="10" fill="#003625"/>
|
||||
<circle cx="226" cy="107" r="10" fill="#003625"/>
|
||||
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
|
||||
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
|
||||
<circle cx="209" cy="147" r="10" fill="#003625"/>
|
||||
<circle cx="209" cy="147" r="10" fill="#003625"/>
|
||||
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
|
||||
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 9.6 KiB |
267
CHANGELOG.md
267
CHANGELOG.md
@ -0,0 +1,267 @@
|
||||
# Changelog
|
||||
|
||||
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
||||
|
||||
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
||||
|
||||
|
||||
### Corrections de bugs
|
||||
|
||||
* Ajout d'un '/' en fin d'URL ([67cea2f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67cea2f1c6edae8eed5e024c79b1e19d08788d4c))
|
||||
|
||||
### 0.0.2 (2025-06-01)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* mise à jour de la doc swagger ([11fc446](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/11fc446b904cc64d63154ad5c6711a8296a7fc51))
|
||||
|
||||
|
||||
### Refactorisations
|
||||
|
||||
* "registerFilesTemplates" -> "registrerFileTemplate" ([83f4d67](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/83f4d67a6fc3f786803343957b276f8419f3058d))
|
||||
* adaptation mobile ([4b8f85e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4b8f85e68dc95585d96a4cbad219ad068cbc8acf))
|
||||
* Affichage des notifications dans la partie "Users" ([af30ae3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/af30ae33b5660c55fa6824498f4325aab3de3c5a))
|
||||
* Affichage des notifications dans la partie "Users" ([e509625](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e5096258110faac37b9457705dd1b51bc231983f))
|
||||
* Augmentation du nombre de données ([95c154a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95c154a4a2d746c4350887bb697af142152ed8d7))
|
||||
* changement de la philosophie de logging ([c7723ec](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c7723eceee650de86eea3263d44d374ad9844282))
|
||||
* Changement des IconTextInput en TextInput, modification du composant step ([a248898](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a248898203286213c3447333611e1a9981dff64a))
|
||||
* Composant *InscriptionForm* ([56e2762](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56e27628f897920659a6ce186539ddec7e94a05a))
|
||||
* Creation d'un provider et d'un systeme de middleware ([5088479](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/508847940c8c35fd982ab935f4d69371869eed5a))
|
||||
* Création de composants et uniformisation des modales ([#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)) ([d51778b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d51778ba54e95283aa6ad7821fda673813c7c7a0))
|
||||
* Création de nouveaux composants / update formulaire de ([7acae47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7acae479da658707fb3e073ebcdfee023d18500b)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||
* Deplacement du JWT dans le back ([eb89a32](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb89a324abbdf69091e5c78530ec62f2c2ccbcd1))
|
||||
* Document Ecole/Parent ([7564865](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7564865d8f414fbefa0731c4ca472a100efb6036))
|
||||
* gestion des erreurs ([f3490a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3490a4e9584b959450ca45c8e74e430396425b3))
|
||||
* Injection des env var dans le frontend ([aae5d27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aae5d27d8c556c5687951f3a04e01d42f69f3085))
|
||||
* je suis une merde ([c4d4542](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c4d45426b520a498f409be8617c7936224195290))
|
||||
* Mise à jour de la doc swagger / URL ([4c95b6a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c95b6a83f15a9989fac5f69a9386664d25ec9f6))
|
||||
* Modification de l'url de l'api Auth ([9bf9c5f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9bf9c5f62df1a6482ba27b897da498592b57e04f))
|
||||
* Modification de la construction docker ([2d128aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2d128aaf30e60813c0c5caa244a93ff46e3985f3))
|
||||
* Partie "School" ([58fe509](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58fe509734a3b5dc6e0b5c6aa3fd713fd4dc821e))
|
||||
* Partie FRONT / School ([24352ef](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24352efad304dee7418dc846681a4b38047431f6))
|
||||
* Refactoring de la section ClassSection ([1a8ef26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1a8ef26f5883abe4855949a54aa50defb98c852d))
|
||||
* refactoring du FRONT page subscribe ([427b6c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/427b6c758892469d07579159511e7ce1ceed20d0))
|
||||
* Refactorisation du login et de admin/subscription ([41aa9d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/41aa9d55d388c0ddf189c7b9ab6057487f86484b))
|
||||
* Remplacement de quelques popup par les notifications ([ce83e02](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ce83e02f7b3e53ef2b859436432784d6eb69200d))
|
||||
* Renommage du menu "Eleves" en "Inscriptions" ([692e845](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/692e8454bf9840ada3f8e052d7ef13cbf1b0d9c0))
|
||||
* Revue de la modale permettant de créer un dossier ([cb3f909](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb3f909fa4e7a53148cd13cf190c13b0670d35de))
|
||||
* Revue de la modale permettant de créer un dossier ([665625e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/665625e0280683fef056e9c950fc6555d889643e))
|
||||
* SpecialitySection + TeacherSection (en cours) ([72dd769](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/72dd7699d6bd61e17b4c3dc0098ca0989a94b2c8))
|
||||
* Suppression des paramètres mail mot de passes des settings ([ec2630a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ec2630a6e40dedaaa8f41a04b44e5ec1f6b2a1e0))
|
||||
* Traduction en anglais des modules "GestionInscription" et ([2b414b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2b414b83913c2f0f81cf226b78577ad522443d7b))
|
||||
* Transformation des requetes vers le back en action ajout des ([147a701](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/147a70135d2f10ac16961c098d85da0a1bcafb38))
|
||||
* Utilisation d'une application "Common" pour tous les modèles ([e65e310](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e65e31014d2b89afe1e5f077e8d4109f07d40d0b))
|
||||
|
||||
|
||||
### Nouvelles fonctionnalités
|
||||
|
||||
* A la signature d'un document, on récupère l'URL du PDF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([2ac4832](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ac48329851f91f6bb02a44e02ad5a90b4ae504c))
|
||||
* Affichage d'icones dans le tableau des inscriptions dans la ([9559db5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9559db59eb418d233682217ef72f315bccc6fe1d))
|
||||
* Ajout d'un composant permettant de visualiser les fichiers signés ([7f442b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f442b9cae008dab4f18438f9ee46be21ed037b0))
|
||||
* Ajout d'un nouveau status avec envoi de mandat SEPA + envoi de ([4c2e2f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c2e2f87565dc6e2be501839c274a5aa6969a9ec))
|
||||
* Ajout d'un nouvel état dans l'automatique lorsqu'un mandat SEPA ([545349c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/545349c7db7d0f653f3ae06b10d441ef975b0cc0))
|
||||
* Ajout d'une colonne dans le tableau des pièces jointes indiquant ([3c0806e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c0806e26c116dbccd808bd8c8b170c5c4d9bc5b))
|
||||
* Ajout d'une fonction de dissociation entre un responsable et un ([3bcc620](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3bcc620ee103690a2ee5f79e6203aba880bda9b7))
|
||||
* Ajout d'une fonction de logout ([c2bba1a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c2bba1abbfafbb7aca1bb07e8019d7fa244a808e))
|
||||
* Ajout d'une fonction de logout ([0ef6a2b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0ef6a2b1192dbd3ecc59ce0e8cbba233ccc9c821))
|
||||
* Ajout de l'emploi du temps sur la page parent ([78d96f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/78d96f82f91ed777073250b960eee8f326cccb43))
|
||||
* Ajout de l'envoie de mail [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([99a882a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/99a882a64acfd9340d6849edd1766de5173a2341))
|
||||
* Ajout de l'option d'envoi automatique [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([a77dd8e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a77dd8ec64bd78ab9c42aad3f93a181e64719d06))
|
||||
* Ajout de la configuration des tarifs de l'école [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([5a0e65b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a0e65bb752a80781517394d7b2a673788f7595e))
|
||||
* Ajout de la fratrie [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([4a382d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a382d523ccfd4cf8fa7e672e9315b86dbdbbb14))
|
||||
* Ajout de la fratrie / Gestion des index de fratrie / Gestion des ([2ab1684](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ab1684791377804dd03c8467a94dbc1244e102f))
|
||||
* Ajout de la gestion des fichier d'inscription [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([3c27133](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c27133cdb9943c5e20b81c03f9e2fa47077dbbb))
|
||||
* Ajout de la photo pour le dossier de l'élève + correction ([5851341](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5851341235998647a4142bdf1996ddc9db21762d))
|
||||
* Ajout de la possibilité de supprimer une association ([c9350a7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9350a796b65ea4eef0e38390ab9fb1d88196210))
|
||||
* Ajout de la sélection des modes de paiements / refactoring de ([5a7661d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a7661db93454b9a73b9f6bd46646c6135a0f203))
|
||||
* Ajout des Bundles de fichiers [[#24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/24)] ([ffc6ce8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ffc6ce8de835e9caf547b6c4a893436aa93513ba))
|
||||
* ajout des documents d'inscription [[#20](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/20)] ([b8ef34a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8ef34a04b14c8a8fb980fcd9255296ceb699ec6))
|
||||
* Ajout des évenements à venir ([c03fa0b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c03fa0ba42d69918501beb5bb98637a449eb2da0))
|
||||
* Ajout des frais d'inscription lors de la création d'un RF [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([ece23de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ece23deb19483c50d9999541a482e3378db19d23))
|
||||
* Ajout des frais de scolarité dans le dossier d'inscription [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([0c2e0b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c2e0b92f43f223adc22db36ecad7fd864737a98))
|
||||
* Ajout des modes de paiements + création d'une commande dans le ([0c5e3aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c5e3aa0988a16b6f9f8c0b411c2c1b443c972a7))
|
||||
* Ajout des payementPlans dans le formulaire / ajout de la photo ([d37aed5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37aed5f6496b8c8ca5519689dfc811d9626e09e))
|
||||
* Ajout du logo de l'école ([6a0b90e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6a0b90e98fbcc707756ae7fbbff921e480f2c695))
|
||||
* Ajout du logo N3wt dans les mails ([8a71fa1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8a71fa1830d0c0fb11467208bc98dc4f71598199))
|
||||
* Ajout du suivi de version dans le footer du Front ([fb7fbaf](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb7fbaf8394ebf41e6f3f31897e6d009c537a481))
|
||||
* Amélioration de la fiche élève pour y ajouter la fratrie et les ([256f995](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/256f995698e79572eb3d51ea60b96b6fad47d553))
|
||||
* Amélioration du dashboard ([eb48523](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb48523f7d466698faa268b8b25e6f1ed90bdfd7))
|
||||
* Amorçage de la gestion des absences [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([cb4fe74](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb4fe74a9e316a92c6b5e1d2550aaf2b1036a744))
|
||||
* Aussi pour la table des parents tant qu'à faire ([a3182c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a3182c0ba7c6ea9f99a4fe34a4a00079b4676d59))
|
||||
* **backend:** Ajout du logger django [[#7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/7)] ([b8511f9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8511f94b633b9bf5bd764b3706c53b74b3a6648))
|
||||
* Bilan de compétence d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([5760c89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5760c89105f38f4481e2cc6fa788bb0c39e8caa8))
|
||||
* Champ de recherche de l'élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([eb7805e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb7805e54e41f6eaefad81fea1616f0613365e8c))
|
||||
* Configuration des compétences par cycle [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([4e5aab6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4e5aab6db74a8d1dfdfb4928f60ad47da52c89e8))
|
||||
* Configuration et gestion du planning [[#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)] ([830d9a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/830d9a48c003e1cca469b1cf4082305e16685181))
|
||||
* Création d'un annuaire / mise à jour du subscribe ([6bd5704](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bd5704983282264bc50c73677495740f7d7e8a9))
|
||||
* Création d'un profile selector [[#37](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/37),[#38](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/38)] ([89b01b7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/89b01b79db884c393db29332b95f570e47d20ed1))
|
||||
* création d'une tooltip pour les informations supplémentaires de ([9197615](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/91976157e44e319b23fd35fa89859164bab71202))
|
||||
* création de 4 JSON de compétences en attendant de les mettre en ([69405c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/69405c577e7af3d07654fca96015d21f475e700d))
|
||||
* Création de clones lors de la création de RF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([d1a0067](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1a0067f7b7125e453ff6fc75efead881a7af37d))
|
||||
* Création nouveau style / pagination profils annuaires ([760ee00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/760ee0009e983776dfd500df7465ae66593dc85d))
|
||||
* Dockerisation d'un serveur docuseal + initialisation d'un compte ([8897d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8897d523dc23fd2d89a0ec66b5cc7fa15b69db5b))
|
||||
* Envoie d'un mail de bienvue au directeur ([5be5f9f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5be5f9f70d4fcf56da29afb19187806ff2e6e428))
|
||||
* Evolution des modèles pour intégrer un planning et du m2m ([85d4c00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/85d4c007cb2091ae1911ca1998f1b830470b8310))
|
||||
* Formulaire de création RF sur une seule pag ([76f9a7d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/76f9a7dd14d7065f4add01718fda499fbb9183c7))
|
||||
* Génération d'une page de suivi pédagogique + fix utilisation ([2a6b3bd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2a6b3bdf63ddc13509b66690ea5d76eac77d1090))
|
||||
* Génération du bilan de compétence en PDF [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0fe6c76](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0fe6c761892097d043902f4f051b9fdb5fef29d0))
|
||||
* Gestion de la création d'un nouveau guardian, de l'association ([fb73f9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb73f9e9a86430d7498aa8a10e5abc46325b7b2c))
|
||||
* Gestion de la mise à jour des profiles / roles / lors de l'édition ([dfd707d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dfd707d7a0c7f4514f5583f07803d20e3c2d6bd7))
|
||||
* Gestion de la sauvegarde du fichier d'inscription / affichage du ([d6edf25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d6edf250bbc1cc1a9862e26174bc24ca4f9ee4c1))
|
||||
* Gestion de la validation du dossier d'inscription ([b23264c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b23264c0d4008a4317c009a73ae11f57ee6917e2))
|
||||
* Gestion des absences du jour [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([030d19d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/030d19d411af8f0d87a3cb72cb401d9dd5fa96ce))
|
||||
* Gestion des documents nécessitant des signatures électroniques et ([e3879f5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e3879f516b81b7e4b784049668b2507f12e8155f)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||
* Gestion des documents parent ([59aee80](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59aee80c2e7592a7cdb119d1d30a5ad2c8bb20b0))
|
||||
* Gestion des documents signés durant l'inscription / possibilité de ([905b95f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/905b95f3a364f0d1ce8348d086870045d942bf92))
|
||||
* gestion des no data dans les table [[#33](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/33)] ([2888f8d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2888f8dcce8d593df8f81a635eaac94af4603829))
|
||||
* Gestion des pièces à fournir par les parents (configuration école) ([a65bd47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a65bd47905cc33c44416c1def0413579b96d820d))
|
||||
* Gestion des profils ADMIN/ECOLE (création des enseignants) ([e0bfd3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e0bfd3e11579c512aa6ad63c73e00e40be4eaf06))
|
||||
* Gestion des profils des enseignants / Visualisation d'une classe [[#4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/4)] ([81d1dfa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/81d1dfa9a70d0cd8d80e7d951a74c9355bba5238))
|
||||
* Gestion des rattachements de Guardian à des RF déjà existants ([7d1b9c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7d1b9c5657439d2fff287f60b9aba79a5dfdf089))
|
||||
* Gestion du planning [3] ([58144ba](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58144ba0d0f1b53e9313f4cd4d3fbc3e6bfdd274))
|
||||
* Gestion multi-profil multi-école ([1617829](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/16178296ec4dd4843be26b6e09b9c0f080df7ee4))
|
||||
* Harmonisation des fees / ajout de type de réduction / mise à jour ([5462306](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5462306a6020493cf747ea3bb8edb3240c36286f)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||
* Merge remote-tracking branch 'origin/WIP_style' into develop ([f887ae1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f887ae18862b740fa904d8ca04a3932eec455908))
|
||||
* Messagerie WIP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([23a593d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23a593dbc77b6f544a17de5a451ff60316f50292))
|
||||
* Mise à jour des Dockerfile préparation d'un environnement de démo [[#12](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/12)] ([32a77c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/32a77c780abe8c0aa9846843ac81d13e4b8cf73a))
|
||||
* Mise à jour des Teacher ([173ac47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/173ac47fb26ba2f101802571621fc4112adb1a9f))
|
||||
* Mise à jour du modèle (possibilité d'associer une réduciton à un ([8d1a41e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8d1a41e2693c3704b68e8d75bd32c4a89a6389e5)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||
* mise en place de la messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([d37145b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37145b73e2012f21a329ee97a565189233ca0f8))
|
||||
* Mise en place des actions pour chaque state du RF, possibilité ([8fc9478](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8fc947878665ca04e1697fa6df140e0d80c5a672))
|
||||
* Mise en place des paiements en plusieurs fois - partie BACK [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([274db24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/274db249aa25f2a0281638c318a68cf88a721a45))
|
||||
* Mise en place des paiements en plusieurs fois (partie BACK) [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([23203c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23203c0397f6247d32462cceca33d964898223a9))
|
||||
* Mise en place du Backend-messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([c6bc0d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6bc0d0b515a5be7b0bf930ff628a5e9b5ebbb33))
|
||||
* Nommage des templates / Intégration dans formulaire d'inscription ([eb81bbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb81bbba9265b9f3a71e500737436ee5301b7a5e)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||
* Ordonnancement de l'inscription sur plusieurs pages + contrôle des ([daad12c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/daad12cf40ce3b628581892f9a894a0841baa5e3))
|
||||
* Oubli fichier [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([d7fca9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d7fca9e942412a8d2fe379f38052f3b41ed9c0f9))
|
||||
* passage des mail au format HTML ([b97cf6e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b97cf6e02b92ba6750662bbf9e9c3af6ad19ab38))
|
||||
* Passage par une variable d'environnement pour les CORS et CSRF ([f9e870e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f9e870e11fe3041be53f4c357427d8060f50199f))
|
||||
* Peuplement de la BDD avec les JSON d'entrée [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([c6d7528](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6d75281a1fab0d9dc27d4da80f91c6fffb1bc0e))
|
||||
* planning events ([c9b0f0d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9b0f0d77a5ec61a239deb71959738f3b0e82d37))
|
||||
* Pre cablage du dashboard [#] ([1911f79](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1911f79f4578f8bc3b455308182c46d2d59e5580))
|
||||
* Préparation de la gestion des compétences en énumérant les élèves ([1c75927](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1c75927bbab497cfc86fc3a9aea11d436318be69)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||
* Preparation des modèles Settings pour l'enregistrement SMTP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([eda6f58](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eda6f587fb21bf041784209228518c8a6f03b1b5))
|
||||
* preparation du dockerfile pour le frontend [[#13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/13)] ([9716373](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9716373fa2d754177d4e71082b9079b71daab971))
|
||||
* Rattachement d'un dossier de compétences à une période scolaire ([7de839e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7de839ee5c9b09f7874575bdaf57436ec11b293f)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||
* Refactoring de la fonction de création de profil sur guardian côté ([753a8d6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/753a8d647ec3e45c8aabecba6d38b1a19741e0c0))
|
||||
* Sauvegarde des compétences d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0513603](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05136035ab1d811c904e35d99ddb884c68b7fd74))
|
||||
* Sauvegarde des fichiers migration ([017c029](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/017c0290dd1fab8afa3a05541e57a321733ff5c9))
|
||||
* Signatures électroniques docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([c8c8941](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c8c8941ec875b541cfb55c3504a0e951f36163ef))
|
||||
* Sortie des calculs des montants totaux de la partie configuration + revue du rendu [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([799e1c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/799e1c6717fceec4b29edfbdd0af52268b7e8fce))
|
||||
* Suite de la gestion des sessions ([8ea68bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8ea68bbad0646d99209d1821a2b71364630005b3))
|
||||
* Suppression de l'ancienne POPUP de RF ([5927e48](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5927e48e6544e8819a29766562107834d44e7a5d))
|
||||
* Suppression des localStorage ([023b46e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/023b46e16e8da56971a8c55c0930e6ab4fbf53ec))
|
||||
* Suppression des templates docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([081dc06](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/081dc060014ee15ca6881fc83b779679a271326d))
|
||||
* Upload du SEPA par les parents / Création d'un composant header ([8417d3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8417d3eb141b116e2e9f8c6038831ce1bbe30e2a))
|
||||
* Utilisation d'une clef API Docuseal par établissement ([23ab7d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23ab7d04ef0c940b7008e8bc7d4b43b373d16d40))
|
||||
* Utilisation de l'établissement en variable de session / gestion de ([f2ad1de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2ad1de5a4215395f3aa7a0e04ac2eb3edc5ec51))
|
||||
* Utilisation des nouvelles alertes dans la page admin de la gestion ([67193a8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67193a8b3601784f37563094f3fdede943523b53))
|
||||
* Validation du dossier d'inscription en affectant l'élève à une ([0f49236](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0f49236965f575f6af17837b9860fa4481227785))
|
||||
|
||||
|
||||
### Corrections de bugs
|
||||
|
||||
* correction des redirections vers la login page ([2e0fe86](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2e0fe86c71e9f02e8ee77ccbd80533a63a31ef63))
|
||||
* Ajout d'un champ is_required pour les documents parents facultatifs ([5866427](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5866427544e24c8e79cb773d38bda683f63f4531))
|
||||
* Ajout d'un message de confirmation lors de la suppression d'un ([9248480](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92484804f6483eab401612315b5513cc78e6a726))
|
||||
* ajout de credential include dans get CSRF ([c161fa7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c161fa7e7568437ba501a565ad53192b9cb3b6f3))
|
||||
* Ajout de l'établissement dans la requête KPI récupérant les ([ada2a44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ada2a44c3ec9ba45462bd7e78984dfa38008e231))
|
||||
* Ajout des niveaux scolaires dans le back [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([05542df](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05542dfc40649fd194ee551f0298f1535753f219))
|
||||
* ajout des urls prod et demo ([043d93d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/043d93dcc476e5eb3962fdbe0f6a81b937122647))
|
||||
* Ajout du % ou € en mode édition de réduction ([f2628bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2628bb45a14da42d014e42b1521820ffeedfb33))
|
||||
* Ajout du controle sur le format des dates ([e538ac3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e538ac3d56294d4e647a38d730168ea567c76f04))
|
||||
* Ajout du mode Visu ([e1c6073](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1c607308c12cf75695e9d4593dc27ebe74e6a4f))
|
||||
* ajustement du handlePhoneChange [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([31fdc61](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/31fdc612b10843ce694b55696f67bd2a80d56769))
|
||||
* Application des périodes à un studentCompetency lors de la création ([d65b171](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d65b171da8a310acca15936a39e44239763c88b9))
|
||||
* application des recommandations linter es pour générer un build de prod ([d1aa8b5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1aa8b54fb71bb946e95a19105f51f7f29c75fda))
|
||||
* Application du formattage sur les fichiers modifiés ([001a5bc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/001a5bc83c0bf54061b2b04967da3fc11e2cd8dc))
|
||||
* boucle inifinie dans UseEffect ([f3c4284](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3c428477879729d36760bb61dac015311c84fec))
|
||||
* Bug lorsqu'on déselectionne un paiementPlan ([d64500f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d64500f4022710423c77d023476065816ecd061d))
|
||||
* build error ([65d5b8c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/65d5b8c424bf0e0a9da1b39500c8252f683725c7))
|
||||
* Calcul du montant total des tarif par RF + affichage des tarifs ([c269b89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c269b89d3d58cc65f254b75f6d713c4fd15f6320)), closes [#26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/26)
|
||||
* calcul nombre de pages dans chaque tab ([5440f5c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5440f5cbdbca8b9435a17914c7e7c4ecc34e6bb3))
|
||||
* Champs requis sur les teachers and classes ([42b4c99](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/42b4c99be86f050ccd76302caf725af5df413d17))
|
||||
* Changement d'icone associé aux documents soumis à validation ([500b6e9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/500b6e9af7ac76dafa35bd830cd0767cece47d27))
|
||||
* code mort ([4fc061f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fc061fc255b4174f794ac58da1b6849419e9f1a))
|
||||
* Condition de validation d'ajout d'un nouveau document parent / ([9e69790](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9e69790683fd83b0e48a9f70150661cb06a7b556))
|
||||
* conflits + closeModal lors de la création d'un RF ([1617b13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1617b132c4f67fdbaf261808a0a9596b7a72a4dc))
|
||||
* coquille ([c9c7e77](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9c7e7715efde8766c3b2ad2c355dc9a9960b19f))
|
||||
* coquille dans les imports ([4ecf25a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4ecf25a6ab90a57da0013f6ed603d6cd5bd4eeeb))
|
||||
* Correction de l'affichage des numéros de téléphone [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([4f774c1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f774c18e47fc57e081022a03ea352638e7211d2))
|
||||
* correction de l'ouverture du dashbord [[#39](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/39)] ([a157d53](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a157d53932d5576fc9768f5c063cf9aafa214d43))
|
||||
* Correction de la désactivation des spécialités lorsqu'on ([afc1632](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/afc1632797c0d35df7da03432eba9ab0f1875f55)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||
* Correction dépendances circulaires ([fc9a1ed](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc9a1ed252e1e115e4a2f7c4a3a04ee6757be683))
|
||||
* Correction des Protected Routes avec multi role ([dd0884b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dd0884bbce6b6549f0f3fca991045f7170889710))
|
||||
* correction des refresh des protected routes [[#36](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/36)] ([839a262](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/839a26257b659a86903d3f982548884cc87366b9))
|
||||
* Correction du Establishment context au refresh ([43e301e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43e301ed641a742323a98c430e30e134babc4aa4))
|
||||
* correction fileGroup lors de l'enregistrement d'un nouveau responsable ([dce2114](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dce2114a7940310e2c4241c2cdbd7e3fd060fb60))
|
||||
* Correction option fusion ([e61cd51](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e61cd51ce2b0665c18f9497e6d2b1f7b8196723e))
|
||||
* Correction sur le calcul du nombre total de pages [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([5946cbd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5946cbdee661527317dac66f99f0abce021c835a))
|
||||
* correction titre mail reset mdp ([cac1519](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cac1519bf311b660831222d76d4d5165ee4f4d7e))
|
||||
* Correction URL ([170f7c4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/170f7c4fa80e1cd40079ac861e7e633c62f143df))
|
||||
* Corrrection typo dans description des tableaux frais/réduction ([175932f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/175932ffa3fb2747cafd158b8142df9b7010a3d4))
|
||||
* csrf ([59a0d40](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59a0d401301fe77226fd5f294a3cd7e589d46fad))
|
||||
* Division par 0 ([a42cf34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a42cf348a0a3cc43c6c6b643b1da158690d67cb8))
|
||||
* double confirmation sur les popup ([677cec1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/677cec1ec2f7a3582327f4747d088c6bccbd2560))
|
||||
* entrypoint access right ([a041ffa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a041ffaee75b74e0d559fb14bc79fbcfae98da14))
|
||||
* faire plaisir à LSO ([9374b00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9374b001c9944cb6af1e10451f0e5f236a7890e8))
|
||||
* formulaire sur toute la larguer + initiation à un autre style de bg ([4fd40ac](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fd40ac5fc90ea1ddda9d73ea290b588074c6e2f))
|
||||
* Fusion documents ([857b8b2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/857b8b26c3722171007399dc66cd9980b33151c5))
|
||||
* Generation d'une fiche d'élève avec le nouveau modèle PayementMode ([4f40d1f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f40d1f29d7abd1e0c6bf889b10f811f184ff10d))
|
||||
* Génération uniquement des compétences évaluées dans le PDF ([eca8d7a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eca8d7a8d59f39313123166859f4c4bf548d150e))
|
||||
* gestion des codes retours ([7f35527](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f3552764979e098ca2e8c3547354c8ae6feaa23))
|
||||
* Gestion des listes d'inscription "vides" [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([edc9724](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/edc97242f2219f48233441b8c7ec97ef9551c60c))
|
||||
* gestion du jour d'échéance ([2576d21](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2576d2173460267664d927bd093580a21c18725b))
|
||||
* import du Loader ([e2a39ff](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e2a39ff74dd9671bb1d00de2b6cec1cd3e4ff614))
|
||||
* inject env var ([fc337b1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc337b1e0b4605f3490435f4819b01d38f921156))
|
||||
* Limite du nombre de responsables légaux à 2 [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([1ced4a1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1ced4a10696057b8df114dc95adf9868e8d7aa43))
|
||||
* Link documents with establishments ([2f6d30b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2f6d30b85b90508cae49081a82eadea5039f60b2))
|
||||
* load the school image eorrectly ([6bc2405](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bc24055cd5a79d59d2b56c7e767ac1b30d99fff))
|
||||
* Lors de la création d'un clone, on prend le nom de l'élève et pas ([db8e1d8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/db8e1d8ab320222370c64d7b7fde3e43c59921e8))
|
||||
* Messages de retour reset/new password ([4a6b7ce](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a6b7ce379747565c77205728e7b0d9c8a7c9585))
|
||||
* Mise à jour correcte du fichier après avoir été signé ([5ea3cbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5ea3cbb0790840d823e799cc64766a99ef5591a9))
|
||||
* Mise à jour des upcomming events ([f93c428](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f93c42825964d91d11af26541eecb9ba5f01e801))
|
||||
* mise à jour settings pour la prod / correction CORS ([25e2799](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/25e2799c0f0e46b1a6d78bcc849cc777e67a01f1))
|
||||
* Mise en page des inscriptions (boutons ajout / barre de recherche) ([cf14431](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cf144310a13fa4cbd01a292002d8a9963acc4598))
|
||||
* Modèle créé 2 fois par erreur ([49907d7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/49907d7ec8847017115191e937ba9f68350c92bd))
|
||||
* Modification d'un guardian sans changer d'adresse mail (même ([95b449d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95b449ddfde4160a55237e0c50e6bed604dcdfe5))
|
||||
* Ne pas dissocier de responsable s'il n'y en a pas d'autre rattaché ([ac0672f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ac0672f3349aa2ac62db0e3927658a3f2d66cebf))
|
||||
* Ne pas retourner d'erreur si pas de dossier d'inscription ([be27fe1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be27fe1232c3808b0a65d5f1b265ef454eb35e74))
|
||||
* Nouvelle amélioration ([8b3f963](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8b3f9637a91fe817c87427015a89ba3e469d525d))
|
||||
* On attend que la session soit mise à jour pour intiialiser le ([ccecd78](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ccecd78704c3e6db58724401b92dd065a7e733ab))
|
||||
* On commence à la page 1 ([3c62cc9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c62cc9ad2cfa691ee798d27ee6b377676e50bb7))
|
||||
* On empêche la sauvegarde d'un document à signer tant qu'aucun ([be013f0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be013f07864345024320114bd734508a033fd5db))
|
||||
* On ne peut sélectionner que les élèves inscrits [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([56c223f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56c223f3cc0498b4a6619d68f0185c36482c4ec9))
|
||||
* Ordre des guardians lors de leur création / déselection correcte si ([3b667d3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3b667d3b150684a685d7d76cf06d050049ee07cd))
|
||||
* pagination annuaire ([980f169](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/980f169c1d1a46f0d47f4b9ff65fa940ac610023))
|
||||
* PieChart ([fe2d4d4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fe2d4d45137df3b1ead4d21c29722fec0bd0fbab))
|
||||
* Positionnement de la variable isSepa ([82573f1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/82573f1b2333a01675cebf575f33ab77e70e138b))
|
||||
* Possibilité d'ajouter un 2ème guardian, même si son mail est ([8cf2290](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8cf22905e533a23ee679107cc0bcae1198badb4a))
|
||||
* Récupération d'un template donné ([9b13d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9b13d52e8d8926bcc6f756ff4d2c9d278a0cc387))
|
||||
* Refresh par profil role ([24069b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24069b894ef2009d9fe0ad884e7a39c29a5a9504))
|
||||
* refresh token ([053d524](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/053d524a513adfec8bd9b3467fc358c257776a85))
|
||||
* régression CORS_ALLOWED_ORIGINS ([a69498d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a69498dd06649b601a16a509c7a80c9f67c7872e))
|
||||
* régression lors de l'uniformisation des modales ([00f7bfd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/00f7bfde4abd10d080dc2035d3607d6c35e7db14))
|
||||
* Remise du message de confirmation supprimé par erreur ([efcc5e6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/efcc5e66722c829dcdf522a6903c616901a14604))
|
||||
* Remise en état du bouton Submit ([e9650c9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e9650c992e6c8339d7acde4000bf4f3dd8e98bac))
|
||||
* Remise en place de l'API_KEY docuseal dans le back ([6d80594](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6d805940fe5524cae1864c0beebcd136bda84eda))
|
||||
* remove lint error ([aef6c19](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aef6c193b1eaaedbb0642ce7929b2cfe8f47d682))
|
||||
* Remplacement des enum par des modèles pour les payementModes et les ([7fe5346](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7fe53465acc2df53351f713ccacd12223d6eff1a))
|
||||
* restore du start.py suite à des tests ([de5f7cd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/de5f7cd41e52b27ee3d8f47cf47fbfdad78216ac))
|
||||
* right ([05f1f16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05f1f16727c2510385425b23fa6ab98fa62d07be))
|
||||
* Scroll de l'emploi du temps élève ([f38a441](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f38a4414c28ec52981201c15b7eda0dccc1f932f))
|
||||
* searchTerm inscription ([8f0cf16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8f0cf16f707ac1cd51f0d619fa1c5ea0ba023f68))
|
||||
* Session storage selectedEstablishmentEvaluationFrequency et selectedEstablishmentTotalCapacity ([e30753f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e30753f1d6d2911d51bb9dfbf32fbae6f2b62b5d))
|
||||
* Suite du commit précédent ([cd9c10a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cd9c10a88af2a05350570c424fb284280c0f65ee))
|
||||
* Suppression d'un profil uniquement s'il ne contient aucun guardian ([330018e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/330018edfdbdc071c15838bc22b8a4e726773204))
|
||||
* Suppression de la top bar admin [[#34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/34)] ([3990d75](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3990d75e521bea007a8f479924507498d9586a71))
|
||||
* Suppression de print inutiles ([43874f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43874f8b9e76b3e9f131f240ee895d958cd73fab))
|
||||
* Suppression event planning ([c117f96](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c117f96e528244ee68ce69b7685880e171976e32))
|
||||
* Unicité des fees + utilisation de l'establishmentID [[#44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/44)] ([d37e6c3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37e6c384d0ef16053ed9fcc1e979f7f902cc8d8))
|
||||
* Uniformisation des Modales et Popup [[#35](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/35)] ([f252efd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f252efdef4fce1f2d19ac7ca1eb9c049706c0d9f))
|
||||
* Utilisation des bonnes colonnes pour les fees et discounts selon si ([9f1f97e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9f1f97e0c56771208305b28a740504c220287053))
|
||||
* Utilisation du signal "post-migrate" pour créer la spécialité par ([e1202c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1202c6e6d4061552fa7d530e3e09b11384843c3))
|
||||
* Variables booléennes par défaut ([6bedf71](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bedf715ccf9bb9bae4f92d735e3d7b714c96849))
|
||||
* variables csrf ([789816e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/789816e9868685e2ae08b536b6b6ada1a6a64595))
|
||||
* warning sur ouverture modale de fichiers ([889a3a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/889a3a48c5c2a3f6cb65de8ede0efbe639408011))
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
https://localhost:4443, https://127.0.0.1:4443 {
|
||||
reverse_proxy docuseal:3000
|
||||
}
|
||||
@ -18,6 +18,19 @@ const nextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: 'www.gravatar.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.demo.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.prod.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8080',
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
@ -29,14 +42,9 @@ const nextConfig = {
|
||||
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
|
||||
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
|
||||
DOCUSEAL_API_KEY: process.env.DOCUSEAL_API_KEY,
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/documents/:path*',
|
||||
destination: 'https://api.docuseal.com/v1/documents/:path*',
|
||||
},
|
||||
{
|
||||
source: '/api/auth/:path*',
|
||||
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
|
||||
|
||||
37
Front-End/package-lock.json
generated
37
Front-End/package-lock.json
generated
@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "n3wt-school-front-end",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "n3wt-school-front-end",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"dependencies": {
|
||||
"@docuseal/react": "^1.0.56",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"date-fns": "^4.1.0",
|
||||
@ -29,6 +28,7 @@
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-international-phone": "^4.5.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-tooltip": "^5.28.0"
|
||||
@ -536,11 +536,6 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@docuseal/react": {
|
||||
"version": "1.0.66",
|
||||
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
|
||||
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@ -8834,6 +8829,21 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.62.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-international-phone": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
||||
@ -11253,11 +11263,6 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@docuseal/react": {
|
||||
"version": "1.0.66",
|
||||
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
|
||||
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
|
||||
},
|
||||
"@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@ -17160,6 +17165,12 @@
|
||||
"scheduler": "^0.23.2"
|
||||
}
|
||||
},
|
||||
"react-hook-form": {
|
||||
"version": "7.62.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-international-phone": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n3wt-school-front-end",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@ -14,7 +14,6 @@
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docuseal/react": "^1.0.56",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"date-fns": "^4.1.0",
|
||||
@ -35,19 +34,20 @@
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-international-phone": "^4.5.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-tooltip": "^5.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.11",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import SelectChoice from '@/components/Form/SelectChoice';
|
||||
import AcademicResults from '@/components/Grades/AcademicResults';
|
||||
import Attendance from '@/components/Grades/Attendance';
|
||||
import Remarks from '@/components/Grades/Remarks';
|
||||
@ -9,7 +9,7 @@ import Homeworks from '@/components/Grades/Homeworks';
|
||||
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
|
||||
import Orientation from '@/components/Grades/Orientation';
|
||||
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
|
||||
import Button from '@/components/Button';
|
||||
import Button from '@/components/Form/Button';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||
@ -29,7 +29,7 @@ import { useClasses } from '@/context/ClassesContext';
|
||||
import { Award, FileText } from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||
import InputText from '@/components/InputText';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import Button from '@/components/Button';
|
||||
import Button from '@/components/Form/Button';
|
||||
import GradeView from '@/components/Grades/GradeView';
|
||||
import {
|
||||
fetchStudentCompetencies,
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import EmailSender from '@/components/Admin/EmailSender';
|
||||
import InstantMessaging from '@/components/Admin/InstantMessaging';
|
||||
import AnnouncementScheduler from '@/components/Admin/AnnouncementScheduler';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function MessageriePage({ csrfToken }) {
|
||||
@ -18,11 +17,6 @@ export default function MessageriePage({ csrfToken }) {
|
||||
label: 'Messagerie Instantanée',
|
||||
content: <InstantMessaging csrfToken={csrfToken} />,
|
||||
},
|
||||
{
|
||||
id: 'announcement',
|
||||
label: 'Planifier une Annonce',
|
||||
content: <AnnouncementScheduler csrfToken={csrfToken} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Users, Clock, CalendarCheck, School, AlertTriangle, CheckCircle2 } from 'lucide-react';
|
||||
import {
|
||||
Users,
|
||||
Clock,
|
||||
CalendarCheck,
|
||||
School,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import Loader from '@/components/Loader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import EventCard from '@/components/EventCard';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
fetchRegisterForms,
|
||||
@ -15,20 +23,6 @@ import Attendance from '@/components/Grades/Attendance';
|
||||
import LineChart from '@/components/Charts/LineChart';
|
||||
import PieChart from '@/components/Charts/PieChart';
|
||||
|
||||
// Composant EventCard pour afficher les événements
|
||||
const EventCard = ({ title, date, description, type }) => (
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarCheck className="text-blue-500" size={20} />
|
||||
<div>
|
||||
<h4 className="font-medium">{title}</h4>
|
||||
<p className="text-sm text-gray-500">{date}</p>
|
||||
<p className="text-sm mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const mockCompletionRate = 72; // en pourcentage
|
||||
|
||||
export default function DashboardPage() {
|
||||
@ -39,8 +33,10 @@ export default function DashboardPage() {
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
|
||||
const [absencesToday, setAbsencesToday] = useState([]);
|
||||
const { selectedEstablishmentId, selectedEstablishmentTotalCapacity, apiDocuseal } =
|
||||
useEstablishment();
|
||||
const {
|
||||
selectedEstablishmentId,
|
||||
selectedEstablishmentTotalCapacity,
|
||||
} = useEstablishment();
|
||||
|
||||
const [statusDistribution, setStatusDistribution] = useState([
|
||||
{ label: 'Non envoyé', value: 0 },
|
||||
@ -168,25 +164,6 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<div key={selectedEstablishmentId} className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
||||
apiDocuseal
|
||||
? 'bg-green-100 text-green-700 border border-green-300'
|
||||
: 'bg-red-100 text-red-700 border border-red-300'
|
||||
}`}
|
||||
>
|
||||
{apiDocuseal ? (
|
||||
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
|
||||
) : (
|
||||
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
|
||||
)}
|
||||
{apiDocuseal
|
||||
? 'Clé API Docuseal renseignée'
|
||||
: 'Clé API Docuseal manquante'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Statistiques principales */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<StatCard
|
||||
@ -220,8 +197,10 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Événements et KPIs */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="lg:col-span-1 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<h2 className="text-lg font-semibold mb-6">
|
||||
{t('inscriptionTrends')}
|
||||
</h2>
|
||||
@ -234,20 +213,20 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Présence et assiduité */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
{/* Colonne de droite : Événements à venir */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1 h-full">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ajout du composant Attendance en dessous, en lecture seule */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Tab from '@/components/Tab';
|
||||
import TabContent from '@/components/TabContent';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/InputText';
|
||||
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
|
||||
import Button from '@/components/Form/Button';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
fetchSmtpSettings,
|
||||
@ -16,7 +16,7 @@ import { useNotification } from '@/context/NotificationContext';
|
||||
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState('structure');
|
||||
const [activeTab, setActiveTab] = useState('smtp');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@ -40,8 +40,6 @@ export default function SettingsPage() {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam === 'smtp') {
|
||||
setActiveTab('smtp');
|
||||
} else if (tabParam === 'structure') {
|
||||
setActiveTab('structure');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
@ -79,18 +77,6 @@ export default function SettingsPage() {
|
||||
}
|
||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpServerChange = (e) => {
|
||||
setSmtpServer(e.target.value);
|
||||
};
|
||||
@ -107,26 +93,6 @@ export default function SettingsPage() {
|
||||
setSmtpPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleUseTlsChange = (e) => {
|
||||
setUseTls(e.target.checked);
|
||||
};
|
||||
|
||||
const handleUseSslChange = (e) => {
|
||||
setUseSsl(e.target.checked);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
showNotification(
|
||||
'Les mots de passe ne correspondent pas',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmtpSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const smtpData = {
|
||||
@ -164,11 +130,6 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Tab
|
||||
text="Informations de la structure"
|
||||
active={activeTab === 'structure'}
|
||||
onClick={() => handleTabClick('structure')}
|
||||
/>
|
||||
<Tab
|
||||
text="Paramètres SMTP"
|
||||
active={activeTab === 'smtp'}
|
||||
@ -176,28 +137,6 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TabContent isActive={activeTab === 'structure'}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputText
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
/>
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
@ -8,9 +8,9 @@ import { fetchClasse } from '@/app/actions/schoolAction';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import logger from '@/utils/logger';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import Button from '@/components/Button';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
import Button from '@/components/Form/Button';
|
||||
import SelectChoice from '@/components/Form/SelectChoice';
|
||||
import CheckBox from '@/components/Form/CheckBox';
|
||||
import {
|
||||
fetchAbsences,
|
||||
createAbsences,
|
||||
|
||||
@ -52,7 +52,7 @@ export default function Page() {
|
||||
);
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
@ -78,7 +78,7 @@ export default function Page() {
|
||||
handleTuitionFees();
|
||||
|
||||
// Fetch data for registration file schoolFileTemplates
|
||||
fetchRegistrationSchoolFileMasters()
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setFichiers(data);
|
||||
})
|
||||
@ -353,7 +353,6 @@ export default function Page() {
|
||||
<FilesGroupsManagement
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal={apiDocuseal}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@ -2,17 +2,17 @@
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { User, Mail } from 'lucide-react';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
||||
import Button from '@/components/Button';
|
||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||
import Button from '@/components/Form/Button';
|
||||
import Table from '@/components/Table';
|
||||
import FeesSection from '@/components/Structure/Tarification/FeesSection';
|
||||
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
|
||||
import SectionTitle from '@/components/SectionTitle';
|
||||
import InputPhone from '@/components/InputPhone';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
import RadioList from '@/components/RadioList';
|
||||
import SelectChoice from '@/components/SelectChoice';
|
||||
import InputPhone from '@/components/Form/InputPhone';
|
||||
import CheckBox from '@/components/Form/CheckBox';
|
||||
import RadioList from '@/components/Form/RadioList';
|
||||
import SelectChoice from '@/components/Form/SelectChoice';
|
||||
import Loader from '@/components/Loader';
|
||||
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
||||
import logger from '@/utils/logger';
|
||||
@ -35,7 +35,6 @@ import {
|
||||
fetchRegistrationFileGroups,
|
||||
fetchRegistrationSchoolFileMasters,
|
||||
fetchRegistrationParentFileMasters,
|
||||
cloneTemplate,
|
||||
createRegistrationSchoolFileTemplate,
|
||||
createRegistrationParentFileTemplate,
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
@ -96,7 +95,7 @@ export default function CreateSubscriptionPage() {
|
||||
const { getNiveauLabel } = useClasses();
|
||||
|
||||
const formDataRef = useRef(formData);
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
@ -313,13 +312,13 @@ export default function CreateSubscriptionPage() {
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
fetchRegistrationSchoolFileMasters()
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setSchoolFileMasters(data);
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
fetchRegistrationParentFileMasters()
|
||||
fetchRegistrationParentFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setParentFileMasters(data);
|
||||
})
|
||||
@ -502,7 +501,11 @@ export default function CreateSubscriptionPage() {
|
||||
// Mode édition
|
||||
editRegisterForm(registerFormID, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Dossier mis à jour avec succès:', response);
|
||||
showNotification(
|
||||
"Dossier d'inscription mis à jour avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -518,123 +521,23 @@ export default function CreateSubscriptionPage() {
|
||||
} else {
|
||||
// Création du dossier d'inscription
|
||||
createRegisterForm(data, csrfToken)
|
||||
.then((data) => {
|
||||
// Clonage des schoolFileTemplates
|
||||
const masters = schoolFileMasters.filter((file) =>
|
||||
file.groups.includes(selectedFileGroup)
|
||||
);
|
||||
const parentMasters = parentFileMasters.filter((file) =>
|
||||
file.groups.includes(selectedFileGroup)
|
||||
);
|
||||
|
||||
const clonePromises = masters.map((templateMaster) =>
|
||||
cloneTemplate(
|
||||
templateMaster.id,
|
||||
formDataRef.current.guardianEmail,
|
||||
templateMaster.is_required,
|
||||
selectedEstablishmentId,
|
||||
apiDocuseal
|
||||
)
|
||||
.then((clonedDocument) => {
|
||||
const cloneData = {
|
||||
name: `${templateMaster.name}_${formDataRef.current.studentFirstName}_${formDataRef.current.studentLastName}`,
|
||||
slug: clonedDocument.slug,
|
||||
id: clonedDocument.id,
|
||||
master: templateMaster.id,
|
||||
registration_form: data.student.id,
|
||||
};
|
||||
|
||||
return createRegistrationSchoolFileTemplate(
|
||||
cloneData,
|
||||
csrfToken
|
||||
)
|
||||
.then((response) =>
|
||||
logger.debug('Template enregistré avec succès:', response)
|
||||
)
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error(
|
||||
"Erreur lors de l'enregistrement du template:",
|
||||
error
|
||||
);
|
||||
.then((response) => {
|
||||
showNotification(
|
||||
"Erreur lors de la création du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur',
|
||||
'ERR_ADM_SUB_03'
|
||||
"Dossier d'inscription créé avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Error during cloning or sending:', error);
|
||||
showNotification(
|
||||
"Erreur lors de la création du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur',
|
||||
'ERR_ADM_SUB_05'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Clonage des parentFileTemplates
|
||||
const parentClonePromises = parentMasters.map((parentMaster) => {
|
||||
const parentTemplateData = {
|
||||
master: parentMaster.id,
|
||||
registration_form: data.student.id,
|
||||
};
|
||||
|
||||
return createRegistrationParentFileTemplate(
|
||||
parentTemplateData,
|
||||
csrfToken
|
||||
)
|
||||
.then((response) =>
|
||||
logger.debug(
|
||||
'Parent template enregistré avec succès:',
|
||||
response
|
||||
)
|
||||
)
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error(
|
||||
"Erreur lors de l'enregistrement du parent template:",
|
||||
error
|
||||
);
|
||||
showNotification(
|
||||
"Erreur lors de la création du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur',
|
||||
'ERR_ADM_SUB_02'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Attendre que tous les clones soient créés
|
||||
Promise.all([...clonePromises, ...parentClonePromises])
|
||||
.then(() => {
|
||||
// Redirection après succès
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
"Erreur lors de la création du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur',
|
||||
'ERR_ADM_SUB_04'
|
||||
);
|
||||
logger.error('Error during cloning or sending:', error);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Erreur lors de la mise à jour du dossier:', error);
|
||||
showNotification(
|
||||
"Erreur lors de la création du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur',
|
||||
'ERR_ADM_SUB_01'
|
||||
);
|
||||
logger.error('Error during register form creation:', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,16 +8,18 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { showNotification } = useNotification();
|
||||
const searchParams = useSearchParams();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
@ -26,11 +28,21 @@ export default function Page() {
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Error:', error.message);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setFormErrors(error.details);
|
||||
@ -47,7 +59,6 @@ export default function Page() {
|
||||
studentId={studentId}
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal = {apiDocuseal}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
|
||||
errors={formErrors}
|
||||
|
||||
@ -40,8 +40,8 @@ import {
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import logger from '@/utils/logger';
|
||||
import { PhoneLabel } from '@/components/PhoneLabel';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import { PhoneLabel } from '@/components/Form/PhoneLabel';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
import FilesModal from '@/components/Inscription/FilesModal';
|
||||
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
||||
|
||||
@ -52,6 +52,7 @@ import {
|
||||
HISTORICAL_FILTER,
|
||||
} from '@/utils/constants';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page({ params: { locale } }) {
|
||||
const t = useTranslations('subscriptions');
|
||||
@ -91,8 +92,6 @@ export default function Page({ params: { locale } }) {
|
||||
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
||||
const [selectedRegisterForm, setSelectedRegisterForm] = useState([]);
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||
@ -103,6 +102,7 @@ export default function Page({ params: { locale } }) {
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const openSepaUploadModal = (row) => {
|
||||
setSelectedRowForUpload(row);
|
||||
@ -133,9 +133,8 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormCurrrentYearDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataCurrentYear(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalCurrentYear(count);
|
||||
@ -153,9 +152,8 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormNextYearDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataNextYear(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalNextYear(count);
|
||||
@ -173,9 +171,7 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormHistoricalDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataHistorical(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
@ -225,12 +221,11 @@ export default function Page({ params: { locale } }) {
|
||||
|
||||
fetchDataAndSetState();
|
||||
}
|
||||
}, [selectedEstablishmentId, reloadFetch, currentSchoolYearPage, searchTerm]);
|
||||
}, [selectedEstablishmentId, reloadFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
const fetchDataAndSetState = () => {
|
||||
setIsLoading(true);
|
||||
fetchRegisterForms(
|
||||
selectedEstablishmentId,
|
||||
CURRENT_YEAR_FILTER,
|
||||
@ -247,7 +242,6 @@ export default function Page({ params: { locale } }) {
|
||||
.then(registerFormHistoricalDataHandler)
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
setIsLoading(false);
|
||||
setReloadFetch(false);
|
||||
};
|
||||
|
||||
@ -256,7 +250,12 @@ export default function Page({ params: { locale } }) {
|
||||
}, 500); // Debounce la recherche
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [searchTerm]);
|
||||
}, [
|
||||
searchTerm,
|
||||
selectedEstablishmentId,
|
||||
currentSchoolYearPage,
|
||||
itemsPerPage,
|
||||
]);
|
||||
|
||||
/**
|
||||
* UseEffect to update page count of tab
|
||||
@ -294,15 +293,21 @@ export default function Page({ params: { locale } }) {
|
||||
editRegisterForm(row.student.id, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Mandat SEPA uploadé avec succès :', response);
|
||||
setPopupMessage('Le mandat SEPA a été uploadé avec succès.');
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
'Le mandat SEPA a été uploadé avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setReloadFetch(true);
|
||||
closeSepaUploadModal();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Erreur lors de l'upload du mandat SEPA :", error);
|
||||
setPopupMessage("Erreur lors de l'upload du mandat SEPA.");
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
"Erreur lors de l'upload du mandat SEPA.",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -321,21 +326,22 @@ export default function Page({ params: { locale } }) {
|
||||
archiveRegisterForm(id)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage(
|
||||
"Le dossier d'inscription a été correctement archivé"
|
||||
showNotification(
|
||||
"Le dossier d'inscription a été correctement archivé",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRegistrationForms(
|
||||
registrationForms.filter((fiche) => fiche.id !== id)
|
||||
);
|
||||
setReloadFetch(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage(
|
||||
"Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur."
|
||||
showNotification(
|
||||
"Erreur lors de l'archivage du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
@ -350,16 +356,20 @@ export default function Page({ params: { locale } }) {
|
||||
sendRegisterForm(id)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("Le dossier d'inscription a été envoyé avec succès");
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
"Le dossier d'inscription a été envoyé avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setReloadFetch(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage(
|
||||
"Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur."
|
||||
showNotification(
|
||||
"Erreur lors de l'envoi du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
@ -694,7 +704,7 @@ export default function Page({ params: { locale } }) {
|
||||
];
|
||||
|
||||
let emptyMessage;
|
||||
if (activeTab === CURRENT_YEAR_FILTER) {
|
||||
if (activeTab === CURRENT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
@ -702,7 +712,7 @@ export default function Page({ params: { locale } }) {
|
||||
message="Veuillez procéder à la création d'un nouveau dossier d'inscription pour l'année scolaire en cours."
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === NEXT_YEAR_FILTER) {
|
||||
} else if (activeTab === NEXT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
@ -710,7 +720,7 @@ export default function Page({ params: { locale } }) {
|
||||
message="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === HISTORICAL_FILTER) {
|
||||
} else if (activeTab === HISTORICAL_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
@ -836,12 +846,6 @@ export default function Page({ params: { locale } }) {
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
|
||||
@ -9,6 +9,7 @@ import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
@ -30,6 +31,7 @@ export default function Page() {
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
@ -63,11 +65,20 @@ export default function Page() {
|
||||
editRegisterForm(studentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF mis à jour avec succès:', response);
|
||||
showNotification(
|
||||
'Le dossier d\'inscription a été validé avec succès',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
setIsLoading(false);
|
||||
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification(
|
||||
"Erreur lors de la validation du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setIsLoading(false);
|
||||
logger.error('Erreur lors de la mise à jour du RF:', error);
|
||||
});
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import Button from '@/components/Form/Button';
|
||||
import Logo from '@/components/Logo'; // Import du composant Logo
|
||||
import FormRenderer from '@/components/Form/FormRenderer';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations('homePage');
|
||||
@ -13,6 +15,7 @@ export default function Home() {
|
||||
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
||||
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
|
||||
<Button text={t('loginButton')} primary href="/users/login" />
|
||||
<FormTemplateBuilder />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,31 +7,52 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
try {
|
||||
const result = await editRegisterForm(studentId, data, csrfToken);
|
||||
const handleSubmit = (data) => {
|
||||
setIsLoading(true);
|
||||
editRegisterForm(studentId, data, csrfToken)
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_PARENTS_HOME_URL);
|
||||
} catch (error) {
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
logger.error('Error:', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
}
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
studentId={studentId}
|
||||
csrfToken={csrfToken}
|
||||
selectedEstablishmentId={selectedEstablishmentId}
|
||||
apiDocuseal = {apiDocuseal}
|
||||
onSubmit={handleSubmit}
|
||||
cancelUrl={FE_PARENTS_HOME_URL}
|
||||
enable={enable}
|
||||
|
||||
@ -6,8 +6,7 @@ import { useRouter, usePathname } from 'next/navigation';
|
||||
import { MessageSquare, Settings, Home, Menu } from 'lucide-react';
|
||||
import {
|
||||
FE_PARENTS_HOME_URL,
|
||||
FE_PARENTS_MESSAGERIE_URL,
|
||||
FE_PARENTS_SETTINGS_URL,
|
||||
FE_PARENTS_MESSAGERIE_URL
|
||||
} from '@/utils/Url';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import { disconnect } from '@/app/actions/authAction';
|
||||
@ -41,13 +40,7 @@ export default function Layout({ children }) {
|
||||
name: 'Messagerie',
|
||||
url: FE_PARENTS_MESSAGERIE_URL,
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
name: 'Paramètres',
|
||||
url: FE_PARENTS_SETTINGS_URL,
|
||||
icon: Settings,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
// Déterminer la page actuelle pour la sidebar
|
||||
|
||||
@ -2,19 +2,31 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Table from '@/components/Table';
|
||||
import { Edit3, Users, Download, Eye, Upload } from 'lucide-react';
|
||||
import {
|
||||
Edit3,
|
||||
Users,
|
||||
Download,
|
||||
Eye,
|
||||
Upload,
|
||||
CalendarDays,
|
||||
} from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||
import {
|
||||
fetchChildren,
|
||||
editRegisterForm,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
||||
import EventCard from '@/components/EventCard';
|
||||
|
||||
export default function ParentHomePage() {
|
||||
const [children, setChildren] = useState([]);
|
||||
@ -22,6 +34,9 @@ export default function ParentHomePage() {
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
||||
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
|
||||
const [showPlanning, setShowPlanning] = useState(false);
|
||||
const [planningClassName, setPlanningClassName] = useState(null);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const [reloadFetch, setReloadFetch] = useState(false);
|
||||
@ -35,7 +50,20 @@ export default function ParentHomePage() {
|
||||
});
|
||||
setReloadFetch(false);
|
||||
}
|
||||
}, [selectedEstablishmentId, reloadFetch]);
|
||||
}, [selectedEstablishmentId, reloadFetch, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch des événements à venir
|
||||
fetchUpcomingEvents(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setUpcomingEvents(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching upcoming events:', error);
|
||||
});
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
function handleView(eleveId) {
|
||||
logger.debug(`View dossier for student id: ${eleveId}`);
|
||||
@ -97,6 +125,13 @@ export default function ParentHomePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const showClassPlanning = (student) => {
|
||||
setPlanningClassName(
|
||||
`${student.associated_class_name} - ${getNiveauLabel(student.level)}`
|
||||
);
|
||||
setShowPlanning(true);
|
||||
};
|
||||
|
||||
const childrenColumns = [
|
||||
{
|
||||
name: 'photo',
|
||||
@ -127,6 +162,12 @@ export default function ParentHomePage() {
|
||||
},
|
||||
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
|
||||
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
|
||||
{
|
||||
name: 'Classe',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.student.associated_class_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
@ -192,7 +233,6 @@ export default function ParentHomePage() {
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
{/* Nouvelle action Upload */}
|
||||
<button
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
uploadingStudentId === row.student.id && uploadState === 'on'
|
||||
@ -201,7 +241,7 @@ export default function ParentHomePage() {
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
|
||||
toggleUpload(row.student.id);
|
||||
}}
|
||||
aria-label="Uploader un fichier"
|
||||
>
|
||||
@ -211,6 +251,7 @@ export default function ParentHomePage() {
|
||||
)}
|
||||
|
||||
{row.status === 5 && (
|
||||
<>
|
||||
<button
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
@ -221,6 +262,17 @@ export default function ParentHomePage() {
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700 ml-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showClassPlanning(row.student);
|
||||
}}
|
||||
aria-label="Voir le planning de la classe"
|
||||
>
|
||||
<CalendarDays className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
@ -228,18 +280,54 @@ export default function ParentHomePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-2 py-4 md:px-4 max-w-full">
|
||||
<div className="w-full h-full">
|
||||
{showPlanning && planningClassName ? (
|
||||
// Affichage grand format mais respectant la sidebar
|
||||
<>
|
||||
<div className="p-4 flex items-center border-b">
|
||||
<button
|
||||
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
|
||||
onClick={() => setShowPlanning(false)}
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<PlanningProvider
|
||||
establishmentId={selectedEstablishmentId}
|
||||
modeSet={PlanningModes.CLASS_SCHEDULE}
|
||||
readOnly={true}
|
||||
>
|
||||
<ParentPlanningSection planningClassName={planningClassName} />
|
||||
</PlanningProvider>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Affichage classique avec le tableau des enfants
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-3 px-1 flex items-center gap-2">
|
||||
<Users className="h-6 w-6 text-emerald-600" />
|
||||
Enfants
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
data={children}
|
||||
columns={childrenColumns}
|
||||
defaultTheme="bg-gray-50"
|
||||
{/* Section des événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<SectionHeader
|
||||
icon={CalendarDays}
|
||||
title="Événements à venir"
|
||||
description="Prochains événements de l'établissement"
|
||||
/>
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100">
|
||||
{upcomingEvents.slice(0, 3).map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SectionHeader
|
||||
icon={Users}
|
||||
title="Vos enfants"
|
||||
description="Suivez le parcours de vos enfants"
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<Table data={children} columns={childrenColumns} />
|
||||
</div>
|
||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
||||
{uploadState === 'on' && uploadingStudentId && (
|
||||
@ -262,6 +350,7 @@ export default function ParentHomePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/InputText';
|
||||
import Button from '@/components/Form/Button';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@ import React, { useState } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Button from '@/components/Form/Button'; // Importez le composant Button
|
||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
|
||||
import { login } from '@/app/actions/authAction';
|
||||
@ -14,20 +14,18 @@ import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsr
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const [passwordFieldError, setPasswordFieldError] = useState('');
|
||||
const { initializeContextWithSession } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
function handleFormLogin(formData) {
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
|
||||
login({
|
||||
email: formData.get('login'),
|
||||
@ -37,7 +35,7 @@ export default function Page() {
|
||||
logger.debug('Sign In Result', result);
|
||||
|
||||
if (result.error) {
|
||||
setErrorMessage(result.error);
|
||||
showNotification(result.error, 'error', 'Erreur');
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
// On initialise le contexte establishement avec la session
|
||||
@ -48,7 +46,7 @@ export default function Page() {
|
||||
if (url) {
|
||||
router.push(url);
|
||||
} else {
|
||||
setErrorMessage('Type de rôle non géré');
|
||||
showNotification('Type de rôle non géré', 'error', 'Erreur');
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
@ -59,8 +57,10 @@ export default function Page() {
|
||||
error
|
||||
);
|
||||
setIsLoading(false);
|
||||
setErrorMessage(
|
||||
'Une erreur est survenue lors de la récupération de la session.'
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la récupération de la session.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -68,7 +68,11 @@ export default function Page() {
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la connexion:', error);
|
||||
setIsLoading(false);
|
||||
setErrorMessage('Une erreur est survenue lors de la connexion.');
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la connexion.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,7 +102,6 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -107,11 +110,9 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={passwordFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<div className="input-group mb-4"></div>
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<label>
|
||||
<a
|
||||
className="float-right mb-4"
|
||||
|
||||
@ -3,68 +3,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Form/Button';
|
||||
import { User } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { sendNewPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setUserFieldError('');
|
||||
setErrorMessage('');
|
||||
setPopupMessage('Mot de passe réinitialisé avec succès !');
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
}, 1000); // Simule un délai de traitement
|
||||
} else {
|
||||
const data = { email: formData.get('email') };
|
||||
setIsLoading(true);
|
||||
sendNewPassword(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setUserFieldError('');
|
||||
setErrorMessage('');
|
||||
if (data.errorMessage === '') {
|
||||
setPopupMessage(data.message);
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
if (data.message !== '') {
|
||||
showNotification(data.message, 'success', 'Succès');
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
if (data.errorFields) {
|
||||
setUserFieldError(data.errorFields.email);
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||
} else if (data.errorFields) {
|
||||
showNotification(data.errorFields.email, 'error', 'Erreur');
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
setIsLoading(false);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@ -89,10 +70,8 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Réinitialiser"
|
||||
@ -112,12 +91,6 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={popupConfirmAction}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,111 +5,63 @@ import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup';
|
||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Form/Button';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { KeySquare } from 'lucide-react';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { getResetPassword, resetPassword } from '@/app/actions/authAction';
|
||||
import { resetPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const uuid = searchParams.get('uuid');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [password1FieldError, setPassword1FieldError] = useState('');
|
||||
const [password2FieldError, setPassword2FieldError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
} else {
|
||||
getResetPassword(uuid)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setIsLoading(true);
|
||||
if (data.errorFields) {
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setPopupMessage('Mot de passe réinitialisé avec succès');
|
||||
setPopupVisible(true);
|
||||
}, 1000);
|
||||
} else {
|
||||
const data = {
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
};
|
||||
setIsLoading(true);
|
||||
resetPassword(uuid, data, csrfToken)
|
||||
.then((data) => {
|
||||
if (data.message !== '') {
|
||||
logger.debug('Success:', data);
|
||||
setPassword1FieldError('');
|
||||
setPassword2FieldError('');
|
||||
setErrorMessage('');
|
||||
if (data.errorMessage === '') {
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
showNotification(data.message, 'success', 'Succès');
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if (data.errorFields) {
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.password1 || data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
setIsOpen={setPopupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
popupClassName="w-full max-w-xs sm:max-w-md"
|
||||
/>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
@ -131,7 +83,6 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={password1FieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -140,10 +91,8 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
errorMsg={password2FieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
|
||||
@ -1,40 +1,29 @@
|
||||
'use client';
|
||||
// src/app/pages/subscribe.js
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Form/Button';
|
||||
import { User, KeySquare } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { subscribe } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const [password1FieldError, setPassword1FieldError] = useState('');
|
||||
const [password2FieldError, setPassword2FieldError] = useState('');
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const establishment_id = searchParams.get('establishment_id');
|
||||
|
||||
function isOK(data) {
|
||||
return data.errorMessage === '';
|
||||
}
|
||||
|
||||
function subscribeFormSubmit(formData) {
|
||||
const data = {
|
||||
email: formData.get('login'),
|
||||
@ -46,25 +35,23 @@ export default function Page() {
|
||||
subscribe(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setUserFieldError('');
|
||||
setPassword1FieldError('');
|
||||
setPassword2FieldError('');
|
||||
setErrorMessage('');
|
||||
if (isOK(data)) {
|
||||
setIsLoading(false);
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
if (data.message !== '') {
|
||||
showNotification(data.message, 'success', 'Succès');
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if (data.errorFields) {
|
||||
setUserFieldError(data.errorFields.email);
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.email ||
|
||||
data.errorFields.password1 ||
|
||||
data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
@ -75,7 +62,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@ -100,7 +87,6 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -109,7 +95,6 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={password1FieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -118,10 +103,8 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
errorMsg={password2FieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
@ -143,18 +126,6 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
setIsOpen={setPopupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
popupClassName="w-full max-w-xs sm:max-w-md"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -199,14 +199,3 @@ export const resetPassword = (uuid, data, csrfToken) => {
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const getResetPassword = (uuid) => {
|
||||
const url = `${BE_AUTH_RESET_PASSWORD_URL}/${uuid}`;
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
@ -3,11 +3,7 @@ import {
|
||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
|
||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL,
|
||||
FE_API_DOCUSEAL_CLONE_URL,
|
||||
FE_API_DOCUSEAL_DOWNLOAD_URL,
|
||||
FE_API_DOCUSEAL_GENERATE_TOKEN,
|
||||
FE_API_DOCUSEAL_DELETE_URL
|
||||
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
|
||||
} from '@/utils/Url';
|
||||
import { errorHandler, requestResponseHandler } from './actionsHandlers';
|
||||
|
||||
@ -47,11 +43,8 @@ export const fetchRegistrationFileFromGroup = async (groupId) => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileMasters = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -61,11 +54,8 @@ export const fetchRegistrationSchoolFileMasters = (id = null) => {
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationParentFileMasters = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationParentFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -75,11 +65,8 @@ export const fetchRegistrationParentFileMasters = (id = null) => {
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileTemplates = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -336,62 +323,3 @@ export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// API requests
|
||||
export const removeTemplate = (templateId, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_DELETE_URL}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
establishment_id :selectedEstablishmentId,
|
||||
apiDocuseal
|
||||
}),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const cloneTemplate = (templateId, email, is_required, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
email,
|
||||
is_required,
|
||||
establishment_id :selectedEstablishmentId,
|
||||
apiDocuseal
|
||||
}),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => {
|
||||
const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`;
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => {
|
||||
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }),
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
@ -9,10 +9,10 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import RecipientInput from '@/components/RecipientInput';
|
||||
import { useRouter } from 'next/navigation'; // Ajoute cette ligne
|
||||
import WisiwigTextArea from '@/components/WisiwigTextArea';
|
||||
import WisiwigTextArea from '@/components/Form/WisiwigTextArea';
|
||||
import logger from '@/utils/logger';
|
||||
import InputText from '@/components/InputText';
|
||||
import Button from '@/components/Button';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import Button from '@/components/Form/Button';
|
||||
|
||||
export default function EmailSender({ csrfToken }) {
|
||||
const [recipients, setRecipients] = useState([]);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) => {
|
||||
const {
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
@ -30,6 +30,8 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
setViewType,
|
||||
events,
|
||||
hiddenSchedules,
|
||||
planningMode,
|
||||
parentView
|
||||
} = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
@ -91,6 +93,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
@ -104,8 +107,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Menu déroulant pour le mois/année */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
@ -120,8 +121,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Menu de sélection du mois/année */}
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
@ -155,7 +154,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
@ -163,9 +161,11 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Numéro de semaine au centre */}
|
||||
{viewType === 'week' && (
|
||||
{/* Centre : numéro de semaine ou classe/niveau */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
@ -173,16 +173,27 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{parentView && (
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-base font-semibold">
|
||||
{/* À adapter selon les props disponibles */}
|
||||
{planningClassName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
)}
|
||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
|
||||
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const { currentDate } = usePlanning();
|
||||
const { currentDate, planningMode, parentView } = usePlanning();
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
||||
|
||||
@ -106,10 +107,14 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
key={event.id}
|
||||
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
||||
style={eventStyle}
|
||||
onClick={(e) => {
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div
|
||||
@ -198,11 +203,15 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
className={`h-20 relative border-b border-gray-100
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
|
||||
onClick={() => {
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: () => {
|
||||
const date = new Date(day);
|
||||
date.setHours(hour);
|
||||
onDateClick(date);
|
||||
}}
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="grid gap-1">
|
||||
{dayEvents
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
// Utilisation de couleurs hexadécimales pour le SVG
|
||||
const COLORS = [
|
||||
'fill-blue-400 text-blue-400',
|
||||
'fill-orange-400 text-orange-400',
|
||||
'fill-purple-400 text-purple-400',
|
||||
'fill-emerald-400 text-emerald-400',
|
||||
'#60a5fa', // bleu-400
|
||||
'#fb923c', // orange-400
|
||||
'#a78bfa', // violet-400
|
||||
'#34d399', // émeraude-400
|
||||
];
|
||||
|
||||
export default function PieChart({ data }) {
|
||||
@ -23,7 +24,20 @@ export default function PieChart({ data }) {
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<svg width={100} height={100} viewBox="0 0 32 32">
|
||||
{data.map((slice, idx) => {
|
||||
if (slice.value === 0) return null;
|
||||
const value = (slice.value / total) * 100;
|
||||
if (value === 100) {
|
||||
// Cas 100% : on dessine un cercle plein
|
||||
return (
|
||||
<circle
|
||||
key={idx}
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="16"
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const startAngle = (cumulative / 100) * 360;
|
||||
const endAngle = ((cumulative + value) / 100) * 360;
|
||||
const largeArc = value > 50 ? 1 : 0;
|
||||
@ -42,7 +56,7 @@ export default function PieChart({ data }) {
|
||||
<path
|
||||
key={idx}
|
||||
d={pathData}
|
||||
className={COLORS[idx % COLORS.length].split(' ')[0]}
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
stroke="#fff"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
@ -50,17 +64,21 @@ export default function PieChart({ data }) {
|
||||
})}
|
||||
</svg>
|
||||
<div className="ml-4 flex flex-col space-y-1">
|
||||
{data.map((slice, idx) => (
|
||||
{data.map((slice, idx) =>
|
||||
slice.value > 0 && (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center text-xs font-semibold ${COLORS[idx % COLORS.length].split(' ')[1]}`}
|
||||
className="flex items-center text-xs font-semibold"
|
||||
style={{ color: COLORS[idx % COLORS.length] }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block w-3 h-3 mr-2 rounded ${COLORS[idx % COLORS.length].split(' ')[0]}`}
|
||||
className="inline-block w-3 h-3 mr-2 rounded"
|
||||
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
|
||||
/>
|
||||
{slice.label} : {slice.value}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
131
Front-End/src/components/EventCard.js
Normal file
131
Front-End/src/components/EventCard.js
Normal file
@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { CalendarCheck } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Formate une date en format français avec jour de la semaine
|
||||
* @param {string} startDateString - Date de début au format ISO
|
||||
* @param {string} endDateString - Date de fin au format ISO (optionnel)
|
||||
* @returns {object} Objet contenant la date formatée et le jour
|
||||
*/
|
||||
const formatEventDate = (startDateString, endDateString) => {
|
||||
if (!startDateString)
|
||||
return { formattedDate: '', dayName: '', timeIndicator: '', timeRange: '' };
|
||||
|
||||
try {
|
||||
const startDate = new Date(startDateString);
|
||||
const endDate = endDateString ? new Date(endDateString) : null;
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const eventDate = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate()
|
||||
);
|
||||
|
||||
// Options pour le formatage de la date
|
||||
const dateOptions = {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const dayOptions = {
|
||||
weekday: 'long',
|
||||
};
|
||||
|
||||
const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
const formattedDate = startDate.toLocaleDateString('fr-FR', dateOptions);
|
||||
const dayName = startDate.toLocaleDateString('fr-FR', dayOptions);
|
||||
|
||||
// Formatage de l'heure
|
||||
let timeRange = '';
|
||||
if (endDate) {
|
||||
const startTime = startDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
const endTime = endDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
timeRange = `${startTime} - ${endTime}`;
|
||||
} else {
|
||||
timeRange = startDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
}
|
||||
|
||||
// Calcul des jours restants
|
||||
const diffTime = eventDate - today;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
let timeIndicator = '';
|
||||
if (diffDays === 0) {
|
||||
timeIndicator = "Aujourd'hui";
|
||||
} else if (diffDays === 1) {
|
||||
timeIndicator = 'Demain';
|
||||
} else if (diffDays > 0 && diffDays <= 7) {
|
||||
timeIndicator = `Dans ${diffDays} jours`;
|
||||
}
|
||||
|
||||
return {
|
||||
formattedDate,
|
||||
dayName: dayName.charAt(0).toUpperCase() + dayName.slice(1),
|
||||
timeIndicator,
|
||||
timeRange,
|
||||
};
|
||||
} catch (error) {
|
||||
// logger.error('Erreur lors du formatage de la date:', error);
|
||||
return {
|
||||
formattedDate: startDateString,
|
||||
dayName: '',
|
||||
timeIndicator: '',
|
||||
timeRange: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Composant EventCard pour afficher les événements à venir
|
||||
* @param {string} title - Titre de l'événement
|
||||
* @param {string} start - Date de début de l'événement (format ISO)
|
||||
* @param {string} end - Date de fin de l'événement (format ISO)
|
||||
* @param {string} date - Date de l'événement (pour compatibilité)
|
||||
* @param {string} description - Description de l'événement
|
||||
* @param {string} type - Type d'événement (optionnel)
|
||||
* @returns {JSX.Element} Carte d'événement
|
||||
*/
|
||||
const EventCard = ({ title, start, end, date, description, type }) => {
|
||||
// Utiliser start si disponible, sinon date pour compatibilité
|
||||
const eventDate = start || date;
|
||||
const { formattedDate, dayName, timeIndicator, timeRange } = formatEventDate(
|
||||
eventDate,
|
||||
end
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 mb-3 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<CalendarCheck className="text-emerald-500" size={20} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900 mb-1">{title}</h4>
|
||||
<div className="flex flex-col text-sm text-gray-500">
|
||||
<span className="font-medium text-emerald-600">{dayName}</span>
|
||||
<span>{formattedDate}</span>
|
||||
{timeRange && (
|
||||
<span className="text-xs text-gray-600 mt-1">{timeRange}</span>
|
||||
)}
|
||||
{timeIndicator && (
|
||||
<span className="text-xs text-blue-600 mt-1 font-medium">
|
||||
{timeIndicator}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm mt-2 text-gray-700">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventCard;
|
||||
@ -33,12 +33,19 @@ export default function FlashNotification({
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, [message, type, errorCode, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type !== 'error') {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false); // Déclenche la disparition
|
||||
setTimeout(onClose, 300); // Appelle onClose après l'animation
|
||||
}, displayPeriod); // Notification visible pendant 3 secondes par défaut
|
||||
}, displayPeriod);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose, displayPeriod]);
|
||||
}
|
||||
// Pour les erreurs, pas de timeout : la notification reste affichée
|
||||
}, [onClose, displayPeriod, type]);
|
||||
|
||||
if (!message || !isVisible) return null;
|
||||
|
||||
|
||||
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
@ -0,0 +1,589 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import Button from './Button';
|
||||
import IconSelector from './IconSelector';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { FIELD_TYPES } from './FormTypes';
|
||||
|
||||
export default function AddFieldModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
editingField = null,
|
||||
editingIndex = -1,
|
||||
}) {
|
||||
const isEditing = editingIndex >= 0;
|
||||
|
||||
const [currentField, setCurrentField] = useState({
|
||||
id: '',
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
icon: '',
|
||||
options: [],
|
||||
text: '',
|
||||
placeholder: '',
|
||||
acceptTypes: '',
|
||||
maxSize: 5, // 5MB par défaut
|
||||
checked: false,
|
||||
validation: {
|
||||
pattern: '',
|
||||
minLength: '',
|
||||
maxLength: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||
const [newOption, setNewOption] = useState('');
|
||||
|
||||
const { control, handleSubmit, reset, setValue } = useForm();
|
||||
|
||||
// Mettre à jour l'état et les valeurs du formulaire lorsque editingField change
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const defaultValues = editingField || {
|
||||
id: '',
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
icon: '',
|
||||
options: [],
|
||||
text: '',
|
||||
placeholder: '',
|
||||
acceptTypes: '',
|
||||
maxSize: 5,
|
||||
checked: false,
|
||||
validation: {
|
||||
pattern: '',
|
||||
minLength: '',
|
||||
maxLength: '',
|
||||
},
|
||||
};
|
||||
|
||||
setCurrentField(defaultValues);
|
||||
|
||||
// Réinitialiser le formulaire avec les valeurs de l'élément à éditer
|
||||
reset({
|
||||
type: defaultValues.type,
|
||||
label: defaultValues.label,
|
||||
placeholder: defaultValues.placeholder,
|
||||
required: defaultValues.required,
|
||||
icon: defaultValues.icon,
|
||||
text: defaultValues.text,
|
||||
acceptTypes: defaultValues.acceptTypes,
|
||||
maxSize: defaultValues.maxSize,
|
||||
checked: defaultValues.checked,
|
||||
validation: defaultValues.validation,
|
||||
});
|
||||
}
|
||||
}, [isOpen, editingField, reset]);
|
||||
|
||||
// Ajouter une option au select
|
||||
const addOption = () => {
|
||||
if (newOption.trim()) {
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
options: [...currentField.options, newOption.trim()],
|
||||
});
|
||||
setNewOption('');
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer une option du select
|
||||
const removeOption = (index) => {
|
||||
const newOptions = currentField.options.filter((_, i) => i !== index);
|
||||
setCurrentField({ ...currentField, options: newOptions });
|
||||
};
|
||||
|
||||
// Sélectionner une icône
|
||||
const selectIcon = (iconName) => {
|
||||
setCurrentField({ ...currentField, icon: iconName });
|
||||
|
||||
// Mettre à jour la valeur dans le formulaire
|
||||
const iconField = control._fields.icon;
|
||||
if (iconField && iconField.onChange) {
|
||||
iconField.onChange(iconName);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldSubmit = (data) => {
|
||||
onSubmit(data, currentField, editingIndex);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-semibold">
|
||||
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(handleFieldSubmit)} className="space-y-4">
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
defaultValue={currentField.type}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SelectChoice
|
||||
label="Type de champ"
|
||||
name="type"
|
||||
selected={value}
|
||||
callback={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
type: e.target.value,
|
||||
});
|
||||
}}
|
||||
choices={FIELD_TYPES}
|
||||
placeHolder="Sélectionner un type"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{![
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
].includes(currentField.type) && (
|
||||
<>
|
||||
<Controller
|
||||
name="label"
|
||||
control={control}
|
||||
defaultValue={currentField.label}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Label du champ"
|
||||
name="label"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
label: e.target.value,
|
||||
});
|
||||
}}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="placeholder"
|
||||
control={control}
|
||||
defaultValue={currentField.placeholder}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Placeholder (optionnel)"
|
||||
name="placeholder"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
placeholder: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name="required"
|
||||
control={control}
|
||||
defaultValue={currentField.required}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id="required"
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.checked);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
required: e.target.checked,
|
||||
});
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label htmlFor="required">Champ obligatoire</label>
|
||||
</div>
|
||||
|
||||
{(currentField.type === 'text' ||
|
||||
currentField.type === 'email' ||
|
||||
currentField.type === 'date') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Icône (optionnel)
|
||||
</label>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
defaultValue={currentField.icon}
|
||||
render={({ field: { onChange } }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 flex items-center gap-2 p-3 border border-gray-300 rounded-md bg-gray-50">
|
||||
{currentField.icon &&
|
||||
LucideIcons[currentField.icon] ? (
|
||||
<>
|
||||
{React.createElement(
|
||||
LucideIcons[currentField.icon],
|
||||
{
|
||||
size: 20,
|
||||
className: 'text-gray-600',
|
||||
}
|
||||
)}
|
||||
<span className="text-sm text-gray-700">
|
||||
{currentField.icon}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">
|
||||
Aucune icône sélectionnée
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
text="Choisir"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowIconPicker(true);
|
||||
}}
|
||||
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||
/>
|
||||
{currentField.icon && (
|
||||
<Button
|
||||
type="button"
|
||||
text="✕"
|
||||
onClick={() => {
|
||||
onChange('');
|
||||
setCurrentField({ ...currentField, icon: '' });
|
||||
}}
|
||||
className="px-2 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{[
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
].includes(currentField.type) && (
|
||||
<Controller
|
||||
name="text"
|
||||
control={control}
|
||||
defaultValue={currentField.text}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{currentField.type === 'paragraph'
|
||||
? 'Texte du paragraphe'
|
||||
: 'Texte du titre'}
|
||||
</label>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
text: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentField.type === 'select' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Options de la liste
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOption}
|
||||
onChange={(e) => setNewOption(e.target.value)}
|
||||
placeholder="Nouvelle option"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onKeyPress={(e) =>
|
||||
e.key === 'Enter' && (e.preventDefault(), addOption())
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text="Ajouter"
|
||||
onClick={addOption}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{currentField.options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<span>{option}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentField.type === 'radio' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Options des boutons radio
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOption}
|
||||
onChange={(e) => setNewOption(e.target.value)}
|
||||
placeholder="Nouvelle option"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onKeyPress={(e) =>
|
||||
e.key === 'Enter' && (e.preventDefault(), addOption())
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text="Ajouter"
|
||||
onClick={addOption}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{currentField.options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<span>{option}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentField.type === 'phone' && (
|
||||
<Controller
|
||||
name="validation.pattern"
|
||||
control={control}
|
||||
defaultValue={currentField.validation?.pattern || ''}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Format de téléphone (optionnel, exemple: ^\\+?[0-9]{10,15}$)"
|
||||
name="phonePattern"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
validation: {
|
||||
...currentField.validation,
|
||||
pattern: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentField.type === 'file' && (
|
||||
<>
|
||||
<Controller
|
||||
name="acceptTypes"
|
||||
control={control}
|
||||
defaultValue={currentField.acceptTypes || ''}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Types de fichiers acceptés (ex: .pdf,.jpg,.png)"
|
||||
name="acceptTypes"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
acceptTypes: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="maxSize"
|
||||
control={control}
|
||||
defaultValue={currentField.maxSize || 5}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Taille maximale (MB)"
|
||||
name="maxSize"
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
maxSize: parseInt(e.target.value) || 5,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentField.type === 'checkbox' && (
|
||||
<>
|
||||
<div className="flex items-center mt-2">
|
||||
<Controller
|
||||
name="checked"
|
||||
control={control}
|
||||
defaultValue={currentField.checked || false}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id="defaultChecked"
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.checked);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
checked: e.target.checked,
|
||||
});
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label htmlFor="defaultChecked">Coché par défaut</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Controller
|
||||
name="horizontal"
|
||||
control={control}
|
||||
defaultValue={currentField.horizontal || false}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id="horizontal"
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.checked);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
horizontal: e.target.checked,
|
||||
});
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label htmlFor="horizontal">Label au-dessus (horizontal)</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentField.type === 'toggle' && (
|
||||
<div className="flex items-center mt-2">
|
||||
<Controller
|
||||
name="checked"
|
||||
control={control}
|
||||
defaultValue={currentField.checked || false}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id="defaultToggled"
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.checked);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
checked: e.target.checked,
|
||||
});
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label htmlFor="defaultToggled">Activé par défaut</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
text={isEditing ? 'Modifier' : 'Ajouter'}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text="Annuler"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Sélecteur d'icônes - déplacé en dehors du formulaire */}
|
||||
<IconSelector
|
||||
isOpen={showIconPicker}
|
||||
onClose={() => setShowIconPicker(false)}
|
||||
onSelect={selectIcon}
|
||||
selectedIcon={currentField.icon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -7,7 +7,7 @@ const CheckBox = ({
|
||||
handleChange,
|
||||
fieldName,
|
||||
itemLabelFunc = () => null,
|
||||
horizontal,
|
||||
horizontal = false,
|
||||
}) => {
|
||||
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
|
||||
const isChecked = Array.isArray(formData[fieldName])
|
||||
@ -22,7 +22,7 @@ const CheckBox = ({
|
||||
{horizontal && (
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
||||
className="block text-sm text-center mb-1 font-medium text-gray-700 cursor-pointer"
|
||||
>
|
||||
{itemLabelFunc(item)}
|
||||
</label>
|
||||
@ -40,7 +40,7 @@ const CheckBox = ({
|
||||
{!horizontal && (
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
className="block text-sm text-center mb-1 font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-gray-700 cursor-pointer"
|
||||
>
|
||||
{itemLabelFunc(item)}
|
||||
</label>
|
||||
443
Front-End/src/components/Form/FormRenderer.js
Normal file
443
Front-End/src/components/Form/FormRenderer.js
Normal file
@ -0,0 +1,443 @@
|
||||
import logger from '@/utils/logger';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import Button from './Button';
|
||||
import DjangoCSRFToken from '../DjangoCSRFToken';
|
||||
import WisiwigTextArea from './WisiwigTextArea';
|
||||
import RadioList from './RadioList';
|
||||
import CheckBox from './CheckBox';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import InputPhone from './InputPhone';
|
||||
import FileUpload from './FileUpload';
|
||||
|
||||
/*
|
||||
* Récupère une icône Lucide par son nom.
|
||||
*/
|
||||
export function getIcon(name) {
|
||||
if (Object.keys(LucideIcons).includes(name)) {
|
||||
const Icon = LucideIcons[name];
|
||||
return Icon ?? null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const formConfigTest = {
|
||||
id: 0,
|
||||
title: 'Mon formulaire dynamique',
|
||||
submitLabel: 'Envoyer',
|
||||
fields: [
|
||||
{ id: 'name', label: 'Nom', type: 'text', required: true },
|
||||
{ id: 'email', label: 'Email', type: 'email' },
|
||||
{
|
||||
id: 'email2',
|
||||
label: 'Email',
|
||||
type: 'text',
|
||||
icon: 'Mail',
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
label: 'Rôle',
|
||||
type: 'select',
|
||||
options: ['Admin', 'Utilisateur', 'Invité'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: "Bonjour, Bienvenue dans ce formulaire d'inscription haha",
|
||||
},
|
||||
{
|
||||
id: 'birthdate',
|
||||
label: 'Date de naissance',
|
||||
type: 'date',
|
||||
icon: 'Calendar',
|
||||
},
|
||||
{
|
||||
id: 'textarea',
|
||||
label: 'toto',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function FormRenderer({
|
||||
formConfig = formConfigTest,
|
||||
csrfToken,
|
||||
onFormSubmit = (data) => {
|
||||
alert(JSON.stringify(data, null, 2));
|
||||
}, // Callback de soumission personnalisé (optionnel)
|
||||
}) {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm();
|
||||
|
||||
// Fonction utilitaire pour envoyer les données au backend
|
||||
const sendFormDataToBackend = async (formData) => {
|
||||
try {
|
||||
// Cette fonction peut être remplacée par votre propre implémentation
|
||||
// Exemple avec fetch:
|
||||
const response = await fetch('/api/submit-form', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// Les en-têtes sont automatiquement définis pour FormData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
logger.debug('Envoi réussi:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error("Erreur lors de l'envoi:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
logger.debug('=== DÉBUT onSubmit ===');
|
||||
logger.debug('Réponses :', data);
|
||||
|
||||
try {
|
||||
// Vérifier si nous avons des fichiers dans les données
|
||||
const hasFiles = Object.keys(data).some((key) => {
|
||||
return (
|
||||
data[key] instanceof FileList ||
|
||||
(data[key] && data[key][0] instanceof File)
|
||||
);
|
||||
});
|
||||
|
||||
if (hasFiles) {
|
||||
// Utiliser FormData pour l'envoi de fichiers
|
||||
const formData = new FormData();
|
||||
|
||||
// Ajouter l'ID du formulaire
|
||||
formData.append('formId', formConfig.id.toString());
|
||||
|
||||
// Traiter chaque champ et ses valeurs
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = data[key];
|
||||
|
||||
if (
|
||||
value instanceof FileList ||
|
||||
(value && value[0] instanceof File)
|
||||
) {
|
||||
// Gérer les champs de type fichier
|
||||
if (value.length > 0) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
formData.append(`files.${key}`, value[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Gérer les autres types de champs
|
||||
formData.append(
|
||||
`data.${key}`,
|
||||
value !== undefined ? value.toString() : ''
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (onFormSubmit) {
|
||||
// Utiliser le callback personnalisé si fourni
|
||||
await onFormSubmit(formData, true);
|
||||
} else {
|
||||
// Sinon, utiliser la fonction par défaut
|
||||
await sendFormDataToBackend(formData);
|
||||
alert('Formulaire avec fichier(s) envoyé avec succès');
|
||||
}
|
||||
} else {
|
||||
// Pas de fichier, on peut utiliser JSON
|
||||
const formattedData = {
|
||||
formId: formConfig.id,
|
||||
responses: { ...data },
|
||||
};
|
||||
|
||||
if (onFormSubmit) {
|
||||
// Utiliser le callback personnalisé si fourni
|
||||
await onFormSubmit(formattedData, false);
|
||||
} else {
|
||||
// Afficher un message pour démonstration
|
||||
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
reset(); // Réinitialiser le formulaire après soumission
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la soumission du formulaire:', error);
|
||||
alert(`Erreur lors de l'envoi du formulaire: ${error.message}`);
|
||||
}
|
||||
|
||||
logger.debug('=== FIN onSubmit ===');
|
||||
};
|
||||
|
||||
const onError = (errors) => {
|
||||
logger.error('=== ERREURS DE VALIDATION ===');
|
||||
logger.error('Erreurs :', errors);
|
||||
alert('Erreurs de validation : ' + JSON.stringify(errors, null, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
className="max-w-md mx-auto"
|
||||
>
|
||||
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
||||
<h2 className="text-2xl font-bold text-center mb-4">
|
||||
{formConfig.title}
|
||||
</h2>
|
||||
|
||||
{formConfig.fields.map((field) => (
|
||||
<div
|
||||
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
||||
className="flex flex-col mt-4"
|
||||
>
|
||||
{field.type === 'heading1' && (
|
||||
<h1 className="text-3xl font-bold mb-3">{field.text}</h1>
|
||||
)}
|
||||
{field.type === 'heading2' && (
|
||||
<h2 className="text-2xl font-bold mb-3">{field.text}</h2>
|
||||
)}
|
||||
{field.type === 'heading3' && (
|
||||
<h3 className="text-xl font-bold mb-2">{field.text}</h3>
|
||||
)}
|
||||
{field.type === 'heading4' && (
|
||||
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
|
||||
)}
|
||||
{field.type === 'heading5' && (
|
||||
<h5 className="text-base font-bold mb-1">{field.text}</h5>
|
||||
)}
|
||||
{field.type === 'heading6' && (
|
||||
<h6 className="text-sm font-bold mb-1">{field.text}</h6>
|
||||
)}
|
||||
|
||||
{field.type === 'paragraph' && <p className="mb-4">{field.text}</p>}
|
||||
|
||||
{(field.type === 'text' ||
|
||||
field.type === 'email' ||
|
||||
field.type === 'date') && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{
|
||||
required: field.required,
|
||||
pattern: field.validation?.pattern
|
||||
? new RegExp(field.validation.pattern)
|
||||
: undefined,
|
||||
minLength: field.validation?.minLength,
|
||||
maxLength: field.validation?.maxLength,
|
||||
}}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<InputTextIcon
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
IconItem={field.icon ? getIcon(field.icon) : null}
|
||||
type={field.type}
|
||||
name={name}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'phone' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<InputPhone
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
name={name}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'select' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<SelectChoice
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
name={name}
|
||||
selected={value || ''}
|
||||
callback={onChange}
|
||||
choices={field.options.map((e) => ({ label: e, value: e }))}
|
||||
placeHolder={`Sélectionner ${field.label.toLowerCase()}`}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'radio' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<RadioList
|
||||
items={field.options.map((option, idx) => ({
|
||||
id: idx,
|
||||
label: option,
|
||||
}))}
|
||||
formData={{
|
||||
[field.id]: value
|
||||
? field.options.findIndex((o) => o === value)
|
||||
: '',
|
||||
}}
|
||||
handleChange={(e) =>
|
||||
onChange(field.options[parseInt(e.target.value)])
|
||||
}
|
||||
fieldName={field.id}
|
||||
sectionLabel={field.label}
|
||||
required={field.required}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'checkbox' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
defaultValue={field.checked || false}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<div>
|
||||
<CheckBox
|
||||
item={{ id: field.id, label: field.label }}
|
||||
formData={{ [field.id]: value || false }}
|
||||
handleChange={(e) => onChange(e.target.checked)}
|
||||
fieldName={field.id}
|
||||
itemLabelFunc={(item) => item.label}
|
||||
horizontal={field.horizontal || false}
|
||||
/>
|
||||
{errors[field.id] && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'toggle' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
defaultValue={field.checked || false}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<div>
|
||||
<ToggleSwitch
|
||||
name={field.id}
|
||||
label={field.label + (field.required ? ' *' : '')}
|
||||
checked={value || false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
{errors[field.id] && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'file' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FileUpload
|
||||
selectionMessage={field.label}
|
||||
required={field.required}
|
||||
uploadedFileName={value ? value[0]?.name : null}
|
||||
onFileSelect={(file) => {
|
||||
// Créer un objet de type FileList similaire pour la compatibilité
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
onChange(dataTransfer.files);
|
||||
}}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'textarea' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<WisiwigTextArea
|
||||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
required={field.required}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text={formConfig.submitLabel ? formConfig.submitLabel : 'Envoyer'}
|
||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
@ -0,0 +1,616 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import FormRenderer from './FormRenderer';
|
||||
import AddFieldModal from './AddFieldModal';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import {
|
||||
Edit2,
|
||||
Trash2,
|
||||
PlusCircle,
|
||||
Download,
|
||||
Upload,
|
||||
GripVertical,
|
||||
TextCursorInput,
|
||||
AtSign,
|
||||
Calendar,
|
||||
ChevronDown,
|
||||
Type,
|
||||
AlignLeft,
|
||||
Save,
|
||||
ChevronUp,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
Code,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Phone,
|
||||
Radio,
|
||||
ToggleLeft,
|
||||
CheckSquare,
|
||||
FileUp,
|
||||
} from 'lucide-react';
|
||||
|
||||
const FIELD_TYPES_ICON = {
|
||||
text: { icon: TextCursorInput },
|
||||
email: { icon: AtSign },
|
||||
phone: { icon: Phone },
|
||||
date: { icon: Calendar },
|
||||
select: { icon: ChevronDown },
|
||||
radio: { icon: Radio },
|
||||
checkbox: { icon: CheckSquare },
|
||||
toggle: { icon: ToggleLeft },
|
||||
file: { icon: FileUp },
|
||||
textarea: { icon: Type },
|
||||
paragraph: { icon: AlignLeft },
|
||||
heading1: { icon: Heading1 },
|
||||
heading2: { icon: Heading2 },
|
||||
heading3: { icon: Heading3 },
|
||||
heading4: { icon: Heading4 },
|
||||
heading5: { icon: Heading5 },
|
||||
heading6: { icon: Heading6 },
|
||||
};
|
||||
|
||||
// Type d'item pour le drag and drop
|
||||
const ItemTypes = {
|
||||
FIELD: 'field',
|
||||
};
|
||||
|
||||
// Composant pour un champ draggable
|
||||
const DraggableFieldItem = ({
|
||||
field,
|
||||
index,
|
||||
moveField,
|
||||
editField,
|
||||
deleteField,
|
||||
}) => {
|
||||
const ref = React.useRef(null);
|
||||
|
||||
// Configuration du drag (ce qu'on peut déplacer)
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: ItemTypes.FIELD,
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Configuration du drop (où on peut déposer)
|
||||
const [, drop] = useDrop({
|
||||
accept: ItemTypes.FIELD,
|
||||
hover: (item, monitor) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
|
||||
// Ne rien faire si on survole le même élément
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Déterminer la position de la souris par rapport à l'élément survolé
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleY =
|
||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// Ne pas remplacer si on n'a pas dépassé la moitié de l'élément
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Effectuer le déplacement
|
||||
moveField(dragIndex, hoverIndex);
|
||||
|
||||
// Mettre à jour l'index de l'élément déplacé
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
// Combiner drag et drop sur le même élément de référence
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex items-center justify-between bg-gray-50 p-3 rounded border border-gray-200 ${
|
||||
isDragging ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="cursor-move text-gray-400 hover:text-gray-600">
|
||||
<GripVertical size={18} />
|
||||
</div>
|
||||
{FIELD_TYPES_ICON[field.type] &&
|
||||
React.createElement(FIELD_TYPES_ICON[field.type].icon, {
|
||||
size: 18,
|
||||
className: 'text-gray-600',
|
||||
})}
|
||||
<span className="font-medium">
|
||||
{field.type === 'paragraph'
|
||||
? 'Paragraphe'
|
||||
: field.type.startsWith('heading')
|
||||
? `Titre ${field.type.replace('heading', '')}`
|
||||
: field.label}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
({field.type}){field.required && ' *'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => editField(index)}
|
||||
className="p-1 text-blue-500 hover:text-blue-700"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteField(index)}
|
||||
className="p-1 text-red-500 hover:text-red-700"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function FormTemplateBuilder() {
|
||||
const [formConfig, setFormConfig] = useState({
|
||||
id: 0,
|
||||
title: 'Nouveau formulaire',
|
||||
submitLabel: 'Envoyer',
|
||||
fields: [],
|
||||
});
|
||||
|
||||
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
|
||||
const [editingIndex, setEditingIndex] = useState(-1);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveMessage, setSaveMessage] = useState({ type: '', text: '' });
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const [showJsonSection, setShowJsonSection] = useState(false);
|
||||
|
||||
const { reset: resetField } = useForm();
|
||||
|
||||
// Gérer l'affichage du bouton de défilement
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
// Afficher le bouton quand on descend d'au moins 300px
|
||||
setShowScrollButton(window.scrollY > 300);
|
||||
};
|
||||
|
||||
// Ajouter l'écouteur d'événement
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Nettoyage de l'écouteur lors du démontage du composant
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fonction pour remonter en haut de la page
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
// Générer un ID unique pour les champs
|
||||
const generateFieldId = (label) => {
|
||||
return label
|
||||
.toLowerCase()
|
||||
.replace(/[àáâãäå]/g, 'a')
|
||||
.replace(/[èéêë]/g, 'e')
|
||||
.replace(/[ìíîï]/g, 'i')
|
||||
.replace(/[òóôõö]/g, 'o')
|
||||
.replace(/[ùúûü]/g, 'u')
|
||||
.replace(/[ç]/g, 'c')
|
||||
.replace(/[^a-z0-9]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
};
|
||||
|
||||
// Ajouter ou modifier un champ
|
||||
const handleFieldSubmit = (data, currentField, editIndex) => {
|
||||
const isHeadingType = data.type.startsWith('heading');
|
||||
const isContentTypeOnly = data.type === 'paragraph' || isHeadingType;
|
||||
|
||||
const fieldData = {
|
||||
...data,
|
||||
id: isContentTypeOnly
|
||||
? undefined
|
||||
: generateFieldId(data.label || 'field'),
|
||||
options: ['select', 'radio'].includes(data.type)
|
||||
? currentField.options
|
||||
: undefined,
|
||||
icon: data.icon || currentField.icon || undefined,
|
||||
placeholder: data.placeholder || undefined,
|
||||
text: isContentTypeOnly ? data.text : undefined,
|
||||
checked: ['checkbox', 'toggle'].includes(data.type)
|
||||
? currentField.checked
|
||||
: undefined,
|
||||
horizontal:
|
||||
data.type === 'checkbox' ? currentField.horizontal : undefined,
|
||||
acceptTypes: data.type === 'file' ? currentField.acceptTypes : undefined,
|
||||
maxSize: data.type === 'file' ? currentField.maxSize : undefined,
|
||||
validation: ['phone', 'email', 'text'].includes(data.type)
|
||||
? currentField.validation
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Nettoyer les propriétés undefined
|
||||
Object.keys(fieldData).forEach((key) => {
|
||||
if (fieldData[key] === undefined || fieldData[key] === '') {
|
||||
delete fieldData[key];
|
||||
}
|
||||
});
|
||||
|
||||
const newFields = [...formConfig.fields];
|
||||
|
||||
if (editIndex >= 0) {
|
||||
newFields[editIndex] = fieldData;
|
||||
} else {
|
||||
newFields.push(fieldData);
|
||||
}
|
||||
|
||||
setFormConfig({ ...formConfig, fields: newFields });
|
||||
setEditingIndex(-1);
|
||||
}; // Modifier un champ existant
|
||||
const editField = (index) => {
|
||||
setEditingIndex(index);
|
||||
setShowAddFieldModal(true);
|
||||
};
|
||||
|
||||
// Supprimer un champ
|
||||
const deleteField = (index) => {
|
||||
const newFields = formConfig.fields.filter((_, i) => i !== index);
|
||||
setFormConfig({ ...formConfig, fields: newFields });
|
||||
};
|
||||
|
||||
// Déplacer un champ
|
||||
const moveField = (dragIndex, hoverIndex) => {
|
||||
const newFields = [...formConfig.fields];
|
||||
const draggedField = newFields[dragIndex];
|
||||
|
||||
// Supprimer l'élément déplacé
|
||||
newFields.splice(dragIndex, 1);
|
||||
|
||||
// Insérer l'élément à sa nouvelle position
|
||||
newFields.splice(hoverIndex, 0, draggedField);
|
||||
|
||||
setFormConfig({ ...formConfig, fields: newFields });
|
||||
};
|
||||
|
||||
// Exporter le JSON
|
||||
const exportJson = () => {
|
||||
const jsonString = JSON.stringify(formConfig, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `formulaire_${formConfig.title.replace(/\s+/g, '_').toLowerCase()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Importer un JSON
|
||||
const importJson = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const imported = JSON.parse(e.target.result);
|
||||
setFormConfig(imported);
|
||||
} catch (error) {
|
||||
alert('Erreur lors de l'importation du fichier JSON');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Sauvegarder le formulaire (pour le backend)
|
||||
const saveFormTemplate = async () => {
|
||||
// Validation basique
|
||||
if (!formConfig.title.trim()) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: 'Le titre du formulaire est requis',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formConfig.fields.length === 0) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: 'Ajoutez au moins un champ au formulaire',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setSaveMessage({ type: '', text: '' });
|
||||
|
||||
try {
|
||||
// Simulation d'envoi au backend (à remplacer par l'appel API réel)
|
||||
// const response = await fetch('/api/form-templates', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify(formConfig),
|
||||
// });
|
||||
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Erreur lors de l\'enregistrement du formulaire');
|
||||
// }
|
||||
|
||||
// const data = await response.json();
|
||||
|
||||
// Simulation d'une réponse du backend
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
setSaveMessage({
|
||||
type: 'success',
|
||||
text: 'Formulaire enregistré avec succès',
|
||||
});
|
||||
|
||||
// Si le backend renvoie un ID, on peut mettre à jour l'ID du formulaire
|
||||
// setFormConfig({ ...formConfig, id: data.id });
|
||||
} catch (error) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text:
|
||||
error.message || "Une erreur est survenue lors de l'enregistrement",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{/* Panel de configuration */}
|
||||
<div
|
||||
className={
|
||||
showJsonSection
|
||||
? 'lg:col-span-3 space-y-6'
|
||||
: 'lg:col-span-5 space-y-6'
|
||||
}
|
||||
>
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-bold mb-6">
|
||||
Configuration du formulaire
|
||||
</h2>
|
||||
|
||||
{/* Configuration générale */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold mr-4">
|
||||
Paramètres généraux
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowJsonSection(!showJsonSection)}
|
||||
className="px-4 py-2 rounded-md inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-600 text-white"
|
||||
title={
|
||||
showJsonSection ? 'Masquer le JSON' : 'Afficher le JSON'
|
||||
}
|
||||
>
|
||||
{showJsonSection ? (
|
||||
<EyeOff size={18} />
|
||||
) : (
|
||||
<Eye size={18} />
|
||||
)}
|
||||
<span>
|
||||
{showJsonSection ? 'Masquer JSON' : 'Afficher JSON'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={saveFormTemplate}
|
||||
disabled={saving}
|
||||
className={`px-4 py-2 rounded-md inline-flex items-center gap-2 ${
|
||||
saving
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
}`}
|
||||
title="Enregistrer le formulaire"
|
||||
>
|
||||
<Save size={18} />
|
||||
<span>
|
||||
{saving ? 'Enregistrement...' : 'Enregistrer'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{saveMessage.text && (
|
||||
<div
|
||||
className={`p-3 rounded ${
|
||||
saveMessage.type === 'error'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}
|
||||
>
|
||||
{saveMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InputTextIcon
|
||||
label="Titre du formulaire"
|
||||
name="title"
|
||||
value={formConfig.title}
|
||||
onChange={(e) =>
|
||||
setFormConfig({ ...formConfig, title: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<InputTextIcon
|
||||
label="Texte du bouton de soumission du formulaire"
|
||||
name="submitLabel"
|
||||
value={formConfig.submitLabel}
|
||||
onChange={(e) =>
|
||||
setFormConfig({
|
||||
...formConfig,
|
||||
submitLabel: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Liste des champs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold mr-4">
|
||||
Champs du formulaire ({formConfig.fields.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingIndex(-1);
|
||||
setShowAddFieldModal(true);
|
||||
}}
|
||||
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
|
||||
title="Ajouter un champ"
|
||||
>
|
||||
<PlusCircle size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{formConfig.fields.length === 0 ? (
|
||||
<div className="text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
||||
<p className="text-gray-500 italic mb-4">
|
||||
Aucun champ ajouté
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingIndex(-1);
|
||||
setShowAddFieldModal(true);
|
||||
}}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
|
||||
>
|
||||
<PlusCircle size={18} />
|
||||
<span>Ajouter mon premier champ</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{formConfig.fields.map((field, index) => (
|
||||
<DraggableFieldItem
|
||||
key={index}
|
||||
field={field}
|
||||
index={index}
|
||||
moveField={moveField}
|
||||
editField={editField}
|
||||
deleteField={deleteField}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-6">
|
||||
{/* Les actions ont été déplacées dans la section JSON généré */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON généré */}
|
||||
{showJsonSection && (
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white p-6 rounded-lg shadow h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold mr-4">JSON généré</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={exportJson}
|
||||
className="p-2 bg-purple-500 text-white rounded-md hover:bg-purple-600 transition-colors"
|
||||
title="Exporter JSON"
|
||||
>
|
||||
<Download size={18} />
|
||||
</button>
|
||||
<label
|
||||
className="p-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 cursor-pointer transition-colors"
|
||||
title="Importer JSON"
|
||||
>
|
||||
<Upload size={18} />
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={importJson}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">
|
||||
{JSON.stringify(formConfig, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Aperçu */}
|
||||
<div className="mt-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h3 className="text-lg font-semibold mb-4">Aperçu du formulaire</h3>
|
||||
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
|
||||
{formConfig.fields.length > 0 ? (
|
||||
<FormRenderer formConfig={formConfig} />
|
||||
) : (
|
||||
<p className="text-gray-500 italic text-center">
|
||||
Ajoutez des champs pour voir l'aperçu
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal d'ajout/modification de champ */}
|
||||
<AddFieldModal
|
||||
isOpen={showAddFieldModal}
|
||||
onClose={() => setShowAddFieldModal(false)}
|
||||
onSubmit={handleFieldSubmit}
|
||||
editingField={
|
||||
editingIndex >= 0 ? formConfig.fields[editingIndex] : null
|
||||
}
|
||||
editingIndex={editingIndex}
|
||||
/>
|
||||
|
||||
{/* Bouton flottant pour remonter en haut */}
|
||||
{showScrollButton && (
|
||||
<div className="fixed bottom-6 right-6 z-10">
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className="p-4 rounded-full shadow-lg flex items-center justify-center bg-gray-500 hover:bg-gray-600 text-white transition-all duration-300"
|
||||
title="Remonter en haut de la page"
|
||||
>
|
||||
<ChevronUp size={24} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
19
Front-End/src/components/Form/FormTypes.js
Normal file
19
Front-End/src/components/Form/FormTypes.js
Normal file
@ -0,0 +1,19 @@
|
||||
export const FIELD_TYPES = [
|
||||
{ value: 'text', label: 'Texte' },
|
||||
{ value: 'email', label: 'Email' },
|
||||
{ value: 'phone', label: 'Téléphone' },
|
||||
{ value: 'date', label: 'Date' },
|
||||
{ value: 'select', label: 'Liste déroulante' },
|
||||
{ value: 'radio', label: 'Boutons radio' },
|
||||
{ value: 'checkbox', label: 'Case à cocher' },
|
||||
{ value: 'toggle', label: 'Interrupteur' },
|
||||
{ value: 'file', label: 'Upload de fichier' },
|
||||
{ value: 'textarea', label: 'Zone de texte riche' },
|
||||
{ value: 'paragraph', label: 'Paragraphe' },
|
||||
{ value: 'heading1', label: 'Titre 1' },
|
||||
{ value: 'heading2', label: 'Titre 2' },
|
||||
{ value: 'heading3', label: 'Titre 3' },
|
||||
{ value: 'heading4', label: 'Titre 4' },
|
||||
{ value: 'heading5', label: 'Titre 5' },
|
||||
{ value: 'heading6', label: 'Titre 6' },
|
||||
];
|
||||
145
Front-End/src/components/Form/IconSelector.js
Normal file
145
Front-End/src/components/Form/IconSelector.js
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import Button from './Button';
|
||||
|
||||
export default function IconSelector({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelect,
|
||||
selectedIcon = '',
|
||||
}) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const excludedKeys = new Set([
|
||||
'Icon',
|
||||
'DynamicIcon',
|
||||
'createLucideIcon',
|
||||
'default',
|
||||
'icons',
|
||||
]);
|
||||
|
||||
const allIcons = Object.keys(LucideIcons).filter((key) => {
|
||||
// Exclure les utilitaires
|
||||
if (excludedKeys.has(key)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
if (!searchTerm) return allIcons;
|
||||
return allIcons.filter((iconName) =>
|
||||
iconName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [searchTerm, allIcons]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const selectIcon = (iconName) => {
|
||||
onSelect(iconName);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Choisir une icône ({filteredIcons.length} / {allIcons.length}{' '}
|
||||
icônes)
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Barre de recherche */}
|
||||
<div className="mb-6">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher une icône..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<LucideIcons.Search
|
||||
className="absolute left-3 top-3.5 text-gray-400"
|
||||
size={18}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<LucideIcons.X size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{filteredIcons.map((iconName) => {
|
||||
try {
|
||||
const IconComponent = LucideIcons[iconName];
|
||||
return (
|
||||
<button
|
||||
key={iconName}
|
||||
onClick={() => selectIcon(iconName)}
|
||||
className={`
|
||||
p-5 rounded-lg border-2 transition-all duration-200
|
||||
hover:bg-blue-50 hover:border-blue-300 hover:shadow-md hover:scale-105
|
||||
flex flex-col items-center justify-center gap-4 min-h-[140px] w-full
|
||||
${
|
||||
selectedIcon === iconName
|
||||
? 'bg-blue-100 border-blue-500 shadow-md scale-105'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}
|
||||
`}
|
||||
title={iconName}
|
||||
>
|
||||
<IconComponent
|
||||
size={32}
|
||||
className="text-gray-700 flex-shrink-0"
|
||||
/>
|
||||
<span className="text-xs text-gray-600 text-center leading-tight break-words px-1 overflow-hidden max-w-full">
|
||||
{iconName}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
} catch (error) {
|
||||
// En cas d'erreur avec une icône spécifique, ne pas la rendre
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-between items-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
{searchTerm ? (
|
||||
<>
|
||||
{filteredIcons.length} icône(s) trouvée(s) sur {allIcons.length}{' '}
|
||||
disponibles
|
||||
</>
|
||||
) : (
|
||||
<>Total : {allIcons.length} icônes disponibles</>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
text="Aucune icône"
|
||||
onClick={() => selectIcon('')}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
||||
/>
|
||||
<Button
|
||||
text="Annuler"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function InputTextIcon({
|
||||
name,
|
||||
type,
|
||||
@ -31,9 +33,11 @@ export default function InputTextIcon({
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{IconItem ? (
|
||||
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||
{IconItem && <IconItem />}
|
||||
<IconItem />
|
||||
</span>
|
||||
) : null}
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user