mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-06-04 13:26:11 +00:00
Compare commits
28 Commits
90b0d14418
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 4aacdc1c28 | |||
| 9e8201a1ec | |||
| 40b2c8d1e5 | |||
| 9d97023cae | |||
| cb782fa109 | |||
| 92c3183153 | |||
| db587ec747 | |||
| a81b76ecea | |||
| 4431c428d3 | |||
| 2ef71f99c3 | |||
| f9c0585b30 | |||
| 12939fca85 | |||
| 1f2a1b88ac | |||
| 762dede0af | |||
| ccdbae1c08 | |||
| 2a223fe3dd | |||
| 409cf05f1a | |||
| b0e04e3adc | |||
| 3c7266608d | |||
| 5bbbcb9dc1 | |||
| 053140c8be | |||
| e9a30b7bde | |||
| ff1d113698 | |||
| 12a6ad1d61 | |||
| 856443d4ed | |||
| ace4dcbf07 | |||
| 61f63f9dc9 | |||
| d9e998d2ff |
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:05
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:04
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:04
|
||||||
|
|
||||||
import Establishment.models
|
import Establishment.models
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
|
|||||||
@ -72,11 +72,11 @@ class ChatConsumer(AsyncWebsocketConsumer):
|
|||||||
if presence:
|
if presence:
|
||||||
await self.broadcast_presence_update(self.user_id, 'online')
|
await self.broadcast_presence_update(self.user_id, 'online')
|
||||||
|
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
# Envoyer les statuts de présence existants des autres utilisateurs connectés
|
# Envoyer les statuts de présence existants des autres utilisateurs connectés
|
||||||
await self.send_existing_user_presences()
|
await self.send_existing_user_presences()
|
||||||
|
|
||||||
await self.accept()
|
|
||||||
|
|
||||||
logger.info(f"User {self.user_id} connected to chat")
|
logger.info(f"User {self.user_id} connected to chat")
|
||||||
|
|
||||||
async def send_existing_user_presences(self):
|
async def send_existing_user_presences(self):
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from .models import Planning, Events, RecursionType
|
|||||||
from .serializers import PlanningSerializer, EventsSerializer
|
from .serializers import PlanningSerializer, EventsSerializer
|
||||||
|
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
from Subscriptions.util import getCurrentSchoolYear
|
||||||
|
|
||||||
|
|
||||||
class PlanningView(APIView):
|
class PlanningView(APIView):
|
||||||
@ -17,6 +18,7 @@ class PlanningView(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
current_school_year = getCurrentSchoolYear()
|
||||||
|
|
||||||
plannings = bdd.getAllObjects(Planning)
|
plannings = bdd.getAllObjects(Planning)
|
||||||
|
|
||||||
@ -25,7 +27,10 @@ class PlanningView(APIView):
|
|||||||
|
|
||||||
# Filtrer en fonction du planning_mode
|
# Filtrer en fonction du planning_mode
|
||||||
if planning_mode == "classSchedule":
|
if planning_mode == "classSchedule":
|
||||||
plannings = plannings.filter(school_class__isnull=False)
|
plannings = plannings.filter(
|
||||||
|
school_class__isnull=False,
|
||||||
|
school_class__school_year=current_school_year,
|
||||||
|
)
|
||||||
elif planning_mode == "planning":
|
elif planning_mode == "planning":
|
||||||
plannings = plannings.filter(school_class__isnull=True)
|
plannings = plannings.filter(school_class__isnull=True)
|
||||||
|
|
||||||
@ -79,6 +84,7 @@ class EventsView(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
planning_mode = request.GET.get('planning_mode', None)
|
planning_mode = request.GET.get('planning_mode', None)
|
||||||
|
current_school_year = getCurrentSchoolYear()
|
||||||
filterParams = {}
|
filterParams = {}
|
||||||
plannings=[]
|
plannings=[]
|
||||||
events = Events.objects.all()
|
events = Events.objects.all()
|
||||||
@ -86,6 +92,8 @@ class EventsView(APIView):
|
|||||||
filterParams['establishment'] = establishment_id
|
filterParams['establishment'] = establishment_id
|
||||||
if planning_mode is not None:
|
if planning_mode is not None:
|
||||||
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
||||||
|
if planning_mode == "classSchedule":
|
||||||
|
filterParams['school_class__school_year'] = current_school_year
|
||||||
if filterParams:
|
if filterParams:
|
||||||
plannings = Planning.objects.filter(**filterParams)
|
plannings = Planning.objects.filter(**filterParams)
|
||||||
events = Events.objects.filter(planning__in=plannings)
|
events = Events.objects.filter(planning__in=plannings)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:05
|
||||||
|
|
||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@ -21,7 +21,6 @@ class Speciality(models.Model):
|
|||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
color_code = models.CharField(max_length=7, default='#FFFFFF')
|
color_code = models.CharField(max_length=7, default='#FFFFFF')
|
||||||
school_year = models.CharField(max_length=9, blank=True)
|
|
||||||
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='specialities')
|
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='specialities')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -32,7 +31,6 @@ class Teacher(models.Model):
|
|||||||
first_name = models.CharField(max_length=100)
|
first_name = models.CharField(max_length=100)
|
||||||
specialities = models.ManyToManyField(Speciality, blank=True)
|
specialities = models.ManyToManyField(Speciality, blank=True)
|
||||||
profile_role = models.OneToOneField('Auth.ProfileRole', on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True)
|
profile_role = models.OneToOneField('Auth.ProfileRole', on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True)
|
||||||
school_year = models.CharField(max_length=9, blank=True)
|
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -73,15 +73,12 @@ class SpecialityListCreateView(APIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
establishment_id = request.GET.get('establishment_id', None)
|
establishment_id = request.GET.get('establishment_id', None)
|
||||||
school_year = request.GET.get('school_year', None)
|
|
||||||
if establishment_id is None:
|
if establishment_id is None:
|
||||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
specialities_list = getAllObjects(Speciality)
|
specialities_list = getAllObjects(Speciality)
|
||||||
if establishment_id:
|
if establishment_id:
|
||||||
specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct()
|
specialities_list = specialities_list.filter(establishment__id=establishment_id).distinct()
|
||||||
if school_year:
|
|
||||||
specialities_list = specialities_list.filter(school_year=school_year)
|
|
||||||
specialities_serializer = SpecialitySerializer(specialities_list, many=True)
|
specialities_serializer = SpecialitySerializer(specialities_list, many=True)
|
||||||
return JsonResponse(specialities_serializer.data, safe=False)
|
return JsonResponse(specialities_serializer.data, safe=False)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:04
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.3 on 2026-04-04 09:15
|
# Generated by Django 5.1.3 on 2026-04-05 14:04
|
||||||
|
|
||||||
import Subscriptions.models
|
import Subscriptions.models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -53,7 +53,9 @@ class Migration(migrations.Migration):
|
|||||||
('name', 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)),
|
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
|
||||||
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
|
('formTemplateData', models.JSONField(blank=True, default=list, null=True)),
|
||||||
('isValidated', models.BooleanField(default=False)),
|
('isValidated', models.BooleanField(blank=True, default=None, null=True)),
|
||||||
|
('electronic_signature', models.TextField(blank=True, help_text='Signature électronique encodée en base64', null=True)),
|
||||||
|
('electronic_signature_date', models.DateTimeField(blank=True, help_text='Date de la signature électronique', null=True)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -169,6 +171,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(default='', max_length=255)),
|
('name', models.CharField(default='', max_length=255)),
|
||||||
('is_required', models.BooleanField(default=False)),
|
('is_required', models.BooleanField(default=False)),
|
||||||
|
('requires_electronic_signature', models.BooleanField(default=False, help_text='Si activé, le parent devra signer électroniquement ce document')),
|
||||||
('formMasterData', models.JSONField(blank=True, default=list, null=True)),
|
('formMasterData', models.JSONField(blank=True, default=list, null=True)),
|
||||||
('file', models.FileField(blank=True, help_text='Fichier du formulaire existant (PDF, DOC, etc.)', null=True, upload_to=Subscriptions.models.registration_school_file_master_upload_to)),
|
('file', models.FileField(blank=True, help_text='Fichier du formulaire existant (PDF, DOC, etc.)', null=True, upload_to=Subscriptions.models.registration_school_file_master_upload_to)),
|
||||||
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')),
|
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_masters', to='Establishment.establishment')),
|
||||||
@ -200,7 +203,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
||||||
('isValidated', models.BooleanField(default=False)),
|
('isValidated', models.BooleanField(blank=True, default=None, null=True)),
|
||||||
('master', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationparentfilemaster')),
|
('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')),
|
('registration_form', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationform')),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -54,17 +54,38 @@ class Sibling(models.Model):
|
|||||||
return "SIBLING"
|
return "SIBLING"
|
||||||
|
|
||||||
def registration_photo_upload_to(instance, filename):
|
def registration_photo_upload_to(instance, filename):
|
||||||
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
"""
|
||||||
|
Génère le chemin de stockage pour la photo élève.
|
||||||
|
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||||
|
"""
|
||||||
|
register_form = getattr(instance, 'registrationform', None)
|
||||||
|
if register_form and register_form.establishment:
|
||||||
|
est_name = register_form.establishment.name
|
||||||
|
elif instance.associated_class and instance.associated_class.establishment:
|
||||||
|
est_name = instance.associated_class.establishment.name
|
||||||
|
else:
|
||||||
|
est_name = "unknown_establishment"
|
||||||
|
|
||||||
|
student_last = instance.last_name if instance and instance.last_name else "unknown"
|
||||||
|
student_first = instance.first_name if instance and instance.first_name else "unknown"
|
||||||
|
return f"{est_name}/dossier_{student_last}_{student_first}/{filename}"
|
||||||
|
|
||||||
def registration_bilan_form_upload_to(instance, filename):
|
def registration_bilan_form_upload_to(instance, filename):
|
||||||
# On récupère le RegistrationForm lié à l'élève
|
"""
|
||||||
|
Génère le chemin de stockage pour les bilans de compétences.
|
||||||
|
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||||
|
"""
|
||||||
register_form = getattr(instance.student, 'registrationform', None)
|
register_form = getattr(instance.student, 'registrationform', None)
|
||||||
if register_form:
|
if register_form and register_form.establishment:
|
||||||
pk = register_form.pk
|
est_name = register_form.establishment.name
|
||||||
|
elif instance.student.associated_class and instance.student.associated_class.establishment:
|
||||||
|
est_name = instance.student.associated_class.establishment.name
|
||||||
else:
|
else:
|
||||||
# fallback sur l'id de l'élève si pas de registrationform
|
est_name = "unknown_establishment"
|
||||||
pk = instance.student.pk
|
|
||||||
return f"registration_files/dossier_rf_{pk}/bilan/{filename}"
|
student_last = instance.student.last_name if instance.student else "unknown"
|
||||||
|
student_first = instance.student.first_name if instance.student else "unknown"
|
||||||
|
return f"{est_name}/dossier_{student_last}_{student_first}/{filename}"
|
||||||
|
|
||||||
class BilanCompetence(models.Model):
|
class BilanCompetence(models.Model):
|
||||||
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||||
@ -363,6 +384,7 @@ class RegistrationSchoolFileMaster(models.Model):
|
|||||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
is_required = models.BooleanField(default=False)
|
is_required = models.BooleanField(default=False)
|
||||||
|
requires_electronic_signature = models.BooleanField(default=False, help_text="Si activé, le parent devra signer électroniquement ce document")
|
||||||
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
upload_to=registration_school_file_master_upload_to,
|
upload_to=registration_school_file_master_upload_to,
|
||||||
@ -537,6 +559,9 @@ class RegistrationSchoolFileTemplate(models.Model):
|
|||||||
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
||||||
# Tri-etat: None=en attente, True=valide, False=refuse
|
# Tri-etat: None=en attente, True=valide, False=refuse
|
||||||
isValidated = models.BooleanField(null=True, blank=True, default=None)
|
isValidated = models.BooleanField(null=True, blank=True, default=None)
|
||||||
|
# Signature électronique (base64 SVG ou PNG)
|
||||||
|
electronic_signature = models.TextField(null=True, blank=True, help_text="Signature électronique encodée en base64")
|
||||||
|
electronic_signature_date = models.DateTimeField(null=True, blank=True, help_text="Date de la signature électronique")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@ -21,7 +21,6 @@ from N3wtSchool import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pytz
|
import pytz
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
from N3wtSchool.mailManager import sendRegisterForm
|
|
||||||
|
|
||||||
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
||||||
student_name = serializers.SerializerMethodField()
|
student_name = serializers.SerializerMethodField()
|
||||||
@ -58,6 +57,8 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
|
|||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
file_url = serializers.SerializerMethodField()
|
file_url = serializers.SerializerMethodField()
|
||||||
master_file_url = serializers.SerializerMethodField()
|
master_file_url = serializers.SerializerMethodField()
|
||||||
|
requires_electronic_signature = serializers.SerializerMethodField()
|
||||||
|
is_electronically_signed = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RegistrationSchoolFileTemplate
|
model = RegistrationSchoolFileTemplate
|
||||||
@ -73,6 +74,27 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
|
|||||||
return obj.master.file.url
|
return obj.master.file.url
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_requires_electronic_signature(self, obj):
|
||||||
|
# Retourne si le document nécessite une signature électronique
|
||||||
|
if obj.master:
|
||||||
|
return obj.master.requires_electronic_signature
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_is_electronically_signed(self, obj):
|
||||||
|
# Retourne True si le document a été signé électroniquement
|
||||||
|
return bool(obj.electronic_signature)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# Auto-remplir la date de signature si electronic_signature est fournie
|
||||||
|
from django.utils import timezone
|
||||||
|
if 'electronic_signature' in validated_data and validated_data['electronic_signature']:
|
||||||
|
# Nouvelle signature ou re-signature : enregistrer la date
|
||||||
|
validated_data['electronic_signature_date'] = timezone.now()
|
||||||
|
# Si le document était refusé, le repasser en attente de validation
|
||||||
|
if instance.isValidated == False:
|
||||||
|
validated_data['isValidated'] = None
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
|
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
file_url = serializers.SerializerMethodField()
|
file_url = serializers.SerializerMethodField()
|
||||||
@ -228,14 +250,6 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
||||||
profile_role_serializer.is_valid(raise_exception=True)
|
profile_role_serializer.is_valid(raise_exception=True)
|
||||||
profile_role = profile_role_serializer.save()
|
profile_role = profile_role_serializer.save()
|
||||||
# Envoi du mail d'inscription si un nouveau profil vient d'être créé
|
|
||||||
email = None
|
|
||||||
if profile_data and 'email' in profile_data:
|
|
||||||
email = profile_data['email']
|
|
||||||
elif profile_role and profile_role.profile:
|
|
||||||
email = profile_role.profile.email
|
|
||||||
if email:
|
|
||||||
sendRegisterForm(email, establishment_id)
|
|
||||||
elif profile_role:
|
elif profile_role:
|
||||||
# Récupérer un ProfileRole existant par son ID
|
# Récupérer un ProfileRole existant par son ID
|
||||||
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
profile_role = ProfileRole.objects.get(id=profile_role.id)
|
||||||
|
|||||||
@ -4,9 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Bilan de compétences</title>
|
<title>Bilan de compétences</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; margin: 2em; }
|
body { font-family: Arial, sans-serif; margin: 1.2em; color: #111827; }
|
||||||
h1, h2 { color: #059669; }
|
h1, h2 { color: #059669; }
|
||||||
.student-info { margin-bottom: 2em; }
|
.top-header { width: 100%; border-bottom: 2px solid #d1fae5; border-collapse: collapse; margin-bottom: 14px; }
|
||||||
|
.top-header td { vertical-align: top; border: none; padding: 0; }
|
||||||
|
.school-logo { width: 54px; height: 54px; object-fit: contain; margin-right: 8px; }
|
||||||
|
.product-logo { width: 58px; }
|
||||||
|
.title-row { margin: 8px 0 10px 0; }
|
||||||
|
.student-info {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.student-info table { width: 100%; border-collapse: collapse; font-size: 0.98em; }
|
||||||
|
.student-info td { border: none; padding: 1px 0; }
|
||||||
.domain-table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
|
.domain-table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
|
||||||
.domain-header th {
|
.domain-header th {
|
||||||
background: #d1fae5;
|
background: #d1fae5;
|
||||||
@ -25,16 +38,77 @@
|
|||||||
th, td { border: 1px solid #e5e7eb; padding: 0.5em; }
|
th, td { border: 1px solid #e5e7eb; padding: 0.5em; }
|
||||||
th.competence-header { background: #d1fae5; }
|
th.competence-header { background: #d1fae5; }
|
||||||
td.competence-nom { word-break: break-word; max-width: 320px; }
|
td.competence-nom { word-break: break-word; max-width: 320px; }
|
||||||
|
.footer-note { margin-top: 32px; }
|
||||||
|
.comment-space {
|
||||||
|
min-height: 180px;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 78px;
|
||||||
|
}
|
||||||
|
.footer-grid { width: 100%; border-collapse: collapse; }
|
||||||
|
.footer-grid td {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.field-line { border-bottom: 1px solid #9ca3af; height: 24px; margin-top: 6px; }
|
||||||
|
.signature-line { border-bottom: 2px solid #059669; height: 30px; margin-top: 6px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Bilan de compétences</h1>
|
<table class="top-header">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 68%; padding-bottom: 8px;">
|
||||||
|
<table style="border-collapse: collapse; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
{% if establishment.logo_path %}
|
||||||
|
<td style="width: 64px; border: none; vertical-align: top;">
|
||||||
|
<img src="{{ establishment.logo_path }}" alt="Logo établissement" class="school-logo">
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td style="border: none; vertical-align: top;">
|
||||||
|
<div style="font-size: 1.25em; font-weight: 700; color: #065f46; margin-top: 2px;">{{ establishment.name }}</div>
|
||||||
|
{% if establishment.address %}
|
||||||
|
<div style="font-size: 0.9em; color: #4b5563; margin-top: 4px;">{{ establishment.address }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td style="width: 32%; text-align: right; padding-bottom: 8px;">
|
||||||
|
<table style="border-collapse: collapse; width: 100%; margin-left: auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; text-align: right;">
|
||||||
|
<div style="font-size: 0.86em; color: #6b7280; margin-bottom: 4px;">Généré avec</div>
|
||||||
|
{% if product.logo_path %}
|
||||||
|
<div style="margin-bottom: 4px;"><img src="{{ product.logo_path }}" alt="Logo n3wt" class="product-logo"></div>
|
||||||
|
{% endif %}
|
||||||
|
<div style="font-size: 0.95em; font-weight: 700; color: #059669;">{{ product.name }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="title-row">
|
||||||
|
<h1>Bilan de compétences</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="student-info">
|
<div class="student-info">
|
||||||
<strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}<br>
|
<table>
|
||||||
<strong>Niveau :</strong> {{ student.level }}<br>
|
<tr>
|
||||||
<strong>Classe :</strong> {{ student.class_name }}<br>
|
<td><strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}</td>
|
||||||
<strong>Période :</strong> {{ period }}<br>
|
<td style="text-align: right;"><strong>Date :</strong> {{ date }}</td>
|
||||||
<strong>Date :</strong> {{ date }}
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Niveau :</strong> {{ student.level }}</td>
|
||||||
|
<td style="text-align: right;"><strong>Période :</strong> {{ period }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Classe :</strong> {{ student.class_name }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for domaine in domaines %}
|
{% for domaine in domaines %}
|
||||||
@ -72,41 +146,33 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div style="margin-top: 60px; padding: 0; max-width: 700px;">
|
<div class="footer-note">
|
||||||
<div style="
|
<div style="font-weight: 700; color: #059669; font-size: 1.1em;">
|
||||||
min-height: 180px;
|
Appréciation générale / Commentaire
|
||||||
background: #fff;
|
|
||||||
border: 1.5px dashed #a7f3d0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px 24px 18px 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 64px; /* Augmente l'espace après l'encadré */
|
|
||||||
">
|
|
||||||
<div style="font-weight: bold; color: #059669; font-size: 1.25em; display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<span>Appréciation générale / Commentaire : </span>
|
|
||||||
</div>
|
|
||||||
<!-- Espace vide pour écrire -->
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 48px; margin-top: 32px;">
|
|
||||||
<div>
|
|
||||||
<span style="font-weight: bold; color: #059669;font-size: 1.25em;">Date :</span>
|
|
||||||
<span style="display: inline-block; min-width: 120px; border-bottom: 1.5px solid #a7f3d0; margin-left: 8px;"> </span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="font-weight: bold; color: #059669;font-size: 1.25em;">Signature :</span>
|
|
||||||
<span style="display: inline-block; min-width: 180px; border-bottom: 2px solid #059669; margin-left: 8px;"> </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="comment-space"></div>
|
||||||
|
|
||||||
|
<table class="footer-grid">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 45%;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 70px; border: none; font-weight: 700; color: #059669;">Date :</td>
|
||||||
|
<td style="border: none;"><div class="field-line"></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td style="width: 10%;"></td>
|
||||||
|
<td style="width: 45%;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 90px; border: none; font-weight: 700; color: #059669;">Signature :</td>
|
||||||
|
<td style="border: none;"><div class="signature-line"></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
245
Back-End/Subscriptions/templates/pdfs/dynamic_form.html
Normal file
245
Back-End/Subscriptions/templates/pdfs/dynamic_form.html
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>{{ pdf_title }}</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1.4cm 1.6cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.35;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
border-left: 1px dashed #cbd5e1;
|
||||||
|
border-right: 1px dashed #cbd5e1;
|
||||||
|
padding: 0 22px 8px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading1 {
|
||||||
|
font-size: 26pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 18px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading2 {
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 14px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading3 {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading4,
|
||||||
|
.heading5,
|
||||||
|
.heading6 {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-block {
|
||||||
|
margin: 10px 0 14px 0;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #475569;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 8px 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-prefix {
|
||||||
|
width: 78px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-value {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-card {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-page {
|
||||||
|
margin: 8px 0;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-page img {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-file {
|
||||||
|
border: 2px dashed #94a3b8;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 18px 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-box {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
height: 84px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-box img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 70px;
|
||||||
|
margin: 6px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-shell">
|
||||||
|
<h1 class="title">{{ pdf_title }}</h1>
|
||||||
|
|
||||||
|
{% for field in fields %}
|
||||||
|
{% if field.kind == 'heading' %}
|
||||||
|
{% if field.level == 'heading1' %}
|
||||||
|
<div class="heading1">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading2' %}
|
||||||
|
<div class="heading2">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading3' %}
|
||||||
|
<div class="heading3">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading4' %}
|
||||||
|
<div class="heading4">{{ field.text }}</div>
|
||||||
|
{% elif field.level == 'heading5' %}
|
||||||
|
<div class="heading5">{{ field.text }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="heading6">{{ field.text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.kind == 'paragraph' %}
|
||||||
|
<p class="paragraph">{{ field.text }}</p>
|
||||||
|
{% elif field.kind == 'file' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="file-card">
|
||||||
|
{% if field.has_preview %}
|
||||||
|
{% for preview in field.preview_pages %}
|
||||||
|
<div class="preview-page">
|
||||||
|
<img src="{{ preview.src }}" alt="{{ preview.alt }}" width="{{ preview.width }}" height="{{ preview.height }}" />
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if field.total_pages > 1 %}
|
||||||
|
<div class="preview-meta">Aperçu de la première page sur {{ field.total_pages }} pages</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-file">Aucun document source fourni</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif field.kind == 'phone' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<table class="phone-table">
|
||||||
|
<tr>
|
||||||
|
<td class="phone-prefix">{{ field.prefix }}</td>
|
||||||
|
<td class="phone-value">
|
||||||
|
{% if field.value %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% else %}
|
||||||
|
<span class="input-placeholder"> </span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% elif field.kind == 'signature' %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="signature-box">
|
||||||
|
{% if field.signature_src %}
|
||||||
|
<img src="{{ field.signature_src }}" alt="Signature" />
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="field-block">
|
||||||
|
<div class="field-label">{{ field.label }}</div>
|
||||||
|
<div class="input-box">
|
||||||
|
{% if field.value %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% else %}
|
||||||
|
<span class="input-placeholder"> </span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if field.field_type %}
|
||||||
|
<div class="field-meta">Type: {{ field.field_type }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -175,13 +175,43 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
domaine_dict["categories"].append(categorie_dict)
|
domaine_dict["categories"].append(categorie_dict)
|
||||||
if domaine_dict["categories"]:
|
if domaine_dict["categories"]:
|
||||||
result.append(domaine_dict)
|
result.append(domaine_dict)
|
||||||
|
|
||||||
|
establishment = None
|
||||||
|
if student.associated_class and student.associated_class.establishment:
|
||||||
|
establishment = student.associated_class.establishment
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
establishment = student.registrationform.establishment
|
||||||
|
except Exception:
|
||||||
|
establishment = None
|
||||||
|
|
||||||
|
establishment_logo_path = None
|
||||||
|
if establishment and establishment.logo:
|
||||||
|
try:
|
||||||
|
if establishment.logo.path and os.path.exists(establishment.logo.path):
|
||||||
|
establishment_logo_path = establishment.logo.path
|
||||||
|
except Exception:
|
||||||
|
establishment_logo_path = None
|
||||||
|
|
||||||
|
n3wt_logo_path = os.path.join(settings.BASE_DIR, 'static', 'img', 'logo_min.svg')
|
||||||
|
if not os.path.exists(n3wt_logo_path):
|
||||||
|
n3wt_logo_path = None
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"student": {
|
"student": {
|
||||||
"first_name": student.first_name,
|
"first_name": student.first_name,
|
||||||
"last_name": student.last_name,
|
"last_name": student.last_name,
|
||||||
"level": student.level,
|
"level": student.level,
|
||||||
"class_name": student.associated_class.atmosphere_name,
|
"class_name": student.associated_class.atmosphere_name if student.associated_class else "Non assignée",
|
||||||
|
},
|
||||||
|
"establishment": {
|
||||||
|
"name": establishment.name if establishment else "Établissement",
|
||||||
|
"address": establishment.address if establishment else "",
|
||||||
|
"logo_path": establishment_logo_path,
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"name": "n3wt-school",
|
||||||
|
"logo_path": n3wt_logo_path,
|
||||||
},
|
},
|
||||||
"period": period,
|
"period": period,
|
||||||
"date": date.today().strftime("%d/%m/%Y"),
|
"date": date.today().strftime("%d/%m/%Y"),
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
__version__ = "0.0.3"
|
__version__ = "1.0.0"
|
||||||
|
|||||||
7
Back-End/static/img/n3wt.svg
Normal file
7
Back-End/static/img/n3wt.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="420" height="120" viewBox="0 0 420 120" role="img" aria-label="n3wt-school">
|
||||||
|
<rect width="420" height="120" rx="16" fill="#F0FDF4"/>
|
||||||
|
<circle cx="56" cy="60" r="30" fill="#10B981"/>
|
||||||
|
<path d="M42 60h28M56 46v28" stroke="#064E3B" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<text x="104" y="70" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#064E3B">n3wt</text>
|
||||||
|
<text x="245" y="70" font-family="Arial, sans-serif" font-size="30" font-weight="600" fill="#059669">school</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 561 B |
70
CHANGELOG.md
70
CHANGELOG.md
@ -2,6 +2,76 @@
|
|||||||
|
|
||||||
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
### [1.0.0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.3...1.0.0) (2026-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* **design-system:** add design system documentation and AI agent instructions ([cb76a23](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb76a23d02a9c0f27c9403e4d06cebfcc65d886b))
|
||||||
|
|
||||||
|
|
||||||
|
### Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
* Gestion de l'arborescence des documents d'école en fonction des requêtes CRUD [N3WTS-17] ([abb4b52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/abb4b525b296ba01fce037c6c63fb7766cfbc9b3))
|
||||||
|
* Ajout bouton de refus de dossier avec zone de saisie de motif [N3WTS-2] ([3779a47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3779a474171d7df716e3b0d06a26f7f9b69356fa))
|
||||||
|
* Ajout d'un système d'historisation et d'export de données en CSV [N3WTS-5] ([f091fa0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f091fa0432135bb5478793ea92b97d13c3c68a2f))
|
||||||
|
* Ajout d'un système de notation par classe et par matière et par élève [N3WTS-6] ([905fa5d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/905fa5dbfb3d50710a3aa04d9b28664c1c86b991))
|
||||||
|
* Ajout de la commande npm permettant de creer un etablissement ([09b1541](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/09b1541dc83b28dcba0aead633755d58e9a534fa))
|
||||||
|
* Ajout des composants manquant dans le FormTemplateBuilder [N3WTS-17] ([5e62ee5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5e62ee510074e30cd33802aae20d3d1c297619a3))
|
||||||
|
* Ajout FormTemplateBuilder [N3WTS-17] ([e89d2fc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e89d2fc4c34a99c6b67827bb895ad31a5eff10e6))
|
||||||
|
* **backend,frontend:** régénération et visualisation inline de la fiche élève PDF ([e37aee2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e37aee2abcf7ed69145d217a30e2dce037d933d0))
|
||||||
|
* Changement du rendu de la page des documents + gestion des ([12f5fc7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/12f5fc7aa9de10fc5591495b9ede478593c1c21a))
|
||||||
|
* creation d'un FormRenderer.js pour creer un formulaire dynamique [NEWTS-17] ([9481a01](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9481a0132d2e1f18da5fc632d924adde04d316d4))
|
||||||
|
* Début de suppression de docuseal côté Front [#N3WTS-17] ([1e5bc6c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1e5bc6ccba9053246e4767aa6c2da46ff776203e)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Envoi mail d'inscription au second responsable [N3WTS-1] ([d66db1b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d66db1b019f2480a8a418acc982a6f9132ff3342))
|
||||||
|
* Envoi mail d'inscription aux enseignants [N3WTS-1] ([bd7dc2b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/bd7dc2b0c228b26e31c930342679e30aafa101e1))
|
||||||
|
* Finalisation de la validation / refus des documents signés par les parents [N3WTS-2] ([0501c1d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0501c1dd7320d734ba6ae0b93ad4a270b903a5df))
|
||||||
|
* Finalisation formulaire dynamique ([90b0d14](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/90b0d144184a1c402aedfe3f70647c4bb43f8c37))
|
||||||
|
* **frontend:** fusion liste des frais et message compte existant [#NEWTS-9] ([e30a41a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e30a41a58b199b68eca687a159736b2025e9f22d)), closes [#NEWTS-9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-9)
|
||||||
|
* **frontend:** refonte mobile planning et ameliorations suivi pedagogique [#NEWTS-4] ([4248a58](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4248a589c5f300fe0702b1c20cae5abaeba32688)), closes [#NEWTS-4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-4) [#NEWTS-4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/NEWTS-4)
|
||||||
|
* Gestion de l'affichage des documents validés et non validés sur la page parent [N3WTS-2] ([4f7d7d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f7d7d002442bb0cb04063734d97f993a40474a7))
|
||||||
|
* Gestion de la sidebar [N3WTS-8] ([ddcaba3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ddcaba382e6573f4a628a691bc0f20226a954fd7))
|
||||||
|
* Gestion du refus définitif d'un dossier [N3WTS-2] ([2fef6d6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2fef6d61a4d38f82c28587a9642aef65b5fd387a))
|
||||||
|
* lister uniquement les élèves inscrits dans une classe [N3WTS-6] ([6fb3c5c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6fb3c5cdb40f05b9c34b96334e785bc184751e4f))
|
||||||
|
* Mise à jour de la page parent ([2d678b7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2d678b732f1be19b4653fbe68486455326da2157))
|
||||||
|
* Page Inscriptions : suppression de la possibilité de créer un nouveau DI [N3WTS-8] ([195579e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/195579e21710427d303e10c97b4a4438ec381da4))
|
||||||
|
* Page Structure : suppression de la possibilité de faire des actions d'admin [N3WTS-8] ([05c68eb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05c68ebfaa0ff30a70ca9c42bcb306261c0fbb85))
|
||||||
|
* Précablage du formulaire dynamique [N3WTS-17] ([dd00cba](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dd00cba385a131ed7db66656738340234bcf5e73))
|
||||||
|
* push test [#N3WTS-17] ([0fb668b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0fb668b21284a682a8dd5addd14e47c580723f13)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Réorganisation items dans la page [N3WTS-17] ([8549699](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8549699dec253a9763a4074e05130c4d7d40ac6e))
|
||||||
|
* Sauvegarde des formulaires d'école dans les bons dossiers / ([b4f70e6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b4f70e6bad2fa31b6c1fd68d8538962b9d5e2650))
|
||||||
|
* Securisation du Backend ([fa84309](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fa843097ba9490c5083b652ec2dc9a9b28c096a0))
|
||||||
|
* Securisation du téléchargement de fichier ([a329126](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a3291262d80d2bddfc11412f55bc271ea6bf8219))
|
||||||
|
* Traitement de clonages des templates de documents dans le back ([7486f6c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7486f6c5ce6c5aac2b051469903ef7b2936f5634)), closes [#N3WTS-17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/N3WTS-17)
|
||||||
|
* Validation document par document [N3WTS-2] ([8fd1b62](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8fd1b62ec09a663f40262b406c2f17f64a8d5a2f))
|
||||||
|
* WIP finalisation partie signature des parents [N3WTS-17] ([9dff32b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9dff32b388fd27a899a44e3424bb7d24b2b26afd))
|
||||||
|
|
||||||
|
|
||||||
|
### Corrections de bugs
|
||||||
|
|
||||||
|
* Boutons de navigation + mise en page de l'aperçu du formulaire dynamique ([762dede](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/762dede0af37c41b2c1616216bd9f986d1e0c57f))
|
||||||
|
* Changement des niveaux de logs [N3WTS-1] ([26d4b56](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/26d4b5633f7ebf5630105c3e9db802cdaeee4e37))
|
||||||
|
* Chat getSession + passage en asyn ces getWebSocketUrl et connectToChat ([409cf05](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/409cf05f1a4554263d5f08185bce97d9425287fa))
|
||||||
|
* coorection démarrage ([2579af9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2579af9b8b397d24e66ee08365e345f81bfb00cf))
|
||||||
|
* Coquille [N3WTS-17] ([a034149](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a034149eae7f3b746af32c392499217cf73ab582))
|
||||||
|
* correction du téléchargement du fichier ([053140c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/053140c8be1e10ac9b127cfb47b5691e70dd26e0))
|
||||||
|
* Edition d'un teacher, champ email désactivé [N3WTS-1] ([176edc5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/176edc5c452fa7b03da266f5a227c730ff4ad525))
|
||||||
|
* Emploi du temps pour les classes de l'année scolaire en cours ([5bbbcb9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5bbbcb9dc1f688cd68f47f8f7629aa03678ba582))
|
||||||
|
* Lint ([2dc0dfa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2dc0dfa26889b319f8c6cfb08b1701f8540d06ed))
|
||||||
|
* messagerie ([a81b76e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a81b76eceac0919fc1873cdf8362e5fe55b97adb))
|
||||||
|
* Mise à jour des plannings ([12939fc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/12939fca85b139a2b0178b11a4a29ee5e97800ff))
|
||||||
|
* Mise en page sur absence de frais ou de tarifs lors de la création d'un DI ([ccdbae1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ccdbae1c08e1d1edb3bc1e3e14e8244f196cac60))
|
||||||
|
* Mise en place de l'auto reload pour Daphne [[#65](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/65)] ([7f002e2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f002e2e6a93d651a8d9c2a03baa0f1035844f96))
|
||||||
|
* Ne pas envoyer de mail lors de la création d'un DI ([3c72666](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c7266608d45cd1650775c9a2060dd1007280096))
|
||||||
|
* On n'historise plus les matières ni les enseignants ([f9c0585](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f9c0585b3061a6ae4a8978c9f0329c8710f9bd60))
|
||||||
|
* Réintégration du bouton de Bilan de compétence + harmonisation des paths d'upload de fichier ([2a223fe](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2a223fe3dd9dfae71117f0f47c07aee8a8893de5))
|
||||||
|
* Revue des modales de création de groupes / formulaire ([1f2a1b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1f2a1b88acab312dc90dcf4ba4d55048a3f302d9))
|
||||||
|
* sélection enseignants dans les plannings ([4431c42](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4431c428d3709ef3b2e8f66edd13beed73e42709))
|
||||||
|
* signature électronique ([92c3183](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92c318315335d126ccd6d8637c0381a3367b7a4f))
|
||||||
|
* Suppression d'un PROFILE si aucun PROFILE_ROLE n'y est associé [N3WTS-1] ([92c6a31](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92c6a31740b930fb5363b9dbf91b664f78c7f1a6))
|
||||||
|
* Suppression envoi mail / création page feedback ([4c56cb6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c56cb647470f86151b1ff78c318e0b6468f009d))
|
||||||
|
* Upload document ([b0e04e3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b0e04e3adc480005b0d0d74bbc0ac28f1f9d6e4b))
|
||||||
|
|
||||||
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.3",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@ -3,16 +3,19 @@ import Logo from '../components/Logo';
|
|||||||
|
|
||||||
export default function Custom500() {
|
export default function Custom500() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
<div className="flex items-center justify-center min-h-screen bg-primary">
|
||||||
<div className="text-center p-6 ">
|
<div className="text-center p-6 bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
<h2 className="font-headline text-2xl font-bold text-secondary mb-4">
|
||||||
500 | Erreur interne
|
500 | Erreur interne
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-emerald-900 mb-4">
|
<p className="font-body text-gray-600 mb-4">
|
||||||
Une erreur interne est survenue.
|
Une erreur interne est survenue.
|
||||||
</p>
|
</p>
|
||||||
<Link className="text-gray-900 hover:underline" href="/">
|
<Link
|
||||||
|
className="inline-flex items-center justify-center min-h-[44px] px-4 py-2 rounded font-label font-medium bg-primary hover:bg-secondary text-white transition-colors"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
Retour Accueil
|
Retour Accueil
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,8 +2,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
|
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
|
||||||
import { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
|
import {
|
||||||
|
Trash2,
|
||||||
|
ToggleLeft,
|
||||||
|
ToggleRight,
|
||||||
|
Info,
|
||||||
|
XCircle,
|
||||||
|
Users,
|
||||||
|
UserPlus,
|
||||||
|
} from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import EmptyState from '@/components/EmptyState';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import StatusLabel from '@/components/StatusLabel';
|
import StatusLabel from '@/components/StatusLabel';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
@ -17,7 +26,6 @@ import { dissociateGuardian } from '@/app/actions/subscriptionAction';
|
|||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
|
||||||
|
|
||||||
const roleTypeToLabel = (roleType) => {
|
const roleTypeToLabel = (roleType) => {
|
||||||
switch (roleType) {
|
switch (roleType) {
|
||||||
@ -39,7 +47,7 @@ const roleTypeToBadgeClass = (roleType) => {
|
|||||||
case 1:
|
case 1:
|
||||||
return 'bg-red-100 text-red-600';
|
return 'bg-red-100 text-red-600';
|
||||||
case 2:
|
case 2:
|
||||||
return 'bg-green-100 text-green-600';
|
return 'bg-tertiary/10 text-tertiary';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-600';
|
return 'bg-gray-100 text-gray-600';
|
||||||
}
|
}
|
||||||
@ -378,7 +386,7 @@ export default function Page() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
row.is_active
|
row.is_active
|
||||||
? 'text-emerald-500 hover:text-emerald-700'
|
? 'text-primary hover:text-secondary'
|
||||||
: 'text-orange-500 hover:text-orange-700'
|
: 'text-orange-500 hover:text-orange-700'
|
||||||
}
|
}
|
||||||
onClick={() => handleConfirmActivateProfile(row)}
|
onClick={() => handleConfirmActivateProfile(row)}
|
||||||
@ -474,7 +482,7 @@ export default function Page() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
row.is_active
|
row.is_active
|
||||||
? 'text-emerald-500 hover:text-emerald-700'
|
? 'text-primary hover:text-secondary'
|
||||||
: 'text-orange-500 hover:text-orange-700'
|
: 'text-orange-500 hover:text-orange-700'
|
||||||
}
|
}
|
||||||
onClick={() => handleConfirmActivateProfile(row)}
|
onClick={() => handleConfirmActivateProfile(row)}
|
||||||
@ -516,10 +524,10 @@ export default function Page() {
|
|||||||
totalPages={totalProfilesParentPages}
|
totalPages={totalProfilesParentPages}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<AlertMessage
|
<EmptyState
|
||||||
type="info"
|
icon={Users}
|
||||||
title="Aucun profil PARENT enregistré"
|
title="Aucun profil parent enregistré"
|
||||||
message="Un profil Parent est ajouté lors de la création d'un nouveau dossier d'inscription."
|
description="Les profils parents sont créés automatiquement lors de la création d'un dossier d'inscription."
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -540,10 +548,10 @@ export default function Page() {
|
|||||||
totalPages={totalProfilesSchoolPages}
|
totalPages={totalProfilesSchoolPages}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<AlertMessage
|
<EmptyState
|
||||||
type="info"
|
icon={UserPlus}
|
||||||
title="Aucun profil ECOLE enregistré"
|
title="Aucun profil école enregistré"
|
||||||
message="Un profil ECOLE est ajouté lors de la création d'un nouvel enseignant."
|
description="Les profils école sont créés automatiquement lors de l'ajout d'un enseignant."
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -82,12 +82,12 @@ export default function FeedbackPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col p-4">
|
<div className="h-full flex flex-col p-4">
|
||||||
<div className="max-w-3xl mx-auto w-full">
|
<div className="max-w-3xl mx-auto w-full">
|
||||||
<h1 className="text-2xl font-headline font-bold text-gray-800 mb-2">
|
<h1 className="font-headline text-2xl font-bold text-gray-800 mb-2">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mb-6">{t('description')}</p>
|
<p className="text-gray-600 mb-6">{t('description')}</p>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-md shadow-sm border border-gray-200 p-6">
|
||||||
{/* Catégorie */}
|
{/* Catégorie */}
|
||||||
<SelectChoice
|
<SelectChoice
|
||||||
name="category"
|
name="category"
|
||||||
|
|||||||
@ -251,31 +251,31 @@ export default function StudentGradesPage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/admin/grades')}
|
onClick={() => router.push('/admin/grades')}
|
||||||
className="p-2 rounded-md hover:bg-gray-100 border border-gray-200"
|
className="p-2 rounded hover:bg-gray-100 border border-gray-200 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors"
|
||||||
aria-label="Retour à la liste"
|
aria-label="Retour à la liste"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-xl font-bold text-gray-800">Suivi pédagogique</h1>
|
<h1 className="font-headline text-xl font-bold text-gray-800">Suivi pédagogique</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Student profile */}
|
{/* Student profile */}
|
||||||
{student && (
|
{student && (
|
||||||
<div className="bg-stone-50 rounded-lg shadow-sm border border-gray-100 p-4 md:p-6 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
<div className="bg-neutral rounded-md shadow-sm border border-gray-100 p-4 md:p-6 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||||
{student.photo ? (
|
{student.photo ? (
|
||||||
<img
|
<img
|
||||||
src={getSecureFileUrl(student.photo)}
|
src={getSecureFileUrl(student.photo)}
|
||||||
alt={`${student.first_name} ${student.last_name}`}
|
alt={`${student.first_name} ${student.last_name}`}
|
||||||
className="w-24 h-24 object-cover rounded-full border-4 border-emerald-200 shadow"
|
className="w-24 h-24 object-cover rounded-full border-4 border-primary/20 shadow"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl border-4 border-emerald-100">
|
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl border-4 border-primary/10">
|
||||||
{student.first_name?.[0]}
|
{student.first_name?.[0]}
|
||||||
{student.last_name?.[0]}
|
{student.last_name?.[0]}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 text-center sm:text-left">
|
<div className="flex-1 text-center sm:text-left">
|
||||||
<div className="text-xl font-bold text-emerald-800">
|
<div className="text-xl font-bold text-secondary">
|
||||||
{student.last_name} {student.first_name}
|
{student.last_name} {student.first_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600 mt-1">
|
<div className="text-sm text-gray-600 mt-1">
|
||||||
@ -322,7 +322,7 @@ export default function StudentGradesPage() {
|
|||||||
`${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodString}`
|
`${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${studentId}&period=${periodString}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full sm:w-auto"
|
className="px-4 py-2 rounded shadow bg-primary text-white font-label font-medium hover:bg-secondary w-full sm:w-auto min-h-[44px] transition-colors"
|
||||||
icon={<Award className="w-5 h-5" />}
|
icon={<Award className="w-5 h-5" />}
|
||||||
text="Évaluer"
|
text="Évaluer"
|
||||||
title="Évaluer l'élève"
|
title="Évaluer l'élève"
|
||||||
@ -351,10 +351,10 @@ export default function StudentGradesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Évaluations par matière */}
|
{/* Évaluations par matière */}
|
||||||
<div className="bg-stone-50 rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
<div className="bg-neutral rounded-md shadow-sm border border-gray-200 p-4 md:p-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<BookOpen className="w-6 h-6 text-emerald-600" />
|
<BookOpen className="w-6 h-6 text-primary" />
|
||||||
<h2 className="text-xl font-semibold text-gray-800">
|
<h2 className="font-headline text-xl font-semibold text-gray-800">
|
||||||
Évaluations par matière
|
Évaluations par matière
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,14 +10,19 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Trash2,
|
Trash2,
|
||||||
Save,
|
Save,
|
||||||
Download
|
Download,
|
||||||
|
FileText,
|
||||||
|
UserPlus,
|
||||||
|
Users,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import EmptyState from '@/components/EmptyState';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
|
||||||
FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL,
|
FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL,
|
||||||
|
FE_ADMIN_SUBSCRIPTIONS_CREATE_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||||
import {
|
import {
|
||||||
@ -35,15 +40,25 @@ import { useClasses } from '@/context/ClassesContext';
|
|||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import { exportToCSV } from '@/utils/exportCSV';
|
import { exportToCSV } from '@/utils/exportCSV';
|
||||||
import SchoolYearFilter from '@/components/SchoolYearFilter';
|
import SchoolYearFilter from '@/components/SchoolYearFilter';
|
||||||
import { getCurrentSchoolYear, getNextSchoolYear, getHistoricalYears } from '@/utils/Date';
|
import {
|
||||||
import { CURRENT_YEAR_FILTER, NEXT_YEAR_FILTER, HISTORICAL_FILTER } from '@/utils/constants';
|
getCurrentSchoolYear,
|
||||||
|
getNextSchoolYear,
|
||||||
|
getHistoricalYears,
|
||||||
|
} from '@/utils/Date';
|
||||||
|
import {
|
||||||
|
CURRENT_YEAR_FILTER,
|
||||||
|
NEXT_YEAR_FILTER,
|
||||||
|
HISTORICAL_FILTER,
|
||||||
|
} from '@/utils/constants';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
function getPeriodString(periodValue, frequency, schoolYear = null) {
|
function getPeriodString(periodValue, frequency, schoolYear = null) {
|
||||||
const year = schoolYear || (() => {
|
const year =
|
||||||
const y = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
schoolYear ||
|
||||||
return `${y}-${y + 1}`;
|
(() => {
|
||||||
})();
|
const y = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||||
|
return `${y}-${y + 1}`;
|
||||||
|
})();
|
||||||
if (frequency === 1) return `T${periodValue}_${year}`;
|
if (frequency === 1) return `T${periodValue}_${year}`;
|
||||||
if (frequency === 2) return `S${periodValue}_${year}`;
|
if (frequency === 2) return `S${periodValue}_${year}`;
|
||||||
if (frequency === 3) return `A_${year}`;
|
if (frequency === 3) return `A_${year}`;
|
||||||
@ -96,7 +111,7 @@ const COMPETENCY_COLUMNS = [
|
|||||||
{
|
{
|
||||||
key: 'acquired',
|
key: 'acquired',
|
||||||
label: 'Acquises',
|
label: 'Acquises',
|
||||||
color: 'bg-emerald-100 text-emerald-700',
|
color: 'bg-primary/10 text-secondary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'inProgress',
|
key: 'inProgress',
|
||||||
@ -144,7 +159,7 @@ function PercentBadge({ value, loading, color }) {
|
|||||||
const badgeColor =
|
const badgeColor =
|
||||||
color ||
|
color ||
|
||||||
(value >= 75
|
(value >= 75
|
||||||
? 'bg-emerald-100 text-emerald-700'
|
? 'bg-primary/10 text-secondary'
|
||||||
: value >= 50
|
: value >= 50
|
||||||
? 'bg-yellow-100 text-yellow-700'
|
? 'bg-yellow-100 text-yellow-700'
|
||||||
: 'bg-red-100 text-red-600');
|
: 'bg-red-100 text-red-600');
|
||||||
@ -176,13 +191,13 @@ export default function Page() {
|
|||||||
const [editingEvalId, setEditingEvalId] = useState(null);
|
const [editingEvalId, setEditingEvalId] = useState(null);
|
||||||
const [editScore, setEditScore] = useState('');
|
const [editScore, setEditScore] = useState('');
|
||||||
const [editAbsent, setEditAbsent] = useState(false);
|
const [editAbsent, setEditAbsent] = useState(false);
|
||||||
|
|
||||||
// Filtrage par année scolaire
|
// Filtrage par année scolaire
|
||||||
const [activeYearFilter, setActiveYearFilter] = useState(CURRENT_YEAR_FILTER);
|
const [activeYearFilter, setActiveYearFilter] = useState(CURRENT_YEAR_FILTER);
|
||||||
const currentSchoolYear = useMemo(() => getCurrentSchoolYear(), []);
|
const currentSchoolYear = useMemo(() => getCurrentSchoolYear(), []);
|
||||||
const nextSchoolYear = useMemo(() => getNextSchoolYear(), []);
|
const nextSchoolYear = useMemo(() => getNextSchoolYear(), []);
|
||||||
const historicalYears = useMemo(() => getHistoricalYears(5), []);
|
const historicalYears = useMemo(() => getHistoricalYears(5), []);
|
||||||
|
|
||||||
// Déterminer l'année scolaire sélectionnée
|
// Déterminer l'année scolaire sélectionnée
|
||||||
const selectedSchoolYear = useMemo(() => {
|
const selectedSchoolYear = useMemo(() => {
|
||||||
if (activeYearFilter === CURRENT_YEAR_FILTER) return currentSchoolYear;
|
if (activeYearFilter === CURRENT_YEAR_FILTER) return currentSchoolYear;
|
||||||
@ -192,10 +207,11 @@ export default function Page() {
|
|||||||
return historicalYears[0];
|
return historicalYears[0];
|
||||||
}, [activeYearFilter, currentSchoolYear, nextSchoolYear, historicalYears]);
|
}, [activeYearFilter, currentSchoolYear, nextSchoolYear, historicalYears]);
|
||||||
|
|
||||||
const periodColumns = useMemo(() => getPeriodColumns(
|
const periodColumns = useMemo(
|
||||||
selectedEstablishmentEvaluationFrequency
|
() => getPeriodColumns(selectedEstablishmentEvaluationFrequency),
|
||||||
), [selectedEstablishmentEvaluationFrequency]);
|
[selectedEstablishmentEvaluationFrequency]
|
||||||
|
);
|
||||||
|
|
||||||
const currentPeriodValue = getCurrentPeriodValue(
|
const currentPeriodValue = getCurrentPeriodValue(
|
||||||
selectedEstablishmentEvaluationFrequency
|
selectedEstablishmentEvaluationFrequency
|
||||||
);
|
);
|
||||||
@ -228,7 +244,11 @@ export default function Page() {
|
|||||||
|
|
||||||
const tasks = students.flatMap((student) =>
|
const tasks = students.flatMap((student) =>
|
||||||
periodColumns.map(({ value: periodValue }) => {
|
periodColumns.map(({ value: periodValue }) => {
|
||||||
const periodStr = getPeriodString(periodValue, frequency, selectedSchoolYear);
|
const periodStr = getPeriodString(
|
||||||
|
periodValue,
|
||||||
|
frequency,
|
||||||
|
selectedSchoolYear
|
||||||
|
);
|
||||||
return fetchStudentCompetencies(student.id, periodStr)
|
return fetchStudentCompetencies(student.id, periodStr)
|
||||||
.then((data) => ({ studentId: student.id, periodValue, data }))
|
.then((data) => ({ studentId: student.id, periodValue, data }))
|
||||||
.catch(() => ({ studentId: student.id, periodValue, data: null }));
|
.catch(() => ({ studentId: student.id, periodValue, data: null }));
|
||||||
@ -280,7 +300,12 @@ export default function Page() {
|
|||||||
setStatsMap(map);
|
setStatsMap(map);
|
||||||
setStatsLoading(false);
|
setStatsLoading(false);
|
||||||
});
|
});
|
||||||
}, [students, selectedEstablishmentEvaluationFrequency, selectedSchoolYear, periodColumns]);
|
}, [
|
||||||
|
students,
|
||||||
|
selectedEstablishmentEvaluationFrequency,
|
||||||
|
selectedSchoolYear,
|
||||||
|
periodColumns,
|
||||||
|
]);
|
||||||
|
|
||||||
const filteredStudents = students.filter(
|
const filteredStudents = students.filter(
|
||||||
(student) =>
|
(student) =>
|
||||||
@ -329,12 +354,16 @@ export default function Page() {
|
|||||||
{ key: 'last_name', label: 'Nom' },
|
{ key: 'last_name', label: 'Nom' },
|
||||||
{ key: 'first_name', label: 'Prénom' },
|
{ key: 'first_name', label: 'Prénom' },
|
||||||
{ key: 'birth_date', label: 'Date de naissance' },
|
{ key: 'birth_date', label: 'Date de naissance' },
|
||||||
{ key: 'level', label: 'Niveau', transform: (value) => getNiveauLabel(value) },
|
{
|
||||||
|
key: 'level',
|
||||||
|
label: 'Niveau',
|
||||||
|
transform: (value) => getNiveauLabel(value),
|
||||||
|
},
|
||||||
{ key: 'associated_class_name', label: 'Classe' },
|
{ key: 'associated_class_name', label: 'Classe' },
|
||||||
{
|
{
|
||||||
key: 'id',
|
key: 'id',
|
||||||
label: 'Absences',
|
label: 'Absences',
|
||||||
transform: (value) => absencesMap[value] || 0
|
transform: (value) => absencesMap[value] || 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -346,7 +375,7 @@ export default function Page() {
|
|||||||
transform: (value) => {
|
transform: (value) => {
|
||||||
const stats = statsMap[value];
|
const stats = statsMap[value];
|
||||||
return stats?.[key] !== undefined ? `${stats[key]}%` : '';
|
return stats?.[key] !== undefined ? `${stats[key]}%` : '';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -435,6 +464,37 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBilanForStudent = (student) => {
|
||||||
|
const bilans = Array.isArray(student?.bilans) ? student.bilans : [];
|
||||||
|
if (!bilans.length) return null;
|
||||||
|
|
||||||
|
const currentPeriodStr = currentPeriodValue
|
||||||
|
? getPeriodString(
|
||||||
|
currentPeriodValue,
|
||||||
|
selectedEstablishmentEvaluationFrequency,
|
||||||
|
selectedSchoolYear
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (currentPeriodStr) {
|
||||||
|
const exact = bilans.find(
|
||||||
|
(bilan) => bilan?.period === currentPeriodStr && bilan?.file
|
||||||
|
);
|
||||||
|
if (exact) return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schoolYearSuffix = `_${selectedSchoolYear}`;
|
||||||
|
const sameYearBilans = bilans.filter(
|
||||||
|
(bilan) => bilan?.file && bilan?.period?.endsWith(schoolYearSuffix)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sameYearBilans.length) return null;
|
||||||
|
|
||||||
|
return [...sameYearBilans].sort(
|
||||||
|
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
|
||||||
|
)[0];
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'Photo', transform: () => null },
|
{ name: 'Photo', transform: () => null },
|
||||||
{ name: 'Élève', transform: () => null },
|
{ name: 'Élève', transform: () => null },
|
||||||
@ -494,7 +554,7 @@ export default function Page() {
|
|||||||
`${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${student.associated_class_id}`
|
`${FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL}?schoolClassId=${student.associated_class_id}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="text-emerald-700 hover:underline font-medium"
|
className="text-secondary hover:underline font-medium"
|
||||||
>
|
>
|
||||||
{student.associated_class_name}
|
{student.associated_class_name}
|
||||||
</button>
|
</button>
|
||||||
@ -510,8 +570,23 @@ export default function Page() {
|
|||||||
<span className="text-gray-400 text-xs">0</span>
|
<span className="text-gray-400 text-xs">0</span>
|
||||||
);
|
);
|
||||||
case 'Actions':
|
case 'Actions':
|
||||||
|
const bilan = getBilanForStudent(student);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{bilan?.file && (
|
||||||
|
<a
|
||||||
|
href={getSecureFileUrl(bilan.file)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-cyan-100 text-cyan-700 hover:bg-cyan-200 transition whitespace-nowrap"
|
||||||
|
title={`Télécharger le bilan de compétences (${bilan.period})`}
|
||||||
|
>
|
||||||
|
<FileText size={14} />
|
||||||
|
Bilan
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -534,7 +609,7 @@ export default function Page() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => handleEvaluer(e, student.id)}
|
onClick={(e) => handleEvaluer(e, student.id)}
|
||||||
disabled={!currentPeriodValue}
|
disabled={!currentPeriodValue}
|
||||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-emerald-100 text-emerald-700 hover:bg-emerald-200 transition whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-primary/10 text-secondary hover:bg-primary/20 transition whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
title="Évaluer"
|
title="Évaluer"
|
||||||
>
|
>
|
||||||
<Award size={14} />
|
<Award size={14} />
|
||||||
@ -587,7 +662,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleExportCSV}
|
onClick={handleExportCSV}
|
||||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors ml-4"
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-secondary bg-primary/10 rounded hover:bg-primary/20 transition-colors ml-4"
|
||||||
title="Exporter en CSV"
|
title="Exporter en CSV"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
@ -604,7 +679,22 @@ export default function Page() {
|
|||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<span className="text-gray-400 text-sm">Aucun élève trouvé</span>
|
students.length === 0 && !searchTerm ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Users}
|
||||||
|
title="Aucun élève inscrit"
|
||||||
|
description="Commencez par inscrire des élèves pour suivre leur parcours pédagogique."
|
||||||
|
actionLabel="Inscrire un élève"
|
||||||
|
actionIcon={UserPlus}
|
||||||
|
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
icon={Search}
|
||||||
|
title="Aucun élève trouvé"
|
||||||
|
description="Modifiez votre recherche pour trouver un élève."
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -615,7 +705,7 @@ export default function Page() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b bg-gray-50">
|
<div className="flex items-center justify-between px-6 py-4 border-b bg-gray-50">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Notes de {gradesModalStudent.first_name}{' '}
|
Notes de {gradesModalStudent.first_name}{' '}
|
||||||
{gradesModalStudent.last_name}
|
{gradesModalStudent.last_name}
|
||||||
</h2>
|
</h2>
|
||||||
@ -636,7 +726,7 @@ export default function Page() {
|
|||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
{gradesLoading ? (
|
{gradesLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
) : Object.keys(groupedBySubject).length === 0 ? (
|
) : Object.keys(groupedBySubject).length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-400">
|
<div className="text-center py-12 text-gray-400">
|
||||||
@ -673,13 +763,13 @@ export default function Page() {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-r from-emerald-50 to-blue-50 rounded-lg p-4 border border-emerald-100">
|
<div className="bg-gradient-to-r from-primary/5 to-blue-50 rounded-lg p-4 border border-primary/10">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">
|
||||||
Résumé
|
Résumé
|
||||||
</span>
|
</span>
|
||||||
{overallAvg !== null && (
|
{overallAvg !== null && (
|
||||||
<span className="text-lg font-bold text-emerald-700">
|
<span className="text-lg font-bold text-secondary">
|
||||||
Moyenne générale : {overallAvg}/20
|
Moyenne générale : {overallAvg}/20
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -831,7 +921,7 @@ export default function Page() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleSaveEval(evalItem)
|
handleSaveEval(evalItem)
|
||||||
}
|
}
|
||||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
className="p-1 text-primary hover:bg-primary/5 rounded"
|
||||||
title="Enregistrer"
|
title="Enregistrer"
|
||||||
>
|
>
|
||||||
<Save size={14} />
|
<Save size={14} />
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export default function StudentCompetenciesPage() {
|
|||||||
'success',
|
'success',
|
||||||
'Succès'
|
'Succès'
|
||||||
);
|
);
|
||||||
router.push(`/admin/grades/${studentId}`);
|
router.push('/admin/grades');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
showNotification(
|
showNotification(
|
||||||
@ -86,12 +86,12 @@ export default function StudentCompetenciesPage() {
|
|||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/admin/grades')}
|
onClick={() => router.push('/admin/grades')}
|
||||||
className="p-2 rounded-md hover:bg-gray-100 border border-gray-200"
|
className="p-2 rounded hover:bg-gray-100 border border-gray-200 min-h-[44px] min-w-[44px] flex items-center justify-center transition-colors"
|
||||||
aria-label="Retour à la fiche élève"
|
aria-label="Retour à la fiche élève"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-xl font-bold text-gray-800">Bilan de compétence</h1>
|
<h1 className="font-headline text-xl font-bold text-gray-800">Bilan de compétence</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
<form
|
<form
|
||||||
|
|||||||
@ -34,12 +34,13 @@ import Footer from '@/components/Footer';
|
|||||||
import MobileTopbar from '@/components/MobileTopbar';
|
import MobileTopbar from '@/components/MobileTopbar';
|
||||||
import { RIGHTS } from '@/utils/rights';
|
import { RIGHTS } from '@/utils/rights';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
import { useChatConnection } from '@/context/ChatConnectionContext';
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { profileRole, establishments, clearContext } =
|
const { profileRole, establishments, clearContext } = useEstablishment();
|
||||||
useEstablishment();
|
const { totalUnreadCount, resetUnreadCount } = useChatConnection();
|
||||||
|
|
||||||
const sidebarItems = {
|
const sidebarItems = {
|
||||||
admin: {
|
admin: {
|
||||||
@ -83,6 +84,7 @@ export default function Layout({ children }) {
|
|||||||
name: t('messagerie'),
|
name: t('messagerie'),
|
||||||
url: FE_ADMIN_MESSAGERIE_URL,
|
url: FE_ADMIN_MESSAGERIE_URL,
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
|
badge: totalUnreadCount,
|
||||||
},
|
},
|
||||||
feedback: {
|
feedback: {
|
||||||
id: 'feedback',
|
id: 'feedback',
|
||||||
@ -119,7 +121,11 @@ export default function Layout({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fermer la sidebar quand on change de page sur mobile
|
// Fermer la sidebar quand on change de page sur mobile
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
}, [pathname]);
|
// Réinitialiser le compteur non lu quand on ouvre la messagerie
|
||||||
|
if (pathname?.includes('/messagerie')) {
|
||||||
|
resetUnreadCount();
|
||||||
|
}
|
||||||
|
}, [pathname, resetUnreadCount]);
|
||||||
|
|
||||||
// Filtrage dynamique des items de la sidebar selon le rôle
|
// Filtrage dynamique des items de la sidebar selon le rôle
|
||||||
let sidebarItemsToDisplay = Object.values(sidebarItems);
|
let sidebarItemsToDisplay = Object.values(sidebarItems);
|
||||||
@ -158,7 +164,7 @@ export default function Layout({ children }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main container */}
|
{/* Main container */}
|
||||||
<div className="absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0">
|
<div className="absolute overflow-auto bg-gradient-to-br from-primary/5 via-sky-50 to-primary/10 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -174,14 +174,14 @@ export default function DashboardPage() {
|
|||||||
<StatCard
|
<StatCard
|
||||||
title={t('pendingRegistrations')}
|
title={t('pendingRegistrations')}
|
||||||
value={pendingRegistrationCount}
|
value={pendingRegistrationCount}
|
||||||
icon={<Clock className="text-green-500" size={24} />}
|
icon={<Clock className="text-tertiary" size={24} />}
|
||||||
color="green"
|
color="tertiary"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('structureCapacity')}
|
title={t('structureCapacity')}
|
||||||
value={selectedEstablishmentTotalCapacity}
|
value={selectedEstablishmentTotalCapacity}
|
||||||
icon={<School className="text-green-500" size={24} />}
|
icon={<School className="text-primary" size={24} />}
|
||||||
color="emerald"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title={t('capacityRate')}
|
title={t('capacityRate')}
|
||||||
@ -200,8 +200,8 @@ export default function DashboardPage() {
|
|||||||
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Graphique des inscriptions */}
|
{/* Graphique des inscriptions */}
|
||||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1">
|
||||||
<h2 className="text-lg font-semibold mb-4 md:mb-6">
|
<h2 className="font-headline text-lg font-semibold mb-4 md:mb-6">
|
||||||
{t('inscriptionTrends')}
|
{t('inscriptionTrends')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col sm:flex-row gap-6 mt-4">
|
<div className="flex flex-col sm:flex-row gap-6 mt-4">
|
||||||
@ -214,14 +214,14 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Présence et assiduité */}
|
{/* Présence et assiduité */}
|
||||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1">
|
||||||
<Attendance absences={absencesToday} readOnly={true} />
|
<Attendance absences={absencesToday} readOnly={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Colonne de droite : Événements à venir */}
|
{/* Colonne de droite : Événements à venir */}
|
||||||
<div className="bg-stone-50 p-4 md:p-6 rounded-lg shadow-sm border border-gray-100 flex-1 h-full">
|
<div className="bg-neutral p-4 md:p-6 rounded-md shadow-sm border border-gray-100 flex-1 h-full">
|
||||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
<h2 className="font-headline text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||||
{upcomingEvents.map((event, index) => (
|
{upcomingEvents.map((event, index) => (
|
||||||
<EventCard key={index} {...event} />
|
<EventCard key={index} {...event} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import EventModal from '@/components/Calendar/EventModal';
|
|||||||
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
import ScheduleNavigation from '@/components/Calendar/ScheduleNavigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
|
import { usePlanning } from '@/context/PlanningContext';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@ -29,33 +30,36 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
|
||||||
const initializeNewEvent = (date = new Date()) => {
|
const PlanningContent = ({ isDrawerOpen, setIsDrawerOpen, isModalOpen, setIsModalOpen, eventData, setEventData }) => {
|
||||||
// S'assurer que date est un objet Date valide
|
const { selectedSchedule, schedules } = usePlanning();
|
||||||
const eventDate = date instanceof Date ? date : new Date();
|
|
||||||
|
|
||||||
setEventData({
|
const initializeNewEvent = (date = new Date()) => {
|
||||||
title: '',
|
const eventDate = date instanceof Date ? date : new Date();
|
||||||
description: '',
|
|
||||||
start: eventDate.toISOString(),
|
|
||||||
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
|
||||||
location: '',
|
|
||||||
planning: '', // Ne pas définir de valeur par défaut ici non plus
|
|
||||||
recursionType: RecurrenceType.NONE,
|
|
||||||
selectedDays: [],
|
|
||||||
recursionEnd: new Date(
|
|
||||||
eventDate.getTime() + 2 * 60 * 60 * 1000
|
|
||||||
).toISOString(),
|
|
||||||
customInterval: 1,
|
|
||||||
customUnit: 'days',
|
|
||||||
});
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const selected =
|
||||||
<PlanningProvider
|
schedules.find((schedule) => Number(schedule.id) === Number(selectedSchedule)) ||
|
||||||
establishmentId={selectedEstablishmentId}
|
schedules[0];
|
||||||
modeSet={PlanningModes.PLANNING}
|
|
||||||
>
|
setEventData({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
start: eventDate.toISOString(),
|
||||||
|
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
||||||
|
location: '',
|
||||||
|
planning: selected?.id || '',
|
||||||
|
color: selected?.color || '',
|
||||||
|
recursionType: RecurrenceType.NONE,
|
||||||
|
selectedDays: [],
|
||||||
|
recursionEnd: new Date(
|
||||||
|
eventDate.getTime() + 2 * 60 * 60 * 1000
|
||||||
|
).toISOString(),
|
||||||
|
customInterval: 1,
|
||||||
|
customUnit: 'days',
|
||||||
|
});
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flex h-full overflow-hidden">
|
<div className="flex h-full overflow-hidden">
|
||||||
<ScheduleNavigation
|
<ScheduleNavigation
|
||||||
isOpen={isDrawerOpen}
|
isOpen={isDrawerOpen}
|
||||||
@ -76,6 +80,22 @@ export default function Page() {
|
|||||||
setEventData={setEventData}
|
setEventData={setEventData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlanningProvider
|
||||||
|
establishmentId={selectedEstablishmentId}
|
||||||
|
modeSet={PlanningModes.PLANNING}
|
||||||
|
>
|
||||||
|
<PlanningContent
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
setIsDrawerOpen={setIsDrawerOpen}
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
setIsModalOpen={setIsModalOpen}
|
||||||
|
eventData={eventData}
|
||||||
|
setEventData={setEventData}
|
||||||
|
/>
|
||||||
</PlanningProvider>
|
</PlanningProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Tab from '@/components/Tab';
|
|
||||||
import TabContent from '@/components/TabContent';
|
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/Form/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||||
@ -13,13 +11,8 @@ import {
|
|||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken
|
import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [activeTab, setActiveTab] = useState('smtp');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [smtpServer, setSmtpServer] = useState('');
|
const [smtpServer, setSmtpServer] = useState('');
|
||||||
const [smtpPort, setSmtpPort] = useState('');
|
const [smtpPort, setSmtpPort] = useState('');
|
||||||
const [smtpUser, setSmtpUser] = useState('');
|
const [smtpUser, setSmtpUser] = useState('');
|
||||||
@ -29,23 +22,10 @@ export default function SettingsPage() {
|
|||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
const csrfToken = useCsrfToken(); // Récupération du csrfToken
|
const csrfToken = useCsrfToken(); // Récupération du csrfToken
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const handleTabClick = (tab) => {
|
|
||||||
setActiveTab(tab);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ajout : sélection automatique de l'onglet via l'ancre ou le paramètre de recherche
|
|
||||||
useEffect(() => {
|
|
||||||
const tabParam = searchParams.get('tab');
|
|
||||||
if (tabParam === 'smtp') {
|
|
||||||
setActiveTab('smtp');
|
|
||||||
}
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
// Charger les paramètres SMTP existants
|
// Charger les paramètres SMTP existants
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'smtp') {
|
if (csrfToken && selectedEstablishmentId) {
|
||||||
fetchSmtpSettings(csrfToken, selectedEstablishmentId) // Passer le csrfToken ici
|
fetchSmtpSettings(csrfToken, selectedEstablishmentId) // Passer le csrfToken ici
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setSmtpServer(data.smtp_server || '');
|
setSmtpServer(data.smtp_server || '');
|
||||||
@ -75,7 +55,7 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
}, [csrfToken, selectedEstablishmentId]);
|
||||||
|
|
||||||
const handleSmtpServerChange = (e) => {
|
const handleSmtpServerChange = (e) => {
|
||||||
setSmtpServer(e.target.value);
|
setSmtpServer(e.target.value);
|
||||||
@ -128,66 +108,63 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-6">
|
||||||
<div className="flex space-x-4 mb-4">
|
<h1 className="font-headline text-2xl font-bold text-gray-900 mb-6">
|
||||||
<Tab
|
Paramètres
|
||||||
text="Paramètres SMTP"
|
</h1>
|
||||||
active={activeTab === 'smtp'}
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-6">
|
||||||
onClick={() => handleTabClick('smtp')}
|
<h2 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
/>
|
Paramètres SMTP
|
||||||
</div>
|
</h2>
|
||||||
<div className="mt-4">
|
<form onSubmit={handleSmtpSubmit}>
|
||||||
<TabContent isActive={activeTab === 'smtp'}>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<form onSubmit={handleSmtpSubmit}>
|
<InputText
|
||||||
<div className="grid grid-cols-2 gap-4">
|
label="Serveur SMTP"
|
||||||
<InputText
|
value={smtpServer}
|
||||||
label="Serveur SMTP"
|
onChange={handleSmtpServerChange}
|
||||||
value={smtpServer}
|
/>
|
||||||
onChange={handleSmtpServerChange}
|
<InputText
|
||||||
|
label="Port SMTP"
|
||||||
|
value={smtpPort}
|
||||||
|
onChange={handleSmtpPortChange}
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
label="Utilisateur SMTP"
|
||||||
|
value={smtpUser}
|
||||||
|
onChange={handleSmtpUserChange}
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
label="Mot de passe SMTP"
|
||||||
|
type="password"
|
||||||
|
value={smtpPassword}
|
||||||
|
onChange={handleSmtpPasswordChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 border-t pt-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: 'useTls' }}
|
||||||
|
formData={{ useTls }}
|
||||||
|
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
|
||||||
|
fieldName="useTls"
|
||||||
|
itemLabelFunc={() => 'Utiliser TLS'}
|
||||||
/>
|
/>
|
||||||
<InputText
|
<CheckBox
|
||||||
label="Port SMTP"
|
item={{ id: 'useSsl' }}
|
||||||
value={smtpPort}
|
formData={{ useSsl }}
|
||||||
onChange={handleSmtpPortChange}
|
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
|
||||||
/>
|
fieldName="useSsl"
|
||||||
<InputText
|
itemLabelFunc={() => 'Utiliser SSL'}
|
||||||
label="Utilisateur SMTP"
|
|
||||||
value={smtpUser}
|
|
||||||
onChange={handleSmtpUserChange}
|
|
||||||
/>
|
|
||||||
<InputText
|
|
||||||
label="Mot de passe SMTP"
|
|
||||||
type="password"
|
|
||||||
value={smtpPassword}
|
|
||||||
onChange={handleSmtpPasswordChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 border-t pt-4">
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<Button
|
||||||
<CheckBox
|
type="submit"
|
||||||
item={{ id: 'useTls' }}
|
primary
|
||||||
formData={{ useTls }}
|
text="Mettre à jour"
|
||||||
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
|
className="mt-6"
|
||||||
fieldName="useTls"
|
></Button>
|
||||||
itemLabelFunc={() => 'Utiliser TLS'}
|
</form>
|
||||||
/>
|
|
||||||
<CheckBox
|
|
||||||
item={{ id: 'useSsl' }}
|
|
||||||
formData={{ useSsl }}
|
|
||||||
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
|
|
||||||
fieldName="useSsl"
|
|
||||||
itemLabelFunc={() => 'Utiliser SSL'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
primary
|
|
||||||
text="Mettre à jour"
|
|
||||||
className="mt-6"
|
|
||||||
></Button>
|
|
||||||
</form>
|
|
||||||
</TabContent>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -204,7 +204,7 @@ export default function FormBuilderPage() {
|
|||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Retour
|
Retour
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-lg font-headline font-semibold text-gray-800">
|
<h1 className="font-headline text-lg font-headline font-semibold text-gray-800">
|
||||||
{isEditing ? 'Modifier le formulaire' : 'Créer un formulaire personnalisé'}
|
{isEditing ? 'Modifier le formulaire' : 'Créer un formulaire personnalisé'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Users, Layers, CheckCircle, Clock, XCircle, ClipboardList, Plus } from 'lucide-react';
|
import {
|
||||||
|
Users,
|
||||||
|
Layers,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
XCircle,
|
||||||
|
ClipboardList,
|
||||||
|
Plus,
|
||||||
|
} from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import { fetchClasse, fetchSpecialities, fetchEvaluations, createEvaluation, updateEvaluation, deleteEvaluation, fetchStudentEvaluations, saveStudentEvaluations, deleteStudentEvaluation } from '@/app/actions/schoolAction';
|
import {
|
||||||
|
fetchClasse,
|
||||||
|
fetchSpecialities,
|
||||||
|
fetchEvaluations,
|
||||||
|
createEvaluation,
|
||||||
|
updateEvaluation,
|
||||||
|
deleteEvaluation,
|
||||||
|
fetchStudentEvaluations,
|
||||||
|
saveStudentEvaluations,
|
||||||
|
deleteStudentEvaluation,
|
||||||
|
} from '@/app/actions/schoolAction';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
@ -17,7 +35,11 @@ import {
|
|||||||
editAbsences,
|
editAbsences,
|
||||||
deleteAbsences,
|
deleteAbsences,
|
||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import { EvaluationForm, EvaluationList, EvaluationGradeTable } from '@/components/Evaluation';
|
import {
|
||||||
|
EvaluationForm,
|
||||||
|
EvaluationList,
|
||||||
|
EvaluationGradeTable,
|
||||||
|
} from '@/components/Evaluation';
|
||||||
|
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
@ -53,14 +75,15 @@ export default function Page() {
|
|||||||
const [editingEvaluation, setEditingEvaluation] = useState(null);
|
const [editingEvaluation, setEditingEvaluation] = useState(null);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } = useEstablishment();
|
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||||
|
useEstablishment();
|
||||||
|
|
||||||
// Périodes selon la fréquence d'évaluation
|
// Périodes selon la fréquence d'évaluation
|
||||||
const getPeriods = () => {
|
const getPeriods = () => {
|
||||||
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1;
|
||||||
const nextYear = (year + 1).toString();
|
const nextYear = (year + 1).toString();
|
||||||
const schoolYear = `${year}-${nextYear}`;
|
const schoolYear = `${year}-${nextYear}`;
|
||||||
|
|
||||||
if (selectedEstablishmentEvaluationFrequency === 1) {
|
if (selectedEstablishmentEvaluationFrequency === 1) {
|
||||||
return [
|
return [
|
||||||
{ label: 'Trimestre 1', value: `T1_${schoolYear}` },
|
{ label: 'Trimestre 1', value: `T1_${schoolYear}` },
|
||||||
@ -212,16 +235,25 @@ export default function Page() {
|
|||||||
const currentSchoolYear = `${year}-${year + 1}`;
|
const currentSchoolYear = `${year}-${year + 1}`;
|
||||||
fetchSpecialities(selectedEstablishmentId, currentSchoolYear)
|
fetchSpecialities(selectedEstablishmentId, currentSchoolYear)
|
||||||
.then((data) => setSpecialities(data))
|
.then((data) => setSpecialities(data))
|
||||||
.catch((error) => logger.error('Erreur lors du chargement des matières:', error));
|
.catch((error) =>
|
||||||
|
logger.error('Erreur lors du chargement des matières:', error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [selectedEstablishmentId]);
|
}, [selectedEstablishmentId]);
|
||||||
|
|
||||||
// Load evaluations when tab is active and period is selected
|
// Load evaluations when tab is active and period is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'evaluations' && selectedEstablishmentId && schoolClassId && selectedPeriod) {
|
if (
|
||||||
|
activeTab === 'evaluations' &&
|
||||||
|
selectedEstablishmentId &&
|
||||||
|
schoolClassId &&
|
||||||
|
selectedPeriod
|
||||||
|
) {
|
||||||
fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod)
|
fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod)
|
||||||
.then((data) => setEvaluations(data))
|
.then((data) => setEvaluations(data))
|
||||||
.catch((error) => logger.error('Erreur lors du chargement des évaluations:', error));
|
.catch((error) =>
|
||||||
|
logger.error('Erreur lors du chargement des évaluations:', error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [activeTab, selectedEstablishmentId, schoolClassId, selectedPeriod]);
|
}, [activeTab, selectedEstablishmentId, schoolClassId, selectedPeriod]);
|
||||||
|
|
||||||
@ -230,7 +262,9 @@ export default function Page() {
|
|||||||
if (selectedEvaluation && schoolClassId) {
|
if (selectedEvaluation && schoolClassId) {
|
||||||
fetchStudentEvaluations(null, selectedEvaluation.id, null, schoolClassId)
|
fetchStudentEvaluations(null, selectedEvaluation.id, null, schoolClassId)
|
||||||
.then((data) => setStudentEvaluations(data))
|
.then((data) => setStudentEvaluations(data))
|
||||||
.catch((error) => logger.error('Erreur lors du chargement des notes:', error));
|
.catch((error) =>
|
||||||
|
logger.error('Erreur lors du chargement des notes:', error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [selectedEvaluation, schoolClassId]);
|
}, [selectedEvaluation, schoolClassId]);
|
||||||
|
|
||||||
@ -241,7 +275,11 @@ export default function Page() {
|
|||||||
showNotification('Évaluation créée avec succès', 'success', 'Succès');
|
showNotification('Évaluation créée avec succès', 'success', 'Succès');
|
||||||
setShowEvaluationForm(false);
|
setShowEvaluationForm(false);
|
||||||
// Reload evaluations
|
// Reload evaluations
|
||||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
const updatedEvaluations = await fetchEvaluations(
|
||||||
|
selectedEstablishmentId,
|
||||||
|
schoolClassId,
|
||||||
|
selectedPeriod
|
||||||
|
);
|
||||||
setEvaluations(updatedEvaluations);
|
setEvaluations(updatedEvaluations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la création:', error);
|
logger.error('Erreur lors de la création:', error);
|
||||||
@ -261,7 +299,11 @@ export default function Page() {
|
|||||||
setShowEvaluationForm(false);
|
setShowEvaluationForm(false);
|
||||||
setEditingEvaluation(null);
|
setEditingEvaluation(null);
|
||||||
// Reload evaluations
|
// Reload evaluations
|
||||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
const updatedEvaluations = await fetchEvaluations(
|
||||||
|
selectedEstablishmentId,
|
||||||
|
schoolClassId,
|
||||||
|
selectedPeriod
|
||||||
|
);
|
||||||
setEvaluations(updatedEvaluations);
|
setEvaluations(updatedEvaluations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la modification:', error);
|
logger.error('Erreur lors de la modification:', error);
|
||||||
@ -272,20 +314,31 @@ export default function Page() {
|
|||||||
const handleDeleteEvaluation = async (evaluationId) => {
|
const handleDeleteEvaluation = async (evaluationId) => {
|
||||||
await deleteEvaluation(evaluationId, csrfToken);
|
await deleteEvaluation(evaluationId, csrfToken);
|
||||||
// Reload evaluations
|
// Reload evaluations
|
||||||
const updatedEvaluations = await fetchEvaluations(selectedEstablishmentId, schoolClassId, selectedPeriod);
|
const updatedEvaluations = await fetchEvaluations(
|
||||||
|
selectedEstablishmentId,
|
||||||
|
schoolClassId,
|
||||||
|
selectedPeriod
|
||||||
|
);
|
||||||
setEvaluations(updatedEvaluations);
|
setEvaluations(updatedEvaluations);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveGrades = async (gradesData) => {
|
const handleSaveGrades = async (gradesData) => {
|
||||||
await saveStudentEvaluations(gradesData, csrfToken);
|
await saveStudentEvaluations(gradesData, csrfToken);
|
||||||
// Reload student evaluations
|
// Reload student evaluations
|
||||||
const updatedStudentEvaluations = await fetchStudentEvaluations(null, selectedEvaluation.id, null, schoolClassId);
|
const updatedStudentEvaluations = await fetchStudentEvaluations(
|
||||||
|
null,
|
||||||
|
selectedEvaluation.id,
|
||||||
|
null,
|
||||||
|
schoolClassId
|
||||||
|
);
|
||||||
setStudentEvaluations(updatedStudentEvaluations);
|
setStudentEvaluations(updatedStudentEvaluations);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteGrade = async (studentEvalId) => {
|
const handleDeleteGrade = async (studentEvalId) => {
|
||||||
await deleteStudentEvaluation(studentEvalId, csrfToken);
|
await deleteStudentEvaluation(studentEvalId, csrfToken);
|
||||||
setStudentEvaluations((prev) => prev.filter((se) => se.id !== studentEvalId));
|
setStudentEvaluations((prev) =>
|
||||||
|
prev.filter((se) => se.id !== studentEvalId)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLevelClick = (label) => {
|
const handleLevelClick = (label) => {
|
||||||
@ -543,14 +596,16 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<h1 className="text-2xl font-bold">{classe?.atmosphere_name}</h1>
|
<h1 className="font-headline text-2xl font-bold">
|
||||||
|
{classe?.atmosphere_name}
|
||||||
|
</h1>
|
||||||
|
|
||||||
{/* Section Niveaux et Enseignants */}
|
{/* Section Niveaux et Enseignants */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Section Niveaux */}
|
{/* Section Niveaux */}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white p-4 rounded-md shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
<h2 className="font-headline text-xl font-semibold mb-4 flex items-center">
|
||||||
<Layers className="w-6 h-6 mr-2" />
|
<Layers className="w-6 h-6 mr-2 text-primary" />
|
||||||
Niveaux
|
Niveaux
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
@ -559,24 +614,24 @@ export default function Page() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{classe?.levels?.length > 0 ? (
|
{classe?.levels?.length > 0 ? (
|
||||||
getNiveauxLabels(classe.levels).map((label, index) => (
|
getNiveauxLabels(classe.levels).map((label, index) => (
|
||||||
<span
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => handleLevelClick(label)} // Gérer le clic sur un niveau
|
onClick={() => handleLevelClick(label)}
|
||||||
className={`px-4 py-2 rounded-full cursor-pointer border transition-all duration-200 ${
|
className={`px-4 py-2 rounded font-label font-medium cursor-pointer border transition-colors min-h-[44px] ${
|
||||||
selectedLevels.includes(label)
|
selectedLevels.includes(label)
|
||||||
? 'bg-emerald-200 text-emerald-800 border-emerald-300 shadow-md'
|
? 'bg-primary/20 text-secondary border-primary/30 shadow-sm'
|
||||||
: 'bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300'
|
: 'bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedLevels.includes(label) ? (
|
{selectedLevels.includes(label) ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
<CheckCircle className="w-4 h-4 text-primary" />
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
label
|
label
|
||||||
)}
|
)}
|
||||||
</span>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500">Aucun niveau associé</span>
|
<span className="text-gray-500">Aucun niveau associé</span>
|
||||||
@ -585,9 +640,9 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section Enseignants */}
|
{/* Section Enseignants */}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white p-4 rounded-md shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 flex items-center">
|
<h2 className="font-headline text-xl font-semibold mb-4 flex items-center">
|
||||||
<Users className="w-6 h-6 mr-2" />
|
<Users className="w-6 h-6 mr-2 text-primary" />
|
||||||
Enseignants
|
Enseignants
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500 mb-4">Liste des enseignants</p>
|
<p className="text-sm text-gray-500 mb-4">Liste des enseignants</p>
|
||||||
@ -595,7 +650,7 @@ export default function Page() {
|
|||||||
{classe?.teachers_details?.map((teacher) => (
|
{classe?.teachers_details?.map((teacher) => (
|
||||||
<span
|
<span
|
||||||
key={teacher.id}
|
key={teacher.id}
|
||||||
className="px-3 py-1 bg-emerald-200 rounded-full text-emerald-800"
|
className="px-3 py-1 bg-primary/20 rounded text-secondary font-label text-sm"
|
||||||
>
|
>
|
||||||
{teacher.last_name} {teacher.first_name}
|
{teacher.last_name} {teacher.first_name}
|
||||||
</span>
|
</span>
|
||||||
@ -605,14 +660,14 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
<div className="bg-white rounded-md shadow-sm overflow-hidden">
|
||||||
<div className="flex border-b border-gray-200">
|
<div className="flex border-b border-gray-200">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('attendance')}
|
onClick={() => setActiveTab('attendance')}
|
||||||
className={`flex-1 py-3 px-4 text-center font-medium transition-colors ${
|
className={`flex-1 py-3 px-4 text-center font-label font-medium transition-colors min-h-[44px] ${
|
||||||
activeTab === 'attendance'
|
activeTab === 'attendance'
|
||||||
? 'text-emerald-600 border-b-2 border-emerald-600 bg-emerald-50'
|
? 'text-primary border-b-2 border-primary bg-primary/5'
|
||||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
: 'text-gray-500 hover:text-secondary hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@ -622,10 +677,10 @@ export default function Page() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('evaluations')}
|
onClick={() => setActiveTab('evaluations')}
|
||||||
className={`flex-1 py-3 px-4 text-center font-medium transition-colors ${
|
className={`flex-1 py-3 px-4 text-center font-label font-medium transition-colors min-h-[44px] ${
|
||||||
activeTab === 'evaluations'
|
activeTab === 'evaluations'
|
||||||
? 'text-emerald-600 border-b-2 border-emerald-600 bg-emerald-50'
|
? 'text-primary border-b-2 border-primary bg-primary/5'
|
||||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
: 'text-gray-500 hover:text-secondary hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@ -640,14 +695,14 @@ export default function Page() {
|
|||||||
{activeTab === 'attendance' && (
|
{activeTab === 'attendance' && (
|
||||||
<>
|
<>
|
||||||
{/* Affichage de la date du jour */}
|
{/* Affichage de la date du jour */}
|
||||||
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-lg shadow-md">
|
<div className="flex justify-between items-center mb-4 bg-white p-4 rounded-md shadow-sm">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center justify-center w-10 h-10 bg-emerald-100 text-emerald-600 rounded-full">
|
<div className="flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded">
|
||||||
<Clock className="w-6 h-6" />
|
<Clock className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Appel du jour :{' '}
|
Appel du jour :{' '}
|
||||||
<span className="ml-2 text-emerald-600">{today}</span>
|
<span className="ml-2 text-primary">{today}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -656,14 +711,14 @@ export default function Page() {
|
|||||||
text="Faire l'appel"
|
text="Faire l'appel"
|
||||||
onClick={handleToggleAttendanceMode}
|
onClick={handleToggleAttendanceMode}
|
||||||
primary
|
primary
|
||||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
className="px-4 py-2 bg-primary text-white font-label font-medium rounded shadow-sm hover:bg-secondary transition-colors min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
text="Valider l'appel"
|
text="Valider l'appel"
|
||||||
onClick={handleValidateAttendance}
|
onClick={handleValidateAttendance}
|
||||||
primary
|
primary
|
||||||
className="px-4 py-2 bg-emerald-500 text-white rounded-lg shadow hover:bg-emerald-600 transition-all"
|
className="px-4 py-2 bg-primary text-white font-label font-medium rounded shadow-sm hover:bg-secondary transition-colors min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -681,218 +736,224 @@ export default function Page() {
|
|||||||
name: 'Prénom',
|
name: 'Prénom',
|
||||||
transform: (row) => (
|
transform: (row) => (
|
||||||
<div className="text-center">{row.first_name}</div>
|
<div className="text-center">{row.first_name}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Niveau',
|
name: 'Niveau',
|
||||||
transform: (row) => (
|
transform: (row) => (
|
||||||
<div className="text-center">{getNiveauLabel(row.level)}</div>
|
<div className="text-center">{getNiveauLabel(row.level)}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
...(isEditingAttendance
|
...(isEditingAttendance
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: "Gestion de l'appel",
|
name: "Gestion de l'appel",
|
||||||
transform: (row) => (
|
transform: (row) => (
|
||||||
<div className="flex flex-col gap-2 items-center">
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{/* Présence */}
|
{/* Présence */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{attendance[row.id] ? (
|
{attendance[row.id] ? (
|
||||||
<>
|
<>
|
||||||
<CheckBox
|
<CheckBox
|
||||||
item={{ id: row.id }}
|
item={{ id: row.id }}
|
||||||
formData={{
|
formData={{
|
||||||
attendance: attendance[row.id] ? [row.id] : [],
|
attendance: attendance[row.id]
|
||||||
}}
|
? [row.id]
|
||||||
handleChange={() =>
|
: [],
|
||||||
handleAttendanceChange(row.id)
|
}}
|
||||||
}
|
handleChange={() =>
|
||||||
fieldName="attendance"
|
handleAttendanceChange(row.id)
|
||||||
/>
|
}
|
||||||
<span className="text-sm font-medium text-gray-700">
|
fieldName="attendance"
|
||||||
Présent
|
/>
|
||||||
</span>
|
<span className="text-sm font-medium text-gray-700">
|
||||||
</>
|
Présent
|
||||||
) : (
|
</span>
|
||||||
<>
|
</>
|
||||||
{/* Icône croix pour remettre l'élève en présent */}
|
) : (
|
||||||
<button
|
<>
|
||||||
type="button"
|
{/* Icône croix pour remettre l'élève en présent */}
|
||||||
onClick={() => handleAttendanceChange(row.id)}
|
<button
|
||||||
className="text-red-500 hover:text-red-700 transition"
|
type="button"
|
||||||
title="Annuler l'absence"
|
onClick={() => handleAttendanceChange(row.id)}
|
||||||
>
|
className="text-red-500 hover:text-red-700 transition"
|
||||||
<XCircle className="w-6 h-6" />
|
title="Annuler l'absence"
|
||||||
</button>
|
>
|
||||||
<span className="text-sm font-medium text-red-600">
|
<XCircle className="w-6 h-6" />
|
||||||
Effacer l'absence
|
</button>
|
||||||
</span>
|
<span className="text-sm font-medium text-red-600">
|
||||||
</>
|
Effacer l'absence
|
||||||
)}
|
</span>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
{/* Détails absence/retard */}
|
|
||||||
{!attendance[row.id] && (
|
|
||||||
<div className="w-full bg-emerald-50 border border-emerald-100 rounded-lg p-3 mt-2 shadow-sm">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Clock className="w-4 h-4 text-emerald-500" />
|
|
||||||
<span className="font-semibold text-emerald-700 text-sm">
|
|
||||||
Motif d'absence
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
|
||||||
{/* Select Absence/Retard */}
|
|
||||||
<SelectChoice
|
|
||||||
name={`type-${row.id}`}
|
|
||||||
label=""
|
|
||||||
placeHolder="Type"
|
|
||||||
selected={formAbsences[row.id]?.type || ''}
|
|
||||||
callback={(e) =>
|
|
||||||
setFormAbsences((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[row.id]: {
|
|
||||||
...prev[row.id],
|
|
||||||
type: e.target.value,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
choices={[
|
|
||||||
{ value: 'absence', label: 'Absence' },
|
|
||||||
{ value: 'retard', label: 'Retard' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Select Moment */}
|
{/* Détails absence/retard */}
|
||||||
<SelectChoice
|
{!attendance[row.id] && (
|
||||||
name={`moment-${row.id}`}
|
<div className="w-full bg-primary/5 border border-primary/10 rounded-lg p-3 mt-2 shadow-sm">
|
||||||
label=""
|
<div className="flex items-center gap-2 mb-2">
|
||||||
placeHolder="Durée"
|
<Clock className="w-4 h-4 text-primary" />
|
||||||
selected={formAbsences[row.id]?.moment || ''}
|
<span className="font-semibold text-secondary text-sm">
|
||||||
callback={(e) =>
|
Motif d'absence
|
||||||
setFormAbsences((prev) => ({
|
</span>
|
||||||
...prev,
|
</div>
|
||||||
[row.id]: {
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 items-center">
|
||||||
...prev[row.id],
|
{/* Select Absence/Retard */}
|
||||||
moment: parseInt(e.target.value, 10),
|
<SelectChoice
|
||||||
},
|
name={`type-${row.id}`}
|
||||||
}))
|
label=""
|
||||||
}
|
placeHolder="Type"
|
||||||
choices={Object.values(AbsenceMoment).map(
|
selected={formAbsences[row.id]?.type || ''}
|
||||||
(moment) => ({
|
callback={(e) =>
|
||||||
value: moment.value,
|
setFormAbsences((prev) => ({
|
||||||
label: moment.label,
|
...prev,
|
||||||
})
|
[row.id]: {
|
||||||
)}
|
...prev[row.id],
|
||||||
/>
|
type: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
choices={[
|
||||||
|
{ value: 'absence', label: 'Absence' },
|
||||||
|
{ value: 'retard', label: 'Retard' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Nouveau champ commentaire */}
|
{/* Select Moment */}
|
||||||
<input
|
<SelectChoice
|
||||||
type="text"
|
name={`moment-${row.id}`}
|
||||||
className="border rounded px-2 py-1 text-sm w-full"
|
label=""
|
||||||
placeholder="Commentaire"
|
placeHolder="Durée"
|
||||||
value={formAbsences[row.id]?.commentaire || ''}
|
selected={formAbsences[row.id]?.moment || ''}
|
||||||
onChange={(e) =>
|
callback={(e) =>
|
||||||
setFormAbsences((prev) => ({
|
setFormAbsences((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[row.id]: {
|
[row.id]: {
|
||||||
...prev[row.id],
|
...prev[row.id],
|
||||||
commentaire: e.target.value,
|
moment: parseInt(e.target.value, 10),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
choices={Object.values(AbsenceMoment).map(
|
||||||
|
(moment) => ({
|
||||||
|
value: moment.value,
|
||||||
|
label: moment.label,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Checkbox Justifié */}
|
{/* Nouveau champ commentaire */}
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<input
|
||||||
<CheckBox
|
type="text"
|
||||||
item={{ id: `justified-${row.id}` }}
|
className="border rounded px-2 py-1 text-sm w-full"
|
||||||
formData={{
|
placeholder="Commentaire"
|
||||||
justified: !!formAbsences[row.id]?.justified,
|
value={
|
||||||
}}
|
formAbsences[row.id]?.commentaire || ''
|
||||||
handleChange={() =>
|
}
|
||||||
setFormAbsences((prev) => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormAbsences((prev) => ({
|
||||||
[row.id]: {
|
...prev,
|
||||||
...prev[row.id],
|
[row.id]: {
|
||||||
justified: !prev[row.id]?.justified,
|
...prev[row.id],
|
||||||
},
|
commentaire: e.target.value,
|
||||||
}))
|
},
|
||||||
}
|
}))
|
||||||
fieldName="justified"
|
}
|
||||||
itemLabelFunc={() => 'Justifié'}
|
/>
|
||||||
/>
|
|
||||||
|
{/* Checkbox Justifié */}
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<CheckBox
|
||||||
|
item={{ id: `justified-${row.id}` }}
|
||||||
|
formData={{
|
||||||
|
justified:
|
||||||
|
!!formAbsences[row.id]?.justified,
|
||||||
|
}}
|
||||||
|
handleChange={() =>
|
||||||
|
setFormAbsences((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[row.id]: {
|
||||||
|
...prev[row.id],
|
||||||
|
justified: !prev[row.id]?.justified,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fieldName="justified"
|
||||||
|
itemLabelFunc={() => 'Justifié'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
),
|
||||||
</div>
|
},
|
||||||
),
|
]
|
||||||
},
|
: [
|
||||||
]
|
{
|
||||||
: [
|
name: 'Statut',
|
||||||
{
|
transform: (row) => {
|
||||||
name: 'Statut',
|
const today = new Date().toISOString().split('T')[0];
|
||||||
transform: (row) => {
|
const absence =
|
||||||
const today = new Date().toISOString().split('T')[0];
|
formAbsences[row.id] ||
|
||||||
const absence =
|
Object.values(fetchedAbsences).find(
|
||||||
formAbsences[row.id] ||
|
(absence) =>
|
||||||
Object.values(fetchedAbsences).find(
|
absence.student === row.id &&
|
||||||
(absence) =>
|
absence.day === today
|
||||||
absence.student === row.id && absence.day === today
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!absence) {
|
if (!absence) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-green-500 flex justify-center items-center gap-2">
|
<div className="text-center text-green-500 flex justify-center items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
Présent
|
Présent
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (absence.reason) {
|
switch (absence.reason) {
|
||||||
case AbsenceReason.JUSTIFIED_LATE.value:
|
case AbsenceReason.JUSTIFIED_LATE.value:
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
|
<div className="text-center text-yellow-500 flex justify-center items-center gap-2">
|
||||||
<Clock className="w-5 h-5" />
|
<Clock className="w-5 h-5" />
|
||||||
Retard justifié
|
Retard justifié
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
case AbsenceReason.UNJUSTIFIED_LATE.value:
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||||
<Clock className="w-5 h-5" />
|
<Clock className="w-5 h-5" />
|
||||||
Retard non justifié
|
Retard non justifié
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
case AbsenceReason.JUSTIFIED_ABSENCE.value:
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
|
<div className="text-center text-blue-500 flex justify-center items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
Absence justifiée
|
Absence justifiée
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
case AbsenceReason.UNJUSTIFIED_ABSENCE.value:
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
<div className="text-center text-red-500 flex justify-center items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
Absence non justifiée
|
Absence non justifiée
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
|
<div className="text-center text-gray-500 flex justify-center items-center gap-2">
|
||||||
<CheckCircle className="w-5 h-5" />
|
<CheckCircle className="w-5 h-5" />
|
||||||
Statut inconnu
|
Statut inconnu
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
]}
|
]}
|
||||||
data={filteredStudents} // Utiliser les élèves filtrés
|
data={filteredStudents} // Utiliser les élèves filtrés
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -900,11 +961,11 @@ export default function Page() {
|
|||||||
{activeTab === 'evaluations' && (
|
{activeTab === 'evaluations' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header avec sélecteur de période et bouton d'ajout */}
|
{/* Header avec sélecteur de période et bouton d'ajout */}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white p-4 rounded-md shadow-sm border border-gray-200">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<ClipboardList className="w-6 h-6 text-emerald-600" />
|
<ClipboardList className="w-6 h-6 text-primary" />
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Évaluations de la classe
|
Évaluations de la classe
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -936,7 +997,11 @@ export default function Page() {
|
|||||||
schoolClassId={parseInt(schoolClassId)}
|
schoolClassId={parseInt(schoolClassId)}
|
||||||
establishmentId={selectedEstablishmentId}
|
establishmentId={selectedEstablishmentId}
|
||||||
initialValues={editingEvaluation}
|
initialValues={editingEvaluation}
|
||||||
onSubmit={editingEvaluation ? handleUpdateEvaluation : handleCreateEvaluation}
|
onSubmit={
|
||||||
|
editingEvaluation
|
||||||
|
? handleUpdateEvaluation
|
||||||
|
: handleCreateEvaluation
|
||||||
|
}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setShowEvaluationForm(false);
|
setShowEvaluationForm(false);
|
||||||
setEditingEvaluation(null);
|
setEditingEvaluation(null);
|
||||||
@ -945,12 +1010,14 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Liste des évaluations */}
|
{/* Liste des évaluations */}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white p-4 rounded-md shadow-sm border border-gray-200">
|
||||||
<EvaluationList
|
<EvaluationList
|
||||||
evaluations={evaluations}
|
evaluations={evaluations}
|
||||||
onDelete={handleDeleteEvaluation}
|
onDelete={handleDeleteEvaluation}
|
||||||
onEdit={handleEditEvaluation}
|
onEdit={handleEditEvaluation}
|
||||||
onGradeStudents={(evaluation) => setSelectedEvaluation(evaluation)}
|
onGradeStudents={(evaluation) =>
|
||||||
|
setSelectedEvaluation(evaluation)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { getCurrentSchoolYear } from '@/utils/Date';
|
||||||
|
|
||||||
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
|
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
|
||||||
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
|
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
|
||||||
@ -54,6 +55,13 @@ export default function Page() {
|
|||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
const csrfToken = useCsrfToken();
|
||||||
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||||
|
const currentSchoolYear = getCurrentSchoolYear();
|
||||||
|
|
||||||
|
const scheduleClasses = classes.filter(
|
||||||
|
(classe) => classe?.school_year === currentSchoolYear
|
||||||
|
);
|
||||||
|
const scheduleSpecialities = specialities;
|
||||||
|
const scheduleTeachers = teachers;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
@ -299,9 +307,9 @@ export default function Page() {
|
|||||||
<ClassesProvider>
|
<ClassesProvider>
|
||||||
<ScheduleManagement
|
<ScheduleManagement
|
||||||
handleUpdatePlanning={handleUpdatePlanning}
|
handleUpdatePlanning={handleUpdatePlanning}
|
||||||
classes={classes}
|
classes={scheduleClasses}
|
||||||
specialities={specialities}
|
specialities={scheduleSpecialities}
|
||||||
teachers={teachers}
|
teachers={scheduleTeachers}
|
||||||
/>
|
/>
|
||||||
</ClassesProvider>
|
</ClassesProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { User, Mail } from 'lucide-react';
|
import { User, Mail, Info } from 'lucide-react';
|
||||||
import InputTextIcon from '@/components/Form/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
@ -43,6 +43,23 @@ import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
|||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
|
||||||
|
function NoInfoAlert({ message }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full rounded border border-orange-300 bg-orange-50 px-4 py-3 text-orange-800"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
<div className="text-sm leading-6">
|
||||||
|
<span className="font-semibold">Information :</span>{' '}
|
||||||
|
<span>{message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateSubscriptionPage() {
|
export default function CreateSubscriptionPage() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
studentLastName: '',
|
studentLastName: '',
|
||||||
@ -729,11 +746,11 @@ export default function CreateSubscriptionPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto p-12 space-y-12">
|
<div className="mx-auto p-12 space-y-12">
|
||||||
{registerFormID ? (
|
{registerFormID ? (
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="font-headline text-2xl font-bold">
|
||||||
Modifier un dossier d'inscription
|
Modifier un dossier d'inscription
|
||||||
</h1>
|
</h1>
|
||||||
) : (
|
) : (
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="font-headline text-2xl font-bold">
|
||||||
Créer un dossier d'inscription
|
Créer un dossier d'inscription
|
||||||
</h1>
|
</h1>
|
||||||
)}
|
)}
|
||||||
@ -936,7 +953,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
}}
|
}}
|
||||||
rowClassName={(row) =>
|
rowClassName={(row) =>
|
||||||
selectedStudent && selectedStudent.id === row.id
|
selectedStudent && selectedStudent.id === row.id
|
||||||
? 'bg-emerald-600 text-white'
|
? 'bg-primary text-white'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
selectedRows={selectedStudent ? [selectedStudent.id] : []} // Assurez-vous que selectedRows est un tableau
|
selectedRows={selectedStudent ? [selectedStudent.id] : []} // Assurez-vous que selectedRows est un tableau
|
||||||
@ -948,7 +965,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
|
|
||||||
{selectedStudent && (
|
{selectedStudent && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="font-bold">
|
<h3 className="font-headline font-bold">
|
||||||
Responsables associés à {selectedStudent.last_name}{' '}
|
Responsables associés à {selectedStudent.last_name}{' '}
|
||||||
{selectedStudent.first_name} :
|
{selectedStudent.first_name} :
|
||||||
</h3>
|
</h3>
|
||||||
@ -1003,22 +1020,13 @@ export default function CreateSubscriptionPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<NoInfoAlert message="Aucune réduction n'a été créée sur les frais d'inscription." />
|
||||||
className="bg-orange-100 border border-orange-400 text-orange-700 px-4 py-3 rounded relative"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<strong className="font-bold">Information</strong>
|
|
||||||
<span className="block sm:inline">
|
|
||||||
Aucune réduction n'a été créée sur les frais
|
|
||||||
d'inscription.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Montant total */}
|
{/* Montant total */}
|
||||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-300 mt-4">
|
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-md shadow-sm border border-gray-300 mt-4">
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">
|
||||||
Montant total des frais d'inscription :
|
Montant total des frais d'inscription :
|
||||||
</span>
|
</span>
|
||||||
@ -1028,15 +1036,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<NoInfoAlert message="Aucun frais d'inscription n'a été créé." />
|
||||||
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<strong className="font-bold">Attention!</strong>
|
|
||||||
<span className="block sm:inline">
|
|
||||||
Aucun frais d'inscription n'a été créé.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
|
|
||||||
@ -1067,22 +1067,13 @@ export default function CreateSubscriptionPage() {
|
|||||||
handleDiscountSelection={handleTuitionDiscountSelection}
|
handleDiscountSelection={handleTuitionDiscountSelection}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<NoInfoAlert message="Aucune réduction n'a été créée sur les frais de scolarité." />
|
||||||
className="bg-orange-100 border border-orange-400 text-orange-700 px-4 py-3 rounded relative"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<strong className="font-bold">Information</strong>
|
|
||||||
<span className="block sm:inline">
|
|
||||||
Aucune réduction n'a été créée sur les frais de
|
|
||||||
scolarité.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Montant total */}
|
{/* Montant total */}
|
||||||
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-300 mt-4">
|
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-md shadow-sm border border-gray-300 mt-4">
|
||||||
<span className="text-sm font-medium text-gray-600">
|
<span className="text-sm font-medium text-gray-600">
|
||||||
Montant total des frais de scolarité :
|
Montant total des frais de scolarité :
|
||||||
</span>
|
</span>
|
||||||
@ -1092,15 +1083,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<NoInfoAlert message="Aucun frais de scolarité n'a été créé." />
|
||||||
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<strong className="font-bold">Attention!</strong>
|
|
||||||
<span className="block sm:inline">
|
|
||||||
Aucun frais de scolarité n'a été créé.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
|
|
||||||
@ -1153,7 +1136,7 @@ export default function CreateSubscriptionPage() {
|
|||||||
className={`px-6 py-2 rounded-md shadow ${
|
className={`px-6 py-2 rounded-md shadow ${
|
||||||
isSubmitDisabled()
|
isSubmitDisabled()
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
: 'bg-primary text-white hover:bg-primary'
|
||||||
}`}
|
}`}
|
||||||
primary
|
primary
|
||||||
disabled={isSubmitDisabled()}
|
disabled={isSubmitDisabled()}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Tab from '@/components/Tab';
|
import SidebarTabs from '@/components/SidebarTabs';
|
||||||
import Textarea from '@/components/Textarea';
|
import Textarea from '@/components/Textarea';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import StatusLabel from '@/components/StatusLabel';
|
import StatusLabel from '@/components/StatusLabel';
|
||||||
@ -55,6 +55,7 @@ import {
|
|||||||
HISTORICAL_FILTER,
|
HISTORICAL_FILTER,
|
||||||
} from '@/utils/constants';
|
} from '@/utils/constants';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
import AlertMessage from '@/components/AlertMessage';
|
||||||
|
import EmptyState from '@/components/EmptyState';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import { exportToCSV } from '@/utils/exportCSV';
|
import { exportToCSV } from '@/utils/exportCSV';
|
||||||
|
|
||||||
@ -167,43 +168,86 @@ export default function Page({ params: { locale } }) {
|
|||||||
|
|
||||||
// Export CSV
|
// Export CSV
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
const dataToExport = activeTab === CURRENT_YEAR_FILTER
|
const dataToExport =
|
||||||
? registrationFormsDataCurrentYear
|
activeTab === CURRENT_YEAR_FILTER
|
||||||
: activeTab === NEXT_YEAR_FILTER
|
? registrationFormsDataCurrentYear
|
||||||
? registrationFormsDataNextYear
|
: activeTab === NEXT_YEAR_FILTER
|
||||||
: registrationFormsDataHistorical;
|
? registrationFormsDataNextYear
|
||||||
|
: registrationFormsDataHistorical;
|
||||||
|
|
||||||
const exportColumns = [
|
const exportColumns = [
|
||||||
{ key: 'student', label: 'Nom', transform: (value) => value?.last_name || '' },
|
{
|
||||||
{ key: 'student', label: 'Prénom', transform: (value) => value?.first_name || '' },
|
key: 'student',
|
||||||
{ key: 'student', label: 'Date de naissance', transform: (value) => value?.birth_date || '' },
|
label: 'Nom',
|
||||||
{ key: 'student', label: 'Email contact', transform: (value) => value?.guardians?.[0]?.associated_profile_email || '' },
|
transform: (value) => value?.last_name || '',
|
||||||
{ key: 'student', label: 'Téléphone contact', transform: (value) => value?.guardians?.[0]?.phone || '' },
|
},
|
||||||
{ key: 'student', label: 'Nom responsable 1', transform: (value) => value?.guardians?.[0]?.last_name || '' },
|
{
|
||||||
{ key: 'student', label: 'Prénom responsable 1', transform: (value) => value?.guardians?.[0]?.first_name || '' },
|
key: 'student',
|
||||||
{ key: 'student', label: 'Nom responsable 2', transform: (value) => value?.guardians?.[1]?.last_name || '' },
|
label: 'Prénom',
|
||||||
{ key: 'student', label: 'Prénom responsable 2', transform: (value) => value?.guardians?.[1]?.first_name || '' },
|
transform: (value) => value?.first_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Date de naissance',
|
||||||
|
transform: (value) => value?.birth_date || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Email contact',
|
||||||
|
transform: (value) =>
|
||||||
|
value?.guardians?.[0]?.associated_profile_email || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Téléphone contact',
|
||||||
|
transform: (value) => value?.guardians?.[0]?.phone || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Nom responsable 1',
|
||||||
|
transform: (value) => value?.guardians?.[0]?.last_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Prénom responsable 1',
|
||||||
|
transform: (value) => value?.guardians?.[0]?.first_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Nom responsable 2',
|
||||||
|
transform: (value) => value?.guardians?.[1]?.last_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'student',
|
||||||
|
label: 'Prénom responsable 2',
|
||||||
|
transform: (value) => value?.guardians?.[1]?.first_name || '',
|
||||||
|
},
|
||||||
{ key: 'school_year', label: 'Année scolaire' },
|
{ key: 'school_year', label: 'Année scolaire' },
|
||||||
{ key: 'status', label: 'Statut', transform: (value) => {
|
{
|
||||||
const statusMap = {
|
key: 'status',
|
||||||
0: 'En attente',
|
label: 'Statut',
|
||||||
1: 'En cours',
|
transform: (value) => {
|
||||||
2: 'Envoyé',
|
const statusMap = {
|
||||||
3: 'À relancer',
|
0: 'En attente',
|
||||||
4: 'À valider',
|
1: 'En cours',
|
||||||
5: 'Validé',
|
2: 'Envoyé',
|
||||||
6: 'Archivé',
|
3: 'À relancer',
|
||||||
};
|
4: 'À valider',
|
||||||
return statusMap[value] || value;
|
5: 'Validé',
|
||||||
}},
|
6: 'Archivé',
|
||||||
|
};
|
||||||
|
return statusMap[value] || value;
|
||||||
|
},
|
||||||
|
},
|
||||||
{ key: 'formatted_last_update', label: 'Dernière mise à jour' },
|
{ key: 'formatted_last_update', label: 'Dernière mise à jour' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const yearLabel = activeTab === CURRENT_YEAR_FILTER
|
const yearLabel =
|
||||||
? currentSchoolYear
|
activeTab === CURRENT_YEAR_FILTER
|
||||||
: activeTab === NEXT_YEAR_FILTER
|
? currentSchoolYear
|
||||||
? nextSchoolYear
|
: activeTab === NEXT_YEAR_FILTER
|
||||||
: 'historique';
|
? nextSchoolYear
|
||||||
|
: 'historique';
|
||||||
const filename = `inscriptions_${yearLabel}_${new Date().toISOString().split('T')[0]}`;
|
const filename = `inscriptions_${yearLabel}_${new Date().toISOString().split('T')[0]}`;
|
||||||
exportToCSV(dataToExport, exportColumns, filename);
|
exportToCSV(dataToExport, exportColumns, filename);
|
||||||
};
|
};
|
||||||
@ -506,7 +550,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
<span title="Envoyer le dossier">
|
<span title="Envoyer le dossier">
|
||||||
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
|
<Send className="w-5 h-5 text-primary hover:text-secondary" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@ -536,7 +580,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
<span title="Renvoyer le dossier">
|
<span title="Renvoyer le dossier">
|
||||||
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
|
<Send className="w-5 h-5 text-primary hover:text-secondary" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@ -575,7 +619,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
<span title="Valider le dossier">
|
<span title="Valider le dossier">
|
||||||
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
|
<CheckCircle className="w-5 h-5 text-primary hover:text-secondary" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@ -690,7 +734,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
<span title="Uploader un mandat SEPA">
|
<span title="Uploader un mandat SEPA">
|
||||||
<Upload className="w-5 h-5 text-emerald-500 hover:text-emerald-700" />
|
<Upload className="w-5 h-5 text-primary hover:text-secondary" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => openSepaUploadModal(row),
|
onClick: () => openSepaUploadModal(row),
|
||||||
@ -800,161 +844,139 @@ export default function Page({ params: { locale } }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let emptyMessage;
|
const getEmptyMessageForTab = (tabFilter) => {
|
||||||
if (activeTab === CURRENT_YEAR_FILTER && searchTerm === '') {
|
if (searchTerm !== '') {
|
||||||
emptyMessage = (
|
return (
|
||||||
<AlertMessage
|
<EmptyState
|
||||||
type="warning"
|
icon={Search}
|
||||||
title="Aucun dossier d'inscription pour l'année en cours"
|
title="Aucun dossier trouvé"
|
||||||
message="Veuillez procéder à la création d'un nouveau dossier d'inscription pour l'année scolaire en cours."
|
description="Modifiez votre recherche pour trouver un dossier d'inscription."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tabFilter === CURRENT_YEAR_FILTER) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={FileText}
|
||||||
|
title="Aucun dossier d'inscription pour l'année en cours"
|
||||||
|
description="Commencez par créer un dossier d'inscription pour l'année scolaire en cours."
|
||||||
|
actionLabel="Créer un dossier"
|
||||||
|
actionIcon={Plus}
|
||||||
|
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tabFilter === NEXT_YEAR_FILTER) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={FileText}
|
||||||
|
title="Aucun dossier pour l'année prochaine"
|
||||||
|
description="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
||||||
|
actionLabel="Créer un dossier"
|
||||||
|
actionIcon={Plus}
|
||||||
|
onAction={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={Archive}
|
||||||
|
title="Aucun dossier archivé"
|
||||||
|
description="Aucun dossier archivé n'est disponible pour les années précédentes."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (activeTab === NEXT_YEAR_FILTER && searchTerm === '') {
|
};
|
||||||
emptyMessage = (
|
|
||||||
<AlertMessage
|
const renderTabContent = (data, currentPage, totalPages, tabFilter) => (
|
||||||
type="info"
|
<div className="p-4">
|
||||||
title="Aucun dossier d'inscription pour l'année prochaine"
|
<div className="flex justify-between items-center mb-4 w-full">
|
||||||
message="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
<div className="relative flex-grow">
|
||||||
|
<Search
|
||||||
|
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('searchStudent')}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
onClick={handleExportCSV}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-secondary bg-primary/10 rounded hover:bg-primary/20 transition-colors"
|
||||||
|
title="Exporter en CSV"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Exporter
|
||||||
|
</button>
|
||||||
|
{profileRole !== 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(FE_ADMIN_SUBSCRIPTIONS_CREATE_URL)}
|
||||||
|
className="flex items-center bg-primary text-white p-2 rounded-full shadow hover:bg-secondary transition duration-200"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||||
|
<Table
|
||||||
|
key={`${tabFilter}-${currentPage}-${searchTerm}`}
|
||||||
|
data={data}
|
||||||
|
columns={columns}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
emptyMessage={getEmptyMessageForTab(tabFilter)}
|
||||||
/>
|
/>
|
||||||
);
|
</div>
|
||||||
} else if (activeTab === HISTORICAL_FILTER && searchTerm === '') {
|
);
|
||||||
emptyMessage = (
|
|
||||||
<AlertMessage
|
|
||||||
type="info"
|
|
||||||
title="Aucun dossier d'inscription historique"
|
|
||||||
message="Aucun dossier archivé n'est disponible pour les années précédentes."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="h-full flex flex-col">
|
||||||
<div className="border-b border-gray-200 mb-6">
|
<SidebarTabs
|
||||||
<div className="flex items-center gap-8">
|
tabs={[
|
||||||
{/* Tab pour l'année scolaire en cours */}
|
{
|
||||||
<Tab
|
id: CURRENT_YEAR_FILTER,
|
||||||
text={
|
label: `${currentSchoolYear}${totalCurrentYear > 0 ? ` (${totalCurrentYear})` : ''}`,
|
||||||
<>
|
content: renderTabContent(
|
||||||
{currentSchoolYear}
|
registrationFormsDataCurrentYear,
|
||||||
<span className="ml-2 text-sm text-gray-400">
|
currentSchoolYearPage,
|
||||||
({totalCurrentYear})
|
totalCurrentSchoolYearPages,
|
||||||
</span>
|
CURRENT_YEAR_FILTER
|
||||||
</>
|
),
|
||||||
}
|
},
|
||||||
active={activeTab === CURRENT_YEAR_FILTER}
|
{
|
||||||
onClick={() => setActiveTab(CURRENT_YEAR_FILTER)}
|
id: NEXT_YEAR_FILTER,
|
||||||
/>
|
label: `${nextSchoolYear}${totalNextYear > 0 ? ` (${totalNextYear})` : ''}`,
|
||||||
|
content: renderTabContent(
|
||||||
{/* Tab pour l'année scolaire prochaine */}
|
registrationFormsDataNextYear,
|
||||||
<Tab
|
currentSchoolNextYearPage,
|
||||||
text={
|
totalNextSchoolYearPages,
|
||||||
<>
|
NEXT_YEAR_FILTER
|
||||||
{nextSchoolYear}
|
),
|
||||||
<span className="ml-2 text-sm text-gray-400">
|
},
|
||||||
({totalNextYear})
|
{
|
||||||
</span>
|
id: HISTORICAL_FILTER,
|
||||||
</>
|
label: `${t('historical')}${totalHistorical > 0 ? ` (${totalHistorical})` : ''}`,
|
||||||
}
|
content: renderTabContent(
|
||||||
active={activeTab === NEXT_YEAR_FILTER}
|
registrationFormsDataHistorical,
|
||||||
onClick={() => setActiveTab(NEXT_YEAR_FILTER)}
|
currentSchoolHistoricalYearPage,
|
||||||
/>
|
totalHistoricalPages,
|
||||||
|
HISTORICAL_FILTER
|
||||||
{/* Tab pour l'historique */}
|
),
|
||||||
<Tab
|
},
|
||||||
text={
|
]}
|
||||||
<>
|
onTabChange={(newTab) => setActiveTab(newTab)}
|
||||||
{t('historical')}
|
/>
|
||||||
<span className="ml-2 text-sm text-gray-400">
|
|
||||||
({totalHistorical})
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
active={activeTab === HISTORICAL_FILTER}
|
|
||||||
onClick={() => setActiveTab(HISTORICAL_FILTER)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-b border-gray-200 mb-6 w-full">
|
|
||||||
{activeTab === CURRENT_YEAR_FILTER ||
|
|
||||||
activeTab === NEXT_YEAR_FILTER ||
|
|
||||||
activeTab === HISTORICAL_FILTER ? (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="flex justify-between items-center mb-4 w-full">
|
|
||||||
<div className="relative flex-grow">
|
|
||||||
<Search
|
|
||||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={t('searchStudent')}
|
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={handleExportCSV}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-100 rounded-lg hover:bg-emerald-200 transition-colors"
|
|
||||||
title="Exporter en CSV"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
Exporter
|
|
||||||
</button>
|
|
||||||
{profileRole !== 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const url = `${FE_ADMIN_SUBSCRIPTIONS_CREATE_URL}`;
|
|
||||||
router.push(url);
|
|
||||||
}}
|
|
||||||
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
|
|
||||||
>
|
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
|
||||||
<Table
|
|
||||||
key={`${currentSchoolYearPage}-${searchTerm}`}
|
|
||||||
data={
|
|
||||||
activeTab === CURRENT_YEAR_FILTER
|
|
||||||
? registrationFormsDataCurrentYear
|
|
||||||
: activeTab === NEXT_YEAR_FILTER
|
|
||||||
? registrationFormsDataNextYear
|
|
||||||
: registrationFormsDataHistorical
|
|
||||||
}
|
|
||||||
columns={columns}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
currentPage={
|
|
||||||
activeTab === CURRENT_YEAR_FILTER
|
|
||||||
? currentSchoolYearPage
|
|
||||||
: activeTab === NEXT_YEAR_FILTER
|
|
||||||
? currentSchoolNextYearPage
|
|
||||||
: currentSchoolHistoricalYearPage
|
|
||||||
}
|
|
||||||
totalPages={
|
|
||||||
activeTab === CURRENT_YEAR_FILTER
|
|
||||||
? totalCurrentSchoolYearPages
|
|
||||||
: activeTab === NEXT_YEAR_FILTER
|
|
||||||
? totalNextSchoolYearPages
|
|
||||||
: totalHistoricalPages
|
|
||||||
}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<Popup
|
<Popup
|
||||||
isOpen={confirmPopupVisible}
|
isOpen={confirmPopupVisible}
|
||||||
message={confirmPopupMessage}
|
message={confirmPopupMessage}
|
||||||
|
|||||||
@ -10,10 +10,10 @@ export default function Home() {
|
|||||||
const t = useTranslations('homePage');
|
const t = useTranslations('homePage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
<div className="flex flex-col items-center justify-center min-h-screen py-2 bg-neutral">
|
||||||
<Logo className="mb-4" /> {/* Ajout du logo */}
|
<Logo className="mb-4" />
|
||||||
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
<h1 className="font-headline text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
|
||||||
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
|
<p className="font-body text-lg mb-8">{t('pleaseLogin')}</p>
|
||||||
<Button text={t('loginButton')} primary href="/users/login" />
|
<Button text={t('loginButton')} primary href="/users/login" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,10 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { MessageSquare, Settings, Home } from 'lucide-react';
|
import { MessageSquare, Settings, Home } from 'lucide-react';
|
||||||
import {
|
import { FE_PARENTS_HOME_URL, FE_PARENTS_MESSAGERIE_URL } from '@/utils/Url';
|
||||||
FE_PARENTS_HOME_URL,
|
|
||||||
FE_PARENTS_MESSAGERIE_URL
|
|
||||||
} from '@/utils/Url';
|
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||||
import { disconnect } from '@/app/actions/authAction';
|
import { disconnect } from '@/app/actions/authAction';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
@ -15,6 +12,7 @@ import MobileTopbar from '@/components/MobileTopbar';
|
|||||||
import { RIGHTS } from '@/utils/rights';
|
import { RIGHTS } from '@/utils/rights';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
|
import { useChatConnection } from '@/context/ChatConnectionContext';
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -22,6 +20,7 @@ export default function Layout({ children }) {
|
|||||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const { clearContext } = useEstablishment();
|
const { clearContext } = useEstablishment();
|
||||||
|
const { totalUnreadCount, resetUnreadCount } = useChatConnection();
|
||||||
const softwareName = 'N3WT School';
|
const softwareName = 'N3WT School';
|
||||||
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
|
||||||
|
|
||||||
@ -41,7 +40,8 @@ export default function Layout({ children }) {
|
|||||||
name: 'Messagerie',
|
name: 'Messagerie',
|
||||||
url: FE_PARENTS_MESSAGERIE_URL,
|
url: FE_PARENTS_MESSAGERIE_URL,
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
}
|
badge: totalUnreadCount,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Déterminer la page actuelle pour la sidebar
|
// Déterminer la page actuelle pour la sidebar
|
||||||
@ -70,7 +70,11 @@ export default function Layout({ children }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fermer la sidebar quand on change de page sur mobile
|
// Fermer la sidebar quand on change de page sur mobile
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
}, [pathname]);
|
// Réinitialiser le compteur non lu quand on ouvre la messagerie
|
||||||
|
if (pathname?.includes('/messagerie')) {
|
||||||
|
resetUnreadCount();
|
||||||
|
}
|
||||||
|
}, [pathname, resetUnreadCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
|
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
|
||||||
@ -100,7 +104,7 @@ export default function Layout({ children }) {
|
|||||||
|
|
||||||
{/* Main container */}
|
{/* Main container */}
|
||||||
<div
|
<div
|
||||||
className={`absolute overflow-auto bg-gradient-to-br from-emerald-50 via-sky-50 to-emerald-100 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0 ${!isMessagingPage ? 'p-4 md:p-8' : ''}`}
|
className={`absolute overflow-auto bg-gradient-to-br from-primary/5 via-sky-50 to-primary/10 top-14 md:top-0 bottom-16 left-0 md:left-64 right-0 ${!isMessagingPage ? 'p-4 md:p-8' : ''}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -282,10 +282,10 @@ export default function ParentHomePage() {
|
|||||||
<>
|
<>
|
||||||
<div className="p-4 flex items-center border-b">
|
<div className="p-4 flex items-center border-b">
|
||||||
<button
|
<button
|
||||||
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
|
className="text-primary hover:text-secondary font-label font-medium min-h-[44px] flex items-center transition-colors"
|
||||||
onClick={() => setShowPlanning(false)}
|
onClick={() => setShowPlanning(false)}
|
||||||
>
|
>
|
||||||
← Retour
|
<ArrowLeft className="w-4 h-4 mr-1" /> Retour
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
@ -309,7 +309,7 @@ export default function ParentHomePage() {
|
|||||||
title="Événements à venir"
|
title="Événements à venir"
|
||||||
description="Prochains événements de l'établissement"
|
description="Prochains événements de l'établissement"
|
||||||
/>
|
/>
|
||||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100">
|
<div className="bg-neutral p-4 rounded-md shadow-sm border border-gray-100">
|
||||||
{upcomingEvents.slice(0, 3).map((event, index) => (
|
{upcomingEvents.slice(0, 3).map((event, index) => (
|
||||||
<EventCard key={index} {...event} />
|
<EventCard key={index} {...event} />
|
||||||
))}
|
))}
|
||||||
@ -343,7 +343,7 @@ export default function ParentHomePage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={student.id}
|
key={student.id}
|
||||||
className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
|
className="bg-white rounded-md shadow-sm border border-gray-200 overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* En-tête de la carte (toujours visible) */}
|
{/* En-tête de la carte (toujours visible) */}
|
||||||
<div
|
<div
|
||||||
@ -373,7 +373,7 @@ export default function ParentHomePage() {
|
|||||||
|
|
||||||
{/* Infos principales */}
|
{/* Infos principales */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-lg font-semibold text-gray-800">
|
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
{student.last_name} {student.first_name}
|
{student.last_name} {student.first_name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@ -399,7 +399,7 @@ export default function ParentHomePage() {
|
|||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{child.status === 2 && (
|
{child.status === 2 && (
|
||||||
<button
|
<button
|
||||||
className="p-2 text-blue-500 hover:text-blue-700 hover:bg-blue-50 rounded-full"
|
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-blue-500 hover:text-blue-700 hover:bg-blue-50 rounded-full transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleEdit(student.id);
|
handleEdit(student.id);
|
||||||
@ -412,7 +412,7 @@ export default function ParentHomePage() {
|
|||||||
|
|
||||||
{(child.status === 3 || child.status === 8 || child.status === 5 || child.status === 7) && (
|
{(child.status === 3 || child.status === 8 || child.status === 5 || child.status === 7) && (
|
||||||
<button
|
<button
|
||||||
className="p-2 text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full"
|
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-purple-500 hover:text-purple-700 hover:bg-purple-50 rounded-full transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleView(student.id);
|
handleView(student.id);
|
||||||
@ -429,14 +429,14 @@ export default function ParentHomePage() {
|
|||||||
href={getSecureFileUrl(child.sepa_file)}
|
href={getSecureFileUrl(child.sepa_file)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 text-green-500 hover:text-green-700 hover:bg-green-50 rounded-full"
|
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-primary hover:text-secondary hover:bg-primary/5 rounded-full transition-colors"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
title="Télécharger le mandat SEPA"
|
title="Télécharger le mandat SEPA"
|
||||||
>
|
>
|
||||||
<Download className="h-5 w-5" />
|
<Download className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
className={`p-2 rounded-full ${
|
className={`p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-full transition-colors ${
|
||||||
uploadingStudentId === student.id && uploadState === 'on'
|
uploadingStudentId === student.id && uploadState === 'on'
|
||||||
? 'bg-blue-100 text-blue-600'
|
? 'bg-blue-100 text-blue-600'
|
||||||
: 'text-blue-500 hover:text-blue-700 hover:bg-blue-50'
|
: 'text-blue-500 hover:text-blue-700 hover:bg-blue-50'
|
||||||
@ -455,7 +455,7 @@ export default function ParentHomePage() {
|
|||||||
{isEnrolled && (
|
{isEnrolled && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="p-2 text-primary hover:text-secondary hover:bg-tertiary/10 rounded-full"
|
className="p-2 min-h-[44px] min-w-[44px] flex items-center justify-center text-primary hover:text-secondary hover:bg-tertiary/10 rounded-full transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showClassPlanning(student);
|
showClassPlanning(student);
|
||||||
@ -484,7 +484,7 @@ export default function ParentHomePage() {
|
|||||||
onFileSelect={handleFileUpload}
|
onFileSelect={handleFileUpload}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className={`mt-4 px-6 py-2 rounded-md ${
|
className={`mt-4 px-4 py-2 rounded font-label font-medium min-h-[44px] transition-colors ${
|
||||||
uploadedFile
|
uploadedFile
|
||||||
? 'bg-primary text-white hover:bg-secondary'
|
? 'bg-primary text-white hover:bg-secondary'
|
||||||
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
: 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||||
@ -499,10 +499,10 @@ export default function ParentHomePage() {
|
|||||||
|
|
||||||
{/* Section détaillée pour les élèves inscrits (expanded) */}
|
{/* Section détaillée pour les élèves inscrits (expanded) */}
|
||||||
{isEnrolled && isExpanded && (
|
{isEnrolled && isExpanded && (
|
||||||
<div className="border-t bg-stone-50 p-4 space-y-6">
|
<div className="border-t bg-neutral p-4 space-y-6">
|
||||||
|
|
||||||
{/* Bloc période : compétences + notes */}
|
{/* Bloc période : compétences + notes */}
|
||||||
<div className="bg-white rounded-lg border border-primary/20 p-4 space-y-4">
|
<div className="bg-white rounded-md border border-primary/20 p-4 space-y-4">
|
||||||
{/* Sélecteur de période */}
|
{/* Sélecteur de période */}
|
||||||
<div className="flex items-center gap-3 pb-3 border-b border-gray-100">
|
<div className="flex items-center gap-3 pb-3 border-b border-gray-100">
|
||||||
<div className="w-full sm:w-48">
|
<div className="w-full sm:w-48">
|
||||||
@ -540,10 +540,10 @@ export default function ParentHomePage() {
|
|||||||
const pctNotEvaluated = total ? Math.round((notEvaluated / total) * 100) : 0;
|
const pctNotEvaluated = total ? Math.round((notEvaluated / total) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-100 rounded-lg p-4">
|
<div className="border border-gray-100 rounded-md p-4">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Award className="w-5 h-5 text-primary" />
|
<Award className="w-5 h-5 text-primary" />
|
||||||
<h3 className="text-lg font-semibold text-gray-800">
|
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Compétences
|
Compétences
|
||||||
</h3>
|
</h3>
|
||||||
{total > 0 && (
|
{total > 0 && (
|
||||||
@ -552,19 +552,19 @@ export default function ParentHomePage() {
|
|||||||
</div>
|
</div>
|
||||||
{total > 0 ? (
|
{total > 0 ? (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
<div className="flex flex-col items-center p-3 bg-emerald-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-primary/5 rounded-md">
|
||||||
<span className="text-2xl font-bold text-emerald-600">{pctAcquired}%</span>
|
<span className="text-2xl font-bold text-primary">{pctAcquired}%</span>
|
||||||
<span className="text-sm text-emerald-700">Acquises</span>
|
<span className="text-sm text-secondary">Acquises</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-yellow-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-yellow-50 rounded-md">
|
||||||
<span className="text-2xl font-bold text-yellow-600">{pctInProgress}%</span>
|
<span className="text-2xl font-bold text-yellow-600">{pctInProgress}%</span>
|
||||||
<span className="text-sm text-yellow-700">En cours</span>
|
<span className="text-sm text-yellow-700">En cours</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-red-50 rounded-md">
|
||||||
<span className="text-2xl font-bold text-red-500">{pctNotAcquired}%</span>
|
<span className="text-2xl font-bold text-red-500">{pctNotAcquired}%</span>
|
||||||
<span className="text-sm text-red-600">Non acquises</span>
|
<span className="text-sm text-red-600">Non acquises</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-gray-100 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-gray-100 rounded-md">
|
||||||
<span className="text-2xl font-bold text-gray-500">{pctNotEvaluated}%</span>
|
<span className="text-2xl font-bold text-gray-500">{pctNotEvaluated}%</span>
|
||||||
<span className="text-sm text-gray-600">Non évaluées</span>
|
<span className="text-sm text-gray-600">Non évaluées</span>
|
||||||
</div>
|
</div>
|
||||||
@ -577,10 +577,10 @@ export default function ParentHomePage() {
|
|||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Notes par matière - Vue simplifiée */}
|
{/* Notes par matière - Vue simplifiée */}
|
||||||
<div className="border border-gray-100 rounded-lg p-4">
|
<div className="border border-gray-100 rounded-md p-4">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Award className="w-5 h-5 text-primary" />
|
<Award className="w-5 h-5 text-primary" />
|
||||||
<h3 className="text-lg font-semibold text-gray-800">
|
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Notes par matière
|
Notes par matière
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -617,7 +617,7 @@ export default function ParentHomePage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={group.name}
|
key={group.name}
|
||||||
className="rounded-lg p-4 border"
|
className="rounded-md p-4 border"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${group.color}10`,
|
backgroundColor: `${group.color}10`,
|
||||||
borderColor: `${group.color}40`,
|
borderColor: `${group.color}40`,
|
||||||
@ -657,28 +657,28 @@ export default function ParentHomePage() {
|
|||||||
{/* Fin bloc période */}
|
{/* Fin bloc période */}
|
||||||
|
|
||||||
{/* Section Absences — toute l'année scolaire */}
|
{/* Section Absences — toute l'année scolaire */}
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
<div className="bg-white rounded-md border border-gray-200 p-4">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Clock className="w-5 h-5 text-primary" />
|
<Clock className="w-5 h-5 text-primary" />
|
||||||
<h3 className="text-lg font-semibold text-gray-800">
|
<h3 className="font-headline text-lg font-semibold text-gray-800">
|
||||||
Absences & Retards
|
Absences & Retards
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 mb-4">Toute l'année scolaire</p>
|
<p className="text-xs text-gray-400 mb-4">Toute l'année scolaire</p>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
<div className="flex flex-col items-center p-3 bg-green-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-primary/5 rounded-md">
|
||||||
<span className="text-2xl font-bold text-green-600">{absenceStats.justifiedAbsence}</span>
|
<span className="text-2xl font-bold text-primary">{absenceStats.justifiedAbsence}</span>
|
||||||
<span className="text-sm text-green-700 text-center">Absences justifiées</span>
|
<span className="text-sm text-secondary text-center">Absences justifiées</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-red-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-red-50 rounded-md">
|
||||||
<span className="text-2xl font-bold text-red-500">{absenceStats.unjustifiedAbsence}</span>
|
<span className="text-2xl font-bold text-red-500">{absenceStats.unjustifiedAbsence}</span>
|
||||||
<span className="text-sm text-red-600 text-center">Absences non justifiées</span>
|
<span className="text-sm text-red-600 text-center">Absences non justifiées</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-blue-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-blue-50 rounded-md">
|
||||||
<span className="text-2xl font-bold text-blue-600">{absenceStats.justifiedLate}</span>
|
<span className="text-2xl font-bold text-blue-600">{absenceStats.justifiedLate}</span>
|
||||||
<span className="text-sm text-blue-700 text-center">Retards justifiés</span>
|
<span className="text-sm text-blue-700 text-center">Retards justifiés</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 bg-orange-50 rounded-lg">
|
<div className="flex flex-col items-center p-3 bg-orange-50 rounded-md">
|
||||||
<span className="text-2xl font-bold text-orange-500">{absenceStats.unjustifiedLate}</span>
|
<span className="text-2xl font-bold text-orange-500">{absenceStats.unjustifiedLate}</span>
|
||||||
<span className="text-sm text-orange-600 text-center">Retards non justifiés</span>
|
<span className="text-sm text-orange-600 text-center">Retards non justifiés</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -39,37 +39,41 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-6">
|
||||||
<h2 className="text-xl mb-4">Paramètres du compte</h2>
|
<h1 className="font-headline text-2xl font-bold text-gray-900 mb-6">
|
||||||
<form onSubmit={handleSubmit}>
|
Paramètres du compte
|
||||||
<InputText
|
</h1>
|
||||||
type="email"
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-6 max-w-md">
|
||||||
id="email"
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
label="Email"
|
<InputText
|
||||||
value={email}
|
type="email"
|
||||||
onChange={handleEmailChange}
|
id="email"
|
||||||
required
|
label="Email"
|
||||||
/>
|
value={email}
|
||||||
<InputText
|
onChange={handleEmailChange}
|
||||||
type="password"
|
required
|
||||||
id="password"
|
/>
|
||||||
label="Nouveau mot de passe"
|
<InputText
|
||||||
value={password}
|
type="password"
|
||||||
onChange={handlePasswordChange}
|
id="password"
|
||||||
required
|
label="Nouveau mot de passe"
|
||||||
/>
|
value={password}
|
||||||
<InputText
|
onChange={handlePasswordChange}
|
||||||
type="password"
|
required
|
||||||
id="confirmPassword"
|
/>
|
||||||
label="Confirmer le mot de passe"
|
<InputText
|
||||||
value={confirmPassword}
|
type="password"
|
||||||
onChange={handleConfirmPasswordChange}
|
id="confirmPassword"
|
||||||
required
|
label="Confirmer le mot de passe"
|
||||||
/>
|
value={confirmPassword}
|
||||||
<div className="flex items-center justify-between">
|
onChange={handleConfirmPasswordChange}
|
||||||
<Button type="submit" primary text={' Mettre à jour'} />
|
required
|
||||||
</div>
|
/>
|
||||||
</form>
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button type="submit" primary text={'Mettre à jour'} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,16 +80,15 @@ export default function Page() {
|
|||||||
return <Loader />; // Affichez le composant Loader
|
return <Loader />; // Affichez le composant Loader
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen flex items-center justify-center p-4 bg-neutral">
|
||||||
<div className="container max mx-auto p-4">
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-6">
|
||||||
<Logo className="h-150 w-150" />
|
<Logo className="h-150 w-150" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-center mb-4">
|
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||||
Authentification
|
Authentification
|
||||||
</h1>
|
</h1>
|
||||||
<form
|
<form
|
||||||
className="max-w-md mx-auto"
|
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleFormLogin(new FormData(e.target));
|
handleFormLogin(new FormData(e.target));
|
||||||
@ -112,27 +111,24 @@ export default function Page() {
|
|||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
className="w-full mb-5"
|
className="w-full mb-5"
|
||||||
/>
|
/>
|
||||||
<div className="input-group mb-4"></div>
|
<div className="flex justify-end mb-4">
|
||||||
<label>
|
|
||||||
<a
|
<a
|
||||||
className="float-right mb-4"
|
className="text-sm text-primary hover:text-secondary font-label transition-colors"
|
||||||
href={`${FE_USERS_NEW_PASSWORD_URL}`}
|
href={`${FE_USERS_NEW_PASSWORD_URL}`}
|
||||||
>
|
>
|
||||||
Mot de passe oublié ?
|
Mot de passe oublié ?
|
||||||
</a>
|
</a>
|
||||||
</label>
|
|
||||||
<div className="form-group-submit mt-4">
|
|
||||||
<Button
|
|
||||||
text="Se Connecter"
|
|
||||||
className="w-full"
|
|
||||||
primary
|
|
||||||
type="submit"
|
|
||||||
name="connect"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
text="Se Connecter"
|
||||||
|
className="w-full"
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
name="connect"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,16 +48,15 @@ export default function Page() {
|
|||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||||
<div className="container max mx-auto p-4">
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-6">
|
||||||
<Logo className="h-150 w-150" />
|
<Logo className="h-150 w-150" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-center mb-4">
|
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||||
Nouveau Mot de passe
|
Nouveau Mot de passe
|
||||||
</h1>
|
</h1>
|
||||||
<form
|
<form
|
||||||
className="max-w-md mx-auto"
|
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
validate(new FormData(e.target));
|
validate(new FormData(e.target));
|
||||||
@ -70,28 +69,23 @@ export default function Page() {
|
|||||||
IconItem={User}
|
IconItem={User}
|
||||||
label="Identifiant"
|
label="Identifiant"
|
||||||
placeholder="Identifiant"
|
placeholder="Identifiant"
|
||||||
className="w-full"
|
className="w-full mb-6"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Réinitialiser"
|
||||||
|
className="w-full mb-3"
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
name="validate"
|
||||||
/>
|
/>
|
||||||
<div className="form-group-submit mt-4">
|
|
||||||
<Button
|
|
||||||
text="Réinitialiser"
|
|
||||||
className="w-full"
|
|
||||||
primary
|
|
||||||
type="submit"
|
|
||||||
name="validate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br />
|
|
||||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
|
||||||
<Button
|
<Button
|
||||||
text="Annuler"
|
text="Annuler"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
href={`${FE_USERS_LOGIN_URL}`}
|
href={`${FE_USERS_LOGIN_URL}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,16 +61,15 @@ export default function Page() {
|
|||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||||
<div className="container max mx-auto p-4">
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-6">
|
||||||
<Logo className="h-150 w-150" />
|
<Logo className="h-150 w-150" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-center mb-4">
|
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||||
Réinitialisation du mot de passe
|
Réinitialisation du mot de passe
|
||||||
</h1>
|
</h1>
|
||||||
<form
|
<form
|
||||||
className="max-w-md mx-auto"
|
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
validate(new FormData(e.target));
|
validate(new FormData(e.target));
|
||||||
@ -91,28 +90,23 @@ export default function Page() {
|
|||||||
IconItem={KeySquare}
|
IconItem={KeySquare}
|
||||||
label="Confirmation mot de passe"
|
label="Confirmation mot de passe"
|
||||||
placeholder="Confirmation mot de passe"
|
placeholder="Confirmation mot de passe"
|
||||||
className="w-full"
|
className="w-full mb-6"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Enregistrer"
|
||||||
|
className="w-full mb-3"
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
name="validate"
|
||||||
/>
|
/>
|
||||||
<div className="form-group-submit mt-4">
|
|
||||||
<Button
|
|
||||||
text="Enregistrer"
|
|
||||||
className="w-full"
|
|
||||||
primary
|
|
||||||
type="submit"
|
|
||||||
name="validate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br />
|
|
||||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
|
||||||
<Button
|
<Button
|
||||||
text="Annuler"
|
text="Annuler"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
href={`${FE_USERS_LOGIN_URL}`}
|
href={`${FE_USERS_LOGIN_URL}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,16 +65,15 @@ export default function Page() {
|
|||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen bg-neutral flex items-center justify-center p-4">
|
||||||
<div className="container max mx-auto p-4">
|
<div className="bg-white rounded-md border border-gray-200 shadow-sm p-8 w-full max-w-md">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-6">
|
||||||
<Logo className="h-150 w-150" />
|
<Logo className="h-150 w-150" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-center mb-4">
|
<h1 className="font-headline text-2xl font-bold text-center text-gray-900 mb-6">
|
||||||
Nouveau profil
|
Nouveau profil
|
||||||
</h1>
|
</h1>
|
||||||
<form
|
<form
|
||||||
className="max-w-md mx-auto"
|
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
subscribeFormSubmit(new FormData(e.target));
|
subscribeFormSubmit(new FormData(e.target));
|
||||||
@ -103,30 +102,23 @@ export default function Page() {
|
|||||||
IconItem={KeySquare}
|
IconItem={KeySquare}
|
||||||
label="Confirmation mot de passe"
|
label="Confirmation mot de passe"
|
||||||
placeholder="Confirmation mot de passe"
|
placeholder="Confirmation mot de passe"
|
||||||
className="w-full"
|
className="w-full mb-6"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Enregistrer"
|
||||||
|
className="w-full mb-3"
|
||||||
|
primary
|
||||||
|
type="submit"
|
||||||
|
name="validate"
|
||||||
/>
|
/>
|
||||||
<div className="form-group-submit mt-4">
|
|
||||||
<Button
|
|
||||||
text="Enregistrer"
|
|
||||||
className="w-full"
|
|
||||||
primary
|
|
||||||
type="submit"
|
|
||||||
name="validate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br />
|
|
||||||
<div className="flex justify-center mt-2 max-w-md mx-auto">
|
|
||||||
<Button
|
<Button
|
||||||
text="Annuler"
|
text="Annuler"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={() => router.push(`${FE_USERS_LOGIN_URL}`)}
|
||||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getMessages } from 'next-intl/server';
|
import { getMessages } from 'next-intl/server';
|
||||||
import { Inter, Manrope } from 'next/font/google';
|
import localFont from 'next/font/local';
|
||||||
import Providers from '@/components/Providers';
|
import Providers from '@/components/Providers';
|
||||||
import ServiceWorkerRegister from '@/components/ServiceWorkerRegister';
|
import ServiceWorkerRegister from '@/components/ServiceWorkerRegister';
|
||||||
import '@/css/tailwind.css';
|
import '@/css/tailwind.css';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = localFont({
|
||||||
subsets: ['latin'],
|
src: '../fonts/Inter-Variable.woff2',
|
||||||
variable: '--font-inter',
|
variable: '--font-inter',
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
const manrope = Manrope({
|
const manrope = localFont({
|
||||||
subsets: ['latin'],
|
src: '../fonts/Manrope-Variable.woff2',
|
||||||
variable: '--font-manrope',
|
variable: '--font-manrope',
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,16 +3,19 @@ import Logo from '../components/Logo';
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
|
<div className="flex items-center justify-center min-h-screen bg-primary">
|
||||||
<div className="text-center p-6 ">
|
<div className="text-center p-6 bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
<Logo className="w-32 h-32 mx-auto mb-4" />
|
<Logo className="w-32 h-32 mx-auto mb-4" />
|
||||||
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
|
<h2 className="font-headline text-2xl font-bold text-secondary mb-4">
|
||||||
404 | Page non trouvée
|
404 | Page non trouvée
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-emerald-900 mb-4">
|
<p className="font-body text-gray-600 mb-4">
|
||||||
La ressource que vous souhaitez consulter n'existe pas ou plus.
|
La ressource que vous souhaitez consulter n'existe pas ou plus.
|
||||||
</p>
|
</p>
|
||||||
<Link className="text-gray-900 hover:underline" href="/">
|
<Link
|
||||||
|
className="inline-flex items-center justify-center min-h-[44px] px-4 py-2 rounded font-label font-medium bg-primary hover:bg-secondary text-white transition-colors"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
Retour Accueil
|
Retour Accueil
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export default function AnnouncementScheduler({ csrfToken }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-white rounded shadow">
|
<div className="p-4 bg-white rounded shadow">
|
||||||
<h2 className="text-xl font-bold mb-4">Planifier une Annonce</h2>
|
<h2 className="font-headline text-xl font-bold mb-4">Planifier une Annonce</h2>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block font-medium">Titre</label>
|
<label className="block font-medium">Titre</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
|||||||
value={classe.id}
|
value={classe.id}
|
||||||
checked={formData.classeAssocie_id === classe.id}
|
checked={formData.classeAssocie_id === classe.id}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
|
className="form-radio h-3 w-3 text-primary focus:ring-primary hover:ring-tertiary checked:bg-primary checked:h-3 checked:w-3"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`classe-${classe.id}`}
|
htmlFor={`classe-${classe.id}`}
|
||||||
@ -57,7 +57,7 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
|
|||||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||||
!formData.classeAssocie_id
|
!formData.classeAssocie_id
|
||||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
: 'bg-primary text-white hover:bg-primary'
|
||||||
}`}
|
}`}
|
||||||
disabled={!formData.classeAssocie_id}
|
disabled={!formData.classeAssocie_id}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ const AlertMessage = ({
|
|||||||
actionLabel,
|
actionLabel,
|
||||||
onAction,
|
onAction,
|
||||||
}) => {
|
}) => {
|
||||||
// Définir les styles en fonction du type d'alerte
|
|
||||||
const typeStyles = {
|
const typeStyles = {
|
||||||
info: 'bg-blue-100 border-blue-500 text-blue-700',
|
info: 'bg-blue-100 border-blue-500 text-blue-700',
|
||||||
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
|
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
|
||||||
@ -18,13 +17,13 @@ const AlertMessage = ({
|
|||||||
const alertStyle = typeStyles[type] || typeStyles.info;
|
const alertStyle = typeStyles[type] || typeStyles.info;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`alert centered border-l-4 p-4 ${alertStyle}`} role="alert">
|
<div className={`alert centered border-l-4 p-4 rounded ${alertStyle}`} role="alert">
|
||||||
<h3 className="font-bold">{title}</h3>
|
<h3 className="font-headline font-bold">{title}</h3>
|
||||||
<p className="mt-2">{message}</p>
|
<p className="mt-2">{message}</p>
|
||||||
{actionLabel && onAction && (
|
{actionLabel && onAction && (
|
||||||
<div className="alert-actions mt-4">
|
<div className="alert-actions mt-4">
|
||||||
<button
|
<button
|
||||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
|
className="bg-primary text-white font-label font-medium rounded px-4 py-2 hover:bg-secondary transition-colors min-h-[44px]"
|
||||||
onClick={onAction}
|
onClick={onAction}
|
||||||
>
|
>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
|
|||||||
@ -14,11 +14,11 @@ const AlertWithModal = ({ title, message, buttonText }) => {
|
|||||||
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
|
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<h3 className="font-bold">{title}</h3>
|
<h3 className="font-headline font-bold">{title}</h3>
|
||||||
<p className="mt-2">{message}</p>
|
<p className="mt-2">{message}</p>
|
||||||
<div className="alert-actions mt-4">
|
<div className="alert-actions mt-4">
|
||||||
<button
|
<button
|
||||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
|
className="btn primary bg-primary text-white rounded-md px-4 py-2 hover:bg-primary flex items-center"
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
>
|
>
|
||||||
{buttonText} <UserPlus size={20} className="ml-2" />
|
{buttonText} <UserPlus size={20} className="ml-2" />
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const AlphabetPaginationNumber = ({ letter, active, onClick }) => (
|
|||||||
<button
|
<button
|
||||||
className={`w-8 h-8 flex items-center justify-center rounded ${
|
className={`w-8 h-8 flex items-center justify-center rounded ${
|
||||||
active
|
active
|
||||||
? 'bg-emerald-500 text-white'
|
? 'bg-primary text-white'
|
||||||
: 'text-gray-600 bg-gray-200 hover:bg-gray-50'
|
: 'text-gray-600 bg-gray-200 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@ -126,7 +126,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName = '',
|
|||||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||||
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="font-headline text-xl font-semibold">
|
||||||
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
|
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
|
||||||
</h2>
|
</h2>
|
||||||
<ChevronDown className="w-4 h-4" />
|
<ChevronDown className="w-4 h-4" />
|
||||||
@ -185,7 +185,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName = '',
|
|||||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||||
<button
|
<button
|
||||||
onClick={onDateClick}
|
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"
|
className="w-10 h-10 flex items-center justify-center bg-primary text-white rounded-full hover:bg-secondary shadow-md transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { getWeekEvents } from '@/utils/events';
|
|||||||
import { CalendarDays, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
|
import { CalendarDays, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
|
||||||
|
|
||||||
const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
||||||
const { currentDate, setCurrentDate, parentView } = usePlanning();
|
const { currentDate, setCurrentDate, parentView, schedules } = usePlanning();
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
|
|
||||||
@ -43,11 +43,28 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
return `${(hours + minutes / 60) * 5}rem`;
|
return `${(hours + minutes / 60) * 5}rem`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getScheduleColor = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
return schedule?.color || event.color || '#6B7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScheduleClassLevelLabel = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
const scheduleName = schedule?.name || '';
|
||||||
|
if (!scheduleName) return '';
|
||||||
|
return scheduleName;
|
||||||
|
};
|
||||||
|
|
||||||
const calculateEventStyle = (event, allDayEvents) => {
|
const calculateEventStyle = (event, allDayEvents) => {
|
||||||
const start = new Date(event.start);
|
const start = new Date(event.start);
|
||||||
const end = new Date(event.end);
|
const end = new Date(event.end);
|
||||||
const startMinutes = (start.getMinutes() / 60) * 5;
|
const startMinutes = (start.getMinutes() / 60) * 5;
|
||||||
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
|
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
|
||||||
|
const scheduleColor = getScheduleColor(event);
|
||||||
|
|
||||||
const overlapping = allDayEvents.filter((other) => {
|
const overlapping = allDayEvents.filter((other) => {
|
||||||
if (other.id === event.id) return false;
|
if (other.id === event.id) return false;
|
||||||
@ -114,7 +131,7 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onDateClick?.(currentDate)}
|
onClick={() => onDateClick?.(currentDate)}
|
||||||
className="w-9 h-9 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
className="w-9 h-9 flex items-center justify-center bg-primary text-white rounded-full hover:bg-secondary shadow-md transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -128,9 +145,9 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
onClick={() => setCurrentDate(day)}
|
onClick={() => setCurrentDate(day)}
|
||||||
className={`flex flex-col items-center min-w-[2.75rem] px-1 py-1.5 rounded-xl transition-colors ${
|
className={`flex flex-col items-center min-w-[2.75rem] px-1 py-1.5 rounded-xl transition-colors ${
|
||||||
isSameDay(day, currentDate)
|
isSameDay(day, currentDate)
|
||||||
? 'bg-emerald-600 text-white'
|
? 'bg-primary text-white'
|
||||||
: isToday(day)
|
: isToday(day)
|
||||||
? 'border border-emerald-400 text-emerald-600'
|
? 'border border-tertiary text-primary'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -146,10 +163,10 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
<div ref={scrollRef} className="flex-1 overflow-y-auto relative">
|
<div ref={scrollRef} className="flex-1 overflow-y-auto relative">
|
||||||
{isCurrentDay && (
|
{isCurrentDay && (
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
|
className="absolute left-0 right-0 z-10 border-primary border pointer-events-none"
|
||||||
style={{ top: getCurrentTimePosition() }}
|
style={{ top: getCurrentTimePosition() }}
|
||||||
>
|
>
|
||||||
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
|
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -163,8 +180,8 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
{`${hour.toString().padStart(2, '0')}:00`}
|
{`${hour.toString().padStart(2, '0')}:00`}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`h-20 relative border-b border-gray-100 ${
|
className={`h-20 relative ${
|
||||||
isCurrentDay ? 'bg-emerald-50/30' : 'bg-white'
|
isCurrentDay ? 'bg-primary/5/30' : 'bg-white'
|
||||||
}`}
|
}`}
|
||||||
onClick={
|
onClick={
|
||||||
parentView
|
parentView
|
||||||
@ -179,8 +196,11 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
>
|
>
|
||||||
{dayEvents
|
{dayEvents
|
||||||
.filter((e) => new Date(e.start).getHours() === hour)
|
.filter((e) => new Date(e.start).getHours() === hour)
|
||||||
.map((event) => (
|
.map((event) => {
|
||||||
<div
|
const scheduleColor = getScheduleColor(event);
|
||||||
|
const classLevelLabel = getScheduleClassLevelLabel(event);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
||||||
style={calculateEventStyle(event, dayEvents)}
|
style={calculateEventStyle(event, dayEvents)}
|
||||||
@ -192,32 +212,53 @@ const DayView = ({ onDateClick, onEventClick, events, onOpenDrawer }) => {
|
|||||||
onEventClick(event);
|
onEventClick(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-1">
|
{classLevelLabel && (
|
||||||
<div
|
|
||||||
className="font-semibold text-xs truncate"
|
|
||||||
style={{ color: event.color }}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-xs"
|
|
||||||
style={{ color: event.color, opacity: 0.75 }}
|
|
||||||
>
|
|
||||||
{format(new Date(event.start), 'HH:mm')} –{' '}
|
|
||||||
{format(new Date(event.end), 'HH:mm')}
|
|
||||||
</div>
|
|
||||||
{event.location && (
|
|
||||||
<div
|
<div
|
||||||
className="text-xs truncate"
|
className="px-1 py-0.5 border-t-2"
|
||||||
style={{ color: event.color, opacity: 0.75 }}
|
style={{
|
||||||
|
borderTopColor: scheduleColor,
|
||||||
|
backgroundColor: `${scheduleColor}22`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{event.location}
|
<span
|
||||||
|
className="text-[10px] font-semibold uppercase tracking-wide truncate block text-center"
|
||||||
|
style={{ color: scheduleColor }}
|
||||||
|
>
|
||||||
|
{classLevelLabel}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="p-1">
|
||||||
|
<div
|
||||||
|
className="font-semibold text-xs truncate flex items-center gap-1"
|
||||||
|
style={{ color: event.color }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: event.color }}
|
||||||
|
/>
|
||||||
|
<span className="truncate flex-1">{event.title}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-xs"
|
||||||
|
style={{ color: event.color, opacity: 0.75 }}
|
||||||
|
>
|
||||||
|
{format(new Date(event.start), 'HH:mm')} –{' '}
|
||||||
|
{format(new Date(event.end), 'HH:mm')}
|
||||||
|
</div>
|
||||||
|
{event.location && (
|
||||||
|
<div
|
||||||
|
className="text-xs truncate"
|
||||||
|
style={{ color: event.color, opacity: 0.75 }}
|
||||||
|
>
|
||||||
|
{event.location}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -10,20 +10,31 @@ export default function EventModal({
|
|||||||
eventData,
|
eventData,
|
||||||
setEventData,
|
setEventData,
|
||||||
}) {
|
}) {
|
||||||
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
|
const {
|
||||||
|
addEvent,
|
||||||
|
handleUpdateEvent,
|
||||||
|
handleDeleteEvent,
|
||||||
|
schedules,
|
||||||
|
selectedSchedule,
|
||||||
|
} =
|
||||||
usePlanning();
|
usePlanning();
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
// S'assurer que planning est défini lors du premier rendu
|
// S'assurer que planning est défini lors du premier rendu
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!eventData?.planning && schedules.length > 0) {
|
if (!eventData?.planning && schedules.length > 0) {
|
||||||
|
const defaultSchedule =
|
||||||
|
schedules.find(
|
||||||
|
(schedule) => Number(schedule.id) === Number(selectedSchedule)
|
||||||
|
) || schedules[0];
|
||||||
|
|
||||||
setEventData((prev) => ({
|
setEventData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
planning: schedules[0].id,
|
planning: defaultSchedule.id,
|
||||||
color: schedules[0].color,
|
color: defaultSchedule.color,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [schedules, eventData?.planning]);
|
}, [schedules, selectedSchedule, eventData?.planning]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@ -105,7 +116,7 @@ export default function EventModal({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEventData({ ...eventData, title: e.target.value })
|
setEventData({ ...eventData, title: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -120,7 +131,7 @@ export default function EventModal({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEventData({ ...eventData, description: e.target.value })
|
setEventData({ ...eventData, description: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -142,7 +153,7 @@ export default function EventModal({
|
|||||||
color: selectedSchedule?.color || '#10b981',
|
color: selectedSchedule?.color || '#10b981',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
{schedules.map((schedule) => (
|
{schedules.map((schedule) => (
|
||||||
@ -185,7 +196,7 @@ export default function EventModal({
|
|||||||
recursionType: e.target.value,
|
recursionType: e.target.value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
>
|
>
|
||||||
{recurrenceOptions.map((option) => (
|
{recurrenceOptions.map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
@ -215,7 +226,7 @@ export default function EventModal({
|
|||||||
}}
|
}}
|
||||||
className={`px-3 py-1 rounded-full text-sm ${
|
className={`px-3 py-1 rounded-full text-sm ${
|
||||||
(eventData.selectedDays || []).includes(day.value)
|
(eventData.selectedDays || []).includes(day.value)
|
||||||
? 'bg-emerald-100 text-emerald-800'
|
? 'bg-primary/10 text-secondary'
|
||||||
: 'bg-gray-100 text-gray-600'
|
: 'bg-gray-100 text-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -247,7 +258,7 @@ export default function EventModal({
|
|||||||
: null,
|
: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -267,7 +278,7 @@ export default function EventModal({
|
|||||||
start: new Date(e.target.value).toISOString(),
|
start: new Date(e.target.value).toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -284,7 +295,7 @@ export default function EventModal({
|
|||||||
end: new Date(e.target.value).toISOString(),
|
end: new Date(e.target.value).toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -301,7 +312,7 @@ export default function EventModal({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEventData({ ...eventData, location: e.target.value })
|
setEventData({ ...eventData, location: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -328,7 +339,7 @@ export default function EventModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
|
className="px-4 py-2 bg-primary text-white rounded hover:bg-secondary"
|
||||||
>
|
>
|
||||||
{eventData.id ? 'Modifier' : 'Créer'}
|
{eventData.id ? 'Modifier' : 'Créer'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -14,7 +14,24 @@ import { fr } from 'date-fns/locale';
|
|||||||
import { getEventsForDate } from '@/utils/events';
|
import { getEventsForDate } from '@/utils/events';
|
||||||
|
|
||||||
const MonthView = ({ onDateClick, onEventClick }) => {
|
const MonthView = ({ onDateClick, onEventClick }) => {
|
||||||
const { currentDate, setViewType, setCurrentDate, events } = usePlanning();
|
const { currentDate, setViewType, setCurrentDate, events, schedules } =
|
||||||
|
usePlanning();
|
||||||
|
|
||||||
|
const getScheduleColor = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
return schedule?.color || event.color || '#6B7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScheduleClassLevelLabel = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
const scheduleName = schedule?.name || '';
|
||||||
|
if (!scheduleName) return '';
|
||||||
|
return scheduleName;
|
||||||
|
};
|
||||||
|
|
||||||
// Obtenir tous les jours du mois actuel
|
// Obtenir tous les jours du mois actuel
|
||||||
const monthStart = startOfMonth(currentDate);
|
const monthStart = startOfMonth(currentDate);
|
||||||
@ -39,22 +56,25 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
|||||||
key={day.toString()}
|
key={day.toString()}
|
||||||
className={`p-2 overflow-y-auto relative flex flex-col
|
className={`p-2 overflow-y-auto relative flex flex-col
|
||||||
${!isCurrentMonth ? 'bg-gray-100 text-gray-400' : ''}
|
${!isCurrentMonth ? 'bg-gray-100 text-gray-400' : ''}
|
||||||
${isCurrentDay ? 'bg-emerald-50' : ''}
|
${isCurrentDay ? 'bg-primary/5' : ''}
|
||||||
hover:bg-gray-100 cursor-pointer border-b border-r`}
|
hover:bg-gray-100 cursor-pointer border-b border-r`}
|
||||||
onClick={() => handleDayClick(day)}
|
onClick={() => handleDayClick(day)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
|
className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
|
||||||
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
|
${isCurrentDay ? 'bg-primary text-white' : ''}
|
||||||
${!isCurrentMonth ? 'text-gray-400' : ''}`}
|
${!isCurrentMonth ? 'text-gray-400' : ''}`}
|
||||||
>
|
>
|
||||||
{format(day, 'd')}
|
{format(day, 'd')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 flex-1">
|
<div className="space-y-1 flex-1">
|
||||||
{dayEvents.map((event, index) => (
|
{dayEvents.map((event) => {
|
||||||
<div
|
const scheduleColor = getScheduleColor(event);
|
||||||
|
const classLevelLabel = getScheduleClassLevelLabel(event);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="text-xs p-1 rounded truncate cursor-pointer"
|
className="text-xs p-1 rounded truncate cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
@ -67,9 +87,32 @@ const MonthView = ({ onDateClick, onEventClick }) => {
|
|||||||
onEventClick(event);
|
onEventClick(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{event.title}
|
{classLevelLabel && (
|
||||||
|
<div
|
||||||
|
className="-mx-1 -mt-1 mb-1 px-1 py-0.5 border-t-2"
|
||||||
|
style={{
|
||||||
|
borderTopColor: scheduleColor,
|
||||||
|
backgroundColor: `${scheduleColor}22`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-semibold uppercase tracking-wide truncate block text-center"
|
||||||
|
style={{ color: scheduleColor }}
|
||||||
|
>
|
||||||
|
{classLevelLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center gap-1 max-w-full">
|
||||||
|
<span
|
||||||
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: event.color }}
|
||||||
|
/>
|
||||||
|
<span className="truncate flex-1">{event.title}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -247,7 +247,7 @@ export default function ScheduleNavigation({ classes, modeSet = 'event', isOpen
|
|||||||
{/* Desktop : sidebar fixe */}
|
{/* Desktop : sidebar fixe */}
|
||||||
<nav className="hidden md:flex flex-col w-64 border-r p-4 h-full overflow-y-auto shrink-0">
|
<nav className="hidden md:flex flex-col w-64 border-r p-4 h-full overflow-y-auto shrink-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="font-semibold">{title}</h2>
|
<h2 className="font-headline font-semibold">{title}</h2>
|
||||||
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -268,7 +268,7 @@ export default function ScheduleNavigation({ classes, modeSet = 'event', isOpen
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
||||||
<h2 className="font-semibold">{title}</h2>
|
<h2 className="font-headline font-semibold">{title}</h2>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
<button onClick={() => setIsAddingNew(true)} className="p-1 hover:bg-gray-100 rounded">
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { isToday } from 'date-fns';
|
|||||||
|
|
||||||
|
|
||||||
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||||
const { currentDate, planningMode, parentView } = usePlanning();
|
const { currentDate, planningMode, parentView, schedules } = usePlanning();
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
||||||
|
|
||||||
@ -54,6 +54,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const todayIndex = weekDays.findIndex((day) => isToday(day));
|
||||||
|
|
||||||
const isWeekend = (date) => {
|
const isWeekend = (date) => {
|
||||||
const day = date.getDay();
|
const day = date.getDay();
|
||||||
return day === 0 || day === 6;
|
return day === 0 || day === 6;
|
||||||
@ -71,11 +73,28 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getScheduleColor = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
return schedule?.color || event.color || '#6B7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScheduleClassLevelLabel = (event) => {
|
||||||
|
const schedule = schedules?.find(
|
||||||
|
(item) => Number(item.id) === Number(event.planning)
|
||||||
|
);
|
||||||
|
const scheduleName = schedule?.name || '';
|
||||||
|
if (!scheduleName) return '';
|
||||||
|
return scheduleName;
|
||||||
|
};
|
||||||
|
|
||||||
const calculateEventStyle = (event, dayEvents) => {
|
const calculateEventStyle = (event, dayEvents) => {
|
||||||
const start = new Date(event.start);
|
const start = new Date(event.start);
|
||||||
const end = new Date(event.end);
|
const end = new Date(event.end);
|
||||||
const startMinutes = (start.getMinutes() / 60) * 5;
|
const startMinutes = (start.getMinutes() / 60) * 5;
|
||||||
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
|
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
|
||||||
|
const scheduleColor = getScheduleColor(event);
|
||||||
|
|
||||||
// Trouver les événements qui se chevauchent
|
// Trouver les événements qui se chevauchent
|
||||||
const overlappingEvents = findOverlappingEvents(event, dayEvents);
|
const overlappingEvents = findOverlappingEvents(event, dayEvents);
|
||||||
@ -101,6 +120,8 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
|
|
||||||
const renderEventInCell = (event, dayEvents) => {
|
const renderEventInCell = (event, dayEvents) => {
|
||||||
const eventStyle = calculateEventStyle(event, dayEvents);
|
const eventStyle = calculateEventStyle(event, dayEvents);
|
||||||
|
const scheduleColor = getScheduleColor(event);
|
||||||
|
const classLevelLabel = getScheduleClassLevelLabel(event);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -116,12 +137,32 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{classLevelLabel && (
|
||||||
|
<div
|
||||||
|
className="px-1 py-0.5 border-t-2"
|
||||||
|
style={{
|
||||||
|
borderTopColor: scheduleColor,
|
||||||
|
backgroundColor: `${scheduleColor}22`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-semibold uppercase tracking-wide truncate block text-center"
|
||||||
|
style={{ color: scheduleColor }}
|
||||||
|
>
|
||||||
|
{classLevelLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div
|
<div
|
||||||
className="font-semibold text-xs truncate"
|
className="font-semibold text-xs truncate flex items-center gap-1"
|
||||||
style={{ color: event.color }}
|
style={{ color: event.color }}
|
||||||
>
|
>
|
||||||
{event.title}
|
<span
|
||||||
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: event.color }}
|
||||||
|
/>
|
||||||
|
<span className="truncate flex-1">{event.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
@ -156,14 +197,14 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
key={day}
|
key={day}
|
||||||
className={`h-14 p-2 text-center border-b
|
className={`h-14 p-2 text-center border-b
|
||||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||||
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
|
${isToday(day) ? 'bg-primary/10 border-x border-primary' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium text-gray-500">
|
<div className="text-xs font-medium text-gray-500">
|
||||||
{format(day, 'EEEE', { locale: fr })}
|
{format(day, 'EEEE', { locale: fr })}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
|
className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
|
||||||
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}
|
${isToday(day) ? 'bg-primary text-white' : ''}`}
|
||||||
>
|
>
|
||||||
{format(day, 'd', { locale: fr })}
|
{format(day, 'd', { locale: fr })}
|
||||||
</div>
|
</div>
|
||||||
@ -173,15 +214,25 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
|
|
||||||
{/* Grille horaire */}
|
{/* Grille horaire */}
|
||||||
<div ref={scrollContainerRef} className="flex-1 relative">
|
<div ref={scrollContainerRef} className="flex-1 relative">
|
||||||
|
{isCurrentWeek && todayIndex >= 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 bottom-0 z-[5] border-x border-primary pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: `calc(2.5rem + ((100% - 2.5rem) / 7) * ${todayIndex})`,
|
||||||
|
width: 'calc((100% - 2.5rem) / 7)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Ligne de temps actuelle */}
|
{/* Ligne de temps actuelle */}
|
||||||
{isCurrentWeek && (
|
{isCurrentWeek && (
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
|
className="absolute left-0 right-0 z-10 border-primary border pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
top: getCurrentTimePosition(),
|
top: getCurrentTimePosition(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-emerald-500" />
|
<div className="absolute -left-2 -top-2 w-2 h-2 rounded-full bg-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -202,7 +253,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
|||||||
key={`${hour}-${day}`}
|
key={`${hour}-${day}`}
|
||||||
className={`h-20 relative border-b border-gray-100
|
className={`h-20 relative border-b border-gray-100
|
||||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||||
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
|
${isToday(day) ? 'bg-primary/10/50' : ''}`}
|
||||||
onClick={
|
onClick={
|
||||||
parentView
|
parentView
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import { isSameMonth } from 'date-fns';
|
|||||||
const MonthCard = ({ month, eventCount, onClick }) => (
|
const MonthCard = ({ month, eventCount, onClick }) => (
|
||||||
<div
|
<div
|
||||||
className={`bg-white p-4 rounded shadow hover:shadow-lg cursor-pointer
|
className={`bg-white p-4 rounded shadow hover:shadow-lg cursor-pointer
|
||||||
${isSameMonth(month, new Date()) ? 'ring-2 ring-emerald-500' : ''}`}
|
${isSameMonth(month, new Date()) ? 'ring-2 ring-primary' : ''}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<h3 className="font-medium text-center mb-2">
|
<h3 className="font-headline font-medium text-center mb-2">
|
||||||
{format(month, 'MMMM', { locale: fr })}
|
{format(month, 'MMMM', { locale: fr })}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
<span className="inline-flex items-center justify-center bg-emerald-100 text-emerald-800 px-2 py-1 rounded-full">
|
<span className="inline-flex items-center justify-center bg-primary/10 text-secondary px-2 py-1 rounded-full">
|
||||||
{eventCount} événements
|
{eventCount} événements
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function LineChart({ data }) {
|
|||||||
style={{ height: chartHeight }}
|
style={{ height: chartHeight }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${isMax ? 'bg-emerald-400' : 'bg-blue-400'} rounded-t w-4`}
|
className={`${isMax ? 'bg-tertiary' : 'bg-blue-400'} rounded-t w-4`}
|
||||||
style={{ height: `${barHeight}px`, transition: 'height 0.3s' }}
|
style={{ height: `${barHeight}px`, transition: 'height 0.3s' }}
|
||||||
title={`${point.month}: ${point.value}`}
|
title={`${point.month}: ${point.value}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const ConversationItem = ({
|
|||||||
const getLastMessageText = () => {
|
const getLastMessageText = () => {
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
return (
|
return (
|
||||||
<span className="text-emerald-500 italic">Tape un message...</span>
|
<span className="text-primary italic">Tape un message...</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ const ConversationItem = ({
|
|||||||
const getPresenceColor = (status) => {
|
const getPresenceColor = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'online':
|
case 'online':
|
||||||
return 'bg-emerald-400';
|
return 'bg-tertiary';
|
||||||
case 'away':
|
case 'away':
|
||||||
return 'bg-yellow-400';
|
return 'bg-yellow-400';
|
||||||
case 'busy':
|
case 'busy':
|
||||||
@ -127,7 +127,7 @@ const ConversationItem = ({
|
|||||||
<div
|
<div
|
||||||
className={`group flex items-center p-3 cursor-pointer rounded-lg transition-all duration-200 hover:bg-gray-50 ${
|
className={`group flex items-center p-3 cursor-pointer rounded-lg transition-all duration-200 hover:bg-gray-50 ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-emerald-50 border-l-4 border-emerald-500'
|
? 'bg-primary/5 border-l-4 border-primary'
|
||||||
: 'hover:bg-gray-50'
|
: 'hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@ -154,8 +154,8 @@ const ConversationItem = ({
|
|||||||
<div className="flex-1 ml-3 overflow-hidden">
|
<div className="flex-1 ml-3 overflow-hidden">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3
|
<h3
|
||||||
className={`font-semibold truncate ${
|
className={`font-headline font-semibold truncate ${
|
||||||
isSelected ? 'text-emerald-700' : 'text-gray-900'
|
isSelected ? 'text-secondary' : 'text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getInterlocutorName()}
|
{getInterlocutorName()}
|
||||||
|
|||||||
@ -116,7 +116,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
const handleWebSocketMessage = useCallback(
|
const handleWebSocketMessage = useCallback(
|
||||||
(data) => {
|
(data) => {
|
||||||
// Debug : vérifier userProfileId à chaque message
|
// Debug : vérifier userProfileId à chaque message
|
||||||
logger.debug('🔍 handleWebSocketMessage appelé:', {
|
logger.debug(' handleWebSocketMessage appelé:', {
|
||||||
messageType: data.type,
|
messageType: data.type,
|
||||||
currentUserProfileId: userProfileId,
|
currentUserProfileId: userProfileId,
|
||||||
userProfileIdType: typeof userProfileId,
|
userProfileIdType: typeof userProfileId,
|
||||||
@ -153,7 +153,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
case 'new_message':
|
case 'new_message':
|
||||||
const newMessage = data.message;
|
const newMessage = data.message;
|
||||||
|
|
||||||
logger.debug('🆕 NOUVEAU MESSAGE WebSocket reçu:', {
|
logger.debug(' NOUVEAU MESSAGE WebSocket reçu:', {
|
||||||
senderId: newMessage.sender?.id,
|
senderId: newMessage.sender?.id,
|
||||||
content: newMessage.content?.substring(0, 50),
|
content: newMessage.content?.substring(0, 50),
|
||||||
conversationId:
|
conversationId:
|
||||||
@ -171,14 +171,14 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
|
|
||||||
// Vérifier si ce message a déjà été traité
|
// Vérifier si ce message a déjà été traité
|
||||||
if (processedMessages.has(messageId)) {
|
if (processedMessages.has(messageId)) {
|
||||||
logger.debug('🔍 Message déjà traité, ignoré:', {
|
logger.debug(' Message déjà traité, ignoré:', {
|
||||||
messageId,
|
messageId,
|
||||||
processedCount: processedMessages.size,
|
processedCount: processedMessages.size,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('🆔 ID unique généré pour le message:', messageId);
|
logger.debug(' ID unique généré pour le message:', messageId);
|
||||||
|
|
||||||
// Marquer le message comme traité
|
// Marquer le message comme traité
|
||||||
setProcessedMessages((prev) => {
|
setProcessedMessages((prev) => {
|
||||||
@ -193,7 +193,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Debug: vérifier le type de message reçu
|
// Debug: vérifier le type de message reçu
|
||||||
logger.debug('🔍 Message reçu:', {
|
logger.debug(' Message reçu:', {
|
||||||
content: newMessage.content?.substring(0, 50),
|
content: newMessage.content?.substring(0, 50),
|
||||||
message_type: newMessage.message_type,
|
message_type: newMessage.message_type,
|
||||||
sender_id: newMessage.sender_id,
|
sender_id: newMessage.sender_id,
|
||||||
@ -235,7 +235,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
String(newMessage.sender.id) === String(userProfileId);
|
String(newMessage.sender.id) === String(userProfileId);
|
||||||
|
|
||||||
// Debug détaillé pour comprendre le problème d'incrémentation
|
// Debug détaillé pour comprendre le problème d'incrémentation
|
||||||
logger.debug('🔍 Analyse du message pour compteur:', {
|
logger.debug(' Analyse du message pour compteur:', {
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
senderId: newMessage.sender.id,
|
senderId: newMessage.sender.id,
|
||||||
userProfileId: userProfileId,
|
userProfileId: userProfileId,
|
||||||
@ -253,12 +253,12 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
|
|
||||||
if (shouldIncrementUnread) {
|
if (shouldIncrementUnread) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'🔺 INCRÉMENTATION du compteur non lu pour conversation:',
|
' INCRÉMENTATION du compteur non lu pour conversation:',
|
||||||
convId
|
convId
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"➡️ Pas d'incrémentation (message de l'utilisateur connecté)"
|
" Pas d'incrémentation (message de l'utilisateur connecté)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +278,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
|
|
||||||
case 'typing_status':
|
case 'typing_status':
|
||||||
const { user_id, is_typing, conversation_id, user_name } = data;
|
const { user_id, is_typing, conversation_id, user_name } = data;
|
||||||
logger.debug('📝 Typing status reçu:', {
|
logger.debug(' Typing status reçu:', {
|
||||||
user_id,
|
user_id,
|
||||||
is_typing,
|
is_typing,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
@ -294,13 +294,13 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
String(currentConversationId) === String(conversation_id)
|
String(currentConversationId) === String(conversation_id)
|
||||||
) {
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'📝 Mise à jour typing pour conversation sélectionnée'
|
' Mise à jour typing pour conversation sélectionnée'
|
||||||
);
|
);
|
||||||
setTypingUsers((prev) => {
|
setTypingUsers((prev) => {
|
||||||
// Utiliser le nom de l'utilisateur s'il est disponible, sinon l'ID
|
// Utiliser le nom de l'utilisateur s'il est disponible, sinon l'ID
|
||||||
const displayName = user_name || `Utilisateur ${user_id}`;
|
const displayName = user_name || `Utilisateur ${user_id}`;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'📝 Display name:',
|
' Display name:',
|
||||||
displayName,
|
displayName,
|
||||||
'is_typing:',
|
'is_typing:',
|
||||||
is_typing
|
is_typing
|
||||||
@ -311,21 +311,21 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
? prev
|
? prev
|
||||||
: [...prev, displayName];
|
: [...prev, displayName];
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'📝 Nouveaux utilisateurs en train de taper:',
|
' Nouveaux utilisateurs en train de taper:',
|
||||||
newUsers
|
newUsers
|
||||||
);
|
);
|
||||||
return newUsers;
|
return newUsers;
|
||||||
} else {
|
} else {
|
||||||
const newUsers = prev.filter((name) => name !== displayName);
|
const newUsers = prev.filter((name) => name !== displayName);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'📝 Utilisateurs en train de taper après suppression:',
|
' Utilisateurs en train de taper après suppression:',
|
||||||
newUsers
|
newUsers
|
||||||
);
|
);
|
||||||
return newUsers;
|
return newUsers;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug('📝 Typing status pour une autre conversation:', {
|
logger.debug(' Typing status pour une autre conversation:', {
|
||||||
selectedConversationId: currentConversationId,
|
selectedConversationId: currentConversationId,
|
||||||
messageConversationId: conversation_id,
|
messageConversationId: conversation_id,
|
||||||
});
|
});
|
||||||
@ -378,7 +378,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
|
|
||||||
setConversations(data || []);
|
setConversations(data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Erreur lors du chargement des conversations:', error);
|
logger.error(' Erreur lors du chargement des conversations:', error);
|
||||||
setConversations([]);
|
setConversations([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -539,30 +539,30 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
// Sélectionner une conversation
|
// Sélectionner une conversation
|
||||||
const selectConversation = useCallback(
|
const selectConversation = useCallback(
|
||||||
(conversation) => {
|
(conversation) => {
|
||||||
logger.debug('🔄 Sélection de la conversation:', conversation);
|
logger.debug(' Sélection de la conversation:', conversation);
|
||||||
setSelectedConversation(conversation);
|
setSelectedConversation(conversation);
|
||||||
setTypingUsers([]);
|
setTypingUsers([]);
|
||||||
setIsMobileSidebarOpen(false);
|
setIsMobileSidebarOpen(false);
|
||||||
|
|
||||||
// Utiliser id ou conversation_id selon ce qui est disponible
|
// Utiliser id ou conversation_id selon ce qui est disponible
|
||||||
const conversationId = conversation.id || conversation.conversation_id;
|
const conversationId = conversation.id || conversation.conversation_id;
|
||||||
logger.debug('🔄 ID de conversation extrait:', conversationId);
|
logger.debug(' ID de conversation extrait:', conversationId);
|
||||||
|
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'🔄 Chargement des messages pour conversation:',
|
' Chargement des messages pour conversation:',
|
||||||
conversationId
|
conversationId
|
||||||
);
|
);
|
||||||
loadMessages(conversationId);
|
loadMessages(conversationId);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'🔄 Tentative de rejoindre la conversation:',
|
' Tentative de rejoindre la conversation:',
|
||||||
conversationId
|
conversationId
|
||||||
);
|
);
|
||||||
const joinResult = joinConversation(conversationId);
|
const joinResult = joinConversation(conversationId);
|
||||||
logger.debug('🔄 Résultat joinConversation:', joinResult);
|
logger.debug(' Résultat joinConversation:', joinResult);
|
||||||
} else {
|
} else {
|
||||||
logger.error("❌ Impossible de trouver l'ID de conversation");
|
logger.error(" Impossible de trouver l'ID de conversation");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadMessages, joinConversation]
|
[loadMessages, joinConversation]
|
||||||
@ -571,20 +571,20 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
// Envoyer un message
|
// Envoyer un message
|
||||||
const handleSendMessage = useCallback(
|
const handleSendMessage = useCallback(
|
||||||
(content, attachment = null) => {
|
(content, attachment = null) => {
|
||||||
logger.debug('📤 handleSendMessage appelé:', {
|
logger.debug(' handleSendMessage appelé:', {
|
||||||
content,
|
content,
|
||||||
attachment,
|
attachment,
|
||||||
selectedConversation,
|
selectedConversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!selectedConversation) {
|
if (!selectedConversation) {
|
||||||
logger.warn('❌ Aucune conversation sélectionnée');
|
logger.warn(' Aucune conversation sélectionnée');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier qu'on a soit du contenu, soit un fichier
|
// Vérifier qu'on a soit du contenu, soit un fichier
|
||||||
if (!content.trim() && !attachment) {
|
if (!content.trim() && !attachment) {
|
||||||
logger.warn('❌ Aucun contenu ni fichier à envoyer');
|
logger.warn(' Aucun contenu ni fichier à envoyer');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +592,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
const conversationId =
|
const conversationId =
|
||||||
selectedConversation.id || selectedConversation.conversation_id;
|
selectedConversation.id || selectedConversation.conversation_id;
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
logger.error("❌ Impossible de trouver l'ID de la conversation");
|
logger.error(" Impossible de trouver l'ID de la conversation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,23 +608,23 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
messageData.attachment = attachment;
|
messageData.attachment = attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('📤 Envoi du message via WebSocket:', messageData);
|
logger.debug(' Envoi du message via WebSocket:', messageData);
|
||||||
logger.debug('📤 Type de message:', messageData.type);
|
logger.debug(' Type de message:', messageData.type);
|
||||||
logger.debug('📤 État de la connexion:', {
|
logger.debug(' État de la connexion:', {
|
||||||
isConnected,
|
isConnected,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = sendChatMessage(messageData);
|
const success = sendChatMessage(messageData);
|
||||||
|
|
||||||
logger.debug('📤 Résultat envoi message:', success);
|
logger.debug(' Résultat envoi message:', success);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"❌ Impossible d'envoyer le message - WebSocket non connecté"
|
" Impossible d'envoyer le message - WebSocket non connecté"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.debug('✅ Message envoyé avec succès');
|
logger.debug(' Message envoyé avec succès');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -834,7 +834,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Messages</h2>
|
<h2 className="font-headline text-lg font-semibold text-gray-900">Messages</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleStartNewConversation}
|
onClick={handleStartNewConversation}
|
||||||
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
@ -857,7 +857,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
className={`w-full pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 ${
|
className={`w-full pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 ${
|
||||||
showSearch ? 'focus:ring-emerald-500' : 'focus:ring-emerald-500'
|
showSearch ? 'focus:ring-primary' : 'focus:ring-primary'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{showSearch && searchQuery && (
|
{showSearch && searchQuery && (
|
||||||
@ -896,7 +896,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : showSearch && searchResults.length > 0 ? (
|
) : showSearch && searchResults.length > 0 ? (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-2 px-2">
|
<h3 className="font-headline text-sm font-medium text-gray-700 mb-2 px-2">
|
||||||
Résultats de recherche
|
Résultats de recherche
|
||||||
</h3>
|
</h3>
|
||||||
{searchResults.map((user) => (
|
{searchResults.map((user) => (
|
||||||
@ -915,7 +915,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
<div
|
<div
|
||||||
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 ${
|
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 ${
|
||||||
userPresences[user.id]?.status === 'online'
|
userPresences[user.id]?.status === 'online'
|
||||||
? 'bg-emerald-400'
|
? 'bg-tertiary'
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400'
|
||||||
} border-2 border-white rounded-full`}
|
} border-2 border-white rounded-full`}
|
||||||
title={
|
title={
|
||||||
@ -937,7 +937,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
<div
|
<div
|
||||||
className={`text-xs ${
|
className={`text-xs ${
|
||||||
userPresences[user.id]?.status === 'online'
|
userPresences[user.id]?.status === 'online'
|
||||||
? 'text-emerald-500'
|
? 'text-primary'
|
||||||
: 'text-gray-500'
|
: 'text-gray-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -1019,7 +1019,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
selectedConversation.interlocuteur?.id &&
|
selectedConversation.interlocuteur?.id &&
|
||||||
userPresences[selectedConversation.interlocuteur.id]
|
userPresences[selectedConversation.interlocuteur.id]
|
||||||
?.status === 'online'
|
?.status === 'online'
|
||||||
? 'bg-emerald-400'
|
? 'bg-tertiary'
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400'
|
||||||
} border-2 border-white rounded-full`}
|
} border-2 border-white rounded-full`}
|
||||||
title={
|
title={
|
||||||
@ -1032,7 +1032,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 ml-3 overflow-hidden">
|
<div className="flex-1 ml-3 overflow-hidden">
|
||||||
<h3 className="font-semibold text-gray-900">
|
<h3 className="font-headline font-semibold text-gray-900">
|
||||||
{selectedConversation.interlocuteur
|
{selectedConversation.interlocuteur
|
||||||
? selectedConversation.interlocuteur.first_name &&
|
? selectedConversation.interlocuteur.first_name &&
|
||||||
selectedConversation.interlocuteur.last_name
|
selectedConversation.interlocuteur.last_name
|
||||||
@ -1046,7 +1046,7 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
selectedConversation.interlocuteur?.id &&
|
selectedConversation.interlocuteur?.id &&
|
||||||
userPresences[selectedConversation.interlocuteur.id]
|
userPresences[selectedConversation.interlocuteur.id]
|
||||||
?.status === 'online'
|
?.status === 'online'
|
||||||
? 'text-emerald-500'
|
? 'text-primary'
|
||||||
: 'text-gray-500'
|
: 'text-gray-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -1140,10 +1140,10 @@ const InstantChat = ({ userProfileId, establishmentId }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center bg-gray-50">
|
<div className="flex-1 flex items-center justify-center bg-gray-50">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-16 h-16 bg-emerald-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<MessageSquare className="w-8 h-8 text-emerald-600" />
|
<MessageSquare className="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="font-headline text-lg font-medium text-gray-900 mb-2">
|
||||||
Sélectionnez une conversation
|
Sélectionnez une conversation
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
|
|||||||
@ -60,19 +60,19 @@ const MessageInput = ({
|
|||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
const trimmedMessage = message.trim();
|
const trimmedMessage = message.trim();
|
||||||
logger.debug('📝 MessageInput: handleSend appelé:', {
|
logger.debug(' MessageInput: handleSend appelé:', {
|
||||||
message,
|
message,
|
||||||
trimmedMessage,
|
trimmedMessage,
|
||||||
disabled,
|
disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!trimmedMessage || disabled) {
|
if (!trimmedMessage || disabled) {
|
||||||
logger.debug('❌ MessageInput: Message vide ou désactivé');
|
logger.debug(' MessageInput: Message vide ou désactivé');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'📤 MessageInput: Appel de onSendMessage avec:',
|
' MessageInput: Appel de onSendMessage avec:',
|
||||||
trimmedMessage
|
trimmedMessage
|
||||||
);
|
);
|
||||||
onSendMessage(trimmedMessage);
|
onSendMessage(trimmedMessage);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const ClasseDetails = ({ classe }) => {
|
|||||||
const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100);
|
const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100);
|
||||||
|
|
||||||
const getColor = (pourcentage) => {
|
const getColor = (pourcentage) => {
|
||||||
if (pourcentage < 50) return 'bg-emerald-500';
|
if (pourcentage < 50) return 'bg-primary';
|
||||||
if (pourcentage < 75) return 'bg-orange-500';
|
if (pourcentage < 75) return 'bg-orange-500';
|
||||||
return 'bg-red-500';
|
return 'bg-red-500';
|
||||||
};
|
};
|
||||||
@ -52,7 +52,7 @@ const ClasseDetails = ({ classe }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mb-4">Liste des élèves</h3>
|
<h3 className="font-headline text-xl font-semibold mb-4">Liste des élèves</h3>
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
|
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
|
||||||
<Table
|
<Table
|
||||||
columns={[
|
columns={[
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const DateTab = ({
|
|||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
{dates[activeTab]?.map((date, index) => (
|
{dates[activeTab]?.map((date, index) => (
|
||||||
<div key={index} className="flex items-center space-x-3">
|
<div key={index} className="flex items-center space-x-3">
|
||||||
<span className="text-emerald-700 font-semibold">
|
<span className="text-secondary font-semibold">
|
||||||
Échéance {index + 1}
|
Échéance {index + 1}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@ -63,7 +63,7 @@ const DateTab = ({
|
|||||||
e.target.value
|
e.target.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="p-2 border border-emerald-300 rounded focus:outline-none focus:ring-2 focus:ring-emerald-500 cursor-pointer"
|
className="p-2 border border-primary/30 rounded focus:outline-none focus:ring-2 focus:ring-primary cursor-pointer"
|
||||||
/>
|
/>
|
||||||
{modifiedDates[`${activeTab}-${index}`] && (
|
{modifiedDates[`${activeTab}-${index}`] && (
|
||||||
<button
|
<button
|
||||||
@ -74,7 +74,7 @@ const DateTab = ({
|
|||||||
due_dates: dates[activeTab],
|
due_dates: dates[activeTab],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="text-emerald-500 hover:text-emerald-800"
|
className="text-primary hover:text-secondary"
|
||||||
>
|
>
|
||||||
<Check className="w-5 h-5" />
|
<Check className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
36
Front-End/src/components/EmptyState.js
Normal file
36
Front-End/src/components/EmptyState.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Inbox } from 'lucide-react';
|
||||||
|
|
||||||
|
const EmptyState = ({
|
||||||
|
icon: Icon = Inbox,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
onAction,
|
||||||
|
actionIcon: ActionIcon,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||||
|
<div className="bg-neutral p-4 rounded-full mb-4">
|
||||||
|
<Icon size={40} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-headline text-lg font-semibold text-gray-700 mb-2">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-gray-500 max-w-md mb-6">{description}</p>
|
||||||
|
)}
|
||||||
|
{actionLabel && onAction && (
|
||||||
|
<button
|
||||||
|
onClick={onAction}
|
||||||
|
className="flex items-center gap-2 bg-primary hover:bg-secondary text-white font-label font-medium px-6 py-2.5 rounded transition-colors min-h-[44px]"
|
||||||
|
>
|
||||||
|
{ActionIcon && <ActionIcon size={18} />}
|
||||||
|
{actionLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyState;
|
||||||
@ -74,7 +74,7 @@ export default function EvaluationForm({
|
|||||||
className="space-y-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm"
|
className="space-y-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-semibold text-lg text-gray-800">
|
<h3 className="font-headline font-semibold text-lg text-gray-800">
|
||||||
{isEditing ? 'Modifier l\'évaluation' : 'Nouvelle évaluation'}
|
{isEditing ? 'Modifier l\'évaluation' : 'Nouvelle évaluation'}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export default function EvaluationGradeTable({
|
|||||||
<div className="p-4 border-b border-gray-200 bg-gray-50 rounded-t-lg">
|
<div className="p-4 border-b border-gray-200 bg-gray-50 rounded-t-lg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-lg text-gray-800">
|
<h3 className="font-headline font-semibold text-lg text-gray-800">
|
||||||
{evaluation.name}
|
{evaluation.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-gray-500 flex gap-3">
|
<div className="text-sm text-gray-500 flex gap-3">
|
||||||
@ -191,7 +191,7 @@ export default function EvaluationGradeTable({
|
|||||||
handleScoreChange(student.id, e.target.value)
|
handleScoreChange(student.id, e.target.value)
|
||||||
}
|
}
|
||||||
disabled={isAbsent}
|
disabled={isAbsent}
|
||||||
className={`w-20 px-2 py-1 text-center border rounded focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 ${
|
className={`w-20 px-2 py-1 text-center border rounded focus:ring-2 focus:ring-primary focus:border-primary ${
|
||||||
isAbsent
|
isAbsent
|
||||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
: 'border-gray-300'
|
: 'border-gray-300'
|
||||||
@ -219,7 +219,7 @@ export default function EvaluationGradeTable({
|
|||||||
handleCommentChange(student.id, e.target.value)
|
handleCommentChange(student.id, e.target.value)
|
||||||
}
|
}
|
||||||
placeholder="Commentaire..."
|
placeholder="Commentaire..."
|
||||||
className="w-full px-2 py-1 border border-gray-300 rounded focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
className="w-full px-2 py-1 border border-gray-300 rounded focus:ring-2 focus:ring-primary focus:border-primary"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{onDeleteGrade && (
|
{onDeleteGrade && (
|
||||||
@ -258,7 +258,7 @@ export default function EvaluationGradeTable({
|
|||||||
<div className="flex gap-4 text-sm text-gray-600">
|
<div className="flex gap-4 text-sm text-gray-600">
|
||||||
<span>
|
<span>
|
||||||
Moyenne:{' '}
|
Moyenne:{' '}
|
||||||
<span className="font-semibold text-emerald-600">
|
<span className="font-semibold text-primary">
|
||||||
{stats.avg.toFixed(2)}
|
{stats.avg.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -123,14 +123,14 @@ export default function EvaluationStudentView({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Moyenne générale */}
|
{/* Moyenne générale */}
|
||||||
{generalAverage !== null && (
|
{generalAverage !== null && (
|
||||||
<div className="bg-emerald-50 border border-emerald-200 rounded-lg p-4 flex items-center justify-between">
|
<div className="bg-primary/5 border border-primary/20 rounded-lg p-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BookOpen className="text-emerald-600" size={24} />
|
<BookOpen className="text-primary" size={24} />
|
||||||
<span className="font-medium text-emerald-800">Moyenne générale</span>
|
<span className="font-medium text-secondary">Moyenne générale</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getAverageIcon(generalAverage)}
|
{getAverageIcon(generalAverage)}
|
||||||
<span className="text-2xl font-bold text-emerald-700">
|
<span className="text-2xl font-bold text-secondary">
|
||||||
{generalAverage.toFixed(2)}/20
|
{generalAverage.toFixed(2)}/20
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -233,7 +233,7 @@ export default function EvaluationStudentView({
|
|||||||
<span className="text-gray-500">/{ev.max_score}</span>
|
<span className="text-gray-500">/{ev.max_score}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSaveEdit(ev, studentEval)}
|
onClick={() => handleSaveEdit(ev, studentEval)}
|
||||||
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded"
|
className="p-1 text-primary hover:bg-primary/5 rounded"
|
||||||
title="Enregistrer"
|
title="Enregistrer"
|
||||||
>
|
>
|
||||||
<Save size={16} />
|
<Save size={16} />
|
||||||
|
|||||||
@ -103,12 +103,12 @@ const EventCard = ({ title, start, end, date, description, type }) => {
|
|||||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 mb-3 hover:shadow-md transition-shadow">
|
<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 items-start gap-3">
|
||||||
<div className="flex-shrink-0 mt-1">
|
<div className="flex-shrink-0 mt-1">
|
||||||
<CalendarCheck className="text-emerald-500" size={20} />
|
<CalendarCheck className="text-primary" size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-medium text-gray-900 mb-1">{title}</h4>
|
<h4 className="font-medium text-gray-900 mb-1">{title}</h4>
|
||||||
<div className="flex flex-col text-sm text-gray-500">
|
<div className="flex flex-col text-sm text-gray-500">
|
||||||
<span className="font-medium text-emerald-600">{dayName}</span>
|
<span className="font-medium text-primary">{dayName}</span>
|
||||||
<span>{formattedDate}</span>
|
<span>{formattedDate}</span>
|
||||||
{timeRange && (
|
{timeRange && (
|
||||||
<span className="text-xs text-gray-600 mt-1">{timeRange}</span>
|
<span className="text-xs text-gray-600 mt-1">{timeRange}</span>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function Footer({ softwareName, softwareVersion }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-light flex items-center justify-between">
|
<div className="text-sm font-light flex items-center justify-between">
|
||||||
<div className="text-sm font-light mr-4">
|
<div className="text-sm font-light mr-4">
|
||||||
{softwareName} - {softwareVersion}
|
{softwareName} - {softwareVersion}
|
||||||
</div>
|
</div>
|
||||||
<Logo className="w-8 h-8" />
|
<Logo className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -159,7 +159,7 @@ export default function AddFieldModal({
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-xl font-semibold">
|
<h3 className="font-headline text-xl font-semibold">
|
||||||
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
@ -720,7 +720,7 @@ export default function AddFieldModal({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
text={isEditing ? 'Modifier' : 'Ajouter'}
|
text={isEditing ? 'Modifier' : 'Ajouter'}
|
||||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
|
className="px-4 py-2 bg-primary text-white rounded hover:bg-secondary"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -12,8 +12,8 @@ const Button = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const baseClass =
|
const baseClass =
|
||||||
'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
'px-4 py-2 rounded font-label font-medium min-h-[44px] flex items-center justify-center transition-colors';
|
||||||
const primaryClass = 'bg-emerald-500 hover:bg-emerald-600';
|
const primaryClass = 'bg-primary hover:bg-secondary text-white';
|
||||||
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
|
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
|
||||||
const buttonClass = `${baseClass} ${primary && !disabled ? primaryClass : secondaryClass} ${className}`;
|
const buttonClass = `${baseClass} ${primary && !disabled ? primaryClass : secondaryClass} ${className}`;
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const CheckBox = ({
|
|||||||
value={item.id}
|
value={item.id}
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`form-checkbox h-4 w-4 rounded-mg text-emerald-600 hover:ring-emerald-400 checked:bg-emerald-600 hover:border-emerald-500 hover:bg-emerald-500 cursor-pointer ${horizontal ? 'mt-1' : 'mr-2'}`}
|
className={`form-checkbox h-4 w-4 rounded-mg text-primary hover:ring-tertiary checked:bg-primary hover:border-primary hover:bg-primary cursor-pointer ${horizontal ? 'mt-1' : 'mr-2'}`}
|
||||||
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
|
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
|
||||||
/>
|
/>
|
||||||
{!horizontal && (
|
{!horizontal && (
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function FieldTypeSelector({ isOpen, onClose, onSelect }) {
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="font-headline text-lg font-semibold">
|
||||||
Choisir un type de champ ({filteredFieldTypes.length} /{' '}
|
Choisir un type de champ ({filteredFieldTypes.length} /{' '}
|
||||||
{FIELD_TYPES.length} types)
|
{FIELD_TYPES.length} types)
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export default function FileUpload({
|
|||||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<h3 className="font-headline text-lg font-semibold mb-4">
|
||||||
{`${selectionMessage}`}
|
{`${selectionMessage}`}
|
||||||
{required && <span className="text-red-500 ml-1">*</span>}
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</h3>
|
</h3>
|
||||||
@ -54,7 +54,7 @@ export default function FileUpload({
|
|||||||
className={`border-2 border-dashed p-6 rounded-lg flex flex-col items-center justify-center ${
|
className={`border-2 border-dashed p-6 rounded-lg flex flex-col items-center justify-center ${
|
||||||
!enable
|
!enable
|
||||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||||
: 'border-gray-500 hover:border-emerald-500'
|
: 'border-gray-500 hover:border-primary'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => enable && fileInputRef.current.click()} // Désactiver le clic si `enable` est false
|
onClick={() => enable && fileInputRef.current.click()} // Désactiver le clic si `enable` est false
|
||||||
onDragOver={(e) => enable && e.preventDefault()}
|
onDragOver={(e) => enable && e.preventDefault()}
|
||||||
@ -62,7 +62,7 @@ export default function FileUpload({
|
|||||||
>
|
>
|
||||||
<CloudUpload
|
<CloudUpload
|
||||||
className={`w-12 h-12 mb-4 ${
|
className={`w-12 h-12 mb-4 ${
|
||||||
!enable ? 'text-gray-400' : 'text-emerald-500'
|
!enable ? 'text-gray-400' : 'text-primary'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -93,7 +93,7 @@ export default function FileUpload({
|
|||||||
{/* Affichage du fichier existant */}
|
{/* Affichage du fichier existant */}
|
||||||
{existingFile && !localFileName && (
|
{existingFile && !localFileName && (
|
||||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
<CloudUpload className="w-6 h-6 text-primary" />
|
||||||
<p className="text-sm font-medium text-gray-800">
|
<p className="text-sm font-medium text-gray-800">
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{typeof existingFile === 'string'
|
{typeof existingFile === 'string'
|
||||||
@ -107,7 +107,7 @@ export default function FileUpload({
|
|||||||
{/* Affichage du fichier sélectionné */}
|
{/* Affichage du fichier sélectionné */}
|
||||||
{localFileName && (
|
{localFileName && (
|
||||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
<CloudUpload className="w-6 h-6 text-primary" />
|
||||||
<p className="text-sm font-medium text-gray-800">
|
<p className="text-sm font-medium text-gray-800">
|
||||||
<span className="font-semibold">{localFileName}</span>
|
<span className="font-semibold">{localFileName}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export default function FormRenderer({
|
|||||||
}, // Callback de soumission personnalisé (optionnel)
|
}, // Callback de soumission personnalisé (optionnel)
|
||||||
masterFile = null,
|
masterFile = null,
|
||||||
}) {
|
}) {
|
||||||
|
const formFields = formConfig?.fields || [];
|
||||||
|
|
||||||
const resolveMasterFileUrl = (fileValue) => {
|
const resolveMasterFileUrl = (fileValue) => {
|
||||||
if (!fileValue) return null;
|
if (!fileValue) return null;
|
||||||
if (typeof fileValue !== 'string') return null;
|
if (typeof fileValue !== 'string') return null;
|
||||||
@ -50,6 +52,52 @@ export default function FormRenderer({
|
|||||||
|
|
||||||
const masterFileUrl = resolveMasterFileUrl(masterFile);
|
const masterFileUrl = resolveMasterFileUrl(masterFile);
|
||||||
|
|
||||||
|
const detectMasterFileType = (fileUrl) => {
|
||||||
|
if (!fileUrl || typeof fileUrl !== 'string') return 'unknown';
|
||||||
|
|
||||||
|
let candidate = fileUrl;
|
||||||
|
|
||||||
|
if (fileUrl.startsWith('/api/download?')) {
|
||||||
|
const queryPart = fileUrl.split('?')[1] || '';
|
||||||
|
const params = new URLSearchParams(queryPart);
|
||||||
|
const pathFromQuery = params.get('path') || params.get('file');
|
||||||
|
if (pathFromQuery) {
|
||||||
|
candidate = pathFromQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanUrl = candidate.split('?')[0];
|
||||||
|
|
||||||
|
let lowerUrl = cleanUrl.toLowerCase();
|
||||||
|
try {
|
||||||
|
lowerUrl = decodeURIComponent(cleanUrl).toLowerCase();
|
||||||
|
} catch {
|
||||||
|
lowerUrl = cleanUrl.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerUrl.endsWith('.pdf')) return 'pdf';
|
||||||
|
if (
|
||||||
|
lowerUrl.endsWith('.png') ||
|
||||||
|
lowerUrl.endsWith('.jpg') ||
|
||||||
|
lowerUrl.endsWith('.jpeg') ||
|
||||||
|
lowerUrl.endsWith('.gif') ||
|
||||||
|
lowerUrl.endsWith('.webp') ||
|
||||||
|
lowerUrl.endsWith('.bmp') ||
|
||||||
|
lowerUrl.endsWith('.svg')
|
||||||
|
) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
const masterFileType = detectMasterFileType(masterFileUrl);
|
||||||
|
const hasFileField = formFields.some((field) => field.type === 'file');
|
||||||
|
|
||||||
|
const formContainerClass = hasFileField
|
||||||
|
? 'w-full max-w-4xl mx-auto'
|
||||||
|
: 'w-full max-w-md mx-auto';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
@ -149,26 +197,26 @@ export default function FormRenderer({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit, onError)}
|
onSubmit={handleSubmit(onSubmit, onError)}
|
||||||
className="max-w-md mx-auto"
|
className={formContainerClass}
|
||||||
>
|
>
|
||||||
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
||||||
<h2 className="text-2xl font-bold text-center mb-4">
|
<h2 className="font-headline text-2xl font-bold text-center mb-4">
|
||||||
{formConfig?.title || 'Formulaire'}
|
{formConfig?.title || 'Formulaire'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{(formConfig?.fields || []).map((field) => (
|
{formFields.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
||||||
className="flex flex-col mt-4"
|
className="flex flex-col mt-4"
|
||||||
>
|
>
|
||||||
{field.type === 'heading1' && (
|
{field.type === 'heading1' && (
|
||||||
<h1 className="text-3xl font-bold mb-3">{field.text}</h1>
|
<h1 className="font-headline text-3xl font-bold mb-3">{field.text}</h1>
|
||||||
)}
|
)}
|
||||||
{field.type === 'heading2' && (
|
{field.type === 'heading2' && (
|
||||||
<h2 className="text-2xl font-bold mb-3">{field.text}</h2>
|
<h2 className="font-headline text-2xl font-bold mb-3">{field.text}</h2>
|
||||||
)}
|
)}
|
||||||
{field.type === 'heading3' && (
|
{field.type === 'heading3' && (
|
||||||
<h3 className="text-xl font-bold mb-2">{field.text}</h3>
|
<h3 className="font-headline text-xl font-bold mb-2">{field.text}</h3>
|
||||||
)}
|
)}
|
||||||
{field.type === 'heading4' && (
|
{field.type === 'heading4' && (
|
||||||
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
|
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
|
||||||
@ -355,12 +403,29 @@ export default function FormRenderer({
|
|||||||
{field.label}
|
{field.label}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<iframe
|
{masterFileType === 'image' ? (
|
||||||
src={masterFileUrl}
|
<img
|
||||||
title={field.label || 'Document'}
|
src={masterFileUrl}
|
||||||
className="w-full rounded border border-gray-200 bg-white"
|
alt={field.label || 'Document'}
|
||||||
style={{ height: '520px', border: 'none' }}
|
className="w-full h-auto rounded border border-gray-200 bg-white"
|
||||||
/>
|
/>
|
||||||
|
) : masterFileType === 'pdf' ? (
|
||||||
|
<iframe
|
||||||
|
src={masterFileUrl}
|
||||||
|
title={field.label || 'Document'}
|
||||||
|
className="w-full rounded border border-gray-200 bg-white"
|
||||||
|
style={{ height: '720px', border: 'none' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={masterFileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center px-4 py-2 rounded bg-primary text-white hover:bg-secondary"
|
||||||
|
>
|
||||||
|
Ouvrir le document
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
@ -451,7 +516,7 @@ export default function FormRenderer({
|
|||||||
type="submit"
|
type="submit"
|
||||||
primary
|
primary
|
||||||
text={formConfig?.submitLabel || 'Envoyer'}
|
text={formConfig?.submitLabel || 'Envoyer'}
|
||||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
className="mb-1 px-4 py-2 rounded-md shadow bg-primary text-white hover:bg-primary w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -459,14 +459,14 @@ export default function FormTemplateBuilder({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
<h2 className="text-xl font-bold mb-6">
|
<h2 className="font-headline text-xl font-bold mb-6">
|
||||||
Configuration du formulaire
|
Configuration du formulaire
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Configuration générale */}
|
{/* Configuration générale */}
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-lg font-semibold mr-4">
|
<h3 className="font-headline text-lg font-semibold mr-4">
|
||||||
Paramètres généraux
|
Paramètres généraux
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@ -509,7 +509,7 @@ export default function FormTemplateBuilder({
|
|||||||
className={`p-3 rounded ${
|
className={`p-3 rounded ${
|
||||||
saveMessage.type === 'error'
|
saveMessage.type === 'error'
|
||||||
? 'bg-red-100 text-red-700'
|
? 'bg-red-100 text-red-700'
|
||||||
: 'bg-green-100 text-green-700'
|
: 'bg-primary/10 text-secondary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{saveMessage.text}
|
{saveMessage.text}
|
||||||
@ -578,7 +578,7 @@ export default function FormTemplateBuilder({
|
|||||||
{/* Liste des champs */}
|
{/* Liste des champs */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-lg font-semibold mr-4">
|
<h3 className="font-headline text-lg font-semibold mr-4">
|
||||||
Champs du formulaire ({formConfig.fields.length})
|
Champs du formulaire ({formConfig.fields.length})
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
@ -587,7 +587,7 @@ export default function FormTemplateBuilder({
|
|||||||
setSelectedFieldType(null);
|
setSelectedFieldType(null);
|
||||||
setShowFieldTypeSelector(true);
|
setShowFieldTypeSelector(true);
|
||||||
}}
|
}}
|
||||||
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
|
className="p-2 bg-primary text-white rounded hover:bg-secondary transition-colors"
|
||||||
title="Ajouter un champ"
|
title="Ajouter un champ"
|
||||||
>
|
>
|
||||||
<PlusCircle size={18} />
|
<PlusCircle size={18} />
|
||||||
@ -605,7 +605,7 @@ export default function FormTemplateBuilder({
|
|||||||
setSelectedFieldType(null);
|
setSelectedFieldType(null);
|
||||||
setShowFieldTypeSelector(true);
|
setShowFieldTypeSelector(true);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
|
className="px-4 py-2 bg-primary text-white rounded hover:bg-secondary transition-colors inline-flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<PlusCircle size={18} />
|
<PlusCircle size={18} />
|
||||||
<span>Ajouter mon premier champ</span>
|
<span>Ajouter mon premier champ</span>
|
||||||
@ -639,7 +639,7 @@ export default function FormTemplateBuilder({
|
|||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<div className="bg-white p-6 rounded-lg shadow h-full">
|
<div className="bg-white p-6 rounded-lg shadow h-full">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold mr-4">JSON généré</h3>
|
<h3 className="font-headline text-lg font-semibold mr-4">JSON généré</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={exportJson}
|
onClick={exportJson}
|
||||||
@ -673,7 +673,7 @@ export default function FormTemplateBuilder({
|
|||||||
{/* Aperçu */}
|
{/* Aperçu */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
<h3 className="text-lg font-semibold mb-4">Aperçu du formulaire</h3>
|
<h3 className="font-headline text-lg font-semibold mb-4">Aperçu du formulaire</h3>
|
||||||
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
|
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
|
||||||
{formConfig.fields.length > 0 ? (
|
{formConfig.fields.length > 0 ? (
|
||||||
<FormRenderer formConfig={formConfig} masterFile={masterFile} />
|
<FormRenderer formConfig={formConfig} masterFile={masterFile} />
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export default function IconSelector({
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="font-headline text-lg font-semibold">
|
||||||
Choisir une icône ({filteredIcons.length} / {allIcons.length}{' '}
|
Choisir une icône ({filteredIcons.length} / {allIcons.length}{' '}
|
||||||
icônes)
|
icônes)
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const MultiSelect = ({
|
|||||||
<div className="relative mt-1">
|
<div className="relative mt-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm hover:border-emerald-500 focus:border-emerald-500"
|
className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm hover:border-primary focus:border-primary"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
{selectedOptions.length > 0 ? (
|
{selectedOptions.length > 0 ? (
|
||||||
@ -53,7 +53,7 @@ const MultiSelect = ({
|
|||||||
{selectedOptions.map((option) => (
|
{selectedOptions.map((option) => (
|
||||||
<span
|
<span
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm"
|
className="bg-primary/10 text-secondary px-2 py-1 rounded-md text-sm"
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</span>
|
</span>
|
||||||
@ -71,8 +71,8 @@ const MultiSelect = ({
|
|||||||
key={option.id}
|
key={option.id}
|
||||||
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
|
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
|
||||||
selectedOptions.some((selected) => selected.id === option.id)
|
selectedOptions.some((selected) => selected.id === option.id)
|
||||||
? 'text-white bg-emerald-600'
|
? 'text-white bg-primary'
|
||||||
: 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
|
: 'text-gray-900 hover:bg-primary/10 hover:text-secondary'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelect(option)}
|
onClick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const RadioList = ({
|
|||||||
return (
|
return (
|
||||||
<div className={`mb-4 ${className}`}>
|
<div className={`mb-4 ${className}`}>
|
||||||
{sectionLabel && (
|
{sectionLabel && (
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-2">
|
||||||
{sectionLabel}
|
{sectionLabel}
|
||||||
{required && <span className="text-red-500 ml-1">*</span>}
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</h3>
|
</h3>
|
||||||
@ -35,7 +35,7 @@ const RadioList = ({
|
|||||||
value={item.id}
|
value={item.id}
|
||||||
checked={parseInt(formData[fieldName], 10) === item.id}
|
checked={parseInt(formData[fieldName], 10) === item.id}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="form-radio h-4 w-4 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 cursor-pointer"
|
className="form-radio h-4 w-4 text-primary focus:ring-primary hover:ring-tertiary checked:bg-primary cursor-pointer"
|
||||||
style={{ outline: 'none', boxShadow: 'none' }}
|
style={{ outline: 'none', boxShadow: 'none' }}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,12 +20,12 @@ const ToggleSwitch = ({ name, label, checked, onChange }) => {
|
|||||||
id={name}
|
id={name}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0"
|
className="hover:text-primary absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-primary checked:right-0 checked:border-primary checked:bg-primary hover:border-primary hover:bg-primary focus:outline-none focus:ring-0"
|
||||||
ref={inputRef} // Reference to the input element
|
ref={inputRef} // Reference to the input element
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-emerald-300' : 'bg-gray-300'}`}
|
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-primary/30' : 'bg-gray-300'}`}
|
||||||
></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,16 +3,16 @@ import React from 'react';
|
|||||||
export default function AcademicResults({ results }) {
|
export default function AcademicResults({ results }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Résultats académiques</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Résultats académiques</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{results.map((result, idx) => (
|
{results.map((result, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="p-4 rounded-lg bg-emerald-50 flex flex-col gap-2 shadow"
|
className="p-4 rounded-lg bg-primary/5 flex flex-col gap-2 shadow"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium">{result.subject}</span>
|
<span className="font-medium">{result.subject}</span>
|
||||||
<span className="text-emerald-700 font-bold text-lg">
|
<span className="text-secondary font-bold text-lg">
|
||||||
{result.grade}/20
|
{result.grade}/20
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,16 +21,16 @@ export default function Attendance({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="w-full bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Présence et assiduité</h2>
|
||||||
{absences.length === 0 ? (
|
{absences.length === 0 ? (
|
||||||
<div className="text-center text-emerald-600 font-medium py-8">
|
<div className="text-center text-primary font-medium py-8">
|
||||||
Aucune absence enregistrée 🎉
|
Aucune absence enregistrée
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ol className="relative border-l border-emerald-200">
|
<ol className="relative border-l border-primary/20">
|
||||||
{absences.map((absence, idx) => (
|
{absences.map((absence, idx) => (
|
||||||
<li key={idx} className="mb-6 ml-4">
|
<li key={idx} className="mb-6 ml-4">
|
||||||
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
|
<div className="absolute w-3 h-3 bg-tertiary rounded-full mt-1.5 -left-1.5 border border-white" />
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
{/* Infos principales à gauche */}
|
{/* Infos principales à gauche */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -45,7 +45,7 @@ export default function Attendance({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{absence.type}</span>
|
<span className="font-medium">{absence.type}</span>
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
|
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-primary/10 text-secondary' : 'bg-red-100 text-red-700'}`}
|
||||||
>
|
>
|
||||||
{absence.justified ? 'Justifiée' : 'Non justifiée'}
|
{absence.justified ? 'Justifiée' : 'Non justifiée'}
|
||||||
</span>
|
</span>
|
||||||
@ -92,7 +92,7 @@ export default function Attendance({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
className="mb-1 px-4 py-2 rounded-md shadow bg-primary text-white hover:bg-primary w-full"
|
||||||
icon={<Trash2 className="w-6 h-6" />}
|
icon={<Trash2 className="w-6 h-6" />}
|
||||||
text="Supprimer"
|
text="Supprimer"
|
||||||
title="Evaluez l'élève"
|
title="Evaluez l'élève"
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const getGradeStyle = (grade) => {
|
|||||||
case 2:
|
case 2:
|
||||||
return 'bg-yellow-50 border-yellow-200';
|
return 'bg-yellow-50 border-yellow-200';
|
||||||
case 3:
|
case 3:
|
||||||
return 'bg-emerald-50 border-emerald-200';
|
return 'bg-primary/5 border-primary/20';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-50 border-gray-200';
|
return 'bg-gray-50 border-gray-200';
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ export default function GradeView({ data, grades, onGradeChange }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4 mr-4 text-right text-emerald-700 font-semibold">
|
<div className="mb-4 mr-4 text-right text-secondary font-semibold">
|
||||||
{totalCompetencies} compétence{totalCompetencies > 1 ? 's' : ''} au
|
{totalCompetencies} compétence{totalCompetencies > 1 ? 's' : ''} au
|
||||||
total
|
total
|
||||||
</div>
|
</div>
|
||||||
@ -76,19 +76,19 @@ export default function GradeView({ data, grades, onGradeChange }) {
|
|||||||
<div key={domaine.domaine_id} className="mb-8">
|
<div key={domaine.domaine_id} className="mb-8">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100'
|
'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-primary/5 border border-primary/20 shadow-sm hover:bg-primary/10'
|
||||||
}
|
}
|
||||||
onClick={() => toggleDomain(domaine.domaine_id)}
|
onClick={() => toggleDomain(domaine.domaine_id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<BookOpen className="w-7 h-7 text-emerald-600" />
|
<BookOpen className="w-7 h-7 text-primary" />
|
||||||
<span className="text-2xl font-bold text-emerald-800">
|
<span className="text-2xl font-bold text-secondary">
|
||||||
{domaine.domaine_nom}
|
{domaine.domaine_nom}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{openDomains[domaine.domaine_id]
|
{openDomains[domaine.domaine_id]
|
||||||
? <ChevronDown className="w-5 h-5 text-emerald-700" />
|
? <ChevronDown className="w-5 h-5 text-secondary" />
|
||||||
: <ChevronRight className="w-5 h-5 text-emerald-700" />
|
: <ChevronRight className="w-5 h-5 text-secondary" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{openDomains[domaine.domaine_id] && (
|
{openDomains[domaine.domaine_id] && (
|
||||||
@ -97,7 +97,7 @@ export default function GradeView({ data, grades, onGradeChange }) {
|
|||||||
<div key={categorie.categorie_id} className="mb-10 mr-4">
|
<div key={categorie.categorie_id} className="mb-10 mr-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 text-lg font-semibold text-emerald-700 mb-4 hover:underline"
|
className="flex items-center gap-2 text-lg font-semibold text-secondary mb-4 hover:underline"
|
||||||
onClick={() => toggleCategory(categorie.categorie_id)}
|
onClick={() => toggleCategory(categorie.categorie_id)}
|
||||||
>
|
>
|
||||||
{openCategories[categorie.categorie_id]
|
{openCategories[categorie.categorie_id]
|
||||||
@ -115,7 +115,7 @@ export default function GradeView({ data, grades, onGradeChange }) {
|
|||||||
key={competence.competence_id}
|
key={competence.competence_id}
|
||||||
className={`border rounded-xl p-6 flex flex-col shadow transition hover:shadow-md ${getGradeStyle(grade)}`}
|
className={`border rounded-xl p-6 flex flex-col shadow transition hover:shadow-md ${getGradeStyle(grade)}`}
|
||||||
>
|
>
|
||||||
<div className="mb-4 pb-4 border-b border-emerald-800 flex items-center min-h-[48px]">
|
<div className="mb-4 pb-4 border-b border-secondary flex items-center min-h-[48px]">
|
||||||
<span className="text-gray-900 font-semibold text-base">
|
<span className="text-gray-900 font-semibold text-base">
|
||||||
{competence.nom}
|
{competence.nom}
|
||||||
</span>
|
</span>
|
||||||
@ -154,7 +154,7 @@ export default function GradeView({ data, grades, onGradeChange }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<hr className="my-6 border-emerald-100" />
|
<hr className="my-6 border-primary/10" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,13 +27,13 @@ export default function GradesDomainBarChart({ studentCompetencies }) {
|
|||||||
if (avg > 0 && avg <= 1) return 'bg-gradient-to-r from-red-200 to-red-400';
|
if (avg > 0 && avg <= 1) return 'bg-gradient-to-r from-red-200 to-red-400';
|
||||||
if (avg > 1 && avg <= 2)
|
if (avg > 1 && avg <= 2)
|
||||||
return 'bg-gradient-to-r from-yellow-200 to-yellow-400';
|
return 'bg-gradient-to-r from-yellow-200 to-yellow-400';
|
||||||
if (avg > 2) return 'bg-gradient-to-r from-emerald-200 to-emerald-500';
|
if (avg > 2) return 'bg-gradient-to-r from-primary/20 to-primary';
|
||||||
return 'bg-gray-200';
|
return 'bg-gray-200';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="w-full flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-2">Moyenne par domaine</h2>
|
<h2 className="font-headline text-xl font-semibold mb-2">Moyenne par domaine</h2>
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
{domainStats.map((d) => (
|
{domainStats.map((d) => (
|
||||||
<div key={d.name} className="flex items-center w-full">
|
<div key={d.name} className="flex items-center w-full">
|
||||||
@ -41,7 +41,7 @@ export default function GradesDomainBarChart({ studentCompetencies }) {
|
|||||||
{d.name}
|
{d.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center" style={{ width: '40%' }}>
|
<div className="flex items-center" style={{ width: '40%' }}>
|
||||||
<div className="w-full bg-emerald-100 h-3 rounded overflow-hidden">
|
<div className="w-full bg-primary/10 h-3 rounded overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-3 rounded ${getBarGradient(d.avg)}`}
|
className={`h-3 rounded ${getBarGradient(d.avg)}`}
|
||||||
style={{ width: `${d.avg * 33.33}%` }}
|
style={{ width: `${d.avg * 33.33}%` }}
|
||||||
@ -49,7 +49,7 @@ export default function GradesDomainBarChart({ studentCompetencies }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className="text-xs font-semibold text-emerald-700 text-left pl-2 flex-shrink-0"
|
className="text-xs font-semibold text-secondary text-left pl-2 flex-shrink-0"
|
||||||
style={{ width: '10%' }}
|
style={{ width: '10%' }}
|
||||||
>
|
>
|
||||||
{d.avg}
|
{d.avg}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function GradesStatsCircle({ grades }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="w-full flex flex-col items-center gap-4 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-2">Statistiques globales</h2>
|
<h2 className="font-headline text-xl font-semibold mb-2">Statistiques globales</h2>
|
||||||
<div style={{ width: 120, height: 120 }}>
|
<div style={{ width: 120, height: 120 }}>
|
||||||
<CircularProgressbar
|
<CircularProgressbar
|
||||||
value={percent}
|
value={percent}
|
||||||
@ -28,7 +28,7 @@ export default function GradesStatsCircle({ grades }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center text-sm mt-2">
|
<div className="flex flex-col items-center text-sm mt-2">
|
||||||
<span className="text-emerald-700 font-semibold">
|
<span className="text-secondary font-semibold">
|
||||||
{acquired} acquis
|
{acquired} acquis
|
||||||
</span>
|
</span>
|
||||||
<span className="text-yellow-600">{inProgress} en cours</span>
|
<span className="text-yellow-600">{inProgress} en cours</span>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
export default function Homeworks({ homeworks }) {
|
export default function Homeworks({ homeworks }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Suivi des devoirs</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Suivi des devoirs</h2>
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100">
|
||||||
{homeworks.map((hw, idx) => (
|
{homeworks.map((hw, idx) => (
|
||||||
<li
|
<li
|
||||||
@ -15,7 +15,7 @@ export default function Homeworks({ homeworks }) {
|
|||||||
<span className="ml-2 text-xs text-gray-400">{hw.dueDate}</span>
|
<span className="ml-2 text-xs text-gray-400">{hw.dueDate}</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-2 py-1 rounded ${hw.status === 'Rendu' ? 'bg-emerald-100 text-emerald-700' : 'bg-yellow-100 text-yellow-700'}`}
|
className={`text-xs px-2 py-1 rounded ${hw.status === 'Rendu' ? 'bg-primary/10 text-secondary' : 'bg-yellow-100 text-yellow-700'}`}
|
||||||
>
|
>
|
||||||
{hw.status}
|
{hw.status}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
export default function Orientation({ orientation }) {
|
export default function Orientation({ orientation }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Orientation & conseils</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Orientation & conseils</h2>
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100">
|
||||||
{orientation.map((item, idx) => (
|
{orientation.map((item, idx) => (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
export default function Remarks({ remarks }) {
|
export default function Remarks({ remarks }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Remarques & observations</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Remarques & observations</h2>
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100">
|
||||||
{remarks.map((remark, idx) => (
|
{remarks.map((remark, idx) => (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
export default function SpecificEvaluations({ specificEvaluations }) {
|
export default function SpecificEvaluations({ specificEvaluations }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">Évaluations spécifiques</h2>
|
<h2 className="font-headline text-xl font-semibold mb-4">Évaluations spécifiques</h2>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{specificEvaluations.map((evalItem, idx) => (
|
{specificEvaluations.map((evalItem, idx) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import React from 'react';
|
|||||||
export default function WorkPlan({ workPlan }) {
|
export default function WorkPlan({ workPlan }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-semibold mb-4">
|
<h2 className="font-headline text-xl font-semibold mb-4">
|
||||||
Plan de travail personnalisé
|
Plan de travail personnalisé
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{workPlan.map((plan, idx) => (
|
{workPlan.map((plan, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="p-3 rounded border border-emerald-100 bg-emerald-50 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
className="p-3 rounded border border-primary/10 bg-primary/5 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">{plan.objective}</span>
|
<span className="font-medium">{plan.objective}</span>
|
||||||
@ -18,7 +18,7 @@ export default function WorkPlan({ workPlan }) {
|
|||||||
({plan.support})
|
({plan.support})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs px-2 py-1 rounded bg-emerald-200 text-emerald-800 mt-1 sm:mt-0">
|
<span className="text-xs px-2 py-1 rounded bg-primary/20 text-secondary mt-1 sm:mt-0">
|
||||||
{plan.followUp}
|
{plan.followUp}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import FormRenderer from '@/components/Form/FormRenderer';
|
import FormRenderer from '@/components/Form/FormRenderer';
|
||||||
import FileUpload from '@/components/Form/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
|
import SignatureField from '@/components/Form/SignatureField';
|
||||||
|
import Button from '@/components/Form/Button';
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Hourglass,
|
Hourglass,
|
||||||
@ -9,6 +11,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
PenTool,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { getSecureFileUrl } from '@/utils/fileUrl';
|
import { getSecureFileUrl } from '@/utils/fileUrl';
|
||||||
@ -20,6 +23,7 @@ import { getSecureFileUrl } from '@/utils/fileUrl';
|
|||||||
* @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
|
* @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
|
||||||
* @param {Boolean} enable - Si les formulaires sont modifiables
|
* @param {Boolean} enable - Si les formulaires sont modifiables
|
||||||
* @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné
|
* @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné
|
||||||
|
* @param {Function} onSignatureSubmit - Callback appelé quand une signature est soumise
|
||||||
*/
|
*/
|
||||||
export default function DynamicFormsList({
|
export default function DynamicFormsList({
|
||||||
schoolFileTemplates,
|
schoolFileTemplates,
|
||||||
@ -27,7 +31,8 @@ export default function DynamicFormsList({
|
|||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
enable = true,
|
enable = true,
|
||||||
onValidationChange,
|
onValidationChange,
|
||||||
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
|
onFileUpload,
|
||||||
|
onSignatureSubmit,
|
||||||
}) {
|
}) {
|
||||||
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
||||||
const [formsData, setFormsData] = useState({});
|
const [formsData, setFormsData] = useState({});
|
||||||
@ -55,6 +60,11 @@ export default function DynamicFormsList({
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDynamicForm = (template) =>
|
||||||
|
template.formTemplateData &&
|
||||||
|
Array.isArray(template.formTemplateData.fields) &&
|
||||||
|
template.formTemplateData.fields.length > 0;
|
||||||
|
|
||||||
const hasLocalCompletion = (templateId) => {
|
const hasLocalCompletion = (templateId) => {
|
||||||
if (formsValidation[templateId] === true) return true;
|
if (formsValidation[templateId] === true) return true;
|
||||||
|
|
||||||
@ -65,11 +75,35 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const savedResponses = existingResponses[templateId];
|
const savedResponses = existingResponses[templateId];
|
||||||
return !!(
|
if (
|
||||||
savedResponses &&
|
savedResponses &&
|
||||||
typeof savedResponses === 'object' &&
|
typeof savedResponses === 'object' &&
|
||||||
Object.keys(savedResponses).length > 0
|
Object.keys(savedResponses).length > 0
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les formulaires non dynamiques (upload de fichier),
|
||||||
|
// vérifier si un fichier a déjà été uploadé sur le template
|
||||||
|
const template = schoolFileTemplates.find((tpl) => tpl.id === templateId);
|
||||||
|
if (template && template.file && !isDynamicForm(template)) {
|
||||||
|
// Si le document est refusé, on ne le considère pas comme complété
|
||||||
|
if (template.isValidated === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le document a été signé électroniquement ET non refusé
|
||||||
|
if (
|
||||||
|
template &&
|
||||||
|
template.is_electronically_signed &&
|
||||||
|
template.isValidated !== false
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialiser les données avec les réponses existantes
|
// Initialiser les données avec les réponses existantes
|
||||||
@ -233,10 +267,29 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDynamicForm = (template) =>
|
// Handler pour soumettre la signature électronique
|
||||||
template.formTemplateData &&
|
const handleSignature = async (templateId, signatureData) => {
|
||||||
Array.isArray(template.formTemplateData.fields) &&
|
if (!signatureData || !templateId) return;
|
||||||
template.formTemplateData.fields.length > 0;
|
try {
|
||||||
|
if (onSignatureSubmit) {
|
||||||
|
await onSignatureSubmit(templateId, signatureData);
|
||||||
|
setFormsData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[templateId]: { ...prev[templateId], signed: true },
|
||||||
|
}));
|
||||||
|
setFormsValidation((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[templateId]: true,
|
||||||
|
}));
|
||||||
|
// Passer au formulaire suivant si disponible
|
||||||
|
if (currentTemplateIndex < schoolFileTemplates.length - 1) {
|
||||||
|
setCurrentTemplateIndex(currentTemplateIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la soumission de la signature :', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
|
if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -248,10 +301,10 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 mb-4 w-full mx-auto flex gap-8">
|
<div className="mt-8 mb-4 w-full mx-auto flex flex-col lg:flex-row gap-8 overflow-x-hidden">
|
||||||
{/* Liste des formulaires */}
|
{/* Liste des formulaires */}
|
||||||
<div className="w-1/4 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
|
<div className="w-full lg:w-1/4 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Formulaires à compléter
|
Formulaires à compléter
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-gray-600 mb-4">
|
<div className="text-sm text-gray-600 mb-4">
|
||||||
@ -302,19 +355,20 @@ export default function DynamicFormsList({
|
|||||||
if (isValidated === true) {
|
if (isValidated === true) {
|
||||||
statusLabel = 'Validé';
|
statusLabel = 'Validé';
|
||||||
statusColor = 'emerald';
|
statusColor = 'emerald';
|
||||||
icon = <CheckCircle className="w-5 h-5 text-emerald-600" />;
|
icon = <CheckCircle className="w-5 h-5 text-primary" />;
|
||||||
bgClass = 'bg-emerald-50';
|
bgClass = 'bg-primary/5';
|
||||||
borderClass = 'border border-emerald-200';
|
borderClass = 'border border-primary/20';
|
||||||
textClass = 'text-emerald-700';
|
textClass = 'text-secondary';
|
||||||
bgClass = isActive ? 'bg-emerald-200' : bgClass;
|
bgClass = isActive ? 'bg-primary/20' : bgClass;
|
||||||
borderClass = isActive
|
borderClass = isActive
|
||||||
? 'border border-emerald-300'
|
? 'border border-primary/30'
|
||||||
: borderClass;
|
: borderClass;
|
||||||
textClass = isActive
|
textClass = isActive
|
||||||
? 'text-emerald-900 font-semibold'
|
? 'text-secondary font-semibold'
|
||||||
: textClass;
|
: textClass;
|
||||||
canEdit = false;
|
canEdit = false;
|
||||||
} else if (isValidated === false) {
|
} else if (isValidated === false && !isCompletedLocally) {
|
||||||
|
// Refusé uniquement si pas re-complété localement
|
||||||
statusLabel = 'Refusé';
|
statusLabel = 'Refusé';
|
||||||
statusColor = 'red';
|
statusColor = 'red';
|
||||||
icon = <Hourglass className="w-5 h-5 text-red-500" />;
|
icon = <Hourglass className="w-5 h-5 text-red-500" />;
|
||||||
@ -326,30 +380,28 @@ export default function DynamicFormsList({
|
|||||||
? 'text-red-900 font-semibold'
|
? 'text-red-900 font-semibold'
|
||||||
: 'text-red-700';
|
: 'text-red-700';
|
||||||
canEdit = true;
|
canEdit = true;
|
||||||
|
} else if (isCompletedLocally) {
|
||||||
|
statusLabel = 'Complété';
|
||||||
|
statusColor = 'orange';
|
||||||
|
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
|
||||||
|
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
|
||||||
|
borderClass = isActive
|
||||||
|
? 'border border-orange-300'
|
||||||
|
: 'border border-orange-200';
|
||||||
|
textClass = isActive
|
||||||
|
? 'text-orange-900 font-semibold'
|
||||||
|
: 'text-orange-700';
|
||||||
|
canEdit = true;
|
||||||
} else {
|
} else {
|
||||||
if (isCompletedLocally) {
|
statusLabel = 'À compléter';
|
||||||
statusLabel = 'Complété';
|
statusColor = 'gray';
|
||||||
statusColor = 'orange';
|
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
|
||||||
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
|
bgClass = isActive ? 'bg-gray-200' : '';
|
||||||
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
|
borderClass = isActive ? 'border border-gray-300' : '';
|
||||||
borderClass = isActive
|
textClass = isActive
|
||||||
? 'border border-orange-300'
|
? 'text-gray-900 font-semibold'
|
||||||
: 'border border-orange-200';
|
: 'text-gray-600';
|
||||||
textClass = isActive
|
canEdit = true;
|
||||||
? 'text-orange-900 font-semibold'
|
|
||||||
: 'text-orange-700';
|
|
||||||
canEdit = true;
|
|
||||||
} else {
|
|
||||||
statusLabel = 'À compléter';
|
|
||||||
statusColor = 'gray';
|
|
||||||
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
|
|
||||||
bgClass = isActive ? 'bg-gray-200' : '';
|
|
||||||
borderClass = isActive ? 'border border-gray-300' : '';
|
|
||||||
textClass = isActive
|
|
||||||
? 'text-gray-900 font-semibold'
|
|
||||||
: 'text-gray-600';
|
|
||||||
canEdit = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -393,27 +445,27 @@ export default function DynamicFormsList({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-3/4">
|
<div className="w-full lg:w-3/4 min-w-0">
|
||||||
{currentTemplate && (
|
{currentTemplate && (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 overflow-x-hidden">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-xl font-semibold text-gray-800">
|
<h3 className="font-headline text-xl font-semibold text-gray-800">
|
||||||
{currentTemplate.name}
|
{currentTemplate.name}
|
||||||
</h3>
|
</h3>
|
||||||
{/* Label d'état */}
|
{/* Label d'état */}
|
||||||
{currentTemplate.isValidated === true ? (
|
{currentTemplate.isValidated === true ? (
|
||||||
<span className="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-primary/10 text-secondary text-sm font-semibold">
|
||||||
Validé
|
Validé
|
||||||
</span>
|
</span>
|
||||||
) : currentTemplate.isValidated === false ? (
|
|
||||||
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
|
||||||
Refusé
|
|
||||||
</span>
|
|
||||||
) : hasLocalCompletion(currentTemplate.id) ? (
|
) : hasLocalCompletion(currentTemplate.id) ? (
|
||||||
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">
|
||||||
Complété
|
Complété
|
||||||
</span>
|
</span>
|
||||||
|
) : currentTemplate.isValidated === false ? (
|
||||||
|
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">
|
||||||
|
Refusé
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-700 text-sm font-semibold">
|
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-700 text-sm font-semibold">
|
||||||
En attente
|
En attente
|
||||||
@ -467,7 +519,9 @@ export default function DynamicFormsList({
|
|||||||
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
||||||
}}
|
}}
|
||||||
masterFile={
|
masterFile={
|
||||||
currentTemplate.master_file_url || currentTemplate.file || null
|
currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file ||
|
||||||
|
null
|
||||||
}
|
}
|
||||||
initialValues={
|
initialValues={
|
||||||
extractResponses(formsData[currentTemplate.id]) ||
|
extractResponses(formsData[currentTemplate.id]) ||
|
||||||
@ -494,32 +548,137 @@ export default function DynamicFormsList({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cas non validé : bouton télécharger + upload */}
|
{/* Cas non validé */}
|
||||||
{currentTemplate.isValidated !== true && (
|
{currentTemplate.isValidated !== true && (
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
{/* Bouton télécharger le document source */}
|
{/* Document à signer électroniquement */}
|
||||||
{currentTemplate.file && (
|
{currentTemplate?.requires_electronic_signature ? (
|
||||||
<a
|
<>
|
||||||
href={getSecureFileUrl(currentTemplate.file)}
|
{/* Affichage du document à signer */}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
{(currentTemplate.master_file_url ||
|
||||||
download
|
currentTemplate.file) && (
|
||||||
>
|
<iframe
|
||||||
<Download className="w-5 h-5" />
|
src={getSecureFileUrl(
|
||||||
Télécharger le document
|
currentTemplate.master_file_url ||
|
||||||
</a>
|
currentTemplate.file
|
||||||
)}
|
)}
|
||||||
|
title={currentTemplate.name}
|
||||||
|
className="w-full border rounded"
|
||||||
|
style={{ height: '500px', border: 'none' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Composant d'upload */}
|
{/* Afficher si déjà signé et non refusé */}
|
||||||
{enable && (
|
{currentTemplate.is_electronically_signed &&
|
||||||
<FileUpload
|
currentTemplate.isValidated !== false ? (
|
||||||
key={currentTemplate.id}
|
<div className="flex items-center gap-2 p-4 bg-green-50 border border-green-200 rounded-lg w-full max-w-md">
|
||||||
selectionMessage={'Sélectionnez le fichier du document'}
|
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||||
onFileSelect={(file) =>
|
<div>
|
||||||
handleUpload(file, currentTemplate)
|
<p className="font-medium text-green-800">
|
||||||
}
|
Document signé électroniquement
|
||||||
required
|
</p>
|
||||||
enable={true}
|
{currentTemplate.electronic_signature_date && (
|
||||||
/>
|
<p className="text-sm text-green-600">
|
||||||
|
Signé le{' '}
|
||||||
|
{new Date(
|
||||||
|
currentTemplate.electronic_signature_date
|
||||||
|
).toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Signature électronique si pas encore signé ou refusé */
|
||||||
|
enable && (
|
||||||
|
<div className="w-full max-w-md mt-4">
|
||||||
|
{/* Message si document refusé */}
|
||||||
|
{currentTemplate.isValidated === false && (
|
||||||
|
<div className="flex items-center gap-2 p-3 mb-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<XCircle className="w-5 h-5 text-red-500" />
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
Ce document a été refusé. Veuillez le
|
||||||
|
signer à nouveau.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SignatureField
|
||||||
|
label="Signature électronique"
|
||||||
|
required={true}
|
||||||
|
value={
|
||||||
|
formsData[currentTemplate.id]?.signature || ''
|
||||||
|
}
|
||||||
|
onChange={(signatureData) => {
|
||||||
|
setFormsData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[currentTemplate.id]: {
|
||||||
|
...(prev[currentTemplate.id] || {}),
|
||||||
|
signature: signatureData,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
text="Signer le document"
|
||||||
|
onClick={() =>
|
||||||
|
handleSignature(
|
||||||
|
currentTemplate.id,
|
||||||
|
formsData[currentTemplate.id]?.signature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!formsData[currentTemplate.id]?.signature
|
||||||
|
}
|
||||||
|
className="mt-4 w-full"
|
||||||
|
icon={<PenTool className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Document classique : télécharger + upload */}
|
||||||
|
{(currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file) && (
|
||||||
|
<a
|
||||||
|
href={getSecureFileUrl(
|
||||||
|
currentTemplate.master_file_url ||
|
||||||
|
currentTemplate.file
|
||||||
|
)}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
Télécharger le document
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Composant d'upload */}
|
||||||
|
{enable && (
|
||||||
|
<FileUpload
|
||||||
|
key={currentTemplate.id}
|
||||||
|
selectionMessage={
|
||||||
|
'Sélectionnez le fichier du document'
|
||||||
|
}
|
||||||
|
onFileSelect={(file) =>
|
||||||
|
handleUpload(file, currentTemplate)
|
||||||
|
}
|
||||||
|
existingFile={
|
||||||
|
currentTemplate.file_url || currentTemplate.file
|
||||||
|
}
|
||||||
|
required
|
||||||
|
enable={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -532,7 +691,7 @@ export default function DynamicFormsList({
|
|||||||
{currentTemplateIndex >= schoolFileTemplates.length && (
|
{currentTemplateIndex >= schoolFileTemplates.length && (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-green-600 mb-2">
|
<h3 className="font-headline text-lg font-semibold text-green-600 mb-2">
|
||||||
Tous les formulaires ont été complétés !
|
Tous les formulaires ont été complétés !
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
|
|||||||
@ -67,11 +67,17 @@ const FilesModal = ({
|
|||||||
: null,
|
: null,
|
||||||
schoolFiles: fetchedSchoolFiles.map((file) => ({
|
schoolFiles: fetchedSchoolFiles.map((file) => ({
|
||||||
name: file.name || 'Document scolaire',
|
name: file.name || 'Document scolaire',
|
||||||
url: file.file ? getSecureFileUrl(file.file) : null,
|
url:
|
||||||
|
file.file_url || file.file
|
||||||
|
? getSecureFileUrl(file.file_url || file.file)
|
||||||
|
: null,
|
||||||
})),
|
})),
|
||||||
parentFiles: parentFiles.map((file) => ({
|
parentFiles: parentFiles.map((file) => ({
|
||||||
name: file.master_name || 'Document parent',
|
name: file.master_name || 'Document parent',
|
||||||
url: file.file ? getSecureFileUrl(file.file) : null,
|
url:
|
||||||
|
file.file_url || file.file
|
||||||
|
? getSecureFileUrl(file.file_url || file.file)
|
||||||
|
: null,
|
||||||
})),
|
})),
|
||||||
sepaFile: selectedRegisterForm.sepa_file
|
sepaFile: selectedRegisterForm.sepa_file
|
||||||
? {
|
? {
|
||||||
@ -101,7 +107,7 @@ const FilesModal = ({
|
|||||||
{/* Section Fiche élève */}
|
{/* Section Fiche élève */}
|
||||||
{files.registrationFile && (
|
{files.registrationFile && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Fiche élève
|
Fiche élève
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -121,7 +127,7 @@ const FilesModal = ({
|
|||||||
{/* Section Documents fusionnés */}
|
{/* Section Documents fusionnés */}
|
||||||
{files.fusionFile && (
|
{files.fusionFile && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Documents fusionnés
|
Documents fusionnés
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -142,7 +148,7 @@ const FilesModal = ({
|
|||||||
|
|
||||||
{/* Section Fichiers École */}
|
{/* Section Fichiers École */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Formulaires de l'établissement
|
Formulaires de l'établissement
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
@ -178,7 +184,7 @@ const FilesModal = ({
|
|||||||
|
|
||||||
{/* Section Fichiers Parent */}
|
{/* Section Fichiers Parent */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Pièces fournies
|
Pièces fournies
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
@ -212,7 +218,7 @@ const FilesModal = ({
|
|||||||
|
|
||||||
{/* Section Mandat SEPA */}
|
{/* Section Mandat SEPA */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="font-headline text-lg font-semibold text-gray-800 mb-4">
|
||||||
Mandat SEPA
|
Mandat SEPA
|
||||||
</h3>
|
</h3>
|
||||||
{files.sepaFile ? (
|
{files.sepaFile ? (
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Table from '@/components/Table';
|
|||||||
export default function FilesToSign({ fileTemplates, columns }) {
|
export default function FilesToSign({ fileTemplates, columns }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-xl font-bold mb-4 text-gray-800">
|
<h2 className="font-headline text-xl font-bold mb-4 text-gray-800">
|
||||||
Fichiers à remplir
|
Fichiers à remplir
|
||||||
</h2>
|
</h2>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@ -185,8 +185,8 @@ export default function FilesToUpload({
|
|||||||
<button
|
<button
|
||||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||||
actionType === 'upload' && selectedFile?.id === row.id
|
actionType === 'upload' && selectedFile?.id === row.id
|
||||||
? 'bg-emerald-100 text-emerald-600 ring-3 ring-emerald-500'
|
? 'bg-primary/10 text-primary ring-3 ring-primary'
|
||||||
: 'text-emerald-500 hover:text-emerald-700'
|
: 'text-primary hover:text-secondary'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (actionType === 'upload' && selectedFile?.id === row.id) {
|
if (actionType === 'upload' && selectedFile?.id === row.id) {
|
||||||
@ -212,11 +212,11 @@ export default function FilesToUpload({
|
|||||||
<div className="mt-8 mb-4 w-3/5">
|
<div className="mt-8 mb-4 w-3/5">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
<div className="bg-primary/10 p-3 rounded-full shadow-md">
|
||||||
<FileText className="w-8 h-8 text-emerald-600" />
|
<FileText className="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-800">
|
<h2 className="font-headline text-2xl font-bold text-gray-800">
|
||||||
Pièces à fournir
|
Pièces à fournir
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500 italic">
|
<p className="text-sm text-gray-500 italic">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import {
|
import {
|
||||||
editRegistrationSchoolFileTemplates,
|
editRegistrationSchoolFileTemplates,
|
||||||
|
editRegistrationParentFileTemplates,
|
||||||
} from '@/app/actions/registerFileGroupAction';
|
} from '@/app/actions/registerFileGroupAction';
|
||||||
import {
|
import {
|
||||||
fetchRegistrationPaymentModes,
|
fetchRegistrationPaymentModes,
|
||||||
@ -30,6 +31,7 @@ import SiblingInputFields from '@/components/Inscription/SiblingInputFields';
|
|||||||
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
||||||
import ProgressStep from '@/components/ProgressStep';
|
import ProgressStep from '@/components/ProgressStep';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { ChevronLeft, ChevronRight, Check, X } from 'lucide-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composant de formulaire d'inscription partagé
|
* Composant de formulaire d'inscription partagé
|
||||||
@ -415,7 +417,8 @@ export default function InscriptionFormShared({
|
|||||||
const templateData = await fetchFormResponses(template.id);
|
const templateData = await fetchFormResponses(template.id);
|
||||||
if (templateData && templateData.formTemplateData) {
|
if (templateData && templateData.formTemplateData) {
|
||||||
if (templateData.formTemplateData.responses) {
|
if (templateData.formTemplateData.responses) {
|
||||||
responsesMap[template.id] = templateData.formTemplateData.responses;
|
responsesMap[template.id] =
|
||||||
|
templateData.formTemplateData.responses;
|
||||||
} else {
|
} else {
|
||||||
// Extraire les réponses depuis les champs
|
// Extraire les réponses depuis les champs
|
||||||
const responses = {};
|
const responses = {};
|
||||||
@ -554,7 +557,7 @@ export default function InscriptionFormShared({
|
|||||||
const updateData = new FormData();
|
const updateData = new FormData();
|
||||||
updateData.append('file', file, finalFileName);
|
updateData.append('file', file, finalFileName);
|
||||||
|
|
||||||
return editRegistrationSchoolFileTemplates(
|
return editRegistrationParentFileTemplates(
|
||||||
selectedFile.id,
|
selectedFile.id,
|
||||||
updateData,
|
updateData,
|
||||||
csrfToken
|
csrfToken
|
||||||
@ -596,6 +599,90 @@ export default function InscriptionFormShared({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSchoolFileUpload = (file, selectedFile) => {
|
||||||
|
if (!file || !selectedFile) {
|
||||||
|
logger.error('Données manquantes pour le téléversement.');
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('Données manquantes pour le téléversement.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = new FormData();
|
||||||
|
updateData.append('file', file);
|
||||||
|
|
||||||
|
return editRegistrationSchoolFileTemplates(
|
||||||
|
selectedFile.id,
|
||||||
|
updateData,
|
||||||
|
csrfToken
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
logger.debug('School file template mis à jour avec succès :', response);
|
||||||
|
|
||||||
|
// Mettre à jour uniquement schoolFileTemplates
|
||||||
|
setSchoolFileTemplates((prevTemplates) =>
|
||||||
|
prevTemplates.map((template) =>
|
||||||
|
template.id === selectedFile.id
|
||||||
|
? {
|
||||||
|
...template,
|
||||||
|
file: response.data.file,
|
||||||
|
file_url: response.data.file_url,
|
||||||
|
}
|
||||||
|
: template
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Erreur lors de la mise à jour du school file :', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignatureSubmit = (templateId, signatureData) => {
|
||||||
|
if (!templateId || !signatureData) {
|
||||||
|
logger.error('Données manquantes pour la signature.');
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('Données manquantes pour la signature.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = new FormData();
|
||||||
|
updateData.append('electronic_signature', signatureData);
|
||||||
|
|
||||||
|
return editRegistrationSchoolFileTemplates(templateId, updateData, csrfToken)
|
||||||
|
.then((response) => {
|
||||||
|
logger.debug('Signature électronique enregistrée avec succès :', response);
|
||||||
|
|
||||||
|
// Mettre à jour schoolFileTemplates avec la signature
|
||||||
|
setSchoolFileTemplates((prevTemplates) =>
|
||||||
|
prevTemplates.map((template) =>
|
||||||
|
template.id === templateId
|
||||||
|
? {
|
||||||
|
...template,
|
||||||
|
electronic_signature: response.data.electronic_signature,
|
||||||
|
electronic_signature_date: response.data.electronic_signature_date,
|
||||||
|
is_electronically_signed: true,
|
||||||
|
// Repasser en attente si le document était refusé
|
||||||
|
isValidated:
|
||||||
|
response.data.isValidated !== undefined
|
||||||
|
? response.data.isValidated
|
||||||
|
: template.isValidated === false
|
||||||
|
? null
|
||||||
|
: template.isValidated,
|
||||||
|
}
|
||||||
|
: template
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Erreur lors de l\'enregistrement de la signature :', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteFile = (templateId) => {
|
const handleDeleteFile = (templateId) => {
|
||||||
const fileToDelete = uploadedFiles.find(
|
const fileToDelete = uploadedFiles.find(
|
||||||
(file) => parseInt(file.id) === templateId && file.fileName
|
(file) => parseInt(file.id) === templateId && file.fileName
|
||||||
@ -719,6 +806,19 @@ export default function InscriptionFormShared({
|
|||||||
'Documents parent',
|
'Documents parent',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hasParentFilesStep = parentFileTemplates.length > 0;
|
||||||
|
|
||||||
|
const activeSteps = hasParentFilesStep ? steps : steps.slice(0, 5);
|
||||||
|
const activeStepTitles = hasParentFilesStep
|
||||||
|
? stepTitles
|
||||||
|
: {
|
||||||
|
1: stepTitles[1],
|
||||||
|
2: stepTitles[2],
|
||||||
|
3: stepTitles[3],
|
||||||
|
4: stepTitles[4],
|
||||||
|
5: stepTitles[5],
|
||||||
|
};
|
||||||
|
|
||||||
const isStepValid = (stepNumber) => {
|
const isStepValid = (stepNumber) => {
|
||||||
switch (stepNumber) {
|
switch (stepNumber) {
|
||||||
case 1:
|
case 1:
|
||||||
@ -738,13 +838,41 @@ export default function InscriptionFormShared({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasParentFilesStep && currentPage > 5) {
|
||||||
|
setCurrentPage(5);
|
||||||
|
}
|
||||||
|
}, [hasParentFilesStep, currentPage]);
|
||||||
|
|
||||||
|
const nextDisabled =
|
||||||
|
(currentPage === 1 && !isPage1Valid) ||
|
||||||
|
(currentPage === 2 && !isPage2Valid) ||
|
||||||
|
(currentPage === 3 && !isPage3Valid) ||
|
||||||
|
(currentPage === 4 && !isPage4Valid) ||
|
||||||
|
(currentPage === 5 && !isPage5Valid);
|
||||||
|
|
||||||
|
const submitDisabled = !isStepValid(currentPage);
|
||||||
|
|
||||||
|
const navButtonBaseClass =
|
||||||
|
'min-w-[124px] min-h-[44px] px-5 rounded font-label text-sm font-semibold transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-primary/30';
|
||||||
|
|
||||||
|
const navPrimaryClass = nextDisabled
|
||||||
|
? `${navButtonBaseClass} bg-gray-200 text-gray-500 cursor-not-allowed`
|
||||||
|
: `${navButtonBaseClass} bg-primary hover:bg-secondary text-white`;
|
||||||
|
|
||||||
|
const navSubmitClass = submitDisabled
|
||||||
|
? `${navButtonBaseClass} bg-gray-200 text-gray-500 cursor-not-allowed`
|
||||||
|
: `${navButtonBaseClass} bg-primary hover:bg-secondary text-white`;
|
||||||
|
|
||||||
|
const navSecondaryClass = `${navButtonBaseClass} bg-neutral text-secondary border border-gray-300 hover:bg-white`;
|
||||||
|
|
||||||
// Rendu du composant
|
// Rendu du composant
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto p-6">
|
<div className="mx-auto p-6">
|
||||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||||
<ProgressStep
|
<ProgressStep
|
||||||
steps={steps}
|
steps={activeSteps}
|
||||||
stepTitles={stepTitles}
|
stepTitles={activeStepTitles}
|
||||||
currentStep={currentPage}
|
currentStep={currentPage}
|
||||||
setStep={setCurrentPage}
|
setStep={setCurrentPage}
|
||||||
isStepValid={isStepValid}
|
isStepValid={isStepValid}
|
||||||
@ -760,6 +888,64 @@ export default function InscriptionFormShared({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Navigation toujours au meme endroit (en haut a gauche) */}
|
||||||
|
<div className="mt-6 mb-4 flex items-center justify-start gap-3">
|
||||||
|
{enable ? (
|
||||||
|
<>
|
||||||
|
{currentPage > 1 && (
|
||||||
|
<Button
|
||||||
|
text="Précédent"
|
||||||
|
icon={<ChevronLeft size={16} strokeWidth={1.8} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePreviousPage();
|
||||||
|
}}
|
||||||
|
primary
|
||||||
|
className={navSecondaryClass}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentPage < activeSteps.length ? (
|
||||||
|
<Button
|
||||||
|
text={
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<span>Suivant</span>
|
||||||
|
<ChevronRight size={16} strokeWidth={1.8} />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNextPage();
|
||||||
|
}}
|
||||||
|
className={navPrimaryClass}
|
||||||
|
disabled={nextDisabled}
|
||||||
|
primary
|
||||||
|
name="Next"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
text="Valider"
|
||||||
|
icon={<Check size={16} strokeWidth={1.8} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}}
|
||||||
|
className={navSubmitClass}
|
||||||
|
disabled={submitDisabled}
|
||||||
|
primary
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push(FE_PARENTS_HOME_URL)}
|
||||||
|
text="Quitter"
|
||||||
|
icon={<X size={16} strokeWidth={1.8} />}
|
||||||
|
primary
|
||||||
|
className={`${navButtonBaseClass} bg-primary text-white hover:bg-secondary shadow-sm`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 h-full mt-6">
|
<div className="flex-1 h-full mt-6">
|
||||||
{/* Page 1 : Informations sur l'élève */}
|
{/* Page 1 : Informations sur l'élève */}
|
||||||
{currentPage === 1 && (
|
{currentPage === 1 && (
|
||||||
@ -827,12 +1013,13 @@ export default function InscriptionFormShared({
|
|||||||
onFormSubmit={handleDynamicFormSubmit}
|
onFormSubmit={handleDynamicFormSubmit}
|
||||||
onValidationChange={handleDynamicFormsValidationChange}
|
onValidationChange={handleDynamicFormsValidationChange}
|
||||||
enable={enable}
|
enable={enable}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleSchoolFileUpload}
|
||||||
|
onSignatureSubmit={handleSignatureSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dernière page : Section Fichiers parents */}
|
{/* Dernière page : Section Fichiers parents */}
|
||||||
{currentPage === 6 && (
|
{currentPage === 6 && hasParentFilesStep && (
|
||||||
<FilesToUpload
|
<FilesToUpload
|
||||||
parentFileTemplates={parentFileTemplates}
|
parentFileTemplates={parentFileTemplates}
|
||||||
uploadedFiles={uploadedFiles}
|
uploadedFiles={uploadedFiles}
|
||||||
@ -842,73 +1029,6 @@ export default function InscriptionFormShared({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Boutons de contrôle */}
|
|
||||||
<div className="flex justify-center space-x-4 mt-12">
|
|
||||||
{enable ? (
|
|
||||||
<>
|
|
||||||
{currentPage > 1 && (
|
|
||||||
<Button
|
|
||||||
text="Précédent"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handlePreviousPage();
|
|
||||||
}}
|
|
||||||
primary
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{currentPage < steps.length ? (
|
|
||||||
<Button
|
|
||||||
text="Suivant"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleNextPage();
|
|
||||||
}}
|
|
||||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
|
||||||
(currentPage === 1 && !isPage1Valid) ||
|
|
||||||
(currentPage === 2 && !isPage2Valid) ||
|
|
||||||
(currentPage === 3 && !isPage3Valid) ||
|
|
||||||
(currentPage === 4 && !isPage4Valid) ||
|
|
||||||
(currentPage === 5 && !isPage5Valid)
|
|
||||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
|
||||||
}`}
|
|
||||||
disabled={
|
|
||||||
(currentPage === 1 && !isPage1Valid) ||
|
|
||||||
(currentPage === 2 && !isPage2Valid) ||
|
|
||||||
(currentPage === 3 && !isPage3Valid) ||
|
|
||||||
(currentPage === 4 && !isPage4Valid) ||
|
|
||||||
(currentPage === 5 && !isPage5Valid)
|
|
||||||
}
|
|
||||||
primary
|
|
||||||
name="Next"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
text="Valider"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit(e);
|
|
||||||
}}
|
|
||||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
|
||||||
currentPage === 6 && !isPage6Valid
|
|
||||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
|
||||||
}`}
|
|
||||||
disabled={currentPage === 6 && !isPage6Valid}
|
|
||||||
primary
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push(FE_PARENTS_HOME_URL)}
|
|
||||||
text="Quitter"
|
|
||||||
primary
|
|
||||||
className="bg-emerald-500 text-white hover:bg-emerald-600"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function PaymentMethodSelector({
|
|||||||
<>
|
<>
|
||||||
{/* Frais d'inscription */}
|
{/* Frais d'inscription */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||||
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
|
<h2 className="font-headline text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
|
||||||
Frais d'inscription
|
Frais d'inscription
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ export default function PaymentMethodSelector({
|
|||||||
|
|
||||||
{/* Frais de scolarité */}
|
{/* Frais de scolarité */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-12">
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-12">
|
||||||
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
|
<h2 className="font-headline text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
|
||||||
Frais de scolarité
|
Frais de scolarité
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export default function ResponsableInputFields({
|
|||||||
{guardians.map((item, index) => (
|
{guardians.map((item, index) => (
|
||||||
<div className="p-6 " key={index}>
|
<div className="p-6 " key={index}>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-xl font-bold">
|
<h3 className="font-headline text-xl font-bold">
|
||||||
{t('responsable')} {index + 1}
|
{t('responsable')} {index + 1}
|
||||||
</h3>
|
</h3>
|
||||||
{guardians.length > 1 && (
|
{guardians.length > 1 && (
|
||||||
@ -259,11 +259,11 @@ export default function ResponsableInputFields({
|
|||||||
className={`w-8 h-8 ${
|
className={`w-8 h-8 ${
|
||||||
guardians.length >= MAX_GUARDIANS
|
guardians.length >= MAX_GUARDIANS
|
||||||
? 'text-gray-400 cursor-not-allowed'
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
: 'text-green-500 cursor-pointer hover:text-green-700'
|
: 'text-primary cursor-pointer hover:text-secondary'
|
||||||
} transition-colors border-2 ${
|
} transition-colors border-2 ${
|
||||||
guardians.length >= MAX_GUARDIANS
|
guardians.length >= MAX_GUARDIANS
|
||||||
? 'border-gray-400'
|
? 'border-gray-400'
|
||||||
: 'border-green-500 hover:border-green-700'
|
: 'border-primary hover:border-secondary'
|
||||||
} rounded-full p-1`}
|
} rounded-full p-1`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (guardians.length < MAX_GUARDIANS) {
|
if (guardians.length < MAX_GUARDIANS) {
|
||||||
|
|||||||
@ -103,7 +103,7 @@ export default function SiblingInputFields({
|
|||||||
{siblings.map((item, index) => (
|
{siblings.map((item, index) => (
|
||||||
<div className="p-6" key={index}>
|
<div className="p-6" key={index}>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-xl font-bold">Frère/Sœur {index + 1}</h3>
|
<h3 className="font-headline text-xl font-bold">Frère/Sœur {index + 1}</h3>
|
||||||
<Trash2
|
<Trash2
|
||||||
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
|
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
|
||||||
onClick={() => deleteSibling(index)}
|
onClick={() => deleteSibling(index)}
|
||||||
@ -160,7 +160,7 @@ export default function SiblingInputFields({
|
|||||||
{enable && (
|
{enable && (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Plus
|
<Plus
|
||||||
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
|
className="w-8 h-8 text-primary cursor-pointer hover:text-secondary transition-colors border-2 border-primary hover:border-secondary rounded-full p-1"
|
||||||
onClick={addSibling}
|
onClick={addSibling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user