mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Rattachement d'un dossier de compétences à une période scolaire
(configuration dans l'établissement) [#16]
This commit is contained in:
@ -223,7 +223,7 @@ def makeToken(user):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Récupérer tous les rôles de l'utilisateur actifs
|
# Récupérer tous les rôles de l'utilisateur actifs
|
||||||
roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name')
|
roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency')
|
||||||
|
|
||||||
# Générer le JWT avec la bonne syntaxe datetime
|
# Générer le JWT avec la bonne syntaxe datetime
|
||||||
access_payload = {
|
access_payload = {
|
||||||
|
|||||||
@ -7,11 +7,17 @@ class StructureType(models.IntegerChoices):
|
|||||||
PRIMAIRE = 2, _('Primaire')
|
PRIMAIRE = 2, _('Primaire')
|
||||||
SECONDAIRE = 3, _('Secondaire')
|
SECONDAIRE = 3, _('Secondaire')
|
||||||
|
|
||||||
|
class EvaluationFrequency(models.IntegerChoices):
|
||||||
|
TRIMESTER = 1, _("Trimestre")
|
||||||
|
SEMESTER = 2, _("Semestre")
|
||||||
|
YEAR = 3, _("Année")
|
||||||
|
|
||||||
class Establishment(models.Model):
|
class Establishment(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
address = models.CharField(max_length=255)
|
address = models.CharField(max_length=255)
|
||||||
total_capacity = models.IntegerField()
|
total_capacity = models.IntegerField()
|
||||||
establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices))
|
establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices))
|
||||||
|
evaluation_frequency = models.IntegerField(choices=EvaluationFrequency.choices, default=EvaluationFrequency.TRIMESTER)
|
||||||
licence_code = models.CharField(max_length=100, blank=True)
|
licence_code = models.CharField(max_length=100, blank=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@ -56,7 +56,23 @@ def registration_photo_upload_to(instance, filename):
|
|||||||
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
||||||
|
|
||||||
def registration_bilan_form_upload_to(instance, filename):
|
def registration_bilan_form_upload_to(instance, filename):
|
||||||
return f"registration_files/dossier_rf_{instance.pk}/bilan/{filename}"
|
# On récupère le RegistrationForm lié à l'élève
|
||||||
|
register_form = getattr(instance.student, 'registrationform', None)
|
||||||
|
if register_form:
|
||||||
|
pk = register_form.pk
|
||||||
|
else:
|
||||||
|
# fallback sur l'id de l'élève si pas de registrationform
|
||||||
|
pk = instance.student.pk
|
||||||
|
return f"registration_files/dossier_rf_{pk}/bilan/{filename}"
|
||||||
|
|
||||||
|
class BilanCompetence(models.Model):
|
||||||
|
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||||
|
file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True)
|
||||||
|
period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.student} - {self.period}"
|
||||||
|
|
||||||
class Student(models.Model):
|
class Student(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -94,7 +110,6 @@ class Student(models.Model):
|
|||||||
birth_place = models.CharField(max_length=200, default="", blank=True)
|
birth_place = models.CharField(max_length=200, default="", blank=True)
|
||||||
birth_postal_code = models.IntegerField(default=0, blank=True)
|
birth_postal_code = models.IntegerField(default=0, blank=True)
|
||||||
attending_physician = models.CharField(max_length=200, default="", blank=True)
|
attending_physician = models.CharField(max_length=200, default="", blank=True)
|
||||||
bilan_form = models.FileField(null=True,blank=True, upload_to=registration_bilan_form_upload_to)
|
|
||||||
|
|
||||||
# Many-to-Many Relationship
|
# Many-to-Many Relationship
|
||||||
profiles = models.ManyToManyField('Auth.Profile', blank=True)
|
profiles = models.ManyToManyField('Auth.Profile', blank=True)
|
||||||
@ -187,6 +202,15 @@ class Student(models.Model):
|
|||||||
return self.birth_date.strftime('%d-%m-%Y')
|
return self.birth_date.strftime('%d-%m-%Y')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
class BilanCompetence(models.Model):
|
||||||
|
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||||
|
file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True)
|
||||||
|
period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.student} - {self.period}"
|
||||||
|
|
||||||
class RegistrationFileGroup(models.Model):
|
class RegistrationFileGroup(models.Model):
|
||||||
name = models.CharField(max_length=255, default="")
|
name = models.CharField(max_length=255, default="")
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
@ -331,16 +355,22 @@ class StudentCompetency(models.Model):
|
|||||||
establishment_competency = models.ForeignKey('School.EstablishmentCompetency', on_delete=models.CASCADE, related_name='student_scores')
|
establishment_competency = models.ForeignKey('School.EstablishmentCompetency', on_delete=models.CASCADE, related_name='student_scores')
|
||||||
score = models.IntegerField(null=True, blank=True)
|
score = models.IntegerField(null=True, blank=True)
|
||||||
comment = models.TextField(blank=True, null=True)
|
comment = models.TextField(blank=True, null=True)
|
||||||
|
period = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
help_text="Période d'évaluation ex: T1-2024_2025, S1-2024_2025, A-2024_2025",
|
||||||
|
default="",
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('student', 'establishment_competency')
|
unique_together = ('student', 'establishment_competency', 'period')
|
||||||
|
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['student', 'establishment_competency']),
|
models.Index(fields=['student', 'establishment_competency', 'period']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.student} - {self.establishment_competency} - Score: {self.score}"
|
return f"{self.student} - {self.establishment_competency} - Score: {self.score} - Period: {self.period}"
|
||||||
|
|
||||||
####### Parent files templates (par dossier d'inscription) #######
|
####### Parent files templates (par dossier d'inscription) #######
|
||||||
class RegistrationParentFileTemplate(models.Model):
|
class RegistrationParentFileTemplate(models.Model):
|
||||||
|
|||||||
@ -10,18 +10,16 @@ from .models import (
|
|||||||
RegistrationSchoolFileTemplate,
|
RegistrationSchoolFileTemplate,
|
||||||
RegistrationParentFileMaster,
|
RegistrationParentFileMaster,
|
||||||
RegistrationParentFileTemplate,
|
RegistrationParentFileTemplate,
|
||||||
AbsenceManagement
|
AbsenceManagement,
|
||||||
|
BilanCompetence
|
||||||
)
|
)
|
||||||
from School.models import SchoolClass, Fee, Discount, FeeType
|
from School.models import SchoolClass, Fee, Discount, FeeType
|
||||||
from School.serializers import FeeSerializer, DiscountSerializer
|
|
||||||
from Auth.models import ProfileRole, Profile
|
from Auth.models import ProfileRole, Profile
|
||||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||||
from GestionMessagerie.models import Messagerie
|
|
||||||
from GestionNotification.models import Notification
|
from GestionNotification.models import Notification
|
||||||
from N3wtSchool import settings
|
from N3wtSchool import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pytz
|
import pytz
|
||||||
from datetime import datetime
|
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
|
|
||||||
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
||||||
@ -415,14 +413,20 @@ class GuardianByDICreationSerializer(serializers.ModelSerializer):
|
|||||||
return obj.profile_role.profile.id # Retourne l'ID du profil associé
|
return obj.profile_role.profile.id # Retourne l'ID du profil associé
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
class BilanCompetenceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BilanCompetence
|
||||||
|
fields = ['id', 'file', 'period', 'created_at']
|
||||||
|
|
||||||
class StudentByRFCreationSerializer(serializers.ModelSerializer):
|
class StudentByRFCreationSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(required=False)
|
id = serializers.IntegerField(required=False)
|
||||||
guardians = GuardianByDICreationSerializer(many=True, required=False)
|
guardians = GuardianByDICreationSerializer(many=True, required=False)
|
||||||
associated_class_name = serializers.SerializerMethodField()
|
associated_class_name = serializers.SerializerMethodField()
|
||||||
|
bilans = BilanCompetenceSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Student
|
model = Student
|
||||||
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo']
|
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo', 'bilans']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs)
|
super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
<strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}<br>
|
<strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}<br>
|
||||||
<strong>Niveau :</strong> {{ student.level }}<br>
|
<strong>Niveau :</strong> {{ student.level }}<br>
|
||||||
<strong>Classe :</strong> {{ student.class_name }}<br>
|
<strong>Classe :</strong> {{ student.class_name }}<br>
|
||||||
|
<strong>Période :</strong> {{ period }}<br>
|
||||||
<strong>Date :</strong> {{ date }}
|
<strong>Date :</strong> {{ date }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -70,5 +71,42 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<div style="margin-top: 60px; padding: 0; max-width: 700px;">
|
||||||
|
<div style="
|
||||||
|
min-height: 180px;
|
||||||
|
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>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -26,6 +26,7 @@ from Subscriptions.models import (
|
|||||||
)
|
)
|
||||||
from Subscriptions.automate import updateStateMachine
|
from Subscriptions.automate import updateStateMachine
|
||||||
from School.models import EstablishmentCompetency
|
from School.models import EstablishmentCompetency
|
||||||
|
from Establishment.models import Establishment
|
||||||
|
|
||||||
from N3wtSchool import settings, bdd
|
from N3wtSchool import settings, bdd
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -392,11 +393,28 @@ class RegisterFormWithIdView(APIView):
|
|||||||
competency__category__domain__cycle=cycle
|
competency__category__domain__cycle=cycle
|
||||||
)
|
)
|
||||||
establishment_competencies = establishment_competencies.distinct()
|
establishment_competencies = establishment_competencies.distinct()
|
||||||
|
|
||||||
|
establishment = registerForm.establishment
|
||||||
|
evaluation_frequency = establishment.evaluation_frequency # 1=Trimestre, 2=Semestre, 3=Année
|
||||||
|
school_year = registerForm.school_year # ex: "2024_2025"
|
||||||
|
|
||||||
|
establishment_competencies = establishment_competencies.distinct()
|
||||||
|
|
||||||
|
periods = []
|
||||||
|
if evaluation_frequency == 1: # Trimestre
|
||||||
|
periods = [f"T{i+1}_{school_year}" for i in range(3)]
|
||||||
|
elif evaluation_frequency == 2: # Semestre
|
||||||
|
periods = [f"S{i+1}_{school_year}" for i in range(2)]
|
||||||
|
elif evaluation_frequency == 3: # Année
|
||||||
|
periods = [f"A_{school_year}"]
|
||||||
|
|
||||||
for ec in establishment_competencies:
|
for ec in establishment_competencies:
|
||||||
StudentCompetency.objects.get_or_create(
|
for period in periods:
|
||||||
student=student,
|
StudentCompetency.objects.get_or_create(
|
||||||
establishment_competency=ec
|
student=student,
|
||||||
)
|
establishment_competency=ec,
|
||||||
|
period=period
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}")
|
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}")
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,14 @@ from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
|
|||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from Subscriptions.models import StudentCompetency, Student
|
from Subscriptions.models import StudentCompetency, Student
|
||||||
from Common.models import Domain
|
from Common.models import Domain
|
||||||
from django.conf import settings
|
from Subscriptions.models import BilanCompetence
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from N3wtSchool.renderers import render_to_pdf
|
from N3wtSchool.renderers import render_to_pdf
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class StudentCompetencyListCreateView(APIView):
|
class StudentCompetencyListCreateView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
student_id = request.GET.get('student_id')
|
student_id = request.GET.get('student_id')
|
||||||
|
period = request.GET.get('period')
|
||||||
if not student_id:
|
if not student_id:
|
||||||
return JsonResponse({'error': 'student_id requis'}, status=400)
|
return JsonResponse({'error': 'student_id requis'}, status=400)
|
||||||
try:
|
try:
|
||||||
@ -28,7 +30,12 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
except Student.DoesNotExist:
|
except Student.DoesNotExist:
|
||||||
return JsonResponse({'error': 'Élève introuvable'}, status=404)
|
return JsonResponse({'error': 'Élève introuvable'}, status=404)
|
||||||
|
|
||||||
student_competencies = StudentCompetency.objects.filter(student=student).select_related(
|
# Filtrer par student ET period si period est fourni
|
||||||
|
filter_kwargs = {'student': student}
|
||||||
|
if period:
|
||||||
|
filter_kwargs['period'] = period
|
||||||
|
|
||||||
|
student_competencies = StudentCompetency.objects.filter(**filter_kwargs).select_related(
|
||||||
'establishment_competency',
|
'establishment_competency',
|
||||||
'establishment_competency__competency',
|
'establishment_competency__competency',
|
||||||
'establishment_competency__competency__category',
|
'establishment_competency__competency__category',
|
||||||
@ -64,6 +71,7 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
"nom": comp.name,
|
"nom": comp.name,
|
||||||
"score": sc.score,
|
"score": sc.score,
|
||||||
"comment": sc.comment or "",
|
"comment": sc.comment or "",
|
||||||
|
"period":sc.period or ""
|
||||||
})
|
})
|
||||||
total_competencies += 1
|
total_competencies += 1
|
||||||
# Cas compétence custom
|
# Cas compétence custom
|
||||||
@ -73,6 +81,7 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
"nom": ec.custom_name,
|
"nom": ec.custom_name,
|
||||||
"score": sc.score,
|
"score": sc.score,
|
||||||
"comment": sc.comment or "",
|
"comment": sc.comment or "",
|
||||||
|
"period":sc.period or ""
|
||||||
})
|
})
|
||||||
total_competencies += 1
|
total_competencies += 1
|
||||||
if categorie_dict["competences"]:
|
if categorie_dict["competences"]:
|
||||||
@ -99,6 +108,7 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
comp_id = item.get("competenceId")
|
comp_id = item.get("competenceId")
|
||||||
grade = item.get("grade")
|
grade = item.get("grade")
|
||||||
student_id = item.get('studentId')
|
student_id = item.get('studentId')
|
||||||
|
period = item.get('period')
|
||||||
if comp_id is None or grade is None:
|
if comp_id is None or grade is None:
|
||||||
errors.append({"competenceId": comp_id, "error": "champ manquant"})
|
errors.append({"competenceId": comp_id, "error": "champ manquant"})
|
||||||
continue
|
continue
|
||||||
@ -106,7 +116,8 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
# Ajoute le filtre student_id
|
# Ajoute le filtre student_id
|
||||||
sc = StudentCompetency.objects.get(
|
sc = StudentCompetency.objects.get(
|
||||||
establishment_competency_id=comp_id,
|
establishment_competency_id=comp_id,
|
||||||
student_id=student_id
|
student_id=student_id,
|
||||||
|
period=period
|
||||||
)
|
)
|
||||||
sc.score = grade
|
sc.score = grade
|
||||||
sc.save()
|
sc.save()
|
||||||
@ -117,8 +128,8 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
# Génération du PDF si au moins une compétence a été mise à jour
|
# Génération du PDF si au moins une compétence a été mise à jour
|
||||||
if updated:
|
if updated:
|
||||||
student = Student.objects.get(id=student_id)
|
student = Student.objects.get(id=student_id)
|
||||||
# Reconstituer la structure "domaines" comme dans le GET
|
# Reconstituer la structure "domaines" pour la période concernée uniquement
|
||||||
student_competencies = StudentCompetency.objects.filter(student=student).select_related(
|
student_competencies = StudentCompetency.objects.filter(student=student, period=period).select_related(
|
||||||
'establishment_competency',
|
'establishment_competency',
|
||||||
'establishment_competency__competency',
|
'establishment_competency__competency',
|
||||||
'establishment_competency__competency__category',
|
'establishment_competency__competency__category',
|
||||||
@ -166,37 +177,38 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
"level": student.level,
|
"level": student.level,
|
||||||
"class_name": student.associated_class.atmosphere_name,
|
"class_name": student.associated_class.atmosphere_name,
|
||||||
},
|
},
|
||||||
|
"period": period,
|
||||||
"date": date.today().strftime("%d/%m/%Y"),
|
"date": date.today().strftime("%d/%m/%Y"),
|
||||||
"domaines": result,
|
"domaines": result,
|
||||||
}
|
}
|
||||||
print('génération du PDF...')
|
|
||||||
pdf_result = render_to_pdf('pdfs/bilan_competences.html', context)
|
pdf_result = render_to_pdf('pdfs/bilan_competences.html', context)
|
||||||
|
|
||||||
# Vérifier si un fichier bilan_form existe déjà et le supprimer
|
|
||||||
if student.bilan_form and student.bilan_form.name:
|
|
||||||
if os.path.isabs(student.bilan_form.path):
|
|
||||||
existing_file_path = student.bilan_form.path
|
|
||||||
else:
|
|
||||||
existing_file_path = os.path.join(settings.MEDIA_ROOT, student.bilan_form.name.lstrip('/'))
|
|
||||||
|
|
||||||
if os.path.exists(existing_file_path):
|
|
||||||
os.remove(existing_file_path)
|
|
||||||
student.bilan_form.delete(save=False)
|
|
||||||
logger.info(f"Ancien PDF supprimé : {existing_file_path}")
|
|
||||||
else:
|
|
||||||
logger.info(f"File does not exist: {existing_file_path}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = f"bilan_competences_{student.last_name}_{student.first_name}.pdf"
|
filename = f"bilan_competences_{student.last_name}_{student.first_name}_{period}.pdf"
|
||||||
student.bilan_form.save(
|
# Vérifier si un bilan existe déjà pour cet élève et cette période
|
||||||
os.path.basename(filename), # Utiliser uniquement le nom de fichier
|
existing_bilan = BilanCompetence.objects.filter(student=student, period=period).first()
|
||||||
|
if existing_bilan:
|
||||||
|
# Supprimer le fichier physique si présent
|
||||||
|
if existing_bilan.file and existing_bilan.file.name:
|
||||||
|
file_path = existing_bilan.file.path
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
existing_bilan.delete()
|
||||||
|
|
||||||
|
bilan = BilanCompetence.objects.create(
|
||||||
|
student=student,
|
||||||
|
period=period
|
||||||
|
)
|
||||||
|
bilan.file.save(
|
||||||
|
os.path.basename(filename),
|
||||||
File(BytesIO(pdf_result.content)),
|
File(BytesIO(pdf_result.content)),
|
||||||
save=True
|
save=True
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
|
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
|
||||||
raise
|
raise
|
||||||
# Exemple : retour du PDF dans la réponse HTTP (pour test)
|
|
||||||
|
|
||||||
return JsonResponse({"updated": updated, "errors": errors}, status=200)
|
return JsonResponse({"updated": updated, "errors": errors}, status=200)
|
||||||
|
|
||||||
@ -205,17 +217,3 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
class StudentCompetencySimpleView(APIView):
|
class StudentCompetencySimpleView(APIView):
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
return JsonResponse("ok", safe=False, status=status.HTTP_200_OK)
|
return JsonResponse("ok", safe=False, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# def put(self, request, id):
|
|
||||||
# try:
|
|
||||||
# absence = AbsenceManagement.objects.get(id=id)
|
|
||||||
# serializer = AbsenceManagementSerializer(absence, data=request.data)
|
|
||||||
# if serializer.is_valid():
|
|
||||||
# serializer.save()
|
|
||||||
# return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK)
|
|
||||||
# return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
# except AbsenceManagement.DoesNotExist:
|
|
||||||
# return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
# def delete(self, request, id):
|
|
||||||
# return delete_object(AbsenceManagement, id)
|
|
||||||
@ -143,7 +143,7 @@ def search_students(request):
|
|||||||
'last_name': student.last_name,
|
'last_name': student.last_name,
|
||||||
'level': getattr(student.level, 'name', ''),
|
'level': getattr(student.level, 'name', ''),
|
||||||
'associated_class_name': student.associated_class.atmosphere_name if student.associated_class else '',
|
'associated_class_name': student.associated_class.atmosphere_name if student.associated_class else '',
|
||||||
'photo': student.photo.url if student.photo else None,
|
'photo': student.photo.url if student.photo else None
|
||||||
}
|
}
|
||||||
for student in students
|
for student in students
|
||||||
]
|
]
|
||||||
|
|||||||
12
Front-End/package-lock.json
generated
12
Front-End/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@ -1960,6 +1961,12 @@
|
|||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
@ -7897,6 +7904,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||||
},
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
|||||||
@ -23,14 +23,16 @@ import {
|
|||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
import StudentInput from '@/components/Grades/StudentInput';
|
import { Award, BookOpen, FileText } from 'lucide-react';
|
||||||
import { Award, BookOpen } from 'lucide-react';
|
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
|
||||||
|
import InputText from '@/components/InputText';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId, selectedEstablishmentEvaluationFrequency } =
|
||||||
|
useEstablishment();
|
||||||
const { getNiveauLabel } = useClasses();
|
const { getNiveauLabel } = useClasses();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
selectedStudent: null,
|
selectedStudent: null,
|
||||||
@ -39,6 +41,48 @@ export default function Page() {
|
|||||||
const [students, setStudents] = useState([]);
|
const [students, setStudents] = useState([]);
|
||||||
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
const [studentCompetencies, setStudentCompetencies] = useState(null);
|
||||||
const [grades, setGrades] = useState({});
|
const [grades, setGrades] = useState({});
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedPeriod, setSelectedPeriod] = useState(null);
|
||||||
|
|
||||||
|
// Définir les périodes selon la fréquence
|
||||||
|
const getPeriods = () => {
|
||||||
|
if (selectedEstablishmentEvaluationFrequency === 1) {
|
||||||
|
return [
|
||||||
|
{ label: 'Trimestre 1', value: 1, start: '09-01', end: '12-31' },
|
||||||
|
{ label: 'Trimestre 2', value: 2, start: '01-01', end: '03-31' },
|
||||||
|
{ label: 'Trimestre 3', value: 3, start: '04-01', end: '07-15' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (selectedEstablishmentEvaluationFrequency === 2) {
|
||||||
|
return [
|
||||||
|
{ label: 'Semestre 1', value: 1, start: '09-01', end: '01-31' },
|
||||||
|
{ label: 'Semestre 2', value: 2, start: '02-01', end: '07-15' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (selectedEstablishmentEvaluationFrequency === 3) {
|
||||||
|
return [{ label: 'Année', value: 1, start: '09-01', end: '07-15' }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sélection automatique de la période courante
|
||||||
|
useEffect(() => {
|
||||||
|
if (!formData.selectedStudent) {
|
||||||
|
setSelectedPeriod(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const periods = getPeriods();
|
||||||
|
const today = dayjs();
|
||||||
|
const current = periods.find((p) => {
|
||||||
|
const start = dayjs(`${today.year()}-${p.start}`);
|
||||||
|
const end = dayjs(`${today.year()}-${p.end}`);
|
||||||
|
return (
|
||||||
|
today.isAfter(start.subtract(1, 'day')) &&
|
||||||
|
today.isBefore(end.add(1, 'day'))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setSelectedPeriod(current ? current.value : null);
|
||||||
|
}, [formData.selectedStudent, selectedEstablishmentEvaluationFrequency]);
|
||||||
|
|
||||||
const academicResults = [
|
const academicResults = [
|
||||||
{
|
{
|
||||||
@ -122,8 +166,12 @@ export default function Page() {
|
|||||||
|
|
||||||
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
|
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.selectedStudent) {
|
if (formData.selectedStudent && selectedPeriod) {
|
||||||
fetchStudentCompetencies(formData.selectedStudent)
|
const periodString = getPeriodString(
|
||||||
|
selectedPeriod,
|
||||||
|
selectedEstablishmentEvaluationFrequency
|
||||||
|
);
|
||||||
|
fetchStudentCompetencies(formData.selectedStudent, periodString)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setStudentCompetencies(data);
|
setStudentCompetencies(data);
|
||||||
// Générer les grades à partir du retour API
|
// Générer les grades à partir du retour API
|
||||||
@ -146,105 +194,218 @@ export default function Page() {
|
|||||||
setGrades({});
|
setGrades({});
|
||||||
setStudentCompetencies(null);
|
setStudentCompetencies(null);
|
||||||
}
|
}
|
||||||
}, [formData.selectedStudent]);
|
}, [formData.selectedStudent, selectedPeriod]);
|
||||||
|
|
||||||
|
// Fonction utilitaire pour convertir la période sélectionnée en string backend
|
||||||
|
function getPeriodString(selectedPeriod, frequency) {
|
||||||
|
const year = dayjs().month() >= 8 ? dayjs().year() : dayjs().year() - 1; // année scolaire commence en septembre
|
||||||
|
const nextYear = (year + 1).toString();
|
||||||
|
const schoolYear = `${year}-${nextYear}`;
|
||||||
|
if (frequency === 1) return `T${selectedPeriod}_${schoolYear}`;
|
||||||
|
if (frequency === 2) return `S${selectedPeriod}_${schoolYear}`;
|
||||||
|
if (frequency === 3) return `A_${schoolYear}`;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8">
|
<div className="p-8 space-y-8">
|
||||||
{/* Sélection de l'élève */}
|
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
icon={BookOpen}
|
icon={BookOpen}
|
||||||
title="Suivi pédagogique"
|
title="Suivi pédagogique"
|
||||||
description="Suivez le parcours d'un élève"
|
description="Suivez le parcours d'un élève"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col md:flex-row md:items-start gap-4">
|
|
||||||
{/* Recherche élève + bouton + fiche élève */}
|
{/* Section haute : filtre + bouton + photo élève */}
|
||||||
<div className="flex-1 flex flex-row gap-4 items-start">
|
<div className="flex flex-row gap-8 items-start">
|
||||||
|
{/* Colonne gauche : InputText + bouton */}
|
||||||
|
<div className="w-4/5 flex items-end gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<StudentInput
|
<InputText
|
||||||
|
name="studentSearch"
|
||||||
|
type="text"
|
||||||
label="Recherche élève"
|
label="Recherche élève"
|
||||||
selectedStudent={
|
value={searchTerm}
|
||||||
students.find((s) => s.id === formData.selectedStudent) || null
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
}
|
placeholder="Rechercher un élève"
|
||||||
setSelectedStudent={(student) =>
|
required={false}
|
||||||
handleChange('selectedStudent', student?.id || '')
|
enable={true}
|
||||||
}
|
|
||||||
searchStudents={searchStudents}
|
|
||||||
establishmentId={selectedEstablishmentId}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Sélecteur de période */}
|
||||||
{formData.selectedStudent && (
|
{formData.selectedStudent && (
|
||||||
<div className="ml-4 flex flex-col items-center min-w-[220px] max-w-xs p-4 rounded-lg border border-emerald-100 shadow">
|
<SelectChoice
|
||||||
{(() => {
|
name="period"
|
||||||
const student = students.find(
|
label="Période"
|
||||||
(s) => s.id === formData.selectedStudent
|
placeHolder="Choisir la période"
|
||||||
);
|
choices={getPeriods().map((period) => {
|
||||||
if (!student) return null;
|
const today = dayjs();
|
||||||
return (
|
const start = dayjs(`${today.year()}-${period.start}`);
|
||||||
<>
|
const end = dayjs(`${today.year()}-${period.end}`);
|
||||||
{student.photo ? (
|
const isPast = today.isAfter(end);
|
||||||
<img
|
return {
|
||||||
src={`${BASE_URL}${student.photo}`}
|
value: period.value,
|
||||||
alt={`${student.first_name} ${student.last_name}`}
|
label: period.label,
|
||||||
className="w-20 h-20 object-cover rounded-full border-4 border-emerald-200 mb-2 shadow"
|
disabled: isPast,
|
||||||
/>
|
};
|
||||||
) : (
|
})}
|
||||||
<div className="w-20 h-20 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl mb-2 border-4 border-emerald-100">
|
selected={selectedPeriod || ''}
|
||||||
{student.first_name?.[0]}
|
callback={(e) => setSelectedPeriod(Number(e.target.value))}
|
||||||
{student.last_name?.[0]}
|
disabled={false}
|
||||||
</div>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="text-center">
|
<Button
|
||||||
<div className="text-base font-semibold text-emerald-800">
|
primary
|
||||||
{student.last_name} {student.first_name}
|
onClick={() => {
|
||||||
</div>
|
const periodString = getPeriodString(
|
||||||
<div className="text-xs text-gray-600 mt-1">
|
selectedPeriod,
|
||||||
Niveau :{' '}
|
selectedEstablishmentEvaluationFrequency
|
||||||
<span className="font-medium">
|
);
|
||||||
{getNiveauLabel(student.level)}
|
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}&period=${periodString}`;
|
||||||
</span>
|
router.push(url);
|
||||||
</div>
|
}}
|
||||||
<div className="text-xs text-gray-600">
|
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600"
|
||||||
Classe :{' '}
|
icon={<Award className="w-6 h-6" />}
|
||||||
<span className="font-medium">
|
text="Evaluer"
|
||||||
{student.associated_class_name}
|
title="Evaluez l'élève"
|
||||||
</span>
|
disabled={!formData.selectedStudent || !selectedPeriod}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Bouton bilan de compétences en dessous */}
|
{/* Colonne droite : Photo élève */}
|
||||||
<Button
|
<div className="w-2/5 flex flex-col items-center justify-center">
|
||||||
primary
|
{formData.selectedStudent &&
|
||||||
onClick={() => {
|
(() => {
|
||||||
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}`;
|
const student = students.find(
|
||||||
router.push(`${url}`);
|
(s) => s.id === formData.selectedStudent
|
||||||
}}
|
);
|
||||||
className="mt-4 px-6 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600"
|
if (!student) return null;
|
||||||
icon={<Award className="w-6 h-6" />}
|
return (
|
||||||
title="Réaliser le bilan de compétences"
|
<>
|
||||||
|
{student.photo ? (
|
||||||
|
<img
|
||||||
|
src={`${BASE_URL}${student.photo}`}
|
||||||
|
alt={`${student.first_name} ${student.last_name}`}
|
||||||
|
className="w-32 h-32 object-cover rounded-full border-4 border-emerald-200 mb-4 shadow"
|
||||||
/>
|
/>
|
||||||
</>
|
) : (
|
||||||
);
|
<div className="w-24 h-24 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-3xl mb-4 border-4 border-emerald-100">
|
||||||
})()}
|
{student.first_name?.[0]}
|
||||||
|
{student.last_name?.[0]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section basse : liste élèves + infos */}
|
||||||
|
<div className="flex flex-row gap-8 items-start mt-8">
|
||||||
|
{/* Colonne 1 : Liste des élèves */}
|
||||||
|
<div className="w-full max-w-xs">
|
||||||
|
<h3 className="text-lg font-semibold text-emerald-700 mb-4">
|
||||||
|
Liste des élèves
|
||||||
|
</h3>
|
||||||
|
<ul className="rounded-lg bg-stone-50 shadow border border-gray-100">
|
||||||
|
{students
|
||||||
|
.filter(
|
||||||
|
(student) =>
|
||||||
|
!searchTerm ||
|
||||||
|
`${student.last_name} ${student.first_name}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
.map((student) => (
|
||||||
|
<li
|
||||||
|
key={student.id}
|
||||||
|
className={`flex items-center gap-4 px-4 py-3 hover:bg-emerald-100 cursor-pointer transition ${
|
||||||
|
formData.selectedStudent === student.id
|
||||||
|
? 'bg-emerald-100 border-l-4 border-emerald-400'
|
||||||
|
: 'border-l-2 border-gray-200'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleChange('selectedStudent', student.id)}
|
||||||
|
>
|
||||||
|
{student.photo ? (
|
||||||
|
<img
|
||||||
|
src={`${BASE_URL}${student.photo}`}
|
||||||
|
alt={`${student.first_name} ${student.last_name}`}
|
||||||
|
className="w-10 h-10 object-cover rounded-full border-2 border-emerald-200"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-bold text-lg border-2 border-emerald-100">
|
||||||
|
{student.first_name?.[0]}
|
||||||
|
{student.last_name?.[0]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold text-emerald-800">
|
||||||
|
{student.last_name} {student.first_name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600">
|
||||||
|
Niveau :{' '}
|
||||||
|
<span className="font-medium">
|
||||||
|
{getNiveauLabel(student.level)}
|
||||||
|
</span>
|
||||||
|
{' | '}
|
||||||
|
Classe :{' '}
|
||||||
|
<span className="font-medium">
|
||||||
|
{student.associated_class_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Icône PDF si bilan dispo pour la période sélectionnée */}
|
||||||
|
{selectedPeriod &&
|
||||||
|
student.bilans &&
|
||||||
|
Array.isArray(student.bilans) &&
|
||||||
|
(() => {
|
||||||
|
// Génère la string de période attendue
|
||||||
|
const periodString = getPeriodString(
|
||||||
|
selectedPeriod,
|
||||||
|
selectedEstablishmentEvaluationFrequency
|
||||||
|
);
|
||||||
|
const bilan = student.bilans.find(
|
||||||
|
(b) => b.period === periodString && b.file
|
||||||
|
);
|
||||||
|
if (bilan) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${BASE_URL}${bilan.file}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="ml-2 text-emerald-600 hover:text-emerald-800"
|
||||||
|
title="Télécharger le bilan de compétences"
|
||||||
|
onClick={(e) => e.stopPropagation()} // Pour ne pas sélectionner à nouveau l'élève
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/* Colonne 2 : Reste des infos */}
|
||||||
|
<div className="flex-1">
|
||||||
|
{formData.selectedStudent && (
|
||||||
|
<div className="flex flex-col gap-8 w-full justify-center items-stretch">
|
||||||
|
<div className="w-full flex flex-row items-stretch gap-4">
|
||||||
|
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||||
|
<Attendance absences={absences} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex items-stretch justify-center h-full">
|
||||||
|
<GradesStatsCircle grades={grades} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<GradesDomainBarChart
|
||||||
|
studentCompetencies={studentCompetencies}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Partie basse : stats à gauche, présence à droite, sans bg */}
|
|
||||||
{formData.selectedStudent && (
|
|
||||||
<div className="flex flex-col gap-8 w-full justify-center items-stretch mt-8">
|
|
||||||
<div className="w-3/4 flex flex-row items-stretch gap-4 mx-auto">
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<Attendance absences={absences} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<GradesStatsCircle grades={grades} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<GradesDomainBarChart studentCompetencies={studentCompetencies} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,10 @@ export default function StudentCompetenciesPage() {
|
|||||||
const [studentCompetencies, setStudentCompetencies] = useState([]);
|
const [studentCompetencies, setStudentCompetencies] = useState([]);
|
||||||
const [grades, setGrades] = useState({});
|
const [grades, setGrades] = useState({});
|
||||||
const studentId = searchParams.get('studentId');
|
const studentId = searchParams.get('studentId');
|
||||||
|
const period = searchParams.get('period');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStudentCompetencies(studentId)
|
fetchStudentCompetencies(studentId, period)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setStudentCompetencies(data);
|
setStudentCompetencies(data);
|
||||||
})
|
})
|
||||||
@ -64,6 +65,7 @@ export default function StudentCompetenciesPage() {
|
|||||||
studentId,
|
studentId,
|
||||||
competenceId,
|
competenceId,
|
||||||
grade: score,
|
grade: score,
|
||||||
|
period: period,
|
||||||
}));
|
}));
|
||||||
editStudentCompetencies(data, csrfToken)
|
editStudentCompetencies(data, csrfToken)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ import {
|
|||||||
import { fetchProfiles } from '@/app/actions/authAction';
|
import { fetchProfiles } from '@/app/actions/authAction';
|
||||||
import { useClasses } from '@/context/ClassesContext';
|
import { useClasses } from '@/context/ClassesContext';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
import { FE_ADMIN_SUBSCRIPTIONS_URL, BASE_URL } from '@/utils/Url';
|
||||||
|
|
||||||
export default function CreateSubscriptionPage() {
|
export default function CreateSubscriptionPage() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|||||||
@ -24,16 +24,18 @@ export const editStudentCompetencies = (data, csrfToken) => {
|
|||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchStudentCompetencies = (id) => {
|
export const fetchStudentCompetencies = (id, period) => {
|
||||||
const request = new Request(
|
// Si period est vide, ne pas l'ajouter à l'URL
|
||||||
`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`,
|
const url = period
|
||||||
{
|
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
|
||||||
method: 'GET',
|
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
const request = new Request(url, {
|
||||||
},
|
method: 'GET',
|
||||||
}
|
headers: {
|
||||||
);
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const typeStyles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function FlashNotification({
|
export default function FlashNotification({
|
||||||
displayPeriod = 3000,
|
displayPeriod = 5000,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type = 'info',
|
type = 'info',
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function GradesDomainBarChart({ studentCompetencies }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-3/4 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="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) => (
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { BASE_URL } from '@/utils/Url';
|
|
||||||
|
|
||||||
export default function StudentInput({
|
|
||||||
label,
|
|
||||||
selectedStudent,
|
|
||||||
setSelectedStudent,
|
|
||||||
searchStudents,
|
|
||||||
establishmentId,
|
|
||||||
required = false,
|
|
||||||
}) {
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [suggestions, setSuggestions] = useState([]);
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
||||||
|
|
||||||
// Désélectionner si l'input ne correspond plus à l'élève sélectionné
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
selectedStudent &&
|
|
||||||
inputValue !==
|
|
||||||
`${selectedStudent.last_name} ${selectedStudent.first_name}`
|
|
||||||
) {
|
|
||||||
setSelectedStudent(null);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [inputValue]);
|
|
||||||
|
|
||||||
const handleInputChange = async (e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setInputValue(value);
|
|
||||||
|
|
||||||
if (value.trim() !== '') {
|
|
||||||
try {
|
|
||||||
const results = await searchStudents(establishmentId, value);
|
|
||||||
setSuggestions(results);
|
|
||||||
} catch {
|
|
||||||
setSuggestions([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSuggestions([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSuggestionClick = (student) => {
|
|
||||||
setSelectedStudent(student);
|
|
||||||
setInputValue(`${student.last_name} ${student.first_name}`);
|
|
||||||
setSuggestions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
|
|
||||||
handleSuggestionClick(suggestions[selectedIndex]);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'ArrowDown') {
|
|
||||||
setSelectedIndex((prev) =>
|
|
||||||
prev < suggestions.length - 1 ? prev + 1 : 0
|
|
||||||
);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
setSelectedIndex((prev) =>
|
|
||||||
prev > 0 ? prev - 1 : suggestions.length - 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Rechercher un élève"
|
|
||||||
className="mt-1 px-3 py-2 block w-full border rounded-md"
|
|
||||||
required={required}
|
|
||||||
/>
|
|
||||||
{suggestions.length > 0 && (
|
|
||||||
<ul className="border rounded mt-2 bg-white shadow">
|
|
||||||
{suggestions.map((student, idx) => (
|
|
||||||
<li
|
|
||||||
key={student.id}
|
|
||||||
className={`flex items-center gap-2 p-2 cursor-pointer transition-colors ${
|
|
||||||
idx === selectedIndex
|
|
||||||
? 'bg-emerald-100 text-emerald-800'
|
|
||||||
: 'hover:bg-emerald-50'
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSuggestionClick(student)}
|
|
||||||
onMouseEnter={() => setSelectedIndex(idx)}
|
|
||||||
>
|
|
||||||
{student.photo ? (
|
|
||||||
<img
|
|
||||||
src={`${BASE_URL}${student.photo}`}
|
|
||||||
alt={`${student.first_name} ${student.last_name}`}
|
|
||||||
className="w-8 h-8 object-cover rounded-full border"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-full text-gray-500 font-semibold">
|
|
||||||
{student.first_name?.[0]}
|
|
||||||
{student.last_name?.[0]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
{student.last_name} {student.first_name} ({student.level}) -{' '}
|
|
||||||
{student.associated_class_name}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -12,6 +12,15 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
return storedEstablishmentId;
|
return storedEstablishmentId;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const [
|
||||||
|
selectedEstablishmentEvaluationFrequency,
|
||||||
|
setSelectedEstablishmentEvaluationFrequencyState,
|
||||||
|
] = useState(() => {
|
||||||
|
const storedEstablishmentEvaluationFrequency = +sessionStorage.getItem(
|
||||||
|
'selectedEstablishmentEvaluationFrequency'
|
||||||
|
);
|
||||||
|
return storedEstablishmentEvaluationFrequency;
|
||||||
|
});
|
||||||
const [selectedRoleId, setSelectedRoleIdState] = useState(() => {
|
const [selectedRoleId, setSelectedRoleIdState] = useState(() => {
|
||||||
const storedRoleId = +sessionStorage.getItem('selectedRoleId');
|
const storedRoleId = +sessionStorage.getItem('selectedRoleId');
|
||||||
return storedRoleId;
|
return storedRoleId;
|
||||||
@ -36,6 +45,12 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
sessionStorage.setItem('selectedEstablishmentId', id);
|
sessionStorage.setItem('selectedEstablishmentId', id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectedEstablishmentEvaluationFrequency = (id) => {
|
||||||
|
setSelectedEstablishmentEvaluationFrequencyState(id);
|
||||||
|
logger.debug('setSelectedEstablishmentEvaluationFrequency', id);
|
||||||
|
sessionStorage.setItem('selectedEstablishmentEvaluationFrequency', id);
|
||||||
|
};
|
||||||
|
|
||||||
const setSelectedRoleId = (id) => {
|
const setSelectedRoleId = (id) => {
|
||||||
setSelectedRoleIdState(id);
|
setSelectedRoleIdState(id);
|
||||||
sessionStorage.setItem('selectedRoleId', id);
|
sessionStorage.setItem('selectedRoleId', id);
|
||||||
@ -72,6 +87,7 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
const userEstablishments = user.roles.map((role, i) => ({
|
const userEstablishments = user.roles.map((role, i) => ({
|
||||||
id: role.establishment__id,
|
id: role.establishment__id,
|
||||||
name: role.establishment__name,
|
name: role.establishment__name,
|
||||||
|
evaluation_frequency: role.establishment__evaluation_frequency,
|
||||||
role_id: i,
|
role_id: i,
|
||||||
role_type: role.role_type,
|
role_type: role.role_type,
|
||||||
}));
|
}));
|
||||||
@ -85,6 +101,9 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
setSelectedRoleId(roleIndexDefault);
|
setSelectedRoleId(roleIndexDefault);
|
||||||
if (userEstablishments.length > 0) {
|
if (userEstablishments.length > 0) {
|
||||||
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
|
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
|
||||||
|
setSelectedEstablishmentEvaluationFrequency(
|
||||||
|
userEstablishments[roleIndexDefault].evaluation_frequency
|
||||||
|
);
|
||||||
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
||||||
}
|
}
|
||||||
if (endInitFunctionHandler) {
|
if (endInitFunctionHandler) {
|
||||||
@ -112,6 +131,8 @@ export const EstablishmentProvider = ({ children }) => {
|
|||||||
clearContext,
|
clearContext,
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
setSelectedEstablishmentId,
|
setSelectedEstablishmentId,
|
||||||
|
selectedEstablishmentEvaluationFrequency,
|
||||||
|
setSelectedEstablishmentEvaluationFrequency,
|
||||||
selectedRoleId,
|
selectedRoleId,
|
||||||
setSelectedRoleId,
|
setSelectedRoleId,
|
||||||
profileRole,
|
profileRole,
|
||||||
|
|||||||
Reference in New Issue
Block a user