From 760ee0009e983776dfd500df7465ae66593dc85d Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Tue, 6 May 2025 19:54:46 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Cr=C3=A9ation=20nouveau=20style=20/=20p?= =?UTF-8?q?agination=20profils=20annuaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/pagination.py | 20 ++ Back-End/Auth/views.py | 49 +++- Back-End/N3wtSchool/settings.py | 7 +- Back-End/School/models.py | 2 +- Back-End/Subscriptions/models.py | 2 +- Back-End/Subscriptions/pagination.py | 4 +- Back-End/Subscriptions/serializers.py | 12 +- .../views/register_form_views.py | 8 +- .../src/app/[locale]/admin/directory/page.js | 121 ++------- Front-End/src/app/[locale]/admin/page.js | 17 +- .../subscriptions/createSubscription/page.js | 4 - .../app/[locale]/admin/subscriptions/page.js | 62 ++--- Front-End/src/app/actions/authAction.js | 21 +- .../src/app/actions/subscriptionAction.js | 4 +- .../src/components/PaymentModeSelector.js | 4 +- Front-End/src/components/ProfileDirectory.js | 242 +++++++++++++++--- Front-End/src/components/Sidebar.js | 8 +- Front-End/src/components/SidebarTabs.js | 42 +-- Front-End/src/components/StatCard.js | 2 +- .../Configuration/StructureManagement.js | 2 +- .../Structure/Files/FilesGroupsManagement.js | 2 +- .../Tarification/DiscountsSection.js | 2 +- .../Structure/Tarification/FeesManagement.js | 2 +- Front-End/src/components/Table.js | 29 ++- Front-End/src/utils/constants.js | 9 +- 25 files changed, 430 insertions(+), 247 deletions(-) create mode 100644 Back-End/Auth/pagination.py diff --git a/Back-End/Auth/pagination.py b/Back-End/Auth/pagination.py new file mode 100644 index 0000000..23ef561 --- /dev/null +++ b/Back-End/Auth/pagination.py @@ -0,0 +1,20 @@ +from rest_framework.pagination import PageNumberPagination + +from N3wtSchool import settings + +class CustomProfilesPagination(PageNumberPagination): + page_size_query_param = 'page_size' + max_page_size = settings.NB_MAX_PAGE + page_size = settings.NB_RESULT_PROFILES_PER_PAGE + + def get_paginated_response(self, data): + return ({ + 'links': { + 'next': self.get_next_link(), + 'previous': self.get_previous_link() + }, + 'count': self.page.paginator.count, + 'page_size': self.page_size, + 'max_page_size' : self.max_page_size, + 'profilesRoles': data } + ) \ No newline at end of file diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index d322df2..f1730cf 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -8,6 +8,7 @@ from django.middleware.csrf import get_token from rest_framework.views import APIView from rest_framework.parsers import JSONParser from rest_framework import status +from Auth.pagination import CustomProfilesPagination from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi @@ -20,13 +21,14 @@ import json from . import validator from .models import Profile, ProfileRole from rest_framework.decorators import action, api_view +from django.db.models import Q from Auth.serializers import ProfileSerializer, ProfileRoleSerializer from Subscriptions.models import RegistrationForm, Guardian import Subscriptions.mailManager as mailer import Subscriptions.util as util import logging -from N3wtSchool import bdd, error +from N3wtSchool import bdd, error, settings from rest_framework_simplejwt.authentication import JWTAuthentication @@ -509,20 +511,55 @@ class ResetPasswordView(APIView): return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False) class ProfileRoleView(APIView): + pagination_class = CustomProfilesPagination @swagger_auto_schema( operation_description="Obtenir la liste des profile_roles", responses={200: ProfileRoleSerializer(many=True)} ) def get(self, request): + filter = request.GET.get('filter', '').strip() + page_size = request.GET.get('page_size', None) establishment_id = request.GET.get('establishment_id', None) + + # Gestion du page_size + if page_size is not None: + try: + page_size = int(page_size) + except ValueError: + page_size = settings.NB_RESULT_PROFILES_PER_PAGE + 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().order_by('-updated_date') - profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True) - return JsonResponse(profile_roles_serializer.data, safe=False) + # Récupérer les ProfileRole en fonction du filtre + profiles_roles_List = ProfileRole.objects.filter(establishment_id=establishment_id) + + if filter == 'parents': + profiles_roles_List = profiles_roles_List.filter(role_type=ProfileRole.RoleType.PROFIL_PARENT) + elif filter == 'school': + profiles_roles_List = profiles_roles_List.filter( + Q(role_type=ProfileRole.RoleType.PROFIL_ECOLE) | + Q(role_type=ProfileRole.RoleType.PROFIL_ADMIN) + ) + else: + return JsonResponse({'error': 'Filtre invalide'}, safe=False, status=status.HTTP_400_BAD_REQUEST) + + # Trier les résultats par date de mise à jour + profiles_roles_List = profiles_roles_List.distinct().order_by('-updated_date') + + if not profiles_roles_List: + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) + + # Pagination + paginator = self.pagination_class() + page = paginator.paginate_queryset(profiles_roles_List, request) + + if page is not None: + profile_roles_serializer = ProfileRoleSerializer(page, many=True) + response_data = paginator.get_paginated_response(profile_roles_serializer.data) + return JsonResponse(response_data, safe=False) + + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) @swagger_auto_schema( operation_description="Créer un nouveau profile_role", diff --git a/Back-End/N3wtSchool/settings.py b/Back-End/N3wtSchool/settings.py index ab74235..14ce667 100644 --- a/Back-End/N3wtSchool/settings.py +++ b/Back-End/N3wtSchool/settings.py @@ -282,12 +282,13 @@ DATE_FORMAT = '%d-%m-%Y %H:%M' EXPIRATION_SESSION_NB_SEC = 10 -NB_RESULT_PER_PAGE = 8 +NB_RESULT_SUBSCRIPTIONS_PER_PAGE = 8 +NB_RESULT_PROFILES_PER_PAGE = 15 NB_MAX_PAGE = 100 REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomPagination', - 'PAGE_SIZE': NB_RESULT_PER_PAGE, + 'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination', + 'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 0ecfeb7..9120112 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -32,7 +32,7 @@ class Teacher(models.Model): last_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100) specialities = models.ManyToManyField(Speciality, blank=True) - profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='teacher_profile', blank=True) + profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True) updated_date = models.DateTimeField(auto_now=True) def __str__(self): diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index daf2b24..c80285c 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -31,7 +31,7 @@ class Guardian(models.Model): address = models.CharField(max_length=200, default="", blank=True) phone = models.CharField(max_length=200, default="", blank=True) profession = models.CharField(max_length=200, default="", blank=True) - profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', blank=True) + profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', null=True, blank=True) @property def email(self): diff --git a/Back-End/Subscriptions/pagination.py b/Back-End/Subscriptions/pagination.py index f2a97dc..bb52e88 100644 --- a/Back-End/Subscriptions/pagination.py +++ b/Back-End/Subscriptions/pagination.py @@ -2,10 +2,10 @@ from rest_framework.pagination import PageNumberPagination from N3wtSchool import settings -class CustomPagination(PageNumberPagination): +class CustomSubscriptionPagination(PageNumberPagination): page_size_query_param = 'page_size' max_page_size = settings.NB_MAX_PAGE - page_size = settings.NB_RESULT_PER_PAGE + page_size = settings.NB_RESULT_SUBSCRIPTIONS_PER_PAGE def get_paginated_response(self, data): return ({ diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index ff37c71..a0f6134 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -137,9 +137,19 @@ class StudentSerializer(serializers.ModelSerializer): def create_or_update_guardians(self, guardians_data): guardians_ids = [] for guardian_data in guardians_data: + guardian_id = guardian_data.get('id', None) profile_role_data = guardian_data.pop('profile_role_data', None) profile_role = guardian_data.pop('profile_role', None) + if guardian_id: + # Si un ID est fourni, récupérer ou mettre à jour le Guardian existant + guardian_instance, created = Guardian.objects.update_or_create( + id=guardian_id, + defaults=guardian_data + ) + guardians_ids.append(guardian_instance.id) + continue + if profile_role_data: # Vérifiez si 'profile_data' est fourni pour créer un nouveau profil profile_data = profile_role_data.pop('profile_data', None) @@ -410,4 +420,4 @@ class NotificationSerializer(serializers.ModelSerializer): class Meta: model = Notification fields = '__all__' - + diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py index 18db66a..d6a4ddf 100644 --- a/Back-End/Subscriptions/views/register_form_views.py +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -15,7 +15,7 @@ import Subscriptions.mailManager as mailer import Subscriptions.util as util from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer -from Subscriptions.pagination import CustomPagination +from Subscriptions.pagination import CustomSubscriptionPagination from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationSchoolFileTemplate, RegistrationFileGroup, RegistrationParentFileTemplate from Subscriptions.automate import updateStateMachine @@ -29,7 +29,7 @@ class RegisterFormView(APIView): """ Gère la liste des dossiers d’inscription, lecture et création. """ - pagination_class = CustomPagination + pagination_class = CustomSubscriptionPagination @swagger_auto_schema( manual_parameters=[ @@ -82,7 +82,7 @@ class RegisterFormView(APIView): try: page_size = int(page_size) except ValueError: - page_size = settings.NB_RESULT_PER_PAGE + page_size = settings.NB_RESULT_SUBSCRIPTIONS_PER_PAGE # Récupérer les années scolaires current_year = util.getCurrentSchoolYear() @@ -179,7 +179,7 @@ class RegisterFormWithIdView(APIView): """ Gère la lecture, création, modification et suppression d’un dossier d’inscription. """ - pagination_class = CustomPagination + pagination_class = CustomSubscriptionPagination @swagger_auto_schema( responses={200: RegistrationFormSerializer()}, diff --git a/Front-End/src/app/[locale]/admin/directory/page.js b/Front-End/src/app/[locale]/admin/directory/page.js index acd8732..ae6947d 100644 --- a/Front-End/src/app/[locale]/admin/directory/page.js +++ b/Front-End/src/app/[locale]/admin/directory/page.js @@ -1,125 +1,50 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { - fetchProfileRoles, - updateProfileRoles, - deleteProfileRoles, -} from '@/app/actions/authAction'; -import { dissociateGuardian } from '@/app/actions/subscriptionAction'; +import { fetchProfileRoles } 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 { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants'; export default function Page() { - const [profileRoles, setProfileRoles] = useState([]); + const [profileRolesDatasParent, setProfileRolesDatasParent] = useState([]); + const [profileRolesDatasSchool, setProfileRolesDatasSchool] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); - const csrfToken = useCsrfToken(); const { selectedEstablishmentId } = useEstablishment(); + const requestErrorHandler = (err) => { + logger.error('Error fetching data:', err); + }; + useEffect(() => { if (selectedEstablishmentId) { - // Fetch data for profileRoles + // Fetch data for profileRolesParent handleProfiles(); } }, [selectedEstablishmentId, reloadFetch]); const handleProfiles = () => { - fetchProfileRoles(selectedEstablishmentId) + fetchProfileRoles(selectedEstablishmentId, PARENT_FILTER) .then((data) => { - setProfileRoles(data); + setProfileRolesDatasParent(data); }) - .catch((error) => logger.error('Error fetching profileRoles:', error)); + .catch(requestErrorHandler); + + fetchProfileRoles(selectedEstablishmentId, SCHOOL_FILTER) + .then((data) => { + setProfileRolesDatasSchool(data); + }) + .catch(requestErrorHandler); setReloadFetch(false); }; - 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; - }); - }; - - const handleDissociate = (studentId, guardianId) => { - return dissociateGuardian(studentId, guardianId) - .then((response) => { - logger.debug('Guardian dissociated successfully:', guardianId); - - // Vérifier si le Guardian a été supprimé - const isGuardianDeleted = response?.isGuardianDeleted; - - // Mettre à jour le modèle profileRoles - setProfileRoles( - (prevState) => - prevState - .map((profileRole) => { - if (profileRole.associated_person?.id === guardianId) { - if (isGuardianDeleted) { - // Si le Guardian est supprimé, retirer le profileRole - return null; - } else { - // Si le Guardian n'est pas supprimé, mettre à jour les élèves associés - const updatedStudents = - profileRole.associated_person.students.filter( - (student) => student.id !== studentId - ); - return { - ...profileRole, - associated_person: { - ...profileRole.associated_person, - students: updatedStudents, // Mettre à jour les élèves associés - }, - }; - } - } - setReloadFetch(true); - return profileRole; // Conserver les autres profileRoles - }) - .filter(Boolean) // Supprimer les entrées nulles - ); - }) - .catch((error) => { - logger.error('Error dissociating guardian:', error); - throw error; - }); - }; - return ( -
- - -
- -
+
+
); } diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 172f12a..781b29e 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -13,7 +13,7 @@ import { useEstablishment } from '@/context/EstablishmentContext'; // Composant EventCard pour afficher les événements const EventCard = ({ title, date, description, type }) => ( -
+
@@ -125,7 +125,7 @@ export default function DashboardPage() { {/* Événements et KPIs */}
{/* Graphique des inscriptions */} -
+

{t('inscriptionTrends')}

@@ -136,24 +136,13 @@ export default function DashboardPage() {
{/* Événements à venir */} -
+

{t('upcomingEvents')}

{upcomingEvents.map((event, index) => ( ))}
- -
- {classes.map((classe) => ( -
- -
- ))} -
); } diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js index e19909a..4c1a159 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -382,10 +382,6 @@ export default function CreateSubscriptionPage() { (profile) => profile.id === formDataRef.current.existingProfileId ); - // Affichez le profil existant dans la console - console.log('Profil existant trouvé :', existingProfile?.email); - console.log('debug : ', initialGuardianEmail); - const guardians = (() => { if (formDataRef.current.selectedGuardians.length > 0) { // Cas 3 : Des guardians sont sélectionnés diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 1f6e324..129bc25 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -47,9 +47,9 @@ import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date'; import { RegistrationFormStatus, - CURRENT_YEAR, - NEXT_YEAR, - HISTORICAL, + CURRENT_YEAR_FILTER, + NEXT_YEAR_FILTER, + HISTORICAL_FILTER, } from '@/utils/constants'; export default function Page({ params: { locale } }) { @@ -75,7 +75,7 @@ export default function Page({ params: { locale } }) { useState(1); const [searchTerm, setSearchTerm] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState(CURRENT_YEAR); + const [activeTab, setActiveTab] = useState(CURRENT_YEAR_FILTER); const [totalCurrentYear, setTotalCurrentYear] = useState(0); const [totalNextYear, setTotalNextYear] = useState(0); const [totalHistorical, setTotalHistorical] = useState(0); @@ -190,7 +190,7 @@ export default function Page({ params: { locale } }) { Promise.all([ fetchRegisterForms( selectedEstablishmentId, - CURRENT_YEAR, + CURRENT_YEAR_FILTER, currentSchoolYearPage, itemsPerPage, searchTerm @@ -204,10 +204,10 @@ export default function Page({ params: { locale } }) { }) .catch(requestErrorHandler), - fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR) + fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER) .then(registerFormNextYearDataHandler) .catch(requestErrorHandler), - fetchRegisterForms(selectedEstablishmentId, HISTORICAL) + fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER) .then(registerFormHistoricalDataHandler) .catch(requestErrorHandler), ]) @@ -232,17 +232,17 @@ export default function Page({ params: { locale } }) { setIsLoading(true); fetchRegisterForms( selectedEstablishmentId, - CURRENT_YEAR, + CURRENT_YEAR_FILTER, currentSchoolYearPage, itemsPerPage, searchTerm ) .then(registerFormCurrrentYearDataHandler) .catch(requestErrorHandler); - fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR) + fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER) .then(registerFormNextYearDataHandler) .catch(requestErrorHandler); - fetchRegisterForms(selectedEstablishmentId, HISTORICAL) + fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER) .then(registerFormHistoricalDataHandler) .catch(requestErrorHandler); @@ -261,13 +261,13 @@ export default function Page({ params: { locale } }) { * UseEffect to update page count of tab */ useEffect(() => { - if (activeTab === CURRENT_YEAR) { + if (activeTab === CURRENT_YEAR_FILTER) { setTotalCurrentSchoolYearPages( Math.ceil(totalCurrentYear / itemsPerPage) ); - } else if (activeTab === NEXT_YEAR) { + } else if (activeTab === NEXT_YEAR_FILTER) { setTotalNextSchoolYearPages(Math.ceil(totalNextYear / itemsPerPage)); - } else if (activeTab === HISTORICAL) { + } else if (activeTab === HISTORICAL_FILTER) { setTotalHistoricalPages(Math.ceil(totalHistorical / itemsPerPage)); } }, [currentSchoolYearPage]); @@ -376,11 +376,11 @@ export default function Page({ params: { locale } }) { }; const handlePageChange = (newPage) => { - if (activeTab === CURRENT_YEAR) { + if (activeTab === CURRENT_YEAR_FILTER) { setCurrentSchoolYearPage(newPage); - } else if (activeTab === NEXT_YEAR) { + } else if (activeTab === NEXT_YEAR_FILTER) { setCurrentSchoolNextYearPage(newPage); - } else if (activeTab === HISTORICAL) { + } else if (activeTab === HISTORICAL_FILTER) { setCurrentSchoolHistoricalYearPage(newPage); } }; @@ -710,8 +710,8 @@ export default function Page({ params: { locale } }) { } - active={activeTab === CURRENT_YEAR} - onClick={() => setActiveTab(CURRENT_YEAR)} + active={activeTab === CURRENT_YEAR_FILTER} + onClick={() => setActiveTab(CURRENT_YEAR_FILTER)} /> {/* Tab pour l'année scolaire prochaine */} @@ -724,8 +724,8 @@ export default function Page({ params: { locale } }) { } - active={activeTab === NEXT_YEAR} - onClick={() => setActiveTab(NEXT_YEAR)} + active={activeTab === NEXT_YEAR_FILTER} + onClick={() => setActiveTab(NEXT_YEAR_FILTER)} /> {/* Tab pour l'historique */} @@ -738,16 +738,16 @@ export default function Page({ params: { locale } }) { } - active={activeTab === HISTORICAL} - onClick={() => setActiveTab(HISTORICAL)} + active={activeTab === HISTORICAL_FILTER} + onClick={() => setActiveTab(HISTORICAL_FILTER)} />
- {activeTab === CURRENT_YEAR || - activeTab === NEXT_YEAR || - activeTab === HISTORICAL ? ( + {activeTab === CURRENT_YEAR_FILTER || + activeTab === NEXT_YEAR_FILTER || + activeTab === HISTORICAL_FILTER ? (
@@ -779,25 +779,25 @@ export default function Page({ params: { locale } }) { { const body = await response.json(); @@ -73,10 +73,21 @@ 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 fetchProfileRoles = ( + establishment, + filter = PARENT_FILTER, + page = '', + pageSize = '' +) => { + let url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}`; + if (page !== '' && pageSize !== '') { + url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`; + } + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler); }; export const updateProfileRoles = (id, data, csrfToken) => { diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index 2c8abcd..1d53db9 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -6,7 +6,7 @@ import { BE_SUBSCRIPTION_ABSENCES_URL, } from '@/utils/Url'; -import { CURRENT_YEAR, NEXT_YEAR, HISTORICAL } from '@/utils/constants'; +import { CURRENT_YEAR_FILTER } from '@/utils/constants'; const requestResponseHandler = async (response) => { const body = await response.json(); @@ -21,7 +21,7 @@ const requestResponseHandler = async (response) => { export const fetchRegisterForms = ( establishment, - filter = CURRENT_YEAR, + filter = CURRENT_YEAR_FILTER, page = '', pageSize = '', search = '' diff --git a/Front-End/src/components/PaymentModeSelector.js b/Front-End/src/components/PaymentModeSelector.js index d381b64..b04e4a2 100644 --- a/Front-End/src/components/PaymentModeSelector.js +++ b/Front-End/src/components/PaymentModeSelector.js @@ -57,8 +57,8 @@ const PaymentModeSelector = ({ onClick={() => handleModeToggle(mode.id)} className={`p-4 rounded-lg shadow-md text-center text-gray-700' ${ activePaymentModes.includes(mode.id) - ? 'bg-emerald-300' - : 'bg-white' + ? 'bg-emerald-100' + : 'bg-stone-50' } hover:bg-emerald-200`} > {mode.name} diff --git a/Front-End/src/components/ProfileDirectory.js b/Front-End/src/components/ProfileDirectory.js index 98af5ab..751dd66 100644 --- a/Front-End/src/components/ProfileDirectory.js +++ b/Front-End/src/components/ProfileDirectory.js @@ -1,9 +1,18 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, act } from 'react'; import { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react'; import Table from '@/components/Table'; import Popup from '@/components/Popup'; import StatusLabel from '@/components/StatusLabel'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; +import SidebarTabs from '@/components/SidebarTabs'; +import { + updateProfileRoles, + deleteProfileRoles, +} from '@/app/actions/authAction'; +import { dissociateGuardian } from '@/app/actions/subscriptionAction'; +import { useCsrfToken } from '@/context/CsrfContext'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken'; +import logger from '@/utils/logger'; const roleTypeToLabel = (roleType) => { switch (roleType) { @@ -31,25 +40,147 @@ const roleTypeToBadgeClass = (roleType) => { } }; -const ProfileDirectory = ({ - profileRoles, - handleActivateProfile, - handleDeleteProfile, - handleDissociateGuardian, -}) => { - const parentProfiles = profileRoles.filter( - (profileRole) => profileRole.role_type === 2 - ); - const schoolAdminProfiles = profileRoles.filter( - (profileRole) => profileRole.role_type !== 2 - ); - +const ProfileDirectory = ({ parentProfiles, schoolProfiles }) => { const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(''); const [confirmPopupVisible, setConfirmPopupVisible] = useState(false); const [confirmPopupMessage, setConfirmPopupMessage] = useState(''); const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {}); const [visibleTooltipId, setVisibleTooltipId] = useState(null); + const [activeTab, setActiveTab] = useState('parent'); // Onglet actif + const [totalProfilesParentPages, setTotalProfilesParentPages] = useState(1); + const [totalProfilesSchoolPages, setTotalProfilesSchoolPages] = useState(1); + const [currentProfilesParentPage, setCurrentProfilesParentPage] = useState(1); + + const [totalProfilesParent, setTotalProfilesParent] = useState(0); + const [totalProfilesSchool, setTotalProfilesSchool] = useState(0); + const [currentProfilesSchoolPage, setCurrentProfilesSchoolPage] = useState(1); + const [profileRolesParent, setProfileRolesParent] = useState([]); + const [profileRolesSchool, setProfileRolesSchool] = useState([]); + const itemsPerPage = 10; // Nombre d'éléments par page + + const csrfToken = useCsrfToken(); + + const handleEdit = (profileRole) => { + const updatedData = { ...profileRole, is_active: !profileRole.is_active }; + return updateProfileRoles(profileRole.id, updatedData, csrfToken) + .then((data) => { + setProfileRolesParent((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(() => { + setProfileRolesParent((prevState) => + prevState.filter((item) => item.id !== id) + ); + logger.debug('Profile deleted successfully:', id); + }) + .catch((error) => { + logger.error('Error deleting profile:', error); + throw error; + }); + }; + + const handleDissociate = (studentId, guardianId) => { + return dissociateGuardian(studentId, guardianId) + .then((response) => { + logger.debug('Guardian dissociated successfully:', guardianId); + + // Vérifier si le Guardian a été supprimé + const isGuardianDeleted = response?.isGuardianDeleted; + + // Mettre à jour le modèle profileRolesParent + setProfileRolesParent( + (prevState) => + prevState + .map((profileRole) => { + if (profileRole.associated_person?.id === guardianId) { + if (isGuardianDeleted) { + // Si le Guardian est supprimé, retirer le profileRole + return null; + } else { + // Si le Guardian n'est pas supprimé, mettre à jour les élèves associés + const updatedStudents = + profileRole.associated_person.students.filter( + (student) => student.id !== studentId + ); + return { + ...profileRole, + associated_person: { + ...profileRole.associated_person, + students: updatedStudents, // Mettre à jour les élèves associés + }, + }; + } + } + return profileRole; // Conserver les autres profileRolesParent + }) + .filter(Boolean) // Supprimer les entrées nulles + ); + }) + .catch((error) => { + logger.error('Error dissociating guardian:', error); + throw error; + }); + }; + + const profilesRoleParentDataHandler = (data) => { + if (data) { + const { profilesRoles, count, page_size } = data; + if (profilesRoles) { + setProfileRolesParent(profilesRoles); + } + const calculatedTotalPages = + count === 0 ? 1 : Math.ceil(count / page_size); + setTotalProfilesParent(count); + setTotalProfilesParentPages(calculatedTotalPages); + } + }; + + const profilesRoleSchoolDataHandler = (data) => { + if (data) { + const { profilesRoles, count, page_size } = data; + if (profilesRoles) { + setProfileRolesSchool(profilesRoles); + } + const calculatedTotalPages = + count === 0 ? 1 : Math.ceil(count / page_size); + setTotalProfilesSchool(count); + setTotalProfilesSchoolPages(calculatedTotalPages); + } + }; + + useEffect(() => { + profilesRoleParentDataHandler(parentProfiles); + profilesRoleSchoolDataHandler(schoolProfiles); + + if (activeTab === 'parent') { + setTotalProfilesParentPages( + Math.ceil(totalProfilesParent / itemsPerPage) + ); + } else if (activeTab === 'school') { + setTotalProfilesSchoolPages( + Math.ceil(totalProfilesSchool / itemsPerPage) + ); + } + }, [parentProfiles, schoolProfiles, activeTab]); + + const handlePageChange = (newPage) => { + if (activeTab === 'parent') { + setCurrentProfilesParentPage(newPage); + } else if (activeTab === 'school') { + setCurrentProfilesSchoolPage(newPage); + } + }; const handleTooltipVisibility = (id) => { setVisibleTooltipId(id); // Définir l'ID de la ligne pour laquelle la tooltip est visible @@ -64,7 +195,7 @@ const ProfileDirectory = ({ `Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?` ); setConfirmPopupOnConfirm(() => () => { - handleActivateProfile(profileRole) + handleEdit(profileRole) .then(() => { setPopupMessage( `Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.` @@ -85,7 +216,7 @@ const ProfileDirectory = ({ const handleConfirmDeleteProfile = (id) => { setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?'); setConfirmPopupOnConfirm(() => () => { - handleDeleteProfile(id) + handleDelete(id) .then(() => { setPopupMessage('Le profil a été supprimé avec succès.'); setPopupVisible(true); @@ -105,7 +236,7 @@ const ProfileDirectory = ({ `Vous êtes sur le point de dissocier le responsable ${profileRole.associated_person?.guardian_name} de l'élève ${student.student_name}. Êtes-vous sûr de vouloir poursuivre cette opération ?` ); setConfirmPopupOnConfirm(() => () => { - handleDissociateGuardian(student.id, profileRole.associated_person?.id) + handleDissociate(student.id, profileRole.associated_person?.id) .then(() => { setPopupMessage('Le responsable a été dissocié avec succès.'); setPopupVisible(true); @@ -315,23 +446,64 @@ const ProfileDirectory = ({ ]; return ( -
-
-
- {parentProfiles.length === 0 ? ( -
Aucun profil trouvé
- ) : ( -
- )} - -
- {schoolAdminProfiles.length === 0 ? ( -
Aucun profil trouvé
- ) : ( -
- )} - - + <> + + +
+ + ), + }, + { + id: 'school', + label: 'École', + content: ( +
+
+ + ), + }, + ]} + onTabChange={(newActiveTab) => { + setActiveTab(newActiveTab); + }} + /> + {/* Popups */} setConfirmPopupVisible(false)} /> - + ); }; diff --git a/Front-End/src/components/Sidebar.js b/Front-End/src/components/Sidebar.js index f8da173..77860b5 100644 --- a/Front-End/src/components/Sidebar.js +++ b/Front-End/src/components/Sidebar.js @@ -6,10 +6,8 @@ import ProfileSelector from '@/components/ProfileSelector'; const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
@@ -35,7 +33,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) { }; return ( -
+
diff --git a/Front-End/src/components/SidebarTabs.js b/Front-End/src/components/SidebarTabs.js index 5f965b4..828dc5b 100644 --- a/Front-End/src/components/SidebarTabs.js +++ b/Front-End/src/components/SidebarTabs.js @@ -1,34 +1,46 @@ import React, { useState } from 'react'; -const SidebarTabs = ({ tabs }) => { +const SidebarTabs = ({ tabs, onTabChange }) => { const [activeTab, setActiveTab] = useState(tabs[0].id); + const handleTabChange = (tabId) => { + setActiveTab(tabId); + if (onTabChange) { + onTabChange(tabId); + } + }; + return ( - <> -
+
+ {/* Tabs Header */} +
{tabs.map((tab) => ( ))}
- {tabs.map((tab) => ( -
- {tab.content} -
- ))} - + + {/* Tabs Content */} +
+ {tabs.map((tab) => ( +
+ {tab.content} +
+ ))} +
+
); }; diff --git a/Front-End/src/components/StatCard.js b/Front-End/src/components/StatCard.js index 20bda27..ad5a426 100644 --- a/Front-End/src/components/StatCard.js +++ b/Front-End/src/components/StatCard.js @@ -1,6 +1,6 @@ // Composant StatCard pour afficher une statistique const StatCard = ({ title, value, icon, color = 'blue' }) => ( -
+

{title}

diff --git a/Front-End/src/components/Structure/Configuration/StructureManagement.js b/Front-End/src/components/Structure/Configuration/StructureManagement.js index faf39d6..a044eab 100644 --- a/Front-End/src/components/Structure/Configuration/StructureManagement.js +++ b/Front-End/src/components/Structure/Configuration/StructureManagement.js @@ -22,7 +22,7 @@ const StructureManagement = ({ handleDelete, }) => { return ( -
+
+
{/* Modal pour les fichiers */} +

diff --git a/Front-End/src/components/Table.js b/Front-End/src/components/Table.js index 7456790..f8cd3ea 100644 --- a/Front-End/src/components/Table.js +++ b/Front-End/src/components/Table.js @@ -13,21 +13,21 @@ const Table = ({ onRowClick, selectedRows, isSelectable = false, - defaultTheme = 'bg-emerald-50', + defaultTheme = 'bg-emerald-50', // Blanc cassé pour les lignes paires }) => { const handlePageChange = (newPage) => { onPageChange(newPage); }; return ( -
-
+
+
{columns.map((column, index) => ( @@ -40,16 +40,21 @@ const Table = ({ key={rowIndex} className={` ${isSelectable ? 'cursor-pointer' : ''} - ${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''} - ${isSelectable ? 'hover:bg-emerald-200' : ''} + ${ + selectedRows?.includes(row.id) + ? 'bg-emerald-200 text-white' + : rowIndex % 2 === 0 + ? `${defaultTheme}` + : 'bg-stone-50' // Blanc cassé pour les lignes impaires + } + ${isSelectable ? 'hover:bg-emerald-100' : ''} `} onClick={() => { if (isSelectable && onRowClick) { - // Si la ligne est déjà sélectionnée, transmettre une indication explicite de désélection if (selectedRows?.includes(row.id)) { - onRowClick({ deselected: true, row }); // Désélectionner + onRowClick({ deselected: true, row }); } else { - onRowClick(row); // Sélectionner + onRowClick(row); } } }} @@ -57,7 +62,11 @@ const Table = ({ {columns.map((column, colIndex) => (
{column.name} {renderCell ? renderCell(row, column.name) diff --git a/Front-End/src/utils/constants.js b/Front-End/src/utils/constants.js index f0ff33d..e08d288 100644 --- a/Front-End/src/utils/constants.js +++ b/Front-End/src/utils/constants.js @@ -26,6 +26,9 @@ export const RegistrationFormStatus = { STATUS_SEPA_TO_SEND: 8, }; -export const CURRENT_YEAR = 'current_year'; -export const NEXT_YEAR = 'next_year'; -export const HISTORICAL = 'historical'; +export const CURRENT_YEAR_FILTER = 'current_year'; +export const NEXT_YEAR_FILTER = 'next_year'; +export const HISTORICAL_FILTER = 'historical'; + +export const PARENT_FILTER = 'parents'; +export const SCHOOL_FILTER = 'school';