mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Création nouveau style / pagination profils annuaires
This commit is contained in:
20
Back-End/Auth/pagination.py
Normal file
20
Back-End/Auth/pagination.py
Normal file
@ -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 }
|
||||||
|
)
|
||||||
@ -8,6 +8,7 @@ from django.middleware.csrf import get_token
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from Auth.pagination import CustomProfilesPagination
|
||||||
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
@ -20,13 +21,14 @@ import json
|
|||||||
from . import validator
|
from . import validator
|
||||||
from .models import Profile, ProfileRole
|
from .models import Profile, ProfileRole
|
||||||
from rest_framework.decorators import action, api_view
|
from rest_framework.decorators import action, api_view
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||||
from Subscriptions.models import RegistrationForm, Guardian
|
from Subscriptions.models import RegistrationForm, Guardian
|
||||||
import Subscriptions.mailManager as mailer
|
import Subscriptions.mailManager as mailer
|
||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
import logging
|
import logging
|
||||||
from N3wtSchool import bdd, error
|
from N3wtSchool import bdd, error, settings
|
||||||
|
|
||||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
@ -509,20 +511,55 @@ class ResetPasswordView(APIView):
|
|||||||
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
||||||
|
|
||||||
class ProfileRoleView(APIView):
|
class ProfileRoleView(APIView):
|
||||||
|
pagination_class = CustomProfilesPagination
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
operation_description="Obtenir la liste des profile_roles",
|
operation_description="Obtenir la liste des profile_roles",
|
||||||
responses={200: ProfileRoleSerializer(many=True)}
|
responses={200: ProfileRoleSerializer(many=True)}
|
||||||
)
|
)
|
||||||
def get(self, request):
|
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)
|
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:
|
if establishment_id is None:
|
||||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole)
|
# Récupérer les ProfileRole en fonction du filtre
|
||||||
if profiles_roles_List:
|
profiles_roles_List = ProfileRole.objects.filter(establishment_id=establishment_id)
|
||||||
profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct().order_by('-updated_date')
|
|
||||||
profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True)
|
if filter == 'parents':
|
||||||
return JsonResponse(profile_roles_serializer.data, safe=False)
|
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(
|
@swagger_auto_schema(
|
||||||
operation_description="Créer un nouveau profile_role",
|
operation_description="Créer un nouveau profile_role",
|
||||||
|
|||||||
@ -282,12 +282,13 @@ DATE_FORMAT = '%d-%m-%Y %H:%M'
|
|||||||
|
|
||||||
EXPIRATION_SESSION_NB_SEC = 10
|
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
|
NB_MAX_PAGE = 100
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomPagination',
|
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
||||||
'PAGE_SIZE': NB_RESULT_PER_PAGE,
|
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
),
|
),
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class Teacher(models.Model):
|
|||||||
last_name = models.CharField(max_length=100)
|
last_name = models.CharField(max_length=100)
|
||||||
first_name = models.CharField(max_length=100)
|
first_name = models.CharField(max_length=100)
|
||||||
specialities = models.ManyToManyField(Speciality, blank=True)
|
specialities = models.ManyToManyField(Speciality, blank=True)
|
||||||
profile_role = models.OneToOneField(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)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -31,7 +31,7 @@ class Guardian(models.Model):
|
|||||||
address = models.CharField(max_length=200, default="", blank=True)
|
address = models.CharField(max_length=200, default="", blank=True)
|
||||||
phone = 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)
|
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
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
|
|||||||
@ -2,10 +2,10 @@ from rest_framework.pagination import PageNumberPagination
|
|||||||
|
|
||||||
from N3wtSchool import settings
|
from N3wtSchool import settings
|
||||||
|
|
||||||
class CustomPagination(PageNumberPagination):
|
class CustomSubscriptionPagination(PageNumberPagination):
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = settings.NB_MAX_PAGE
|
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):
|
def get_paginated_response(self, data):
|
||||||
return ({
|
return ({
|
||||||
|
|||||||
@ -137,9 +137,19 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
def create_or_update_guardians(self, guardians_data):
|
def create_or_update_guardians(self, guardians_data):
|
||||||
guardians_ids = []
|
guardians_ids = []
|
||||||
for guardian_data in guardians_data:
|
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_data = guardian_data.pop('profile_role_data', None)
|
||||||
profile_role = guardian_data.pop('profile_role', 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:
|
if profile_role_data:
|
||||||
# Vérifiez si 'profile_data' est fourni pour créer un nouveau profil
|
# Vérifiez si 'profile_data' est fourni pour créer un nouveau profil
|
||||||
profile_data = profile_role_data.pop('profile_data', None)
|
profile_data = profile_role_data.pop('profile_data', None)
|
||||||
@ -410,4 +420,4 @@ class NotificationSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import Subscriptions.mailManager as mailer
|
|||||||
import Subscriptions.util as util
|
import Subscriptions.util as util
|
||||||
|
|
||||||
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
|
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.models import Student, Guardian, RegistrationForm, RegistrationSchoolFileTemplate, RegistrationFileGroup, RegistrationParentFileTemplate
|
||||||
from Subscriptions.automate import updateStateMachine
|
from Subscriptions.automate import updateStateMachine
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ class RegisterFormView(APIView):
|
|||||||
"""
|
"""
|
||||||
Gère la liste des dossiers d’inscription, lecture et création.
|
Gère la liste des dossiers d’inscription, lecture et création.
|
||||||
"""
|
"""
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomSubscriptionPagination
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
@ -82,7 +82,7 @@ class RegisterFormView(APIView):
|
|||||||
try:
|
try:
|
||||||
page_size = int(page_size)
|
page_size = int(page_size)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
page_size = settings.NB_RESULT_PER_PAGE
|
page_size = settings.NB_RESULT_SUBSCRIPTIONS_PER_PAGE
|
||||||
|
|
||||||
# Récupérer les années scolaires
|
# Récupérer les années scolaires
|
||||||
current_year = util.getCurrentSchoolYear()
|
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.
|
Gère la lecture, création, modification et suppression d’un dossier d’inscription.
|
||||||
"""
|
"""
|
||||||
pagination_class = CustomPagination
|
pagination_class = CustomSubscriptionPagination
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
responses={200: RegistrationFormSerializer()},
|
responses={200: RegistrationFormSerializer()},
|
||||||
|
|||||||
@ -1,125 +1,50 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import { fetchProfileRoles } from '@/app/actions/authAction';
|
||||||
fetchProfileRoles,
|
|
||||||
updateProfileRoles,
|
|
||||||
deleteProfileRoles,
|
|
||||||
} from '@/app/actions/authAction';
|
|
||||||
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
|
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
|
||||||
import ProfileDirectory from '@/components/ProfileDirectory';
|
import ProfileDirectory from '@/components/ProfileDirectory';
|
||||||
|
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [profileRoles, setProfileRoles] = useState([]);
|
const [profileRolesDatasParent, setProfileRolesDatasParent] = useState([]);
|
||||||
|
const [profileRolesDatasSchool, setProfileRolesDatasSchool] = useState([]);
|
||||||
const [reloadFetch, setReloadFetch] = useState(false);
|
const [reloadFetch, setReloadFetch] = useState(false);
|
||||||
|
|
||||||
const csrfToken = useCsrfToken();
|
|
||||||
const { selectedEstablishmentId } = useEstablishment();
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
|
|
||||||
|
const requestErrorHandler = (err) => {
|
||||||
|
logger.error('Error fetching data:', err);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
// Fetch data for profileRoles
|
// Fetch data for profileRolesParent
|
||||||
handleProfiles();
|
handleProfiles();
|
||||||
}
|
}
|
||||||
}, [selectedEstablishmentId, reloadFetch]);
|
}, [selectedEstablishmentId, reloadFetch]);
|
||||||
|
|
||||||
const handleProfiles = () => {
|
const handleProfiles = () => {
|
||||||
fetchProfileRoles(selectedEstablishmentId)
|
fetchProfileRoles(selectedEstablishmentId, PARENT_FILTER)
|
||||||
.then((data) => {
|
.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);
|
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 (
|
return (
|
||||||
<div className="p-8">
|
<div className="w-full h-full">
|
||||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
<ProfileDirectory
|
||||||
|
parentProfiles={profileRolesDatasParent}
|
||||||
<div className="w-full p-4">
|
schoolProfiles={profileRolesDatasSchool}
|
||||||
<ProfileDirectory
|
/>
|
||||||
profileRoles={profileRoles}
|
|
||||||
handleActivateProfile={handleEdit}
|
|
||||||
handleDeleteProfile={handleDelete}
|
|
||||||
handleDissociateGuardian={handleDissociate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
|||||||
|
|
||||||
// Composant EventCard pour afficher les événements
|
// Composant EventCard pour afficher les événements
|
||||||
const EventCard = ({ title, date, description, type }) => (
|
const EventCard = ({ title, date, description, type }) => (
|
||||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CalendarCheck className="text-blue-500" size={20} />
|
<CalendarCheck className="text-blue-500" size={20} />
|
||||||
<div>
|
<div>
|
||||||
@ -125,7 +125,7 @@ export default function DashboardPage() {
|
|||||||
{/* Événements et KPIs */}
|
{/* Événements et KPIs */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
{/* Graphique des inscriptions */}
|
{/* Graphique des inscriptions */}
|
||||||
<div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
<div className="lg:col-span-2 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
<h2 className="text-lg font-semibold mb-4">
|
<h2 className="text-lg font-semibold mb-4">
|
||||||
{t('inscriptionTrends')}
|
{t('inscriptionTrends')}
|
||||||
</h2>
|
</h2>
|
||||||
@ -136,24 +136,13 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Événements à venir */}
|
{/* Événements à venir */}
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||||
{upcomingEvents.map((event, index) => (
|
{upcomingEvents.map((event, index) => (
|
||||||
<EventCard key={index} {...event} />
|
<EventCard key={index} {...event} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap">
|
|
||||||
{classes.map((classe) => (
|
|
||||||
<div
|
|
||||||
key={classe.id}
|
|
||||||
className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-100 mr-4"
|
|
||||||
>
|
|
||||||
<ClasseDetails classe={classe} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -382,10 +382,6 @@ export default function CreateSubscriptionPage() {
|
|||||||
(profile) => profile.id === formDataRef.current.existingProfileId
|
(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 = (() => {
|
const guardians = (() => {
|
||||||
if (formDataRef.current.selectedGuardians.length > 0) {
|
if (formDataRef.current.selectedGuardians.length > 0) {
|
||||||
// Cas 3 : Des guardians sont sélectionnés
|
// Cas 3 : Des guardians sont sélectionnés
|
||||||
|
|||||||
@ -47,9 +47,9 @@ import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
RegistrationFormStatus,
|
RegistrationFormStatus,
|
||||||
CURRENT_YEAR,
|
CURRENT_YEAR_FILTER,
|
||||||
NEXT_YEAR,
|
NEXT_YEAR_FILTER,
|
||||||
HISTORICAL,
|
HISTORICAL_FILTER,
|
||||||
} from '@/utils/constants';
|
} from '@/utils/constants';
|
||||||
|
|
||||||
export default function Page({ params: { locale } }) {
|
export default function Page({ params: { locale } }) {
|
||||||
@ -75,7 +75,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
useState(1);
|
useState(1);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState(CURRENT_YEAR);
|
const [activeTab, setActiveTab] = useState(CURRENT_YEAR_FILTER);
|
||||||
const [totalCurrentYear, setTotalCurrentYear] = useState(0);
|
const [totalCurrentYear, setTotalCurrentYear] = useState(0);
|
||||||
const [totalNextYear, setTotalNextYear] = useState(0);
|
const [totalNextYear, setTotalNextYear] = useState(0);
|
||||||
const [totalHistorical, setTotalHistorical] = useState(0);
|
const [totalHistorical, setTotalHistorical] = useState(0);
|
||||||
@ -190,7 +190,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
fetchRegisterForms(
|
fetchRegisterForms(
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
CURRENT_YEAR,
|
CURRENT_YEAR_FILTER,
|
||||||
currentSchoolYearPage,
|
currentSchoolYearPage,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
searchTerm
|
searchTerm
|
||||||
@ -204,10 +204,10 @@ export default function Page({ params: { locale } }) {
|
|||||||
})
|
})
|
||||||
.catch(requestErrorHandler),
|
.catch(requestErrorHandler),
|
||||||
|
|
||||||
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR)
|
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER)
|
||||||
.then(registerFormNextYearDataHandler)
|
.then(registerFormNextYearDataHandler)
|
||||||
.catch(requestErrorHandler),
|
.catch(requestErrorHandler),
|
||||||
fetchRegisterForms(selectedEstablishmentId, HISTORICAL)
|
fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER)
|
||||||
.then(registerFormHistoricalDataHandler)
|
.then(registerFormHistoricalDataHandler)
|
||||||
.catch(requestErrorHandler),
|
.catch(requestErrorHandler),
|
||||||
])
|
])
|
||||||
@ -232,17 +232,17 @@ export default function Page({ params: { locale } }) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchRegisterForms(
|
fetchRegisterForms(
|
||||||
selectedEstablishmentId,
|
selectedEstablishmentId,
|
||||||
CURRENT_YEAR,
|
CURRENT_YEAR_FILTER,
|
||||||
currentSchoolYearPage,
|
currentSchoolYearPage,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
searchTerm
|
searchTerm
|
||||||
)
|
)
|
||||||
.then(registerFormCurrrentYearDataHandler)
|
.then(registerFormCurrrentYearDataHandler)
|
||||||
.catch(requestErrorHandler);
|
.catch(requestErrorHandler);
|
||||||
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR)
|
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER)
|
||||||
.then(registerFormNextYearDataHandler)
|
.then(registerFormNextYearDataHandler)
|
||||||
.catch(requestErrorHandler);
|
.catch(requestErrorHandler);
|
||||||
fetchRegisterForms(selectedEstablishmentId, HISTORICAL)
|
fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER)
|
||||||
.then(registerFormHistoricalDataHandler)
|
.then(registerFormHistoricalDataHandler)
|
||||||
.catch(requestErrorHandler);
|
.catch(requestErrorHandler);
|
||||||
|
|
||||||
@ -261,13 +261,13 @@ export default function Page({ params: { locale } }) {
|
|||||||
* UseEffect to update page count of tab
|
* UseEffect to update page count of tab
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === CURRENT_YEAR) {
|
if (activeTab === CURRENT_YEAR_FILTER) {
|
||||||
setTotalCurrentSchoolYearPages(
|
setTotalCurrentSchoolYearPages(
|
||||||
Math.ceil(totalCurrentYear / itemsPerPage)
|
Math.ceil(totalCurrentYear / itemsPerPage)
|
||||||
);
|
);
|
||||||
} else if (activeTab === NEXT_YEAR) {
|
} else if (activeTab === NEXT_YEAR_FILTER) {
|
||||||
setTotalNextSchoolYearPages(Math.ceil(totalNextYear / itemsPerPage));
|
setTotalNextSchoolYearPages(Math.ceil(totalNextYear / itemsPerPage));
|
||||||
} else if (activeTab === HISTORICAL) {
|
} else if (activeTab === HISTORICAL_FILTER) {
|
||||||
setTotalHistoricalPages(Math.ceil(totalHistorical / itemsPerPage));
|
setTotalHistoricalPages(Math.ceil(totalHistorical / itemsPerPage));
|
||||||
}
|
}
|
||||||
}, [currentSchoolYearPage]);
|
}, [currentSchoolYearPage]);
|
||||||
@ -376,11 +376,11 @@ export default function Page({ params: { locale } }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage) => {
|
const handlePageChange = (newPage) => {
|
||||||
if (activeTab === CURRENT_YEAR) {
|
if (activeTab === CURRENT_YEAR_FILTER) {
|
||||||
setCurrentSchoolYearPage(newPage);
|
setCurrentSchoolYearPage(newPage);
|
||||||
} else if (activeTab === NEXT_YEAR) {
|
} else if (activeTab === NEXT_YEAR_FILTER) {
|
||||||
setCurrentSchoolNextYearPage(newPage);
|
setCurrentSchoolNextYearPage(newPage);
|
||||||
} else if (activeTab === HISTORICAL) {
|
} else if (activeTab === HISTORICAL_FILTER) {
|
||||||
setCurrentSchoolHistoricalYearPage(newPage);
|
setCurrentSchoolHistoricalYearPage(newPage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -710,8 +710,8 @@ export default function Page({ params: { locale } }) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
active={activeTab === CURRENT_YEAR}
|
active={activeTab === CURRENT_YEAR_FILTER}
|
||||||
onClick={() => setActiveTab(CURRENT_YEAR)}
|
onClick={() => setActiveTab(CURRENT_YEAR_FILTER)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab pour l'année scolaire prochaine */}
|
{/* Tab pour l'année scolaire prochaine */}
|
||||||
@ -724,8 +724,8 @@ export default function Page({ params: { locale } }) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
active={activeTab === NEXT_YEAR}
|
active={activeTab === NEXT_YEAR_FILTER}
|
||||||
onClick={() => setActiveTab(NEXT_YEAR)}
|
onClick={() => setActiveTab(NEXT_YEAR_FILTER)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab pour l'historique */}
|
{/* Tab pour l'historique */}
|
||||||
@ -738,16 +738,16 @@ export default function Page({ params: { locale } }) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
active={activeTab === HISTORICAL}
|
active={activeTab === HISTORICAL_FILTER}
|
||||||
onClick={() => setActiveTab(HISTORICAL)}
|
onClick={() => setActiveTab(HISTORICAL_FILTER)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-b border-gray-200 mb-6 w-full">
|
<div className="border-b border-gray-200 mb-6 w-full">
|
||||||
{activeTab === CURRENT_YEAR ||
|
{activeTab === CURRENT_YEAR_FILTER ||
|
||||||
activeTab === NEXT_YEAR ||
|
activeTab === NEXT_YEAR_FILTER ||
|
||||||
activeTab === HISTORICAL ? (
|
activeTab === HISTORICAL_FILTER ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="flex justify-between items-center mb-4 w-full">
|
<div className="flex justify-between items-center mb-4 w-full">
|
||||||
<div className="relative flex-grow">
|
<div className="relative flex-grow">
|
||||||
@ -779,25 +779,25 @@ export default function Page({ params: { locale } }) {
|
|||||||
<Table
|
<Table
|
||||||
key={`${currentSchoolYearPage}-${searchTerm}`}
|
key={`${currentSchoolYearPage}-${searchTerm}`}
|
||||||
data={
|
data={
|
||||||
activeTab === CURRENT_YEAR
|
activeTab === CURRENT_YEAR_FILTER
|
||||||
? registrationFormsDataCurrentYear
|
? registrationFormsDataCurrentYear
|
||||||
: activeTab === NEXT_YEAR
|
: activeTab === NEXT_YEAR_FILTER
|
||||||
? registrationFormsDataNextYear
|
? registrationFormsDataNextYear
|
||||||
: registrationFormsDataHistorical
|
: registrationFormsDataHistorical
|
||||||
}
|
}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
itemsPerPage={itemsPerPage}
|
itemsPerPage={itemsPerPage}
|
||||||
currentPage={
|
currentPage={
|
||||||
activeTab === CURRENT_YEAR
|
activeTab === CURRENT_YEAR_FILTER
|
||||||
? currentSchoolYearPage
|
? currentSchoolYearPage
|
||||||
: activeTab === NEXT_YEAR
|
: activeTab === NEXT_YEAR_FILTER
|
||||||
? currentSchoolNextYearPage
|
? currentSchoolNextYearPage
|
||||||
: currentSchoolHistoricalYearPage
|
: currentSchoolHistoricalYearPage
|
||||||
}
|
}
|
||||||
totalPages={
|
totalPages={
|
||||||
activeTab === CURRENT_YEAR
|
activeTab === CURRENT_YEAR_FILTER
|
||||||
? totalCurrentSchoolYearPages
|
? totalCurrentSchoolYearPages
|
||||||
: activeTab === NEXT_YEAR
|
: activeTab === NEXT_YEAR_FILTER
|
||||||
? totalNextSchoolYearPages
|
? totalNextSchoolYearPages
|
||||||
: totalHistoricalPages
|
: totalHistoricalPages
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
BE_AUTH_NEW_PASSWORD_URL,
|
BE_AUTH_NEW_PASSWORD_URL,
|
||||||
FE_USERS_LOGIN_URL,
|
FE_USERS_LOGIN_URL,
|
||||||
} from '@/utils/Url';
|
} from '@/utils/Url';
|
||||||
import logger from '@/utils/logger';
|
import { PARENT_FILTER } from '@/utils/constants';
|
||||||
|
|
||||||
const requestResponseHandler = async (response) => {
|
const requestResponseHandler = async (response) => {
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
@ -73,10 +73,21 @@ export const disconnect = () => {
|
|||||||
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
|
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProfileRoles = (establishment) => {
|
export const fetchProfileRoles = (
|
||||||
return fetch(
|
establishment,
|
||||||
`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`
|
filter = PARENT_FILTER,
|
||||||
).then(requestResponseHandler);
|
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) => {
|
export const updateProfileRoles = (id, data, csrfToken) => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
BE_SUBSCRIPTION_ABSENCES_URL,
|
BE_SUBSCRIPTION_ABSENCES_URL,
|
||||||
} from '@/utils/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 requestResponseHandler = async (response) => {
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
@ -21,7 +21,7 @@ const requestResponseHandler = async (response) => {
|
|||||||
|
|
||||||
export const fetchRegisterForms = (
|
export const fetchRegisterForms = (
|
||||||
establishment,
|
establishment,
|
||||||
filter = CURRENT_YEAR,
|
filter = CURRENT_YEAR_FILTER,
|
||||||
page = '',
|
page = '',
|
||||||
pageSize = '',
|
pageSize = '',
|
||||||
search = ''
|
search = ''
|
||||||
|
|||||||
@ -57,8 +57,8 @@ const PaymentModeSelector = ({
|
|||||||
onClick={() => handleModeToggle(mode.id)}
|
onClick={() => handleModeToggle(mode.id)}
|
||||||
className={`p-4 rounded-lg shadow-md text-center text-gray-700' ${
|
className={`p-4 rounded-lg shadow-md text-center text-gray-700' ${
|
||||||
activePaymentModes.includes(mode.id)
|
activePaymentModes.includes(mode.id)
|
||||||
? 'bg-emerald-300'
|
? 'bg-emerald-100'
|
||||||
: 'bg-white'
|
: 'bg-stone-50'
|
||||||
} hover:bg-emerald-200`}
|
} hover:bg-emerald-200`}
|
||||||
>
|
>
|
||||||
{mode.name}
|
{mode.name}
|
||||||
|
|||||||
@ -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 { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import StatusLabel from '@/components/StatusLabel';
|
import StatusLabel from '@/components/StatusLabel';
|
||||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||||
|
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) => {
|
const roleTypeToLabel = (roleType) => {
|
||||||
switch (roleType) {
|
switch (roleType) {
|
||||||
@ -31,25 +40,147 @@ const roleTypeToBadgeClass = (roleType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileDirectory = ({
|
const ProfileDirectory = ({ parentProfiles, schoolProfiles }) => {
|
||||||
profileRoles,
|
|
||||||
handleActivateProfile,
|
|
||||||
handleDeleteProfile,
|
|
||||||
handleDissociateGuardian,
|
|
||||||
}) => {
|
|
||||||
const parentProfiles = profileRoles.filter(
|
|
||||||
(profileRole) => profileRole.role_type === 2
|
|
||||||
);
|
|
||||||
const schoolAdminProfiles = profileRoles.filter(
|
|
||||||
(profileRole) => profileRole.role_type !== 2
|
|
||||||
);
|
|
||||||
|
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [popupMessage, setPopupMessage] = useState('');
|
const [popupMessage, setPopupMessage] = useState('');
|
||||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||||
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
|
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) => {
|
const handleTooltipVisibility = (id) => {
|
||||||
setVisibleTooltipId(id); // Définir l'ID de la ligne pour laquelle la tooltip est visible
|
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 ?`
|
`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`
|
||||||
);
|
);
|
||||||
setConfirmPopupOnConfirm(() => () => {
|
setConfirmPopupOnConfirm(() => () => {
|
||||||
handleActivateProfile(profileRole)
|
handleEdit(profileRole)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setPopupMessage(
|
setPopupMessage(
|
||||||
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
|
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
|
||||||
@ -85,7 +216,7 @@ const ProfileDirectory = ({
|
|||||||
const handleConfirmDeleteProfile = (id) => {
|
const handleConfirmDeleteProfile = (id) => {
|
||||||
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
|
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
|
||||||
setConfirmPopupOnConfirm(() => () => {
|
setConfirmPopupOnConfirm(() => () => {
|
||||||
handleDeleteProfile(id)
|
handleDelete(id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setPopupMessage('Le profil a été supprimé avec succès.');
|
setPopupMessage('Le profil a été supprimé avec succès.');
|
||||||
setPopupVisible(true);
|
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 ?`
|
`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(() => () => {
|
setConfirmPopupOnConfirm(() => () => {
|
||||||
handleDissociateGuardian(student.id, profileRole.associated_person?.id)
|
handleDissociate(student.id, profileRole.associated_person?.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setPopupMessage('Le responsable a été dissocié avec succès.');
|
setPopupMessage('Le responsable a été dissocié avec succès.');
|
||||||
setPopupVisible(true);
|
setPopupVisible(true);
|
||||||
@ -315,23 +446,64 @@ const ProfileDirectory = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-lg w-3/5 p-6">
|
<>
|
||||||
<div className="space-y-8">
|
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||||
<div className="max-h-128 overflow-y-auto border rounded p-4">
|
<SidebarTabs
|
||||||
{parentProfiles.length === 0 ? (
|
tabs={[
|
||||||
<div>Aucun profil trouvé</div>
|
{
|
||||||
) : (
|
id: 'parent',
|
||||||
<Table data={parentProfiles} columns={parentColumns} />
|
label: 'Parents',
|
||||||
)}
|
content: (
|
||||||
</div>
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="max-h-128 overflow-y-auto border rounded p-4">
|
<Table
|
||||||
{schoolAdminProfiles.length === 0 ? (
|
key={`parent-${currentProfilesParentPage}`}
|
||||||
<div>Aucun profil trouvé</div>
|
data={
|
||||||
) : (
|
Array.isArray(profileRolesParent)
|
||||||
<Table data={schoolAdminProfiles} columns={schoolAdminColumns} />
|
? profileRolesParent.slice(
|
||||||
)}
|
(currentProfilesParentPage - 1) * itemsPerPage,
|
||||||
</div>
|
currentProfilesParentPage * itemsPerPage
|
||||||
</div>
|
)
|
||||||
|
: [] // Fallback to an empty array if profileRolesParent is not an array
|
||||||
|
}
|
||||||
|
columns={parentColumns}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
currentPage={currentProfilesParentPage}
|
||||||
|
totalPages={totalProfilesParentPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'school',
|
||||||
|
label: 'École',
|
||||||
|
content: (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<Table
|
||||||
|
key={`school-${currentProfilesSchoolPage}`}
|
||||||
|
data={
|
||||||
|
Array.isArray(profileRolesSchool)
|
||||||
|
? profileRolesSchool.slice(
|
||||||
|
(currentProfilesSchoolPage - 1) * itemsPerPage,
|
||||||
|
currentProfilesSchoolPage * itemsPerPage
|
||||||
|
)
|
||||||
|
: [] // Fallback to an empty array if profileRolesSchool is not an array
|
||||||
|
}
|
||||||
|
columns={schoolAdminColumns}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
currentPage={currentProfilesSchoolPage}
|
||||||
|
totalPages={totalProfilesSchoolPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onTabChange={(newActiveTab) => {
|
||||||
|
setActiveTab(newActiveTab);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Popups */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={popupVisible}
|
||||||
message={popupMessage}
|
message={popupMessage}
|
||||||
@ -344,7 +516,7 @@ const ProfileDirectory = ({
|
|||||||
onConfirm={confirmPopupOnConfirm}
|
onConfirm={confirmPopupOnConfirm}
|
||||||
onCancel={() => setConfirmPopupVisible(false)}
|
onCancel={() => setConfirmPopupVisible(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,8 @@ import ProfileSelector from '@/components/ProfileSelector';
|
|||||||
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
|
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer ${
|
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer hover:bg-emerald-100 ${
|
||||||
active
|
active ? 'bg-emerald-50 text-emerald-600' : 'text-gray-600'
|
||||||
? 'bg-emerald-50 text-emerald-600'
|
|
||||||
: 'text-gray-600 hover:bg-gray-50'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
@ -35,7 +33,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 bg-white border-r h-full border-gray-200">
|
<div className="w-64 bg-stone-50 border-r h-full border-gray-200">
|
||||||
<div className="border-b border-gray-200 ">
|
<div className="border-b border-gray-200 ">
|
||||||
<ProfileSelector className="border-none" />
|
<ProfileSelector className="border-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,34 +1,46 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
const SidebarTabs = ({ tabs }) => {
|
const SidebarTabs = ({ tabs, onTabChange }) => {
|
||||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||||
|
|
||||||
|
const handleTabChange = (tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
if (onTabChange) {
|
||||||
|
onTabChange(tabId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex h-14 border-b-2 border-gray-200">
|
{/* Tabs Header */}
|
||||||
|
<div className="flex h-14 bg-gray-50 border-b border-gray-200 shadow-sm">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`flex-1 p-4 ${
|
className={`flex-1 text-center p-4 font-medium transition-colors duration-200 ${
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'border-b-2 border-emerald-500 text-emerald-500'
|
? 'border-b-4 border-emerald-500 text-emerald-600 bg-emerald-50 font-semibold'
|
||||||
: 'text-gray-500 hover:text-emerald-500'
|
: 'text-gray-500 hover:text-emerald-500'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => handleTabChange(tab.id)}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{tabs.map((tab) => (
|
|
||||||
<div
|
{/* Tabs Content */}
|
||||||
key={tab.id}
|
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner">
|
||||||
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
|
{tabs.map((tab) => (
|
||||||
>
|
<div
|
||||||
{tab.content}
|
key={tab.id}
|
||||||
</div>
|
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
|
||||||
))}
|
>
|
||||||
</>
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Composant StatCard pour afficher une statistique
|
// Composant StatCard pour afficher une statistique
|
||||||
const StatCard = ({ title, value, icon, color = 'blue' }) => (
|
const StatCard = ({ title, value, icon, color = 'blue' }) => (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
|
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const StructureManagement = ({
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 mx-auto mt-6">
|
<div className="w-full">
|
||||||
<ClassesProvider>
|
<ClassesProvider>
|
||||||
<div className="mt-8 w-2/5">
|
<div className="mt-8 w-2/5">
|
||||||
<SpecialitiesSection
|
<SpecialitiesSection
|
||||||
|
|||||||
@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 mx-auto mt-6">
|
<div className="w-full">
|
||||||
{/* Modal pour les fichiers */}
|
{/* Modal pour les fichiers */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
|
|||||||
@ -360,7 +360,7 @@ const DiscountsSection = ({
|
|||||||
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
renderCell={renderDiscountCell}
|
renderCell={renderDiscountCell}
|
||||||
defaultTheme="bg-yellow-100"
|
defaultTheme="bg-yellow-50"
|
||||||
/>
|
/>
|
||||||
<Popup
|
<Popup
|
||||||
visible={popupVisible}
|
visible={popupVisible}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const FeesManagement = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 mx-auto mt-6">
|
<div className="w-full">
|
||||||
<div className="w-4/5 mx-auto flex items-center mt-8">
|
<div className="w-4/5 mx-auto flex items-center mt-8">
|
||||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||||
<span className="mx-4 text-gray-600 font-semibold">
|
<span className="mx-4 text-gray-600 font-semibold">
|
||||||
|
|||||||
@ -13,21 +13,21 @@ const Table = ({
|
|||||||
onRowClick,
|
onRowClick,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
isSelectable = false,
|
isSelectable = false,
|
||||||
defaultTheme = 'bg-emerald-50',
|
defaultTheme = 'bg-emerald-50', // Blanc cassé pour les lignes paires
|
||||||
}) => {
|
}) => {
|
||||||
const handlePageChange = (newPage) => {
|
const handlePageChange = (newPage) => {
|
||||||
onPageChange(newPage);
|
onPageChange(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-gray-200">
|
<div className="bg-stone-50 rounded-lg border border-gray-300 shadow-md">
|
||||||
<table className="min-w-full bg-white">
|
<table className="min-w-full bg-stone-50">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<th
|
<th
|
||||||
key={index}
|
key={index}
|
||||||
className="py-2 px-4 border-b border-gray-200 bg-gray-100 text-center text-sm font-semibold text-gray-600"
|
className="py-2 px-4 border-b border-gray-300 bg-gray-100 text-center text-sm font-semibold text-gray-700"
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
</th>
|
</th>
|
||||||
@ -40,16 +40,21 @@ const Table = ({
|
|||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
className={`
|
className={`
|
||||||
${isSelectable ? 'cursor-pointer' : ''}
|
${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={() => {
|
onClick={() => {
|
||||||
if (isSelectable && onRowClick) {
|
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)) {
|
if (selectedRows?.includes(row.id)) {
|
||||||
onRowClick({ deselected: true, row }); // Désélectionner
|
onRowClick({ deselected: true, row });
|
||||||
} else {
|
} else {
|
||||||
onRowClick(row); // Sélectionner
|
onRowClick(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -57,7 +62,11 @@ const Table = ({
|
|||||||
{columns.map((column, colIndex) => (
|
{columns.map((column, colIndex) => (
|
||||||
<td
|
<td
|
||||||
key={colIndex}
|
key={colIndex}
|
||||||
className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`}
|
className={`py-2 px-4 border-b border-gray-300 text-center text-sm ${
|
||||||
|
selectedRows?.includes(row.id)
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-gray-700'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{renderCell
|
{renderCell
|
||||||
? renderCell(row, column.name)
|
? renderCell(row, column.name)
|
||||||
|
|||||||
@ -26,6 +26,9 @@ export const RegistrationFormStatus = {
|
|||||||
STATUS_SEPA_TO_SEND: 8,
|
STATUS_SEPA_TO_SEND: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CURRENT_YEAR = 'current_year';
|
export const CURRENT_YEAR_FILTER = 'current_year';
|
||||||
export const NEXT_YEAR = 'next_year';
|
export const NEXT_YEAR_FILTER = 'next_year';
|
||||||
export const HISTORICAL = 'historical';
|
export const HISTORICAL_FILTER = 'historical';
|
||||||
|
|
||||||
|
export const PARENT_FILTER = 'parents';
|
||||||
|
export const SCHOOL_FILTER = 'school';
|
||||||
|
|||||||
Reference in New Issue
Block a user