From 6bd5704983282264bc50c73677495740f7d7e8a9 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 14 Mar 2025 19:51:35 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Cr=C3=A9ation=20d'un=20annuaire=20/=20m?= =?UTF-8?q?ise=20=C3=A0=20jour=20du=20subscribe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/serializers.py | 46 +++- Back-End/Auth/views.py | 46 ++-- .../management/commands/init_mock_datas.py | 40 ++-- Back-End/School/serializers.py | 6 +- Back-End/Subscriptions/mailManager.py | 5 +- .../templates/emails/inscription.html | 2 +- .../views/register_form_views.py | 4 +- Front-End/messages/en/sidebar.json | 1 + Front-End/messages/fr/sidebar.json | 1 + .../src/app/[locale]/admin/directory/page.js | 66 ++++++ Front-End/src/app/[locale]/admin/layout.js | 17 +- .../src/app/[locale]/admin/structure/page.js | 34 +-- .../app/[locale]/admin/subscriptions/page.js | 4 +- .../src/app/[locale]/users/subscribe/page.js | 74 ++---- Front-End/src/app/actions/authAction.js | 36 +++ Front-End/src/components/CheckBoxList.js | 2 +- .../components/Inscription/InscriptionForm.js | 18 ++ Front-End/src/components/ProfileDirectory.js | 218 ++++++++++++++++++ Front-End/src/components/Providers.js | 18 +- .../Configuration/TeachersSection.js | 127 ++++++---- Front-End/src/utils/Telephone.js | 1 + Front-End/src/utils/Url.js | 4 + 22 files changed, 585 insertions(+), 185 deletions(-) create mode 100644 Front-End/src/app/[locale]/admin/directory/page.js create mode 100644 Front-End/src/components/ProfileDirectory.js diff --git a/Back-End/Auth/serializers.py b/Back-End/Auth/serializers.py index 7c11b85..98686d2 100644 --- a/Back-End/Auth/serializers.py +++ b/Back-End/Auth/serializers.py @@ -1,6 +1,8 @@ from rest_framework import serializers from Auth.models import Profile, ProfileRole from Establishment.models import Establishment +from Subscriptions.models import Guardian, RegistrationForm +from School.models import Teacher class ProfileSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) @@ -14,7 +16,7 @@ class ProfileSerializer(serializers.ModelSerializer): def get_roles(self, obj): roles = ProfileRole.objects.filter(profile=obj) - return [{'role_type': role.role_type, 'establishment': role.establishment.id, 'is_active': role.is_active} for role in roles] + return [{'role_type': role.role_type, 'establishment': role.establishment.id, 'establishment_name': role.establishment.name, 'is_active': role.is_active} for role in roles] def create(self, validated_data): user = Profile( @@ -48,10 +50,12 @@ class ProfileSerializer(serializers.ModelSerializer): class ProfileRoleSerializer(serializers.ModelSerializer): profile = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), required=False) profile_data = ProfileSerializer(write_only=True, required=False) + associated_profile_email = serializers.SerializerMethodField() + associated_person = serializers.SerializerMethodField() class Meta: model = ProfileRole - fields = ['role_type', 'establishment', 'is_active', 'profile', 'profile_data'] + fields = ['id', 'role_type', 'establishment', 'is_active', 'profile', 'profile_data', 'associated_profile_email', 'associated_person'] def create(self, validated_data): profile_data = validated_data.pop('profile_data', None) @@ -82,4 +86,40 @@ class ProfileRoleSerializer(serializers.ModelSerializer): instance.establishment_id = validated_data.get('establishment', instance.establishment.id) instance.is_active = validated_data.get('is_active', instance.is_active) instance.save() - return instance \ No newline at end of file + return instance + + def get_associated_profile_email(self, obj): + if obj.profile: + return obj.profile.email + return None + + def get_associated_person(self, obj): + if obj.role_type == ProfileRole.RoleType.PROFIL_PARENT: + guardian = Guardian.objects.filter(profile_role=obj).first() + if guardian: + students = guardian.student_set.all() + students_list = [] + for student in students: + registration_form = RegistrationForm.objects.filter(student=student).first() + registration_status = registration_form.status if registration_form else None + students_list.append({ + "student_name": f"{student.last_name} {student.first_name}", + "registration_status": registration_status + }) + return { + "guardian_name": f"{guardian.last_name} {guardian.first_name}", + "students": students_list + } + else: + teacher = Teacher.objects.filter(profile_role=obj).first() + if teacher: + classes = teacher.schoolclass_set.all() + classes_list = [{"id": classe.id, "name": classe.atmosphere_name} for classe in classes] + specialities = teacher.specialities.all() + specialities_list = [{"name": speciality.name, "color_code": speciality.color_code} for speciality in specialities] + return { + "teacher_name": f"{teacher.last_name} {teacher.first_name}", + "classes": classes_list, + "specialities": specialities_list + } + return None \ No newline at end of file diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index 0f4d669..079f765 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -200,7 +200,7 @@ class LoginView(APIView): primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first() if not primary_role: - return JsonResponse({"errorMessage": "Role not assigned to the user"}, status=status.HTTP_401_UNAUTHORIZED) + return JsonResponse({"errorMessage": "Profil inactif"}, status=status.HTTP_401_UNAUTHORIZED) login(request, user) user.save() @@ -305,10 +305,10 @@ class RefreshJWTView(APIView): role_type = payload.get('role_type') # Récupérer le rôle principal de l'utilisateur - primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type).first() + primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first() if not primary_role: - return JsonResponse({'errorMessage': 'No role assigned to the user'}, status=400) + return JsonResponse({'errorMessage': 'Profil inactif'}, status=400) # Générer un nouveau Access Token avec les informations complètes new_access_payload = { @@ -383,10 +383,7 @@ class SubscribeView(APIView): retourErreur = error.returnMessage[error.BAD_URL] retour = '' newProfilConnection = JSONParser().parse(request) - establishment_id = request.GET.get('establishment_id') - - if not establishment_id: - return JsonResponse({'message': retour, 'errorMessage': 'establishment_id manquant', "errorFields": {}, "id": -1}, safe=False, status=status.HTTP_400_BAD_REQUEST) + establishment_id = newProfilConnection['establishment_id'] validatorSubscription = validator.ValidatorSubscription(data=newProfilConnection) validationOk, errorFields = validatorSubscription.validate() @@ -398,7 +395,7 @@ class SubscribeView(APIView): retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS] else: # Vérifier si le profil a déjà un rôle actif pour l'établissement donné - active_roles = ProfileRole.objects.filter(profile=profil, establishment_id=establishment_id, is_active=True) + active_roles = ProfileRole.objects.filter(profile=profil, establishment=establishment_id, is_active=True) if active_roles.exists(): retourErreur = error.returnMessage[error.PROFIL_ACTIVE] return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False) @@ -408,18 +405,23 @@ class SubscribeView(APIView): profil.full_clean() profil.save() - # Utiliser le sérialiseur ProfileRoleSerializer pour créer ou mettre à jour le rôle - role_data = { - 'profile': profil.id, - 'establishment_id': establishment_id, - 'role_type': ProfileRole.RoleType.PROFIL_PARENT, - 'is_active': True - } - role_serializer = ProfileRoleSerializer(data=role_data) - if role_serializer.is_valid(): - role_serializer.save() + # Récupérer le ProfileRole existant pour l'établissement et le profil + profile_role = ProfileRole.objects.filter(profile=profil, establishment=establishment_id).first() + if profile_role: + profile_role.is_active = True + profile_role.save() else: - return JsonResponse(role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + # Si aucun ProfileRole n'existe, en créer un nouveau + role_data = { + 'profile': profil.id, + 'establishment': establishment_id, + 'is_active': True + } + role_serializer = ProfileRoleSerializer(data=role_data) + if role_serializer.is_valid(): + role_serializer.save() + else: + return JsonResponse(role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) clear_cache() retour = error.returnMessage[error.MESSAGE_ACTIVATION_PROFILE] @@ -536,7 +538,13 @@ class ProfileRoleView(APIView): responses={200: ProfileRoleSerializer(many=True)} ) def get(self, request): + establishment_id = request.GET.get('establishment_id', None) + if establishment_id is None: + return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole) + if profiles_roles_List: + profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct() profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True) return JsonResponse(profile_roles_serializer.data, safe=False) diff --git a/Back-End/School/management/commands/init_mock_datas.py b/Back-End/School/management/commands/init_mock_datas.py index 77c5a07..b570cc2 100644 --- a/Back-End/School/management/commands/init_mock_datas.py +++ b/Back-End/School/management/commands/init_mock_datas.py @@ -103,20 +103,26 @@ class Command(BaseCommand): # Créer entre 1 et 3 ProfileRole pour chaque profil num_roles = random.randint(1, 3) + created_roles = set() for _ in range(num_roles): establishment = random.choice(self.establishments) role_type = random.choice([ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN, ProfileRole.RoleType.PROFIL_PARENT]) + # Vérifier si le rôle existe déjà pour cet établissement + if (establishment.id, role_type) in created_roles: + continue + profile_role_data = { "profile": profile.id, "establishment": establishment.id, "role_type": role_type, - "is_active": True + "is_active": random.choice([True, False]) } profile_role_serializer = ProfileRoleSerializer(data=profile_role_data) if profile_role_serializer.is_valid(): profile_role_serializer.save() + created_roles.add((establishment.id, role_type)) self.stdout.write(self.style.SUCCESS(f'ProfileRole for {profile.email} created successfully with role type {role_type}')) else: self.stdout.write(self.style.ERROR(f'Error in data for profile role: {profile_role_serializer.errors}')) @@ -129,7 +135,7 @@ class Command(BaseCommand): for fee_data in fees_data: establishment = random.choice(self.establishments) print(f'establishment : {establishment}') - fee_data["name"] = f"{fee_data['name']} - {establishment.name}" + fee_data["name"] = fee_data['name'] fee_data["establishment"] = establishment.id fee_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]) @@ -145,7 +151,7 @@ class Command(BaseCommand): for discount_data in discounts_data: establishment = random.choice(self.establishments) - discount_data["name"] = f"{discount_data['name']} - {establishment.name}" + discount_data["name"] = discount_data['name'] discount_data["establishment"] = establishment.id discount_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]) discount_data["discount_type"] = random.choice([DiscountType.CURRENCY, DiscountType.PERCENT]) @@ -216,7 +222,7 @@ class Command(BaseCommand): for speciality_data in specialities_data: establishment = random.choice(self.establishments) - speciality_data["name"] = f"{speciality_data['name']} - {establishment.name}" + speciality_data["name"] = speciality_data['name'] speciality_data["establishment"] = establishment.id serializer = SpecialitySerializer(data=speciality_data) @@ -260,7 +266,7 @@ class Command(BaseCommand): # Générer des données fictives pour l'enseignant teacher_data = { "last_name": fake.last_name(), - "first_name": f"{fake.first_name()} - {profile_role.establishment.name}", + "first_name": fake.first_name(), "profile_role": profile_role.id } @@ -287,7 +293,7 @@ class Command(BaseCommand): for index, class_data in enumerate(school_classes_data, start=1): # Randomize establishment establishment = random.choice(self.establishments) - class_data["atmosphere_name"] = f"Classe {index} - {establishment.name}" + class_data["atmosphere_name"] = f"Classe {index}" class_data["establishment"] = establishment.id # Randomize levels @@ -295,9 +301,12 @@ class Command(BaseCommand): # Randomize teachers establishment_teachers = list(Teacher.objects.filter(profile_role__establishment=establishment)) - num_teachers = min(random.randint(1, 10), len(establishment_teachers)) - selected_teachers = random.sample(establishment_teachers, num_teachers) - teachers_ids = [teacher.id for teacher in selected_teachers] + if len(establishment_teachers) > 0: + num_teachers = min(2, len(establishment_teachers)) + selected_teachers = random.sample(establishment_teachers, num_teachers) + teachers_ids = [teacher.id for teacher in selected_teachers] + else: + teachers_ids = [] # Use the serializer to create or update the school class class_data["teachers"] = teachers_ids @@ -315,7 +324,7 @@ class Command(BaseCommand): for establishment in self.establishments: for i in range(1, 4): # Créer 3 groupes de fichiers par établissement - name = f"Fichiers d'inscription - {fake.word()} - {establishment.name}" + name = f"Fichiers d'inscription - {fake.word()}" description = fake.sentence() group_data = { "name": name, @@ -342,8 +351,6 @@ class Command(BaseCommand): used_profiles = set() for _ in range(50): - establishment = random.choice(self.establishments) - # Récupérer un profil aléatoire qui n'a pas encore été utilisé available_profiles = profiles_with_parent_role.exclude(id__in=used_profiles) if not available_profiles.exists(): @@ -354,7 +361,8 @@ class Command(BaseCommand): used_profiles.add(profile.id) # Récupérer le ProfileRole Parent associé au profil - profile_role = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT).first() + profile_roles = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT) + profile_role = random.choice(profile_roles) # Générer des données fictives pour le guardian guardian_data = { @@ -370,7 +378,7 @@ class Command(BaseCommand): # Générer des données fictives pour l'étudiant student_data = { "last_name": fake.last_name(), - "first_name": f"{fake.first_name()} - {establishment.name}", + "first_name": fake.first_name(), "address": fake.address(), "birth_date": fake.date_of_birth(), "birth_place": fake.city(), @@ -398,14 +406,14 @@ class Command(BaseCommand): # Créer les données du formulaire d'inscription register_form_data = { "fileGroup": RegistrationFileGroup.objects.get(id=fake.random_int(min=1, max=file_group_count)), - "establishment": establishment, + "establishment": profile_role.establishment, "status": fake.random_int(min=1, max=3) } # Créer ou mettre à jour le formulaire d'inscription register_form, created = RegistrationForm.objects.get_or_create( student=student, - establishment=establishment, + establishment=profile_role.establishment, defaults=register_form_data ) diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index fb26e21..5e31270 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -49,7 +49,9 @@ class TeacherSerializer(serializers.ModelSerializer): if profile_role_data: establishment_id = profile_role_data.pop('establishment').id + profile_id = profile_role_data.pop('profile').id profile_role_data['establishment'] = establishment_id + profile_role_data['profile'] = profile_id # Créer l'instance de ProfileRole profile_role_serializer = ProfileRoleSerializer(data=profile_role_data) @@ -95,7 +97,9 @@ class TeacherSerializer(serializers.ModelSerializer): def get_role_type(self, obj): profile_role = obj.profile_role - return {'role_type': profile_role.role_type, 'establishment': profile_role.establishment.name} + if profile_role: + return {'role_type': profile_role.role_type, 'establishment': profile_role.establishment.name} + return None def get_specialities_details(self, obj): return [{'id': speciality.id, 'name': speciality.name, 'color_code': speciality.color_code} for speciality in obj.specialities.all()] diff --git a/Back-End/Subscriptions/mailManager.py b/Back-End/Subscriptions/mailManager.py index b783d0c..26342e3 100644 --- a/Back-End/Subscriptions/mailManager.py +++ b/Back-End/Subscriptions/mailManager.py @@ -23,7 +23,7 @@ def envoieReinitMotDePasse(recipients, code): return errorMessage -def sendRegisterForm(recipients): +def sendRegisterForm(recipients, establishment_id): errorMessage = '' try: print(f'{settings.EMAIL_HOST_USER}') @@ -31,7 +31,8 @@ def sendRegisterForm(recipients): EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription' context = { 'BASE_URL': settings.BASE_URL, - 'email': recipients + 'email': recipients, + 'establishment': establishment_id } subject = EMAIL_INSCRIPTION_SUBJECT diff --git a/Back-End/Subscriptions/templates/emails/inscription.html b/Back-End/Subscriptions/templates/emails/inscription.html index e0daed5..fdd4cdd 100644 --- a/Back-End/Subscriptions/templates/emails/inscription.html +++ b/Back-End/Subscriptions/templates/emails/inscription.html @@ -38,7 +38,7 @@

Bonjour,

Nous vous confirmons la réception de votre demande d'inscription, vous trouverez ci-joint le lien vers la page d'authentification : {{BASE_URL}}/users/login

-

S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : {{BASE_URL}}/users/subscribe

+

S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : {{BASE_URL}}/users/subscribe

votre identifiant est : {{ email }}

Merci de compléter votre dossier d'inscription en suivant les instructions fournies.

Cordialement,

diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index f47ea89..ce72e85 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -332,8 +332,8 @@ def send(request,id): if register_form != None: student = register_form.student guardian = student.getMainGuardian() - email = guardian.email - errorMessage = mailer.sendRegisterForm(email) + email = guardian.profile_role.profile.email + errorMessage = mailer.sendRegisterForm(email, register_form.establishment.pk) if errorMessage == '': register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') updateStateMachine(register_form, 'envoiDI') diff --git a/Front-End/messages/en/sidebar.json b/Front-End/messages/en/sidebar.json index 92a6836..2fce791 100644 --- a/Front-End/messages/en/sidebar.json +++ b/Front-End/messages/en/sidebar.json @@ -2,6 +2,7 @@ "dashboard": "Dashboard", "subscriptions": "Subscriptions", "structure": "Structure", + "directory": "Directory", "events": "Events", "grades": "Grades", "settings": "Settings", diff --git a/Front-End/messages/fr/sidebar.json b/Front-End/messages/fr/sidebar.json index 20d351f..5be92eb 100644 --- a/Front-End/messages/fr/sidebar.json +++ b/Front-End/messages/fr/sidebar.json @@ -2,6 +2,7 @@ "dashboard": "Tableau de bord", "subscriptions": "Inscriptions", "structure": "Structure", + "directory": "Annuaire", "events": "Evenements", "grades": "Notes", "settings": "Paramètres", diff --git a/Front-End/src/app/[locale]/admin/directory/page.js b/Front-End/src/app/[locale]/admin/directory/page.js new file mode 100644 index 0000000..9b3b51d --- /dev/null +++ b/Front-End/src/app/[locale]/admin/directory/page.js @@ -0,0 +1,66 @@ +'use client' +import React, { useState, useEffect } from 'react'; +import { fetchProfileRoles, updateProfileRoles, deleteProfileRoles } from '@/app/actions/authAction'; +import logger from '@/utils/logger'; +import { useEstablishment } from '@/context/EstablishmentContext'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken'; +import { useCsrfToken } from '@/context/CsrfContext'; +import ProfileDirectory from '@/components/ProfileDirectory'; +import { BE_AUTH_PROFILES_ROLES_URL } from '@/utils/Url'; + +export default function Page() { + const [profileRoles, setProfileRoles] = useState([]); + + const csrfToken = useCsrfToken(); + const { selectedEstablishmentId } = useEstablishment(); + + useEffect(() => { + if (selectedEstablishmentId) { + // Fetch data for profileRoles + handleProfiles(); + } + }, [selectedEstablishmentId]); + + const handleProfiles = () => { + fetchProfileRoles(selectedEstablishmentId) + .then(data => { + setProfileRoles(data); + }) + .catch(error => logger.error('Error fetching profileRoles:', error)); + }; + + const handleEdit = (profileRole) => { + const updatedData = { ...profileRole, is_active: !profileRole.is_active }; + return updateProfileRoles(profileRole.id, updatedData, csrfToken) + .then(data => { + setProfileRoles(prevState => prevState.map(item => item.id === profileRole.id ? data : item)); + return data; + }) + .catch(error => { + logger.error('Error editing data:', error); + throw error; + }); + }; + + const handleDelete = (id) => { + return deleteProfileRoles(id, csrfToken) + .then(() => { + setProfileRoles(prevState => prevState.filter(item => item.id !== id)); + logger.debug("Profile deleted successfully:", id); + }) + .catch(error => { + logger.error('Error deleting profile:', error); + throw error; + }); + }; + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index 35196f7..79cdabd 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -5,12 +5,13 @@ import { usePathname } from 'next/navigation'; import { useTranslations } from 'next-intl'; import Image from 'next/image'; import { + LayoutDashboard, + FileText, + School, Users, - Building, - Home, + Award, Calendar, Settings, - FileText, LogOut, Menu, X @@ -22,6 +23,7 @@ import { FE_ADMIN_HOME_URL, FE_ADMIN_SUBSCRIPTIONS_URL, FE_ADMIN_STRUCTURE_URL, + FE_ADMIN_DIRECTORY_URL, FE_ADMIN_GRADES_URL, FE_ADMIN_PLANNING_URL, FE_ADMIN_SETTINGS_URL @@ -43,10 +45,11 @@ export default function Layout({ const [isSidebarOpen, setIsSidebarOpen] = useState(false); const sidebarItems = { - "admin": { "id": "admin", "name": t('dashboard'), "url": FE_ADMIN_HOME_URL, "icon": Home }, - "subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FE_ADMIN_SUBSCRIPTIONS_URL, "icon": Users }, - "structure": { "id": "structure", "name": t('structure'), "url": FE_ADMIN_STRUCTURE_URL, "icon": Building }, - "grades": { "id": "grades", "name": t('grades'), "url": FE_ADMIN_GRADES_URL, "icon": FileText }, + "admin": { "id": "admin", "name": t('dashboard'), "url": FE_ADMIN_HOME_URL, "icon": LayoutDashboard }, + "subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FE_ADMIN_SUBSCRIPTIONS_URL, "icon": FileText }, + "structure": { "id": "structure", "name": t('structure'), "url": FE_ADMIN_STRUCTURE_URL, "icon": School }, + "directory": { "id": "directory", "name": t('directory'), "url": FE_ADMIN_DIRECTORY_URL, "icon": Users }, + "grades": { "id": "grades", "name": t('grades'), "url": FE_ADMIN_GRADES_URL, "icon": Award }, "planning": { "id": "planning", "name": t('events'), "url": FE_ADMIN_PLANNING_URL, "icon": Calendar }, "settings": { "id": "settings", "name": t('settings'), "url": FE_ADMIN_SETTINGS_URL, "icon": Settings } }; diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 0ee44f7..28663e3 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,26 +6,26 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import { useCsrfToken } from '@/context/CsrfContext'; import { ClassesProvider } from '@/context/ClassesContext'; -import { createDatas, - updateDatas, - removeDatas, - fetchSpecialities, - fetchTeachers, - fetchClasses, - fetchSchedules, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, - fetchTuitionFees, - fetchRegistrationPaymentPlans, - fetchTuitionPaymentPlans, - fetchRegistrationPaymentModes, +import { + createDatas, + updateDatas, + removeDatas, + fetchSpecialities, + fetchTeachers, + fetchClasses, + fetchSchedules, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees, + fetchRegistrationPaymentPlans, + fetchTuitionPaymentPlans, + fetchRegistrationPaymentModes, fetchTuitionPaymentModes } from '@/app/actions/schoolAction'; +import { fetchProfileRoles } from '@/app/actions/authAction'; import SidebarTabs from '@/components/SidebarTabs'; import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement'; -import { - fetchRegistrationTemplateMaster -} from "@/app/actions/registerFileGroupAction"; +import { fetchRegistrationTemplateMaster } from "@/app/actions/registerFileGroupAction"; import logger from '@/utils/logger'; import { useEstablishment } from '@/context/EstablishmentContext'; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 15e9c91..064dc2a 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -453,8 +453,8 @@ useEffect(()=>{ const columns = [ { name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name }, - { name: t('mainContactMail'), transform: (row) => row.student.guardians[0].associated_profile_email }, - { name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0].phone) }, + { name: t('mainContactMail'), transform: (row) => (row.student.guardians && row.student.guardians.length > 0) ? row.student.guardians[0].associated_profile_email : '' }, + { name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0]?.phone) }, { name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update}, { name: t('registrationFileStatus'), transform: (row) => (
diff --git a/Front-End/src/app/[locale]/users/subscribe/page.js b/Front-End/src/app/[locale]/users/subscribe/page.js index c43d978..db7e37e 100644 --- a/Front-End/src/app/[locale]/users/subscribe/page.js +++ b/Front-End/src/app/[locale]/users/subscribe/page.js @@ -13,8 +13,8 @@ import { User, KeySquare } from 'lucide-react'; // Importez directement les icô import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { useCsrfToken } from '@/context/CsrfContext'; import { subscribe } from '@/app/actions/authAction'; -const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; import logger from '@/utils/logger'; +import { useEstablishment } from '@/context/EstablishmentContext'; export default function Page() { const searchParams = useSearchParams(); @@ -29,40 +29,28 @@ export default function Page() { const router = useRouter(); const csrfToken = useCsrfToken(); - - useEffect(() => { - if (useFakeData) { - setIsLoading(true); - // Simuler une réponse réussie - const data = { - errorFields: {}, - errorMessage: "" - }; - setUserFieldError("") - setPassword1FieldError("") - setPassword2FieldError("") - setErrorMessage("") - setIsLoading(false); - } - }, []); + + const establishment_id = searchParams.get('establishment_id'); function isOK(data) { return data.errorMessage === "" } function subscribeFormSubmit(formData) { - if (useFakeData) { - // Simuler une réponse réussie - const data = { - errorFields: {}, - errorMessage: "" - }; + const data ={ + email: formData.get('login'), + password1: formData.get('password1'), + password2: formData.get('password2'), + establishment_id: establishment_id + } + subscribe(data,csrfToken).then(data => { + logger.debug('Success:', data); setUserFieldError("") setPassword1FieldError("") setPassword2FieldError("") setErrorMessage("") if(isOK(data)){ - setPopupMessage("Votre compte a été créé avec succès"); + setPopupMessage(data.message); setPopupVisible(true); } else { if(data.errorMessage){ @@ -74,38 +62,12 @@ export default function Page() { setPassword2FieldError(data.errorFields.password2) } } - } else { - const data ={ - email: formData.get('login'), - password1: formData.get('password1'), - password2: formData.get('password2'), - } - subscribe(data,csrfToken).then(data => { - logger.debug('Success:', data); - setUserFieldError("") - setPassword1FieldError("") - setPassword2FieldError("") - setErrorMessage("") - if(isOK(data)){ - setPopupMessage(data.message); - setPopupVisible(true); - } else { - if(data.errorMessage){ - setErrorMessage(data.errorMessage); - } - if(data.errorFields){ - setUserFieldError(data.errorFields.email) - setPassword1FieldError(data.errorFields.password1) - setPassword2FieldError(data.errorFields.password2) - } - } - }) - .catch(error => { - logger.error('Error fetching data:', error); - error = error.errorMessage; - logger.debug(error); - }); - } + }) + .catch(error => { + logger.error('Error fetching data:', error); + error = error.errorMessage; + logger.debug(error); + }); } if (isLoading === true) { diff --git a/Front-End/src/app/actions/authAction.js b/Front-End/src/app/actions/authAction.js index 42241f0..80a3a6a 100644 --- a/Front-End/src/app/actions/authAction.js +++ b/Front-End/src/app/actions/authAction.js @@ -4,6 +4,7 @@ import { BE_AUTH_REFRESH_JWT_URL, BE_AUTH_REGISTER_URL, BE_AUTH_PROFILES_URL, + BE_AUTH_PROFILES_ROLES_URL, BE_AUTH_RESET_PASSWORD_URL, BE_AUTH_NEW_PASSWORD_URL, FE_USERS_LOGIN_URL, @@ -77,6 +78,41 @@ export const disconnect = () => { signOut({ callbackUrl: FE_USERS_LOGIN_URL }); }; +export const fetchProfileRoles = (establishment) => { + return fetch(`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`) + .then(requestResponseHandler) +}; + +export const updateProfileRoles = (id, data, csrfToken) => { + const request = new Request( + `${BE_AUTH_PROFILES_ROLES_URL}/${id}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include', + body: JSON.stringify(data), + } + ); + return fetch(request).then(requestResponseHandler); +}; + +export const deleteProfileRoles = (id, csrfToken) => { + const request = new Request( + `${BE_AUTH_PROFILES_ROLES_URL}/${id}`, + { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken + }, + credentials: 'include' + } + ); + return fetch(request).then(requestResponseHandler); +}; + export const createProfile = (data, csrfToken) => { const request = new Request( `${BE_AUTH_PROFILES_URL}`, diff --git a/Front-End/src/components/CheckBoxList.js b/Front-End/src/components/CheckBoxList.js index 0207e52..832f9b7 100644 --- a/Front-End/src/components/CheckBoxList.js +++ b/Front-End/src/components/CheckBoxList.js @@ -22,7 +22,7 @@ const CheckBoxList = ({
{items.map(item => ( {formData.responsableType === 'new' && ( <> + + { + switch (roleType) { + case 0: + return 'ECOLE'; + case 1: + return 'ADMIN'; + case 2: + return 'PARENT'; + default: + return 'UNKNOWN'; + } +}; + +const roleTypeToBadgeClass = (roleType) => { + switch (roleType) { + case 0: + return 'bg-blue-100 text-blue-600'; + case 1: + return 'bg-red-100 text-red-600'; + case 2: + return 'bg-green-100 text-green-600'; + default: + return 'bg-gray-100 text-gray-600'; + } +}; + +const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeleteProfile }) => { + const parentProfiles = profileRoles.filter(profileRole => profileRole.role_type === 2); + const schoolAdminProfiles = profileRoles.filter(profileRole => profileRole.role_type !== 2); + + const [popupVisible, setPopupVisible] = useState(false); + const [popupMessage, setPopupMessage] = useState(""); + const [confirmPopupVisible, setConfirmPopupVisible] = useState(false); + const [confirmPopupMessage, setConfirmPopupMessage] = useState(""); + const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {}); + + const handleConfirmActivateProfile = (profileRole) => { + setConfirmPopupMessage(`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`); + setConfirmPopupOnConfirm(() => () => { + handleActivateProfile(profileRole) + .then(() => { + setPopupMessage(`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`); + setPopupVisible(true); + }) + .catch(error => { + setPopupMessage(`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`); + setPopupVisible(true); + }); + setConfirmPopupVisible(false); + }); + setConfirmPopupVisible(true); + }; + + const handleConfirmDeleteProfile = (id) => { + setConfirmPopupMessage("Êtes-vous sûr de vouloir supprimer ce profil ?"); + setConfirmPopupOnConfirm(() => () => { + handleDeleteProfile(id) + .then(() => { + setPopupMessage("Le profil a été supprimé avec succès."); + setPopupVisible(true); + }) + .catch(error => { + setPopupMessage("Erreur lors de la suppression du profil."); + setPopupVisible(true); + }); + setConfirmPopupVisible(false); + }); + setConfirmPopupVisible(true); + }; + + const parentColumns = [ + { name: 'Identifiant', transform: (row) => row.associated_profile_email }, + { name: 'Rôle', transform: (row) => ( + + {roleTypeToLabel(row.role_type)} + + ) + }, + { name: 'Utilisateur', transform: (row) => row.associated_person?.guardian_name }, + { name: 'Elève(s) associé(s)', transform: (row) => ( +
+ {row.associated_person?.students?.map(student => ( + + {student.student_name} + + ))} +
+ ) + }, + { name: 'Etat du dossier d\'inscription', transform: (row) => ( +
+ {row.associated_person?.students?.map(student => ( + + ))} +
+ ) + }, + { + name: 'Actions', + transform: (row) => ( +
+ + +
+ ) + } + ]; + + const schoolAdminColumns = [ + { name: 'Identifiant', transform: (row) => row.associated_profile_email }, + { name: 'Rôle', transform: (row) => ( + + {roleTypeToLabel(row.role_type)} + + ) + }, + { name: 'Utilisateur', transform: (row) => row.associated_person?.teacher_name }, + { name: 'Classe(s) associée(s)', transform: (row) => ( +
+ {row.associated_person?.classes?.map(classe => ( + + {classe.name} + + ))} +
+ ) + }, + { name: 'Spécialités', transform: (row) => ( +
+ {row.associated_person?.specialities?.map(speciality => ( + + ))} +
+ ) + }, + { + name: 'Actions', + transform: (row) => ( +
+ + +
+ ) + } + ]; + + return ( +
+
+
+ {parentProfiles.length === 0 ? ( +
Aucun profil trouvé
+ ) : ( + + )} + +
+ {schoolAdminProfiles.length === 0 ? ( +
Aucun profil trouvé
+ ) : ( +
+ )} + + + setPopupVisible(false)} + uniqueConfirmButton={true} + /> + setConfirmPopupVisible(false)} + /> + + ); +}; + +export default ProfileDirectory; \ No newline at end of file diff --git a/Front-End/src/components/Providers.js b/Front-End/src/components/Providers.js index 8af8f17..9c3bc62 100644 --- a/Front-End/src/components/Providers.js +++ b/Front-End/src/components/Providers.js @@ -4,6 +4,8 @@ import { SessionProvider } from "next-auth/react" import { CsrfProvider } from '@/context/CsrfContext' import { NextIntlClientProvider } from 'next-intl' import { EstablishmentProvider } from '@/context/EstablishmentContext'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; export default function Providers({ children, messages, locale, session }) { @@ -13,13 +15,15 @@ export default function Providers({ children, messages, locale, session }) { } return ( - - - - {children} - - - + + + + + {children} + + + + ) } \ No newline at end of file diff --git a/Front-End/src/components/Structure/Configuration/TeachersSection.js b/Front-End/src/components/Structure/Configuration/TeachersSection.js index f982282..3ec4444 100644 --- a/Front-End/src/components/Structure/Configuration/TeachersSection.js +++ b/Front-End/src/components/Structure/Configuration/TeachersSection.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react'; +import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand, Search } from 'lucide-react'; import Table from '@/components/Table'; import Popup from '@/components/Popup'; import ToggleSwitch from '@/components/ToggleSwitch'; @@ -11,6 +11,8 @@ import InputText from '@/components/InputText'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import TeacherItem from './TeacherItem'; import logger from '@/utils/logger'; +import { fetchProfiles } from '@/app/actions/authAction'; +import { useEstablishment } from '@/context/EstablishmentContext'; const ItemTypes = { SPECIALITY: 'speciality', @@ -103,14 +105,40 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha const [removePopupMessage, setRemovePopupMessage] = useState(""); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); + const [confirmPopupVisible, setConfirmPopupVisible] = useState(false); + const [confirmPopupMessage, setConfirmPopupMessage] = useState(""); + const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {}); + + const { selectedEstablishmentId } = useEstablishment(); + + const handleSelectProfile = (profile) => { + setNewTeacher((prevData) => ({ + ...prevData, + selectedProfile: profile, + })); + setFormData((prevData) => ({ + ...prevData, + selectedProfile: profile + })); + setConfirmPopupMessage(`Vous êtes sur le point de rattacher l'enseignant ${newTeacher?.first_name} ${newTeacher?.last_name} au profil ${profile.email} ID = ${profile.id}.`); + setConfirmPopupOnConfirm(() => () => { + setConfirmPopupVisible(false); + }); + setConfirmPopupVisible(true); + }; + + const handleCancelConfirmation = () => { + setConfirmPopupVisible(false); + }; + // Récupération des messages d'erreur const getError = (field) => { return localErrors?.[field]?.[0]; }; const handleAddTeacher = () => { - setNewTeacher({ id: Date.now(), last_name: '', first_name: '', email: '', specialities: [], droit: 0 }); - setFormData({ last_name: '', first_name: '', email: '', specialities: [], droit: 0 }); + setNewTeacher({ id: Date.now(), last_name: '', first_name: '', selectedProfile: null, specialities: [], droit: 0 }); + setFormData({ last_name: '', first_name: '', selectedProfile: null, specialities: [], droit: 0}); }; const handleRemoveTeacher = (id) => { @@ -124,43 +152,32 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha }; const handleSaveNewTeacher = () => { - if (formData.last_name && formData.first_name && formData.email) { + if (formData.last_name && formData.first_name && formData.selectedProfile) { const data = { - email: formData.email, - password: 'Provisoire01!', - username: formData.email, - is_active: 1, - droit: formData.droit, + last_name: formData.last_name, + first_name: formData.first_name, + profile_role_data: { + role_type: formData.droit, + establishment: selectedEstablishmentId, + is_active: true, + profile: formData.selectedProfile.id + }, + specialities: formData.specialities }; - createProfile(data, csrfToken) - .then(response => { - logger.debug('Success:', response); - if (response.id) { - let idProfil = response.id; - newTeacher.associated_profile = idProfil; - handleCreate(newTeacher) - .then((createdTeacher) => { - setTeachers([createdTeacher, ...teachers]); - setNewTeacher(null); - setLocalErrors({}); - }) - .catch((error) => { - logger.error('Error:', error.message); - if (error.details) { - logger.error('Form errors:', error.details); - setLocalErrors(error.details); - } - }); - } - setLocalErrors({}); - }) - .catch((error) => { - logger.error('Error:', error.message); - if (error.details) { + + handleCreate(data) + .then((createdTeacher) => { + setTeachers([createdTeacher, ...teachers]); + setNewTeacher(null); + setLocalErrors({}); + }) + .catch((error) => { + logger.error('Error:', error.message); + if (error.details) { logger.error('Form errors:', error.details); setLocalErrors(error.details); - } - }); + } + }); } else { setPopupMessage("Tous les champs doivent être remplis et valides"); setPopupVisible(true); @@ -260,7 +277,6 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
); - case 'EMAIL': - return ( - - ); + case 'EMAIL': + return ( +
+ {currentData.selectedProfile ? ( + + {currentData.selectedProfile.email} + + ) : ( + + Rechercher un profil existant + + )} + +
+ ); case 'SPECIALITES': return ( @@ -339,9 +364,9 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha ); case 'ADMINISTRATEUR': - if (teacher.associated_profile) { - const badgeClass = teacher.droit === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'; - const label = teacher.droit === 1 ? 'OUI' : 'NON'; + if (teacher.associated_profile_email) { + const badgeClass = teacher.role_type.role_type === 1 ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'; + const label = teacher.role_type.role_type === 1 ? 'OUI' : 'NON'; return (
diff --git a/Front-End/src/utils/Telephone.js b/Front-End/src/utils/Telephone.js index 7fefca3..aafa2e0 100644 --- a/Front-End/src/utils/Telephone.js +++ b/Front-End/src/utils/Telephone.js @@ -6,6 +6,7 @@ const localePrefixes = { export function formatPhoneNumber(phoneString, fromFormat = 'XX-XX-XX-XX-XX', toFormat = 'LX-XX-XX-XX-XX', locale = "fr-FR") { + if (!phoneString) return; // Extraire les chiffres du numéro de téléphone const digits = phoneString.replace(/\D/g, ''); diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 29e0002..0b9c691 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -18,6 +18,7 @@ export const BE_AUTH_LOGIN_URL = `${BASE_URL}/Auth/login` export const BE_AUTH_REFRESH_JWT_URL = `${BASE_URL}/Auth/refreshJWT` export const BE_AUTH_LOGOUT_URL = `${BASE_URL}/Auth/logout` export const BE_AUTH_PROFILES_URL = `${BASE_URL}/Auth/profiles` +export const BE_AUTH_PROFILES_ROLES_URL = `${BASE_URL}/Auth/profileRoles` export const BE_AUTH_CSRF_URL = `${BASE_URL}/Auth/csrf` export const BE_AUTH_INFO_SESSION = `${BASE_URL}/Auth/infoSession` @@ -79,6 +80,9 @@ export const FE_ADMIN_CLASSES_URL = `/admin/classes` //ADMIN/STRUCTURE URL export const FE_ADMIN_STRUCTURE_URL = `/admin/structure` +//ADMIN/DIRECTORY URL +export const FE_ADMIN_DIRECTORY_URL = `/admin/directory` + //ADMIN/GRADES URL export const FE_ADMIN_GRADES_URL = `/admin/grades`