feat: Merge remote-tracking branch 'origin/WIP_style' into develop

This commit is contained in:
N3WT DE COMPET
2025-05-06 21:02:24 +02:00
26 changed files with 446 additions and 248 deletions

View 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 }
)

View File

@ -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",

View File

@ -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',
), ),

View File

@ -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):

View File

@ -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):

View File

@ -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 ({

View File

@ -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)

View File

@ -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 dinscription, lecture et création. Gère la liste des dossiers dinscription, 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 dun dossier dinscription. Gère la lecture, création, modification et suppression dun dossier dinscription.
""" """
pagination_class = CustomPagination pagination_class = CustomSubscriptionPagination
@swagger_auto_schema( @swagger_auto_schema(
responses={200: RegistrationFormSerializer()}, responses={200: RegistrationFormSerializer()},

View File

@ -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} />
<div className="w-full p-4">
<ProfileDirectory <ProfileDirectory
profileRoles={profileRoles} parentProfiles={profileRolesDatasParent}
handleActivateProfile={handleEdit} schoolProfiles={profileRolesDatasSchool}
handleDeleteProfile={handleDelete}
handleDissociateGuardian={handleDissociate}
/> />
</div> </div>
</div>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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) => {

View File

@ -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 = ''

View File

@ -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}

View File

@ -124,25 +124,40 @@ const PaymentPlanSelector = ({
const handleRowClick = (row) => { const handleRowClick = (row) => {
const value = row.id; const value = row.id;
if (selectedFrequency === value) { if (selectedFrequency === value) {
setSelectedFrequency(null); // Désélectionner l'onglet si la ligne est déjà sélectionnée setSelectedFrequency(null); // Désélectionner l'onglet si la ligne est déjà sélectionnée
} else { } else {
setSelectedFrequency(value); setSelectedFrequency(value);
if (!dates[value]) { if (!dates[value]) {
const frequencyValue = const frequencyValue =
paymentPlansOptions.find((plan) => plan.id === value)?.frequency || 1; paymentPlansOptions.find((plan) => plan.id === value)?.frequency || 1;
const newDates = Array(frequencyValue) const newDates = Array(frequencyValue)
.fill('') .fill('')
.map((_, index) => { .map((_, index) => {
const newDate = new Date(); const newDate = new Date();
newDate.setDate(defaultDay);
// Validate defaultDay
const day =
typeof defaultDay === 'number' &&
defaultDay >= 1 &&
defaultDay <= 31
? defaultDay
: 1; // Fallback to 1 if defaultDay is invalid
newDate.setDate(day);
if (value === 1) { if (value === 1) {
newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois
} else { } else {
newDate.setMonth(newDate.getMonth() + index); newDate.setMonth(newDate.getMonth() + index);
} }
return newDate.toISOString().split('T')[0]; return newDate.toISOString().split('T')[0];
}); });
setDates((prevDates) => ({ ...prevDates, [value]: newDates })); setDates((prevDates) => ({ ...prevDates, [value]: newDates }));
} }
} }

View File

@ -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,
currentProfilesParentPage * itemsPerPage
)
: [] // Fallback to an empty array if profileRolesParent is not an array
}
columns={parentColumns}
itemsPerPage={itemsPerPage}
currentPage={currentProfilesParentPage}
totalPages={totalProfilesParentPages}
onPageChange={handlePageChange}
/>
</div> </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> </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> </>
); );
}; };

View File

@ -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>

View File

@ -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 Content */}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner">
{tabs.map((tab) => ( {tabs.map((tab) => (
<div <div
key={tab.id} key={tab.id}
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
> >
{tab.content} {tab.content}
</div> </div>
))} ))}
</> </div>
</div>
); );
}; };

View File

@ -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>

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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">

View File

@ -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)

View File

@ -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';