1 Commits

Author SHA1 Message Date
6de0d3be1a feat: Dockerisation du Docuseal 2025-05-30 14:21:42 +02:00
87 changed files with 963 additions and 2524 deletions

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
.env
node_modules/
hardcoded-strings-report.md
backend.env

View File

@ -1 +1 @@
#node scripts/prepare-commit-msg.js "$1" "$2"
node scripts/prepare-commit-msg.js "$1" "$2"

View File

@ -1,75 +0,0 @@
# 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),
),
]

View File

@ -223,29 +223,14 @@ def makeToken(user):
"""
try:
# Récupérer tous les rôles de l'utilisateur actifs
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__api_docuseal": role.establishment.api_docuseal,
"establishment__logo": logo_url,
})
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')
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'roleIndexLoginDefault': user.roleIndexLoginDefault,
'roles': roles,
'roleIndexLoginDefault':user.roleIndexLoginDefault,
'roles': list(roles),
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
@ -376,7 +361,7 @@ class SubscribeView(APIView):
}
)
def post(self, request):
retourErreur = ''
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection = JSONParser().parse(request)
establishment_id = newProfilConnection['establishment_id']
@ -452,7 +437,7 @@ class NewPasswordView(APIView):
}
)
def post(self, request):
retourErreur = ''
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection = JSONParser().parse(request)
@ -502,7 +487,7 @@ class ResetPasswordView(APIView):
}
)
def post(self, request, code):
retourErreur = ''
retourErreur = error.returnMessage[error.BAD_URL]
retour = ''
newProfilConnection = JSONParser().parse(request)
@ -513,7 +498,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]
retourErreur = error.returnMessage[error.EXPIRED_URL] % (_uuid)
elif validationOk:
retour = error.returnMessage[error.PASSWORD_CHANGED]

View File

@ -1,63 +0,0 @@
# 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')),
],
),
]

View File

@ -1,29 +0,0 @@
# 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)),
],
),
]

View File

@ -1,18 +0,0 @@
# 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),
),
]

View File

@ -1,19 +0,0 @@
# 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),
),
]

View File

@ -2,12 +2,6 @@ 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')
@ -28,11 +22,6 @@ class Establishment(models.Model):
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

View File

@ -1,19 +1,16 @@
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, MultiPartParser, FormParser
from rest_framework.parsers import JSONParser
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, getObject
from N3wtSchool.bdd import delete_object, getAllObjects
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')
@ -44,8 +41,6 @@ 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)
@ -55,20 +50,15 @@ class EstablishmentDetailView(APIView):
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
"""
Met à jour un établissement existant.
Accepte les données en multipart/form-data pour permettre l'upload de fichiers (ex : logo).
"""
establishment_data = JSONParser().parse(request)
try:
establishment = Establishment.objects.get(id=id)
except Establishment.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
# Utilise request.data pour supporter multipart/form-data (fichiers et champs classiques)
establishment_serializer = EstablishmentSerializer(establishment, data=request.data, partial=True)
establishment_serializer = EstablishmentSerializer(establishment, data=establishment_data, partial=True)
if establishment_serializer.is_valid():
establishment_serializer.save()
return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_200_OK)
return JsonResponse(establishment_serializer.data, safe=False)
return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
@ -76,7 +66,6 @@ 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", {})
@ -101,8 +90,6 @@ 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
@ -110,7 +97,7 @@ def create_establishment_with_directeur(establishment_data):
profile=profile,
establishment=establishment,
role_type=ProfileRole.RoleType.PROFIL_ADMIN,
defaults={"is_active": False}
defaults={"is_active": True}
)
# Création ou mise à jour du Directeur lié à ce ProfileRole
@ -127,6 +114,4 @@ 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

View File

@ -1,30 +0,0 @@
# 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)),
],
),
]

View File

@ -1,87 +0,0 @@
# 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')},
},
),
]

View File

@ -1,28 +0,0 @@
# 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)),
],
),
]

View File

@ -1,5 +1,4 @@
from typing import Final
from N3wtSchool import settings
WRONG_ID: Final = 1
INCOMPLETE: Final = 2
@ -9,14 +8,11 @@ DIFFERENT_PASWWORD: Final = 5
PROFIL_NOT_EXISTS: Final = 6
MESSAGE_REINIT_PASSWORD: Final = 7
EXPIRED_URL: Final = 8
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"
PASSWORD_CHANGED: Final = 8
WRONG_MAIL_FORMAT: Final = 9
PROFIL_INACTIVE: Final = 10
MESSAGE_ACTIVATION_PROFILE: Final = 11
PROFIL_ACTIVE: Final = 12
returnMessage = {
WRONG_ID:'Identifiants invalides',
@ -26,7 +22,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: get_expired_url_message(),
EXPIRED_URL:'L\'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : http://localhost:3000/password/reset?uuid=%s',
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',

View File

@ -101,27 +101,6 @@ 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 = ''

View File

@ -1,11 +1,8 @@
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'] = f"frame-ancestors 'self' {settings.BASE_URL}"
response['Content-Security-Policy'] = "frame-ancestors 'self' http://localhost:3000"
return response

View File

@ -24,16 +24,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_URL = '/data/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
BASE_URL = os.getenv('BASE_URL', 'http://localhost:3000')
BASE_URL = os.getenv('CORS_ALLOWED_ORIGINS', '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 = os.getenv('DJANGO_DEBUG', True)
DEBUG = True
ALLOWED_HOSTS = ['*']
@ -209,6 +211,8 @@ 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 = [
@ -228,18 +232,33 @@ 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
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 = os.getenv('EMAIL_USE_TLS', 'true').lower() == 'true'
EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'false').lower() == 'true'
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_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}")
DOCUMENT_DIR = 'documents'
# Configuration CORS temporaire pour debug
CORS_ALLOW_ALL_HEADERS = True
CORS_ALLOW_CREDENTIALS = True
# Configuration CORS spécifique pour la production
@ -272,26 +291,20 @@ 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 = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
CSRF_COOKIE_SECURE = False
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": DB_NAME,
"USER": DB_USER,
"PASSWORD": DB_PASSWORD,
"HOST": DB_HOST,
"PORT": DB_PORT,
"NAME": "school",
"USER": "postgres",
"PASSWORD": "postgres",
"HOST": "database",
"PORT": "5432",
}
}
@ -326,14 +339,14 @@ CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Paris'
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
URL_DJANGO = os.getenv('URL_DJANGO', 'http://localhost:8080/')
URL_DJANGO = 'http://localhost:8080/'
REDIS_HOST = 'redis'
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = None
SECRET_KEY = os.getenv('SECRET_KEY', 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3')
SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3'
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),

View File

@ -1,44 +0,0 @@
# 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')),
],
),
]

View File

@ -147,7 +147,7 @@ class EventsWithIdView(APIView):
return JsonResponse({'error': 'Event not found'}, status=404)
event.delete()
return JsonResponse({'message': 'Event deleted'}, status=200)
return JsonResponse({'message': 'Event deleted'}, status=204)
class UpcomingEventsView(APIView):
def get(self, request):

View File

@ -1,145 +0,0 @@
# 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'),
),
]

View File

@ -33,10 +33,6 @@ 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')
@ -585,6 +581,7 @@ 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,
@ -601,32 +598,13 @@ class EstablishmentCompetencyListCreateView(APIView):
custom_category=category,
is_required=False
)
# 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
# Associer à tous les élèves de l'établissement
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,
period=period
)
StudentCompetency.objects.get_or_create(
student=student,
establishment_competency=ec
)
created.append({
"competence_id": ec.id,

View File

@ -1,29 +0,0 @@
# 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')),
],
),
]

View File

@ -1,213 +0,0 @@
# 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')},
},
),
]

View File

@ -1,18 +0,0 @@
# 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),
),
]

View File

@ -6,9 +6,6 @@ from django.utils.translation import gettext_lazy as _
from datetime import datetime
import os
import logging
logger = logging.getLogger("SubscriptionModels")
class Language(models.Model):
"""
@ -397,8 +394,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)
filenames.append(reg_file.file.path)
return filenames
class AbsenceMoment(models.IntegerChoices):

View File

@ -381,20 +381,16 @@ 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', 'associated_class_name']
fields = ['id', 'last_name', 'first_name', 'level', 'photo']
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)

View File

@ -33,7 +33,6 @@
<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">

View File

@ -1,59 +0,0 @@
<!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>

View File

@ -1,228 +1,233 @@
<!DOCTYPE html>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="UTF-8">
<title>Fiche élève de {{ student.last_name }} {{ student.first_name }}</title>
<style>
@page {
size: A4;
margin: 2cm;
}
body {
font-family: 'Arial', sans-serif;
font-size: 12pt;
color: #222;
background: #fff;
margin: 0;
padding: 0;
}
.container {
width: 100%;
padding: 0;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 24px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 12px;
position: relative;
}
.title {
font-size: 22pt;
font-weight: bold;
color: #4CAF50;
margin: 0;
}
.photo {
position: absolute;
top: 0;
right: 0;
width: 90px;
height: 90px;
object-fit: cover;
border: 1px solid #4CAF50;
border-radius: 8px;
}
.section {
margin-bottom: 32px; /* Espacement augmenté entre les sections */
}
.section-title {
font-size: 15pt;
font-weight: bold;
color: #4CAF50;
margin-bottom: 18px; /* Espacement sous le titre de section */
border-bottom: 1px solid #4CAF50;
padding-bottom: 2px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 8px;
}
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;
width: 30%;
background: #f3f3f3;
}
.value-cell {
width: 70%;
}
.signature {
margin-top: 30px;
text-align: right;
font-style: italic;
color: #555;
}
.signature-text {
font-weight: bold;
color: #333;
}
.subsection-title {
font-size: 12pt;
color: #333;
margin: 8px 0 4px 0;
font-weight: bold;
}
</style>
</head>
<body>
{% load myTemplateTag %}
<div class="container">
<!-- Header Section -->
<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" />
{% else %}
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
{% endif %}
</div>
<head>
<meta charset="UTF-8">
<title>Fiche élève de {{ student.last_name }} {{ student.first_name }}</title>
<style>
@page {
size: A4;
margin: 2cm;
}
body {
font-family: 'Arial', sans-serif;
font-size: 12pt;
line-height: 1.6;
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);
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 3px solid #4CAF50;
padding-bottom: 15px;
position: relative;
}
.title {
font-size: 20pt;
font-weight: bold;
color: #4CAF50;
margin: 0;
}
.photo {
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
object-fit: cover;
border: 2px solid #4CAF50;
border-radius: 10px;
}
.section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fefefe;
}
.section-title {
font-size: 18pt;
font-weight: bold;
color: #4CAF50;
margin-bottom: 10px;
text-align: left;
border-bottom: 2px solid #4CAF50;
padding-bottom: 5px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
td {
padding: 8px;
vertical-align: top;
}
.label-cell {
font-weight: bold;
color: #555;
width: 35%;
text-align: right;
padding-right: 10px;
}
.value-cell {
color: #333;
width: 65%;
text-align: left;
}
.phone {
white-space: nowrap;
}
.signature {
margin-top: 30px;
text-align: right;
font-style: italic;
color: #555;
}
.signature-text {
font-weight: bold;
color: #333;
}
</style>
</head>
<body>
{% load myTemplateTag %}
<div class="container">
<!-- Header Section -->
<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"
/>
{% else %}
<img
src="/static/img/default-photo.jpg"
alt="Photo par défaut"
class="photo"
/>
{% endif %}
</div>
<!-- Élève -->
<div class="section">
<div class="section-title">ÉLÈVE</div>
<table>
<tr>
<td class="label-cell">Nom</td>
<td class="value-cell">{{ student.last_name }}</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 class="value-cell" colspan="3">{{ student.address }}</td>
</tr>
<tr>
<td class="label-cell">Genre</td>
<td class="value-cell">{{ student|getStudentGender }}</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 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">Niveau</td>
<td class="value-cell">{{ student|getStudentLevel }}</td>
<td class="label-cell"></td>
<td class="value-cell"></td>
</tr>
</table>
</div>
<!-- Responsables -->
<div class="section">
<div class="section-title">RESPONSABLES</div>
{% for guardian in student.getGuardians %}
<div>
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
<!-- Student Section -->
<div class="section">
<h2 class="section-title">ÉLÈVE</h2>
<table>
<tr>
<td class="label-cell">Nom</td>
<td class="value-cell">{{ guardian.last_name }}</td>
<td class="label-cell">Prénom</td>
<td class="value-cell">{{ guardian.first_name }}</td>
<td class="label-cell">Nom :</td>
<td class="value-cell">{{ student.last_name }}</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 class="value-cell" colspan="3">{{ guardian.address }}</td>
<td class="label-cell">Adresse :</td>
<td colspan="3" class="value-cell">{{ student.address }}</td>
</tr>
<tr>
<td class="label-cell">Email</td>
<td class="value-cell" colspan="3">{{ guardian.email }}</td>
<td class="label-cell">Genre :</td>
<td class="value-cell">{{ student|getStudentGender }}</td>
<td class="label-cell">Né(e) le :</td>
<td class="value-cell">{{ student.birth_date }}</td>
</tr>
<tr>
<td class="label-cell">Né(e) le</td>
<td class="value-cell">{{ guardian.birth_date }}</td>
<td class="label-cell">Téléphone</td>
<td class="value-cell">{{ guardian.phone|phone_format }}</td>
<td class="label-cell">À :</td>
<td colspan="3" class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
</tr>
<tr>
<td class="label-cell">Profession</td>
<td class="value-cell" colspan="3">{{ guardian.profession }}</td>
<td class="label-cell">Nationalité :</td>
<td class="value-cell">{{ student.nationality }}</td>
<td class="label-cell">Niveau :</td>
<td class="value-cell">{{ student|getStudentLevel }}</td>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- Fratrie -->
<div class="section">
<div class="section-title">FRATRIE</div>
{% for sibling in student.getSiblings %}
<div>
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
<!-- Guardians Section -->
<div class="section">
<h2 class="section-title">RESPONSABLES</h2>
{% for guardian in student.getGuardians %}
<div class="subsection">
<h3 class="subsection-title">Responsable {{ forloop.counter }}</h3>
<table>
<tr>
<td class="label-cell">Nom :</td>
<td class="value-cell">{{ guardian.last_name }}</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>
</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>
</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>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- Siblings Section -->
<div class="section">
<h2 class="section-title">FRATRIE</h2>
{% for sibling in student.getSiblings %}
<div class="subsection">
<h3 class="subsection-title">Frère/Soeur {{ forloop.counter }}</h3>
<table>
<tr>
<td class="label-cell">Nom :</td>
<td class="value-cell">{{ sibling.last_name }}</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>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- Payment Section -->
<div class="section">
<h2 class="section-title">MODALITÉS DE PAIEMENT</h2>
<table>
<tr>
<td class="label-cell">Nom</td>
<td class="value-cell">{{ sibling.last_name }}</td>
<td class="label-cell">Prénom</td>
<td class="value-cell">{{ sibling.first_name }}</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">Né(e) le</td>
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
<td class="label-cell">Frais de scolarité :</td>
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- Paiement -->
<div class="section">
<div class="section-title">MODALITÉS DE PAIEMENT</div>
<table>
<tr>
<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="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
</tr>
</table>
<!-- Signature Section -->
<div class="signature">
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
</div>
</div>
<!-- Signature -->
<div class="signature">
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
</div>
</div>
</body>
</body>
</html>

View File

@ -88,7 +88,6 @@ 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:
@ -114,14 +113,7 @@ class RegisterFormView(APIView):
registerForms_List = None
if registerForms_List:
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')
registerForms_List = registerForms_List.filter(establishment=establishment_id).order_by('-last_update')
if not registerForms_List:
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)

View File

@ -12,27 +12,11 @@ from N3wtSchool import bdd
class RegistrationSchoolFileMasterView(APIView):
@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
)
],
operation_description="Récupère tous les masters de templates d'inscription",
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()
masters = RegistrationSchoolFileMaster.objects.all()
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
return Response(serializer.data)
@ -102,27 +86,11 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
class RegistrationSchoolFileTemplateView(APIView):
@swagger_auto_schema(
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
)
],
operation_description="Récupère tous les templates d'inscription",
responses={200: RegistrationSchoolFileTemplateSerializer(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 liés à l'établissement via master.groups.establishment
templates = RegistrationSchoolFileTemplate.objects.filter(
master__groups__establishment__id=establishment_id
).distinct()
templates = RegistrationSchoolFileTemplate.objects.all()
serializer = RegistrationSchoolFileTemplateSerializer(templates, many=True)
return Response(serializer.data)
@ -192,27 +160,11 @@ class RegistrationSchoolFileTemplateSimpleView(APIView):
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
)
],
operation_description="Récupère tous les fichiers parents",
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()
templates = RegistrationParentFileMaster.objects.all()
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
return Response(serializer.data)
@ -282,27 +234,11 @@ class RegistrationParentFileMasterSimpleView(APIView):
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
)
],
operation_description="Récupère tous les templates d'inscription",
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()
templates = RegistrationParentFileTemplate.objects.all()
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
return Response(serializer.data)

View File

@ -1 +1 @@
__version__ = "0.0.3"
__version__ = "0.0.1"

View File

@ -10,21 +10,21 @@ def run_command(command):
print(f"stderr: {stderr.decode()}")
return process.returncode
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
test_mode = os.getenv('TEST_MODE', 'False') == '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"]
]

View File

@ -1,42 +1,8 @@
<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 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>

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 903 B

View File

@ -1,267 +0,0 @@
# 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))

3
Caddyfile Normal file
View File

@ -0,0 +1,3 @@
https://localhost:4443, https://127.0.0.1:4443 {
reverse_proxy docuseal:3000
}

View File

@ -18,19 +18,6 @@ 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: {

View File

@ -1,6 +1,6 @@
{
"name": "n3wt-school-front-end",
"version": "0.0.3",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",

View File

@ -1,8 +1,9 @@
'use client';
import React from 'react';
import React, { useEffect } 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 }) {
@ -17,6 +18,11 @@ export default function MessageriePage({ csrfToken }) {
label: 'Messagerie Instantanée',
content: <InstantMessaging csrfToken={csrfToken} />,
},
{
id: 'announcement',
label: 'Planifier une Annonce',
content: <AnnouncementScheduler csrfToken={csrfToken} />,
},
];
return (

View File

@ -1,17 +1,9 @@
'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,
@ -23,6 +15,20 @@ 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() {
@ -33,11 +39,8 @@ export default function DashboardPage() {
const [upcomingEvents, setUpcomingEvents] = useState([]);
const [absencesToday, setAbsencesToday] = useState([]);
const {
selectedEstablishmentId,
selectedEstablishmentTotalCapacity,
apiDocuseal,
} = useEstablishment();
const { selectedEstablishmentId, selectedEstablishmentTotalCapacity, apiDocuseal } =
useEstablishment();
const [statusDistribution, setStatusDistribution] = useState([
{ label: 'Non envoyé', value: 0 },
@ -217,36 +220,34 @@ 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="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>
<div className="flex flex-row gap-4">
<div className="flex-1 p-6">
<LineChart data={monthlyRegistrations} />
</div>
<div className="flex-1 flex items-center justify-center">
<PieChart data={statusDistribution} />
</div>
{/* Graphique des inscriptions */}
<div className="lg:col-span-1 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
<h2 className="text-lg font-semibold mb-6">
{t('inscriptionTrends')}
</h2>
<div className="flex flex-row gap-4">
<div className="flex-1 p-6">
<LineChart data={monthlyRegistrations} />
</div>
<div className="flex-1 flex items-center justify-center">
<PieChart data={statusDistribution} />
</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>
{/* 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">
{/* Événements à venir */}
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
<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>
);
}

View File

@ -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('smtp');
const [activeTab, setActiveTab] = useState('structure');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
@ -40,6 +40,8 @@ export default function SettingsPage() {
const tabParam = searchParams.get('tab');
if (tabParam === 'smtp') {
setActiveTab('smtp');
} else if (tabParam === 'structure') {
setActiveTab('structure');
}
}, [searchParams]);
@ -77,6 +79,18 @@ 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);
};
@ -93,6 +107,26 @@ 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 = {
@ -130,6 +164,11 @@ 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'}
@ -137,6 +176,28 @@ 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">

View File

@ -78,7 +78,7 @@ export default function Page() {
handleTuitionFees();
// Fetch data for registration file schoolFileTemplates
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
fetchRegistrationSchoolFileMasters()
.then((data) => {
setFichiers(data);
})

View File

@ -313,13 +313,13 @@ export default function CreateSubscriptionPage() {
})
.catch(requestErrorHandler);
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
fetchRegistrationSchoolFileMasters()
.then((data) => {
setSchoolFileMasters(data);
})
.catch(requestErrorHandler);
fetchRegistrationParentFileMasters(selectedEstablishmentId)
fetchRegistrationParentFileMasters()
.then((data) => {
setParentFileMasters(data);
})
@ -502,11 +502,7 @@ export default function CreateSubscriptionPage() {
// Mode édition
editRegisterForm(registerFormID, formData, csrfToken)
.then((response) => {
showNotification(
"Dossier d'inscription mis à jour avec succès",
'success',
'Succès'
);
logger.debug('Dossier mis à jour avec succès:', response);
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
})
.catch((error) => {
@ -617,11 +613,6 @@ export default function CreateSubscriptionPage() {
Promise.all([...clonePromises, ...parentClonePromises])
.then(() => {
// Redirection après succès
showNotification(
"Dossier d'inscription créé avec succès",
'success',
'Succès'
);
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
})
.catch((error) => {

View File

@ -8,11 +8,9 @@ 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';
@ -28,21 +26,11 @@ 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);

View File

@ -52,7 +52,6 @@ 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');
@ -92,6 +91,8 @@ 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(() => {});
@ -102,7 +103,6 @@ export default function Page({ params: { locale } }) {
const csrfToken = useCsrfToken();
const router = useRouter();
const { selectedEstablishmentId } = useEstablishment();
const { showNotification } = useNotification();
const openSepaUploadModal = (row) => {
setSelectedRowForUpload(row);
@ -133,8 +133,9 @@ export default function Page({ params: { locale } }) {
const registerFormCurrrentYearDataHandler = (data) => {
if (data) {
const { registerForms, count, page_size } = data;
setRegistrationFormsDataCurrentYear(registerForms);
if (registerForms) {
setRegistrationFormsDataCurrentYear(registerForms);
}
const calculatedTotalPages =
count === 0 ? count : Math.ceil(count / page_size);
setTotalCurrentYear(count);
@ -152,8 +153,9 @@ export default function Page({ params: { locale } }) {
const registerFormNextYearDataHandler = (data) => {
if (data) {
const { registerForms, count, page_size } = data;
setRegistrationFormsDataNextYear(registerForms);
if (registerForms) {
setRegistrationFormsDataNextYear(registerForms);
}
const calculatedTotalPages =
count === 0 ? count : Math.ceil(count / page_size);
setTotalNextYear(count);
@ -171,7 +173,9 @@ export default function Page({ params: { locale } }) {
const registerFormHistoricalDataHandler = (data) => {
if (data) {
const { registerForms, count, page_size } = data;
setRegistrationFormsDataHistorical(registerForms);
if (registerForms) {
setRegistrationFormsDataHistorical(registerForms);
}
const calculatedTotalPages =
count === 0 ? count : Math.ceil(count / page_size);
@ -221,11 +225,12 @@ export default function Page({ params: { locale } }) {
fetchDataAndSetState();
}
}, [selectedEstablishmentId, reloadFetch]);
}, [selectedEstablishmentId, reloadFetch, currentSchoolYearPage, searchTerm]);
useEffect(() => {
if (selectedEstablishmentId) {
const fetchDataAndSetState = () => {
setIsLoading(true);
fetchRegisterForms(
selectedEstablishmentId,
CURRENT_YEAR_FILTER,
@ -242,6 +247,7 @@ export default function Page({ params: { locale } }) {
.then(registerFormHistoricalDataHandler)
.catch(requestErrorHandler);
setIsLoading(false);
setReloadFetch(false);
};
@ -250,7 +256,7 @@ export default function Page({ params: { locale } }) {
}, 500); // Debounce la recherche
return () => clearTimeout(timeoutId);
}
}, [searchTerm, selectedEstablishmentId, currentSchoolYearPage, itemsPerPage]);
}, [searchTerm]);
/**
* UseEffect to update page count of tab
@ -288,21 +294,15 @@ export default function Page({ params: { locale } }) {
editRegisterForm(row.student.id, formData, csrfToken)
.then((response) => {
logger.debug('Mandat SEPA uploadé avec succès :', response);
showNotification(
'Le mandat SEPA a été uploadé avec succès.',
'success',
'Succès'
);
setPopupMessage('Le mandat SEPA a été uploadé avec succès.');
setPopupVisible(true);
setReloadFetch(true);
closeSepaUploadModal();
})
.catch((error) => {
logger.error("Erreur lors de l'upload du mandat SEPA :", error);
showNotification(
"Erreur lors de l'upload du mandat SEPA.",
'error',
'Erreur'
);
setPopupMessage("Erreur lors de l'upload du mandat SEPA.");
setPopupVisible(true);
});
};
@ -321,22 +321,21 @@ export default function Page({ params: { locale } }) {
archiveRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
showNotification(
"Le dossier d'inscription a été correctement archivé",
'success',
'Succès'
setPopupMessage(
"Le dossier d'inscription a été correctement archivé"
);
setPopupVisible(true);
setRegistrationForms(
registrationForms.filter((fiche) => fiche.id !== id)
);
setReloadFetch(true);
})
.catch((error) => {
showNotification(
"Erreur lors de l'archivage du dossier d'inscription",
'error',
'Erreur'
logger.error('Error archiving data:', error);
setPopupMessage(
"Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur."
);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
@ -351,20 +350,16 @@ export default function Page({ params: { locale } }) {
sendRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
showNotification(
"Le dossier d'inscription a été envoyé avec succès",
'success',
'Succès'
);
setPopupMessage("Le dossier d'inscription a été envoyé avec succès");
setPopupVisible(true);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error archiving data:', error);
showNotification(
"Erreur lors de l'envoi du dossier d'inscription",
'error',
'Erreur'
setPopupMessage(
"Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur."
);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
@ -699,7 +694,7 @@ export default function Page({ params: { locale } }) {
];
let emptyMessage;
if (activeTab === CURRENT_YEAR_FILTER && searchTerm === '') {
if (activeTab === CURRENT_YEAR_FILTER) {
emptyMessage = (
<AlertMessage
type="warning"
@ -707,7 +702,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 && searchTerm === '') {
} else if (activeTab === NEXT_YEAR_FILTER) {
emptyMessage = (
<AlertMessage
type="info"
@ -715,7 +710,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 && searchTerm === '') {
} else if (activeTab === HISTORICAL_FILTER) {
emptyMessage = (
<AlertMessage
type="info"
@ -841,6 +836,12 @@ 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}

View File

@ -9,7 +9,6 @@ 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();
@ -31,7 +30,6 @@ export default function Page() {
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const { showNotification } = useNotification();
const requestErrorHandler = (err) => {
logger.error('Error fetching data:', err);
@ -65,20 +63,11 @@ 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);
});

View File

@ -7,47 +7,25 @@ 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 [isLoading, setIsLoading] = useState(false);
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) => {
setIsLoading(false);
showNotification(
"Erreur lors de la soumission du dossier d'inscription",
'error',
'Erreur'
);
logger.error('Error:', error);
});
const handleSubmit = async (data) => {
try {
const result = await editRegisterForm(studentId, data, csrfToken);
logger.debug('Success:', result);
router.push(FE_PARENTS_HOME_URL);
} catch (error) {
logger.error('Error:', error);
}
};
if (isLoading === true) {
return <Loader />; // Affichez le composant Loader
}
return (
<InscriptionFormShared
studentId={studentId}

View File

@ -6,7 +6,8 @@ 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_MESSAGERIE_URL,
FE_PARENTS_SETTINGS_URL,
} from '@/utils/Url';
import ProtectedRoute from '@/components/ProtectedRoute';
import { disconnect } from '@/app/actions/authAction';
@ -40,7 +41,13 @@ 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

View File

@ -2,14 +2,7 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import {
Edit3,
Users,
Download,
Eye,
Upload,
CalendarDays,
} from 'lucide-react';
import { Edit3, Users, Download, Eye, Upload } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
@ -17,16 +10,11 @@ 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([]);
@ -34,9 +22,6 @@ 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);
@ -50,20 +35,7 @@ export default function ParentHomePage() {
});
setReloadFetch(false);
}
}, [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]);
}, [selectedEstablishmentId, reloadFetch]);
function handleView(eleveId) {
logger.debug(`View dossier for student id: ${eleveId}`);
@ -125,13 +97,6 @@ export default function ParentHomePage() {
}
};
const showClassPlanning = (student) => {
setPlanningClassName(
`${student.associated_class_name} - ${getNiveauLabel(student.level)}`
);
setShowPlanning(true);
};
const childrenColumns = [
{
name: 'photo',
@ -162,12 +127,6 @@ 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) => (
@ -233,6 +192,7 @@ 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'
@ -241,7 +201,7 @@ export default function ParentHomePage() {
}`}
onClick={(e) => {
e.stopPropagation();
toggleUpload(row.student.id);
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
}}
aria-label="Uploader un fichier"
>
@ -251,28 +211,16 @@ export default function ParentHomePage() {
)}
{row.status === 5 && (
<>
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
<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>
</>
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
<Eye className="h-5 w-5" />
</button>
)}
</div>
),
@ -280,77 +228,40 @@ export default function ParentHomePage() {
];
return (
<div className="w-full h-full">
{showPlanning && planningClassName ? (
// Affichage grand format mais respectant la sidebar
<>
<div className="p-4 flex items-center border-b">
<div className="px-2 py-4 md:px-4 max-w-full">
<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"
/>
</div>
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
{uploadState === 'on' && uploadingStudentId && (
<div className="mt-4">
<FileUpload
selectionMessage="Sélectionnez un fichier à uploader"
onFileSelect={handleFileUpload}
/>
<button
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
onClick={() => setShowPlanning(false)}
className={`mt-4 px-6 py-2 rounded-md ${
uploadedFile
? 'bg-emerald-500 text-white hover:bg-emerald-600'
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
}`}
onClick={handleSubmit}
disabled={!uploadedFile}
>
Retour
Valider
</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>
{/* 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 && (
<div className="mt-4">
<FileUpload
selectionMessage="Sélectionnez un fichier à uploader"
onFileSelect={handleFileUpload}
/>
<button
className={`mt-4 px-6 py-2 rounded-md ${
uploadedFile
? 'bg-emerald-500 text-white hover:bg-emerald-600'
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
}`}
onClick={handleSubmit}
disabled={!uploadedFile}
>
Valider
</button>
</div>
)}
</div>
)}
)}
</div>
</div>
);
}

View File

@ -14,18 +14,20 @@ 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'),
@ -35,11 +37,7 @@ export default function Page() {
logger.debug('Sign In Result', result);
if (result.error) {
showNotification(
result.error,
'error',
'Erreur'
);
setErrorMessage(result.error);
setIsLoading(false);
} else {
// On initialise le contexte establishement avec la session
@ -50,11 +48,7 @@ export default function Page() {
if (url) {
router.push(url);
} else {
showNotification(
'Type de rôle non géré',
'error',
'Erreur'
);
setErrorMessage('Type de rôle non géré');
}
});
setIsLoading(false);
@ -65,10 +59,8 @@ export default function Page() {
error
);
setIsLoading(false);
showNotification(
'Une erreur est survenue lors de la récupération de la session.',
'error',
'Erreur'
setErrorMessage(
'Une erreur est survenue lors de la récupération de la session.'
);
});
}
@ -76,11 +68,7 @@ export default function Page() {
.catch((error) => {
logger.error('Erreur lors de la connexion:', error);
setIsLoading(false);
showNotification(
'Une erreur est survenue lors de la connexion.',
'error',
'Erreur'
);
setErrorMessage('Une erreur est survenue lors de la connexion.');
});
}
@ -110,6 +98,7 @@ export default function Page() {
IconItem={User}
label="Identifiant"
placeholder="Identifiant"
errorMsg={userFieldError}
className="w-full mb-5"
/>
<InputTextIcon
@ -118,9 +107,11 @@ 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"

View File

@ -3,61 +3,68 @@
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';
import Button from '@/components/Button';
import { User } from 'lucide-react';
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 { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext';
import { sendNewPassword } from '@/app/actions/authAction';
import logger from '@/utils/logger';
import { useNotification } from '@/context/NotificationContext';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const { showNotification } = useNotification();
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState('');
const [userFieldError, setUserFieldError] = useState('');
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) {
const data = { email: formData.get('email') };
setIsLoading(true);
sendNewPassword(data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
if (data.message !== '') {
showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`);
} else {
if (data.errorMessage) {
showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) {
showNotification(
data.errorFields.email,
'error',
'Erreur'
);
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') };
sendNewPassword(data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setUserFieldError('');
setErrorMessage('');
if (data.errorMessage === '') {
setPopupMessage(data.message);
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
} else {
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
}
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
}
}
setIsLoading(false);
})
.catch((error) => {
logger.error('Error fetching data:', error);
setIsLoading(false);
error = error.errorMessage;
logger.debug(error);
});
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader />;
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
@ -82,8 +89,10 @@ 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"
@ -103,6 +112,12 @@ export default function Page() {
/>
</div>
</div>
<Popup
isOpen={popupVisible}
message={popupMessage}
onConfirm={popupConfirmAction}
onCancel={() => setPopupVisible(false)}
/>
</>
);
}

View File

@ -6,71 +6,110 @@ 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';
import Button from '@/components/Button';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { KeySquare } from 'lucide-react';
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { useCsrfToken } from '@/context/CsrfContext';
import { resetPassword } from '@/app/actions/authAction';
import { getResetPassword, resetPassword } from '@/app/actions/authAction';
import logger from '@/utils/logger';
import { useNotification } from '@/context/NotificationContext';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
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();
function validate(formData) {
const data = {
password1: formData.get('password1'),
password2: formData.get('password2'),
};
setIsLoading(true);
resetPassword(uuid, data, csrfToken)
.then((data) => {
if (data.message !== '') {
useEffect(() => {
if (useFakeData) {
setTimeout(() => {
setIsLoading(false);
}, 1000);
} else {
getResetPassword(uuid)
.then((data) => {
logger.debug('Success:', data);
showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`);
} else {
if (data.errorMessage) {
showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) {
showNotification(
data.errorFields.password1 || data.errorFields.password2,
'error',
'Erreur'
);
setIsLoading(true);
if (data.errorFields) {
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
}
setIsLoading(false);
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
setIsLoading(false);
});
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'),
};
resetPassword(uuid, data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setPassword1FieldError('');
setPassword2FieldError('');
setErrorMessage('');
if (data.errorMessage === '') {
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
if (data.errorFields) {
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
}
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader />;
return <Loader />; // Affichez le composant 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" />
@ -92,6 +131,7 @@ export default function Page() {
IconItem={KeySquare}
label="Mot de passe"
placeholder="Mot de passe"
errorMsg={password1FieldError}
className="w-full mb-5"
/>
<InputTextIcon
@ -100,8 +140,10 @@ 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"

View File

@ -1,29 +1,40 @@
'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';
import Button from '@/components/Button';
import { User, KeySquare } from 'lucide-react';
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 { 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 { showNotification } = useNotification();
const [errorMessage, setErrorMessage] = useState('');
const [userFieldError, setUserFieldError] = 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();
const establishment_id = searchParams.get('establishment_id');
function isOK(data) {
return data.errorMessage === '';
}
function subscribeFormSubmit(formData) {
const data = {
email: formData.get('login'),
@ -35,29 +46,25 @@ export default function Page() {
subscribe(data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
if (data.message !== '') {
showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`);
setUserFieldError('');
setPassword1FieldError('');
setPassword2FieldError('');
setErrorMessage('');
if (isOK(data)) {
setIsLoading(false);
setPopupMessage(data.message);
setPopupVisible(true);
} else {
setIsLoading(false);
if (data.errorMessage) {
showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) {
showNotification(
data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
'error',
'Erreur'
);
setErrorMessage(data.errorMessage);
}
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
}
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
@ -68,7 +75,7 @@ export default function Page() {
}
if (isLoading === true) {
return <Loader />;
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
@ -93,6 +100,7 @@ export default function Page() {
IconItem={User}
label="Identifiant"
placeholder="Identifiant"
errorMsg={userFieldError}
className="w-full mb-5"
/>
<InputTextIcon
@ -101,6 +109,7 @@ export default function Page() {
IconItem={KeySquare}
label="Mot de passe"
placeholder="Mot de passe"
errorMsg={password1FieldError}
className="w-full mb-5"
/>
<InputTextIcon
@ -109,8 +118,10 @@ 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"
@ -132,6 +143,18 @@ 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"
/>
</>
);
}

View File

@ -199,3 +199,14 @@ 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);
};

View File

@ -47,8 +47,11 @@ export const fetchRegistrationFileFromGroup = async (groupId) => {
return response.json();
};
export const fetchRegistrationSchoolFileMasters = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
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}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
@ -58,8 +61,11 @@ export const fetchRegistrationSchoolFileMasters = (establishment) => {
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchRegistrationParentFileMasters = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
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}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {
@ -69,8 +75,11 @@ export const fetchRegistrationParentFileMasters = (establishment) => {
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
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}`;
}
const request = new Request(`${url}`, {
method: 'GET',
headers: {

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
import { usePlanning } 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, planningClassName='' }) => {
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
const {
currentDate,
setCurrentDate,
@ -30,8 +30,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
setViewType,
events,
hiddenSchedules,
planningMode,
parentView
} = usePlanning();
const [visibleEvents, setVisibleEvents] = useState([]);
const [showDatePicker, setShowDatePicker] = useState(false);
@ -93,107 +91,98 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' })
<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">
<div className="flex items-center gap-4">
<button
onClick={() => setCurrentDate(new Date())}
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
Aujourd&apos;hui
</button>
<button
onClick={() => navigateDate('prev')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronLeft className="w-5 h-5" />
</button>
{/* Menu déroulant pour le mois/année */}
<div className="relative">
<button
onClick={() => setCurrentDate(new Date())}
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
onClick={() => setShowDatePicker(!showDatePicker)}
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
>
Aujourd&apos;hui
<h2 className="text-xl font-semibold">
{format(
currentDate,
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
{ locale: fr }
)}
</h2>
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={() => navigateDate('prev')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="relative">
<button
onClick={() => setShowDatePicker(!showDatePicker)}
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
>
<h2 className="text-xl font-semibold">
{format(
currentDate,
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
{ locale: fr }
)}
</h2>
<ChevronDown className="w-4 h-4" />
</button>
{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' && (
<div className="p-2 border-b">
<div className="grid grid-cols-3 gap-1">
{months.map((month) => (
<button
key={month.value}
onClick={() => handleMonthSelect(month.value)}
className="p-2 text-sm hover:bg-gray-100 rounded-md"
>
{month.label}
</button>
))}
</div>
</div>
)}
<div className="p-2">
{/* 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' && (
<div className="p-2 border-b">
<div className="grid grid-cols-3 gap-1">
{years.map((year) => (
{months.map((month) => (
<button
key={year.value}
onClick={() => handleYearSelect(year.value)}
key={month.value}
onClick={() => handleMonthSelect(month.value)}
className="p-2 text-sm hover:bg-gray-100 rounded-md"
>
{year.label}
{month.label}
</button>
))}
</div>
</div>
)}
<div className="p-2">
<div className="grid grid-cols-3 gap-1">
{years.map((year) => (
<button
key={year.value}
onClick={() => handleYearSelect(year.value)}
className="p-2 text-sm hover:bg-gray-100 rounded-md"
>
{year.label}
</button>
))}
</div>
</div>
)}
</div>
<button
onClick={() => navigateDate('next')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</div>
<button
onClick={() => navigateDate('next')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Numéro de semaine au centre */}
{viewType === 'week' && (
<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">
{getWeek(currentDate, { weekStartsOn: 1 })}
</span>
</div>
)}
{/* 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">
{getWeek(currentDate, { weekStartsOn: 1 })}
</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>
)}
<ToggleView viewType={viewType} setViewType={setViewType} />
<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>

View File

@ -1,13 +1,12 @@
import React, { useEffect, useState, useRef } from 'react';
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
import { usePlanning } 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, planningMode, parentView } = usePlanning();
const { currentDate } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date());
const scrollContainerRef = useRef(null); // Ajouter cette référence
@ -107,14 +106,10 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
key={event.id}
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
style={eventStyle}
onClick={
parentView
? undefined
: (e) => {
e.stopPropagation();
onEventClick(event);
}
}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
<div className="p-1">
<div
@ -203,15 +198,11 @@ 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={
parentView
? undefined
: () => {
const date = new Date(day);
date.setHours(hour);
onDateClick(date);
}
}
onClick={() => {
const date = new Date(day);
date.setHours(hour);
onDateClick(date);
}}
>
<div className="grid gap-1">
{dayEvents

View File

@ -1,11 +1,10 @@
import React from 'react';
// Utilisation de couleurs hexadécimales pour le SVG
const COLORS = [
'#60a5fa', // bleu-400
'#fb923c', // orange-400
'#a78bfa', // violet-400
'#34d399', // émeraude-400
'fill-blue-400 text-blue-400',
'fill-orange-400 text-orange-400',
'fill-purple-400 text-purple-400',
'fill-emerald-400 text-emerald-400',
];
export default function PieChart({ data }) {
@ -24,20 +23,7 @@ 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;
@ -56,7 +42,7 @@ export default function PieChart({ data }) {
<path
key={idx}
d={pathData}
fill={COLORS[idx % COLORS.length]}
className={COLORS[idx % COLORS.length].split(' ')[0]}
stroke="#fff"
strokeWidth="0.5"
/>
@ -64,21 +50,17 @@ export default function PieChart({ data }) {
})}
</svg>
<div className="ml-4 flex flex-col space-y-1">
{data.map((slice, idx) =>
slice.value > 0 && (
<div
key={idx}
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"
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
/>
{slice.label} : {slice.value}
</div>
)
)}
{data.map((slice, idx) => (
<div
key={idx}
className={`flex items-center text-xs font-semibold ${COLORS[idx % COLORS.length].split(' ')[1]}`}
>
<span
className={`inline-block w-3 h-3 mr-2 rounded ${COLORS[idx % COLORS.length].split(' ')[0]}`}
/>
{slice.label} : {slice.value}
</div>
))}
</div>
</div>
);

View File

@ -1,131 +0,0 @@
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;

View File

@ -33,19 +33,12 @@ 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);
return () => clearTimeout(timer);
}
// Pour les erreurs, pas de timeout : la notification reste affichée
}, [onClose, displayPeriod, type]);
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
return () => clearTimeout(timer);
}, [onClose, displayPeriod]);
if (!message || !isVisible) return null;

View File

@ -230,7 +230,7 @@ export default function FilesToUpload({
<div className="mt-4">
{actionType === 'view' && selectedFile.fileName ? (
<iframe
src={`${BASE_URL}${selectedFile.fileName}`}
src={`${BASE_URL}/${selectedFile.fileName}`}
title="Document Viewer"
className="w-full"
style={{

View File

@ -112,13 +112,8 @@ export default function StudentInfoForm({
(field === 'birth_place' &&
(!formData.birth_place || formData.birth_place.trim() === '')) ||
(field === 'birth_postal_code' &&
(
!formData.birth_postal_code ||
String(formData.birth_postal_code).trim() === '' ||
isNaN(Number(formData.birth_postal_code)) ||
!Number.isInteger(Number(formData.birth_postal_code))
)
) ||
(!formData.birth_postal_code ||
String(formData.birth_postal_code).trim() === '')) ||
(field === 'address' &&
(!formData.address || formData.address.trim() === '')) ||
(field === 'attending_physician' &&

View File

@ -81,13 +81,14 @@ export default function ValidateSubscription({
};
const handleAssignClass = () => {
const fusionParam = mergeDocuments ? 'true' : 'false';
if (formData.associated_class) {
const data = {
student: {
associated_class: formData.associated_class,
},
status: 5,
fusionParam: mergeDocuments,
fusionParam: fusionParam,
};
onAccept(data);
@ -146,7 +147,7 @@ export default function ValidateSubscription({
{allTemplates[currentTemplateIndex].name || 'Document sans nom'}
</h3>
<iframe
src={`${BASE_URL}${allTemplates[currentTemplateIndex].file}`}
src={`${BASE_URL}/${allTemplates[currentTemplateIndex].file}`}
title={
allTemplates[currentTemplateIndex].type === 'main'
? 'Document Principal'

View File

@ -1,30 +0,0 @@
import React, { useEffect } from 'react';
import Calendar from '@/components/Calendar/Calendar';
import { usePlanning } from '@/context/PlanningContext';
const ParentPlanningSection = ({ planningClassName }) => {
const { setHiddenSchedules, schedules } = usePlanning();
useEffect(() => {
if (schedules && schedules.length > 0 && planningClassName) {
const visibleSchedule = schedules.find(s => s.name === planningClassName);
const hidden = schedules
.filter(s => s.name !== planningClassName)
.map(s => s.id);
setHiddenSchedules(hidden);
// Optionnel : si aucun planning ne correspond, tout masquer
if (!visibleSchedule) {
setHiddenSchedules(schedules.map(s => s.id));
}
}
}, [schedules, planningClassName, setHiddenSchedules]);
return (
<div className="flex w-full h-full overflow-hidden">
<Calendar planningClassName={planningClassName} />
</div>
);
};
export default ParentPlanningSection;

View File

@ -9,9 +9,6 @@ import { usePopup } from '@/context/PopupContext';
import { getRightStr } from '@/utils/rights';
import { ChevronDown } from 'lucide-react'; // Import de l'icône
import Image from 'next/image'; // Import du composant Image
import {
BASE_URL,
} from '@/utils/Url';
const ProfileSelector = ({ onRoleChange, className = '' }) => {
const {
@ -23,9 +20,6 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
user,
setSelectedEstablishmentEvaluationFrequency,
setSelectedEstablishmentTotalCapacity,
selectedEstablishmentLogo,
setSelectedEstablishmentLogo,
setApiDocuseal
} = useEstablishment();
const { isConnected, connectionStatus } = useChatConnection();
const [dropdownOpen, setDropdownOpen] = useState(false);
@ -39,18 +33,12 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
user.roles[roleId].establishment__evaluation_frequency;
const establishmentTotalCapacity =
user.roles[roleId].establishment__total_capacity;
const establishmentLogo =
user.roles[roleId].establishment__logo;
const establishmentApiDocuseal =
user.roles[roleId].establishment__api_docuseal;
setProfileRole(role);
setSelectedEstablishmentId(establishmentId);
setSelectedEstablishmentEvaluationFrequency(
establishmentEvaluationFrequency
);
setSelectedEstablishmentTotalCapacity(establishmentTotalCapacity);
setSelectedEstablishmentLogo(establishmentLogo);
setApiDocuseal(establishmentApiDocuseal);
setSelectedRoleId(roleId);
if (onRoleChange) {
onRoleChange(roleId);
@ -111,14 +99,14 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
<div className={`relative ${className}`}>
<DropdownMenu
buttonContent={
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white h-24">
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white">
<div className="relative">
<Image
src={selectedEstablishmentLogo ? `${BASE_URL}${selectedEstablishmentLogo}` : getGravatarUrl(user?.email)}
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-16 h-16 rounded-full object-cover shadow-md"
width={64}
height={64}
className="w-10 h-10 rounded-full object-cover shadow-md"
width={32}
height={32}
/>
{/* Bulle de statut de connexion au chat */}
<div
@ -128,22 +116,19 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
</div>
<div className="flex-1 min-w-0">
<div
className="font-semibold text-base text-gray-900 text-left truncate max-w-full"
className="font-bold text-left truncate max-w-full"
title={user?.email}
>
{user?.email}
</div>
<div
className="font-semibold text-base text-emerald-700 text-left truncate max-w-full"
title={selectedEstablishment?.name || ''}
>
{selectedEstablishment?.name || ''}
</div>
<div
className="italic text-sm text-emerald-600 text-left truncate max-w-full"
title={getRightStr(selectedEstablishment?.role_type) || ''}
className="text-sm text-gray-500 text-left truncate max-w-full"
title={`${getRightStr(selectedEstablishment?.role_type) || ''}${selectedEstablishment?.name ? ', ' + selectedEstablishment.name : ''}`}
>
{getRightStr(selectedEstablishment?.role_type) || ''}
{selectedEstablishment?.name
? `, ${selectedEstablishment.name}`
: ''}
</div>
</div>
<ChevronDown
@ -165,17 +150,12 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
...establishments.map((establishment) => ({
type: 'item',
label: (
<div className="flex items-center text-left">
<Image
src={establishment.logo ? `${BASE_URL}${establishment.logo}` : getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full object-cover shadow-md mr-3"
width={32}
height={32}
/>
<div>
<div className="font-bold ext-sm text-gray-500">{establishment.name}</div>
<div className="italic text-sm text-gray-500">{getRightStr(establishment.role_type)}</div>
<div className="text-left">
<div className="font-bold">
{getRightStr(establishment.role_type)}
</div>
<div className="text-sm text-gray-500">
{establishment.name}
</div>
</div>
),

View File

@ -35,7 +35,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) {
return (
<div className="w-64 bg-stone-50 border-r h-full border-gray-200">
<div className="border-b border-gray-200 ">
<ProfileSelector className="border-none h-24" />
<ProfileSelector className="border-none" />
</div>
<nav className="space-y-1 px-4 py-6">
{items.map((item) => (

View File

@ -78,10 +78,10 @@ export default function FilesGroupsManagement({
useEffect(() => {
if (selectedEstablishmentId) {
Promise.all([
fetchRegistrationSchoolFileMasters(selectedEstablishmentId),
fetchRegistrationSchoolFileMasters(),
fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationSchoolFileTemplates(selectedEstablishmentId),
fetchRegistrationParentFileMasters(selectedEstablishmentId),
fetchRegistrationSchoolFileTemplates(),
fetchRegistrationParentFileMasters(),
])
.then(
([

View File

@ -2,6 +2,7 @@
import React, { useState } from 'react';
import logger from '@/utils/logger';
import { RecurrenceType } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar/Calendar';
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';

View File

@ -50,10 +50,6 @@ export const EstablishmentProvider = ({ children }) => {
const storedApiDocuseal = sessionStorage.getItem('apiDocuseal');
return storedApiDocuseal ? JSON.parse(storedApiDocuseal) : null;
});
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
return storedLogo ? JSON.parse(storedLogo) : null;
});
// Sauvegarder dans sessionStorage à chaque mise à jour
const setSelectedEstablishmentId = (id) => {
@ -99,11 +95,6 @@ export const EstablishmentProvider = ({ children }) => {
sessionStorage.setItem('apiDocuseal', JSON.stringify(api));
};
const setSelectedEstablishmentLogo = (logo) => {
setSelectedEstablishmentLogoState(logo);
sessionStorage.setItem('selectedEstablishmentLogo', JSON.stringify(logo));
};
/**
* Fonction d'initialisation du contexte avec la session (appelée lors du login)
* @param {*} session
@ -123,7 +114,6 @@ export const EstablishmentProvider = ({ children }) => {
evaluation_frequency: role.establishment__evaluation_frequency,
total_capacity: role.establishment__total_capacity,
api_docuseal: role.establishment__api_docuseal,
logo: role.establishment__logo,
role_id: i,
role_type: role.role_type,
}));
@ -146,9 +136,6 @@ export const EstablishmentProvider = ({ children }) => {
setApiDocuseal(
userEstablishments[roleIndexDefault].api_docuseal
);
setSelectedEstablishmentLogo(
userEstablishments[roleIndexDefault].logo
);
setProfileRole(userEstablishments[roleIndexDefault].role_type);
}
if (endInitFunctionHandler) {
@ -169,7 +156,6 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentEvaluationFrequencyState(null);
setSelectedEstablishmentTotalCapacityState(null);
setApiDocusealState(null);
setSelectedEstablishmentLogoState(null);
sessionStorage.clear();
};
@ -186,8 +172,6 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentTotalCapacity,
apiDocuseal,
setApiDocuseal,
selectedEstablishmentLogo,
setSelectedEstablishmentLogo,
selectedRoleId,
setSelectedRoleId,
profileRole,

View File

@ -33,13 +33,12 @@ export const RecurrenceType = Object.freeze({
export const PlanningModes = Object.freeze({
CLASS_SCHEDULE: 'classSchedule',
PLANNING: 'planning'
PLANNING: 'planning',
});
export function PlanningProvider({
children,
modeSet = PlanningModes.PLANNING,
readOnly = false
}) {
const [events, setEvents] = useState([]);
const [schedules, setSchedules] = useState([]);
@ -47,7 +46,6 @@ export function PlanningProvider({
const [planningMode, setPlanningMode] = useState(modeSet);
const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
const [parentView, setParentView] = useState(readOnly);
const [hiddenSchedules, setHiddenSchedules] = useState([]);
const { selectedEstablishmentId } = useEstablishment();
@ -138,12 +136,10 @@ export function PlanningProvider({
viewType,
setViewType,
hiddenSchedules,
setHiddenSchedules,
toggleScheduleVisibility,
planningMode,
reloadEvents,
reloadPlanning,
parentView
};
return (

View File

@ -129,6 +129,7 @@ export const FE_ADMIN_MESSAGERIE_URL = '/admin/messagerie';
// PARENT HOME
export const FE_PARENTS_HOME_URL = '/parents';
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
export const FE_PARENTS_SETTINGS_URL = '/parents/settings';
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
// API DOCUSEAL

View File

@ -6,11 +6,11 @@ export const RIGHTS = {
export function getRightStr(right) {
if (right === RIGHTS.ADMIN) {
return 'Administrateur';
return 'ADMINISTRATEUR';
} else if (right === RIGHTS.TEACHER) {
return 'Professeur';
return 'PROFESSEUR';
} else if (right === RIGHTS.PARENT) {
return 'Parent';
return 'PARENT';
} else {
return 'NON DEFINI';
}

View File

@ -1,22 +0,0 @@
TZ="Europe/Paris"
TEST_MODE=true
CSRF_COOKIE_SECURE=true
CSRF_COOKIE_DOMAIN=".localhost"
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
BASE_URL=http://localhost:3000
DEBUG=false
EMAIL_HOST="smtp.hostinger.com"
EMAIL_PORT="587"
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=''
EMAIL_USE_TLS=true
EMAIL_USE_SSL=false
DB_NAME="school"
DB_USER="postgres"
DB_PASSWORD="postgres"
DB_HOST="database"
DB_PORT="5432"
URL_DJANGO="http://localhost:8080"
SECRET_KEY="<SIGNINGKEY>"

View File

@ -1,19 +1,15 @@
services:
redis:
image: "redis:latest"
volumes:
- redis-data:/data
image: 'redis:latest'
expose:
- 6379
environment:
- TZ=Europe/Paris
database:
image: "postgres:latest"
image: 'postgres:latest'
expose:
- 5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@ -24,13 +20,17 @@ services:
image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest
ports:
- 8080:8080
env_file: "./conf/backend.env"
environment:
- TZ=Europe/Paris
- TEST_MODE=True
links:
- "database:database"
- "redis:redis"
depends_on:
- redis
- database
volumes:
- ./conf/application.json:/Back-End/Subscriptions/Configuration/application.json
command: python start.py
frontend:
@ -40,8 +40,6 @@ services:
environment:
- TZ=Europe/Paris
- NODE_ENV=production
volumes:
- ./conf/env:/app/.env
volumes:
postgres-data:
redis-data:
- NEXT_PUBLIC_API_URL=http://toto:8080
depends_on:
- backend

View File

@ -1,24 +1,55 @@
services:
redis:
image: "redis:latest"
volumes:
- redis-data:/data
expose:
- 6379
ports:
- 6379:6379
environment:
- TZ=Europe/Paris
database:
image: "postgres:latest"
expose:
- 5432
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: school
TZ: Europe/Paris
docuseal_db:
image: postgres:latest
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DOCUSEAL_DB_HOST: docuseal_db
POSTGRES_DB: docuseal
ports:
- 5433:5432 # port différent si besoin d'accès direct depuis l'hôte
docuseal:
image: docuseal/docuseal:latest
container_name: docuseal_app
depends_on:
- docuseal_db
ports:
- "3001:3000"
environment:
DATABASE_URL: postgresql://postgres:postgres@docuseal_db:5432/docuseal
volumes:
- ./docuseal:/data/docuseal
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "4000:4443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- docuseal
backend:
build:
@ -27,15 +58,35 @@ services:
- 8080:8080
volumes:
- ./Back-End:/Back-End
env_file: "./conf/backend.env"
environment:
- TZ=Europe/Paris
- TEST_MODE=True
- CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
- CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
links:
- "database:database"
- "redis:redis"
depends_on:
- redis
- database
#- docuseal
command: python start.py
# frontend:
# build:
# context: ./Front-End
# args:
# - BUILD_MODE=development
# ports:
# - 3000:3000
# volumes:
# - ./Front-End:/app
# env_file:
# - .env
# environment:
# - TZ=Europe/Paris
# depends_on:
# - backend
volumes:
postgres-data:
redis-data:
caddy_data:
caddy_config:

View File

@ -1,41 +0,0 @@
#!/bin/bash
set -e
# Attendre que le service docuseal soit prêt
until curl -s http://docuseal:3000 > /dev/null; do
echo "Waiting for docuseal to be ready..."
sleep 5
done
encrypt_password() {
local password=$1
ruby -e "require 'bcrypt'; puts BCrypt::Password.create('$password')"
}
# Mot de passe encrypté
ENCRYPTED_PASSWORD=$(encrypt_password "$USER_PASSWORD")
# Insérer les données dans les tables accounts et users si elles n'existent pas déjà
PGPASSWORD=$POSTGRES_PASSWORD psql -v ON_ERROR_STOP=1 --host=database --port=5432 --username="$POSTGRES_USER" --dbname=docuseal <<-EOSQL
-- Vérifier l'existence du compte
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM accounts WHERE name = '$USER_COMPANY') THEN
-- Insérer dans la table accounts
INSERT INTO accounts (name, timezone, locale, created_at, updated_at, uuid) VALUES
('$USER_COMPANY', 'UTC', 'en', NOW(), NOW(), gen_random_uuid());
END IF;
END
\$\$;
-- Vérifier l'existence de l'utilisateur
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM users WHERE email = '$USER_EMAIL') THEN
-- Insérer dans la table users
INSERT INTO users (first_name, last_name, email, encrypted_password, account_id, role, sign_in_count, failed_attempts, created_at, updated_at, uuid, otp_required_for_login) VALUES
('$USER_FIRST_NAME', '$USER_LAST_NAME', '$USER_EMAIL', '$ENCRYPTED_PASSWORD', (SELECT id FROM accounts WHERE name = '$USER_COMPANY'), 'admin', 1, 0, NOW(), NOW(), gen_random_uuid(), false);
END IF;
END
\$\$;
EOSQL

View File

@ -1,6 +1,6 @@
{
"name": "n3wt-school",
"version": "0.0.3",
"version": "0.0.1",
"scripts": {
"prepare": "husky",
"release": "standard-version",