feat: Création nouveau style / pagination profils annuaires

This commit is contained in:
N3WT DE COMPET
2025-05-06 19:54:46 +02:00
parent 4fd40ac5fc
commit 760ee0009e
25 changed files with 430 additions and 247 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.parsers import JSONParser
from rest_framework import status
from Auth.pagination import CustomProfilesPagination
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
@ -20,13 +21,14 @@ import json
from . import validator
from .models import Profile, ProfileRole
from rest_framework.decorators import action, api_view
from django.db.models import Q
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
from Subscriptions.models import RegistrationForm, Guardian
import Subscriptions.mailManager as mailer
import Subscriptions.util as util
import logging
from N3wtSchool import bdd, error
from N3wtSchool import bdd, error, settings
from rest_framework_simplejwt.authentication import JWTAuthentication
@ -509,20 +511,55 @@ class ResetPasswordView(APIView):
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
class ProfileRoleView(APIView):
pagination_class = CustomProfilesPagination
@swagger_auto_schema(
operation_description="Obtenir la liste des profile_roles",
responses={200: ProfileRoleSerializer(many=True)}
)
def get(self, request):
filter = request.GET.get('filter', '').strip()
page_size = request.GET.get('page_size', None)
establishment_id = request.GET.get('establishment_id', None)
# Gestion du page_size
if page_size is not None:
try:
page_size = int(page_size)
except ValueError:
page_size = settings.NB_RESULT_PROFILES_PER_PAGE
if establishment_id is None:
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
profiles_roles_List = bdd.getAllObjects(_objectName=ProfileRole)
if profiles_roles_List:
profiles_roles_List = profiles_roles_List.filter(establishment=establishment_id).distinct().order_by('-updated_date')
profile_roles_serializer = ProfileRoleSerializer(profiles_roles_List, many=True)
return JsonResponse(profile_roles_serializer.data, safe=False)
# Récupérer les ProfileRole en fonction du filtre
profiles_roles_List = ProfileRole.objects.filter(establishment_id=establishment_id)
if filter == 'parents':
profiles_roles_List = profiles_roles_List.filter(role_type=ProfileRole.RoleType.PROFIL_PARENT)
elif filter == 'school':
profiles_roles_List = profiles_roles_List.filter(
Q(role_type=ProfileRole.RoleType.PROFIL_ECOLE) |
Q(role_type=ProfileRole.RoleType.PROFIL_ADMIN)
)
else:
return JsonResponse({'error': 'Filtre invalide'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
# Trier les résultats par date de mise à jour
profiles_roles_List = profiles_roles_List.distinct().order_by('-updated_date')
if not profiles_roles_List:
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
# Pagination
paginator = self.pagination_class()
page = paginator.paginate_queryset(profiles_roles_List, request)
if page is not None:
profile_roles_serializer = ProfileRoleSerializer(page, many=True)
response_data = paginator.get_paginated_response(profile_roles_serializer.data)
return JsonResponse(response_data, safe=False)
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
@swagger_auto_schema(
operation_description="Créer un nouveau profile_role",

View File

@ -282,12 +282,13 @@ DATE_FORMAT = '%d-%m-%Y %H:%M'
EXPIRATION_SESSION_NB_SEC = 10
NB_RESULT_PER_PAGE = 8
NB_RESULT_SUBSCRIPTIONS_PER_PAGE = 8
NB_RESULT_PROFILES_PER_PAGE = 15
NB_MAX_PAGE = 100
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomPagination',
'PAGE_SIZE': NB_RESULT_PER_PAGE,
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),

View File

@ -32,7 +32,7 @@ class Teacher(models.Model):
last_name = models.CharField(max_length=100)
first_name = models.CharField(max_length=100)
specialities = models.ManyToManyField(Speciality, blank=True)
profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='teacher_profile', blank=True)
profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='teacher_profile', null=True, blank=True)
updated_date = models.DateTimeField(auto_now=True)
def __str__(self):

View File

@ -31,7 +31,7 @@ class Guardian(models.Model):
address = models.CharField(max_length=200, default="", blank=True)
phone = models.CharField(max_length=200, default="", blank=True)
profession = models.CharField(max_length=200, default="", blank=True)
profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', blank=True)
profile_role = models.OneToOneField(ProfileRole, on_delete=models.CASCADE, related_name='guardian_profile', null=True, blank=True)
@property
def email(self):

View File

@ -2,10 +2,10 @@ from rest_framework.pagination import PageNumberPagination
from N3wtSchool import settings
class CustomPagination(PageNumberPagination):
class CustomSubscriptionPagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = settings.NB_MAX_PAGE
page_size = settings.NB_RESULT_PER_PAGE
page_size = settings.NB_RESULT_SUBSCRIPTIONS_PER_PAGE
def get_paginated_response(self, data):
return ({

View File

@ -137,9 +137,19 @@ class StudentSerializer(serializers.ModelSerializer):
def create_or_update_guardians(self, guardians_data):
guardians_ids = []
for guardian_data in guardians_data:
guardian_id = guardian_data.get('id', None)
profile_role_data = guardian_data.pop('profile_role_data', None)
profile_role = guardian_data.pop('profile_role', None)
if guardian_id:
# Si un ID est fourni, récupérer ou mettre à jour le Guardian existant
guardian_instance, created = Guardian.objects.update_or_create(
id=guardian_id,
defaults=guardian_data
)
guardians_ids.append(guardian_instance.id)
continue
if profile_role_data:
# Vérifiez si 'profile_data' est fourni pour créer un nouveau profil
profile_data = profile_role_data.pop('profile_data', None)

View File

@ -15,7 +15,7 @@ import Subscriptions.mailManager as mailer
import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.pagination import CustomPagination
from Subscriptions.pagination import CustomSubscriptionPagination
from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationSchoolFileTemplate, RegistrationFileGroup, RegistrationParentFileTemplate
from Subscriptions.automate import updateStateMachine
@ -29,7 +29,7 @@ class RegisterFormView(APIView):
"""
Gère la liste des dossiers dinscription, lecture et création.
"""
pagination_class = CustomPagination
pagination_class = CustomSubscriptionPagination
@swagger_auto_schema(
manual_parameters=[
@ -82,7 +82,7 @@ class RegisterFormView(APIView):
try:
page_size = int(page_size)
except ValueError:
page_size = settings.NB_RESULT_PER_PAGE
page_size = settings.NB_RESULT_SUBSCRIPTIONS_PER_PAGE
# Récupérer les années scolaires
current_year = util.getCurrentSchoolYear()
@ -179,7 +179,7 @@ class RegisterFormWithIdView(APIView):
"""
Gère la lecture, création, modification et suppression dun dossier dinscription.
"""
pagination_class = CustomPagination
pagination_class = CustomSubscriptionPagination
@swagger_auto_schema(
responses={200: RegistrationFormSerializer()},

View File

@ -1,125 +1,50 @@
'use client';
import React, { useState, useEffect } from 'react';
import {
fetchProfileRoles,
updateProfileRoles,
deleteProfileRoles,
} from '@/app/actions/authAction';
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
import { fetchProfileRoles } from '@/app/actions/authAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext';
import ProfileDirectory from '@/components/ProfileDirectory';
import { PARENT_FILTER, SCHOOL_FILTER } from '@/utils/constants';
export default function Page() {
const [profileRoles, setProfileRoles] = useState([]);
const [profileRolesDatasParent, setProfileRolesDatasParent] = useState([]);
const [profileRolesDatasSchool, setProfileRolesDatasSchool] = useState([]);
const [reloadFetch, setReloadFetch] = useState(false);
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const requestErrorHandler = (err) => {
logger.error('Error fetching data:', err);
};
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for profileRoles
// Fetch data for profileRolesParent
handleProfiles();
}
}, [selectedEstablishmentId, reloadFetch]);
const handleProfiles = () => {
fetchProfileRoles(selectedEstablishmentId)
fetchProfileRoles(selectedEstablishmentId, PARENT_FILTER)
.then((data) => {
setProfileRoles(data);
setProfileRolesDatasParent(data);
})
.catch((error) => logger.error('Error fetching profileRoles:', error));
.catch(requestErrorHandler);
fetchProfileRoles(selectedEstablishmentId, SCHOOL_FILTER)
.then((data) => {
setProfileRolesDatasSchool(data);
})
.catch(requestErrorHandler);
setReloadFetch(false);
};
const handleEdit = (profileRole) => {
const updatedData = { ...profileRole, is_active: !profileRole.is_active };
return updateProfileRoles(profileRole.id, updatedData, csrfToken)
.then((data) => {
setProfileRoles((prevState) =>
prevState.map((item) => (item.id === profileRole.id ? data : item))
);
return data;
})
.catch((error) => {
logger.error('Error editing data:', error);
throw error;
});
};
const handleDelete = (id) => {
return deleteProfileRoles(id, csrfToken)
.then(() => {
setProfileRoles((prevState) =>
prevState.filter((item) => item.id !== id)
);
logger.debug('Profile deleted successfully:', id);
})
.catch((error) => {
logger.error('Error deleting profile:', error);
throw error;
});
};
const handleDissociate = (studentId, guardianId) => {
return dissociateGuardian(studentId, guardianId)
.then((response) => {
logger.debug('Guardian dissociated successfully:', guardianId);
// Vérifier si le Guardian a été supprimé
const isGuardianDeleted = response?.isGuardianDeleted;
// Mettre à jour le modèle profileRoles
setProfileRoles(
(prevState) =>
prevState
.map((profileRole) => {
if (profileRole.associated_person?.id === guardianId) {
if (isGuardianDeleted) {
// Si le Guardian est supprimé, retirer le profileRole
return null;
} else {
// Si le Guardian n'est pas supprimé, mettre à jour les élèves associés
const updatedStudents =
profileRole.associated_person.students.filter(
(student) => student.id !== studentId
);
return {
...profileRole,
associated_person: {
...profileRole.associated_person,
students: updatedStudents, // Mettre à jour les élèves associés
},
};
}
}
setReloadFetch(true);
return profileRole; // Conserver les autres profileRoles
})
.filter(Boolean) // Supprimer les entrées nulles
);
})
.catch((error) => {
logger.error('Error dissociating guardian:', error);
throw error;
});
};
return (
<div className="p-8">
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<ProfileDirectory
profileRoles={profileRoles}
handleActivateProfile={handleEdit}
handleDeleteProfile={handleDelete}
handleDissociateGuardian={handleDissociate}
/>
</div>
<div className="w-full h-full">
<ProfileDirectory
parentProfiles={profileRolesDatasParent}
schoolProfiles={profileRolesDatasSchool}
/>
</div>
);
}

View File

@ -13,7 +13,7 @@ import { useEstablishment } from '@/context/EstablishmentContext';
// Composant EventCard pour afficher les événements
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">
<CalendarCheck className="text-blue-500" size={20} />
<div>
@ -125,7 +125,7 @@ export default function DashboardPage() {
{/* Événements et KPIs */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* 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">
{t('inscriptionTrends')}
</h2>
@ -136,24 +136,13 @@ export default function DashboardPage() {
</div>
{/* É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>
{upcomingEvents.map((event, index) => (
<EventCard key={index} {...event} />
))}
</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>
);
}

View File

@ -382,10 +382,6 @@ export default function CreateSubscriptionPage() {
(profile) => profile.id === formDataRef.current.existingProfileId
);
// Affichez le profil existant dans la console
console.log('Profil existant trouvé :', existingProfile?.email);
console.log('debug : ', initialGuardianEmail);
const guardians = (() => {
if (formDataRef.current.selectedGuardians.length > 0) {
// Cas 3 : Des guardians sont sélectionnés

View File

@ -47,9 +47,9 @@ import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
import {
RegistrationFormStatus,
CURRENT_YEAR,
NEXT_YEAR,
HISTORICAL,
CURRENT_YEAR_FILTER,
NEXT_YEAR_FILTER,
HISTORICAL_FILTER,
} from '@/utils/constants';
export default function Page({ params: { locale } }) {
@ -75,7 +75,7 @@ export default function Page({ params: { locale } }) {
useState(1);
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState(CURRENT_YEAR);
const [activeTab, setActiveTab] = useState(CURRENT_YEAR_FILTER);
const [totalCurrentYear, setTotalCurrentYear] = useState(0);
const [totalNextYear, setTotalNextYear] = useState(0);
const [totalHistorical, setTotalHistorical] = useState(0);
@ -190,7 +190,7 @@ export default function Page({ params: { locale } }) {
Promise.all([
fetchRegisterForms(
selectedEstablishmentId,
CURRENT_YEAR,
CURRENT_YEAR_FILTER,
currentSchoolYearPage,
itemsPerPage,
searchTerm
@ -204,10 +204,10 @@ export default function Page({ params: { locale } }) {
})
.catch(requestErrorHandler),
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR)
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER)
.then(registerFormNextYearDataHandler)
.catch(requestErrorHandler),
fetchRegisterForms(selectedEstablishmentId, HISTORICAL)
fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER)
.then(registerFormHistoricalDataHandler)
.catch(requestErrorHandler),
])
@ -232,17 +232,17 @@ export default function Page({ params: { locale } }) {
setIsLoading(true);
fetchRegisterForms(
selectedEstablishmentId,
CURRENT_YEAR,
CURRENT_YEAR_FILTER,
currentSchoolYearPage,
itemsPerPage,
searchTerm
)
.then(registerFormCurrrentYearDataHandler)
.catch(requestErrorHandler);
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR)
fetchRegisterForms(selectedEstablishmentId, NEXT_YEAR_FILTER)
.then(registerFormNextYearDataHandler)
.catch(requestErrorHandler);
fetchRegisterForms(selectedEstablishmentId, HISTORICAL)
fetchRegisterForms(selectedEstablishmentId, HISTORICAL_FILTER)
.then(registerFormHistoricalDataHandler)
.catch(requestErrorHandler);
@ -261,13 +261,13 @@ export default function Page({ params: { locale } }) {
* UseEffect to update page count of tab
*/
useEffect(() => {
if (activeTab === CURRENT_YEAR) {
if (activeTab === CURRENT_YEAR_FILTER) {
setTotalCurrentSchoolYearPages(
Math.ceil(totalCurrentYear / itemsPerPage)
);
} else if (activeTab === NEXT_YEAR) {
} else if (activeTab === NEXT_YEAR_FILTER) {
setTotalNextSchoolYearPages(Math.ceil(totalNextYear / itemsPerPage));
} else if (activeTab === HISTORICAL) {
} else if (activeTab === HISTORICAL_FILTER) {
setTotalHistoricalPages(Math.ceil(totalHistorical / itemsPerPage));
}
}, [currentSchoolYearPage]);
@ -376,11 +376,11 @@ export default function Page({ params: { locale } }) {
};
const handlePageChange = (newPage) => {
if (activeTab === CURRENT_YEAR) {
if (activeTab === CURRENT_YEAR_FILTER) {
setCurrentSchoolYearPage(newPage);
} else if (activeTab === NEXT_YEAR) {
} else if (activeTab === NEXT_YEAR_FILTER) {
setCurrentSchoolNextYearPage(newPage);
} else if (activeTab === HISTORICAL) {
} else if (activeTab === HISTORICAL_FILTER) {
setCurrentSchoolHistoricalYearPage(newPage);
}
};
@ -710,8 +710,8 @@ export default function Page({ params: { locale } }) {
</span>
</>
}
active={activeTab === CURRENT_YEAR}
onClick={() => setActiveTab(CURRENT_YEAR)}
active={activeTab === CURRENT_YEAR_FILTER}
onClick={() => setActiveTab(CURRENT_YEAR_FILTER)}
/>
{/* Tab pour l'année scolaire prochaine */}
@ -724,8 +724,8 @@ export default function Page({ params: { locale } }) {
</span>
</>
}
active={activeTab === NEXT_YEAR}
onClick={() => setActiveTab(NEXT_YEAR)}
active={activeTab === NEXT_YEAR_FILTER}
onClick={() => setActiveTab(NEXT_YEAR_FILTER)}
/>
{/* Tab pour l'historique */}
@ -738,16 +738,16 @@ export default function Page({ params: { locale } }) {
</span>
</>
}
active={activeTab === HISTORICAL}
onClick={() => setActiveTab(HISTORICAL)}
active={activeTab === HISTORICAL_FILTER}
onClick={() => setActiveTab(HISTORICAL_FILTER)}
/>
</div>
</div>
<div className="border-b border-gray-200 mb-6 w-full">
{activeTab === CURRENT_YEAR ||
activeTab === NEXT_YEAR ||
activeTab === HISTORICAL ? (
{activeTab === CURRENT_YEAR_FILTER ||
activeTab === NEXT_YEAR_FILTER ||
activeTab === HISTORICAL_FILTER ? (
<React.Fragment>
<div className="flex justify-between items-center mb-4 w-full">
<div className="relative flex-grow">
@ -779,25 +779,25 @@ export default function Page({ params: { locale } }) {
<Table
key={`${currentSchoolYearPage}-${searchTerm}`}
data={
activeTab === CURRENT_YEAR
activeTab === CURRENT_YEAR_FILTER
? registrationFormsDataCurrentYear
: activeTab === NEXT_YEAR
: activeTab === NEXT_YEAR_FILTER
? registrationFormsDataNextYear
: registrationFormsDataHistorical
}
columns={columns}
itemsPerPage={itemsPerPage}
currentPage={
activeTab === CURRENT_YEAR
activeTab === CURRENT_YEAR_FILTER
? currentSchoolYearPage
: activeTab === NEXT_YEAR
: activeTab === NEXT_YEAR_FILTER
? currentSchoolNextYearPage
: currentSchoolHistoricalYearPage
}
totalPages={
activeTab === CURRENT_YEAR
activeTab === CURRENT_YEAR_FILTER
? totalCurrentSchoolYearPages
: activeTab === NEXT_YEAR
: activeTab === NEXT_YEAR_FILTER
? totalNextSchoolYearPages
: totalHistoricalPages
}

View File

@ -9,7 +9,7 @@ import {
BE_AUTH_NEW_PASSWORD_URL,
FE_USERS_LOGIN_URL,
} from '@/utils/Url';
import logger from '@/utils/logger';
import { PARENT_FILTER } from '@/utils/constants';
const requestResponseHandler = async (response) => {
const body = await response.json();
@ -73,10 +73,21 @@ export const disconnect = () => {
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
};
export const fetchProfileRoles = (establishment) => {
return fetch(
`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
export const fetchProfileRoles = (
establishment,
filter = PARENT_FILTER,
page = '',
pageSize = ''
) => {
let url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}`;
if (page !== '' && pageSize !== '') {
url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};
export const updateProfileRoles = (id, data, csrfToken) => {

View File

@ -6,7 +6,7 @@ import {
BE_SUBSCRIPTION_ABSENCES_URL,
} from '@/utils/Url';
import { CURRENT_YEAR, NEXT_YEAR, HISTORICAL } from '@/utils/constants';
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
const requestResponseHandler = async (response) => {
const body = await response.json();
@ -21,7 +21,7 @@ const requestResponseHandler = async (response) => {
export const fetchRegisterForms = (
establishment,
filter = CURRENT_YEAR,
filter = CURRENT_YEAR_FILTER,
page = '',
pageSize = '',
search = ''

View File

@ -57,8 +57,8 @@ const PaymentModeSelector = ({
onClick={() => handleModeToggle(mode.id)}
className={`p-4 rounded-lg shadow-md text-center text-gray-700' ${
activePaymentModes.includes(mode.id)
? 'bg-emerald-300'
: 'bg-white'
? 'bg-emerald-100'
: 'bg-stone-50'
} hover:bg-emerald-200`}
>
{mode.name}

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 Table from '@/components/Table';
import Popup from '@/components/Popup';
import StatusLabel from '@/components/StatusLabel';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import SidebarTabs from '@/components/SidebarTabs';
import {
updateProfileRoles,
deleteProfileRoles,
} from '@/app/actions/authAction';
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
import { useCsrfToken } from '@/context/CsrfContext';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import logger from '@/utils/logger';
const roleTypeToLabel = (roleType) => {
switch (roleType) {
@ -31,25 +40,147 @@ const roleTypeToBadgeClass = (roleType) => {
}
};
const ProfileDirectory = ({
profileRoles,
handleActivateProfile,
handleDeleteProfile,
handleDissociateGuardian,
}) => {
const parentProfiles = profileRoles.filter(
(profileRole) => profileRole.role_type === 2
);
const schoolAdminProfiles = profileRoles.filter(
(profileRole) => profileRole.role_type !== 2
);
const ProfileDirectory = ({ parentProfiles, schoolProfiles }) => {
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
const [activeTab, setActiveTab] = useState('parent'); // Onglet actif
const [totalProfilesParentPages, setTotalProfilesParentPages] = useState(1);
const [totalProfilesSchoolPages, setTotalProfilesSchoolPages] = useState(1);
const [currentProfilesParentPage, setCurrentProfilesParentPage] = useState(1);
const [totalProfilesParent, setTotalProfilesParent] = useState(0);
const [totalProfilesSchool, setTotalProfilesSchool] = useState(0);
const [currentProfilesSchoolPage, setCurrentProfilesSchoolPage] = useState(1);
const [profileRolesParent, setProfileRolesParent] = useState([]);
const [profileRolesSchool, setProfileRolesSchool] = useState([]);
const itemsPerPage = 10; // Nombre d'éléments par page
const csrfToken = useCsrfToken();
const handleEdit = (profileRole) => {
const updatedData = { ...profileRole, is_active: !profileRole.is_active };
return updateProfileRoles(profileRole.id, updatedData, csrfToken)
.then((data) => {
setProfileRolesParent((prevState) =>
prevState.map((item) => (item.id === profileRole.id ? data : item))
);
return data;
})
.catch((error) => {
logger.error('Error editing data:', error);
throw error;
});
};
const handleDelete = (id) => {
return deleteProfileRoles(id, csrfToken)
.then(() => {
setProfileRolesParent((prevState) =>
prevState.filter((item) => item.id !== id)
);
logger.debug('Profile deleted successfully:', id);
})
.catch((error) => {
logger.error('Error deleting profile:', error);
throw error;
});
};
const handleDissociate = (studentId, guardianId) => {
return dissociateGuardian(studentId, guardianId)
.then((response) => {
logger.debug('Guardian dissociated successfully:', guardianId);
// Vérifier si le Guardian a été supprimé
const isGuardianDeleted = response?.isGuardianDeleted;
// Mettre à jour le modèle profileRolesParent
setProfileRolesParent(
(prevState) =>
prevState
.map((profileRole) => {
if (profileRole.associated_person?.id === guardianId) {
if (isGuardianDeleted) {
// Si le Guardian est supprimé, retirer le profileRole
return null;
} else {
// Si le Guardian n'est pas supprimé, mettre à jour les élèves associés
const updatedStudents =
profileRole.associated_person.students.filter(
(student) => student.id !== studentId
);
return {
...profileRole,
associated_person: {
...profileRole.associated_person,
students: updatedStudents, // Mettre à jour les élèves associés
},
};
}
}
return profileRole; // Conserver les autres profileRolesParent
})
.filter(Boolean) // Supprimer les entrées nulles
);
})
.catch((error) => {
logger.error('Error dissociating guardian:', error);
throw error;
});
};
const profilesRoleParentDataHandler = (data) => {
if (data) {
const { profilesRoles, count, page_size } = data;
if (profilesRoles) {
setProfileRolesParent(profilesRoles);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
setTotalProfilesParent(count);
setTotalProfilesParentPages(calculatedTotalPages);
}
};
const profilesRoleSchoolDataHandler = (data) => {
if (data) {
const { profilesRoles, count, page_size } = data;
if (profilesRoles) {
setProfileRolesSchool(profilesRoles);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
setTotalProfilesSchool(count);
setTotalProfilesSchoolPages(calculatedTotalPages);
}
};
useEffect(() => {
profilesRoleParentDataHandler(parentProfiles);
profilesRoleSchoolDataHandler(schoolProfiles);
if (activeTab === 'parent') {
setTotalProfilesParentPages(
Math.ceil(totalProfilesParent / itemsPerPage)
);
} else if (activeTab === 'school') {
setTotalProfilesSchoolPages(
Math.ceil(totalProfilesSchool / itemsPerPage)
);
}
}, [parentProfiles, schoolProfiles, activeTab]);
const handlePageChange = (newPage) => {
if (activeTab === 'parent') {
setCurrentProfilesParentPage(newPage);
} else if (activeTab === 'school') {
setCurrentProfilesSchoolPage(newPage);
}
};
const handleTooltipVisibility = (id) => {
setVisibleTooltipId(id); // Définir l'ID de la ligne pour laquelle la tooltip est visible
@ -64,7 +195,7 @@ const ProfileDirectory = ({
`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`
);
setConfirmPopupOnConfirm(() => () => {
handleActivateProfile(profileRole)
handleEdit(profileRole)
.then(() => {
setPopupMessage(
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
@ -85,7 +216,7 @@ const ProfileDirectory = ({
const handleConfirmDeleteProfile = (id) => {
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
setConfirmPopupOnConfirm(() => () => {
handleDeleteProfile(id)
handleDelete(id)
.then(() => {
setPopupMessage('Le profil a été supprimé avec succès.');
setPopupVisible(true);
@ -105,7 +236,7 @@ const ProfileDirectory = ({
`Vous êtes sur le point de dissocier le responsable ${profileRole.associated_person?.guardian_name} de l'élève ${student.student_name}. Êtes-vous sûr de vouloir poursuivre cette opération ?`
);
setConfirmPopupOnConfirm(() => () => {
handleDissociateGuardian(student.id, profileRole.associated_person?.id)
handleDissociate(student.id, profileRole.associated_person?.id)
.then(() => {
setPopupMessage('Le responsable a été dissocié avec succès.');
setPopupVisible(true);
@ -315,23 +446,64 @@ const ProfileDirectory = ({
];
return (
<div className="bg-white rounded-lg shadow-lg w-3/5 p-6">
<div className="space-y-8">
<div className="max-h-128 overflow-y-auto border rounded p-4">
{parentProfiles.length === 0 ? (
<div>Aucun profil trouvé</div>
) : (
<Table data={parentProfiles} columns={parentColumns} />
)}
</div>
<div className="max-h-128 overflow-y-auto border rounded p-4">
{schoolAdminProfiles.length === 0 ? (
<div>Aucun profil trouvé</div>
) : (
<Table data={schoolAdminProfiles} columns={schoolAdminColumns} />
)}
</div>
</div>
<>
<DjangoCSRFToken csrfToken={csrfToken} />
<SidebarTabs
tabs={[
{
id: 'parent',
label: 'Parents',
content: (
<div className="h-full overflow-y-auto">
<Table
key={`parent-${currentProfilesParentPage}`}
data={
Array.isArray(profileRolesParent)
? 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>
),
},
{
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
visible={popupVisible}
message={popupMessage}
@ -344,7 +516,7 @@ const ProfileDirectory = ({
onConfirm={confirmPopupOnConfirm}
onCancel={() => setConfirmPopupVisible(false)}
/>
</div>
</>
);
};

View File

@ -6,10 +6,8 @@ import ProfileSelector from '@/components/ProfileSelector';
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
<div
onClick={onClick}
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer ${
active
? 'bg-emerald-50 text-emerald-600'
: 'text-gray-600 hover:bg-gray-50'
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer hover:bg-emerald-100 ${
active ? 'bg-emerald-50 text-emerald-600' : 'text-gray-600'
}`}
>
<Icon size={20} />
@ -35,7 +33,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) {
};
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 ">
<ProfileSelector className="border-none" />
</div>

View File

@ -1,34 +1,46 @@
import React, { useState } from 'react';
const SidebarTabs = ({ tabs }) => {
const SidebarTabs = ({ tabs, onTabChange }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
const handleTabChange = (tabId) => {
setActiveTab(tabId);
if (onTabChange) {
onTabChange(tabId);
}
};
return (
<>
<div className="flex h-14 border-b-2 border-gray-200">
<div className="flex flex-col h-full">
{/* Tabs Header */}
<div className="flex h-14 bg-gray-50 border-b border-gray-200 shadow-sm">
{tabs.map((tab) => (
<button
key={tab.id}
className={`flex-1 p-4 ${
className={`flex-1 text-center p-4 font-medium transition-colors duration-200 ${
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'
}`}
onClick={() => setActiveTab(tab.id)}
onClick={() => handleTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab) => (
<div
key={tab.id}
className={`${activeTab === tab.id ? 'block h-[calc(100%-3.5rem)]' : 'hidden'}`}
>
{tab.content}
</div>
))}
</>
{/* Tabs Content */}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner">
{tabs.map((tab) => (
<div
key={tab.id}
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
>
{tab.content}
</div>
))}
</div>
</div>
);
};

View File

@ -1,6 +1,6 @@
// Composant StatCard pour afficher une statistique
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>
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>

View File

@ -22,7 +22,7 @@ const StructureManagement = ({
handleDelete,
}) => {
return (
<div className="w-full p-4 mx-auto mt-6">
<div className="w-full">
<ClassesProvider>
<div className="mt-8 w-2/5">
<SpecialitiesSection

View File

@ -462,7 +462,7 @@ export default function FilesGroupsManagement({
}
return (
<div className="w-full p-4 mx-auto mt-6">
<div className="w-full">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}

View File

@ -360,7 +360,7 @@ const DiscountsSection = ({
data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={columns}
renderCell={renderDiscountCell}
defaultTheme="bg-yellow-100"
defaultTheme="bg-yellow-50"
/>
<Popup
visible={popupVisible}

View File

@ -50,7 +50,7 @@ const FeesManagement = ({
};
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">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">

View File

@ -13,21 +13,21 @@ const Table = ({
onRowClick,
selectedRows,
isSelectable = false,
defaultTheme = 'bg-emerald-50',
defaultTheme = 'bg-emerald-50', // Blanc cassé pour les lignes paires
}) => {
const handlePageChange = (newPage) => {
onPageChange(newPage);
};
return (
<div className="bg-white rounded-lg border border-gray-200">
<table className="min-w-full bg-white">
<div className="bg-stone-50 rounded-lg border border-gray-300 shadow-md">
<table className="min-w-full bg-stone-50">
<thead>
<tr>
{columns.map((column, index) => (
<th
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}
</th>
@ -40,16 +40,21 @@ const Table = ({
key={rowIndex}
className={`
${isSelectable ? 'cursor-pointer' : ''}
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''}
${
selectedRows?.includes(row.id)
? 'bg-emerald-200 text-white'
: rowIndex % 2 === 0
? `${defaultTheme}`
: 'bg-stone-50' // Blanc cassé pour les lignes impaires
}
${isSelectable ? 'hover:bg-emerald-100' : ''}
`}
onClick={() => {
if (isSelectable && onRowClick) {
// Si la ligne est déjà sélectionnée, transmettre une indication explicite de désélection
if (selectedRows?.includes(row.id)) {
onRowClick({ deselected: true, row }); // Désélectionner
onRowClick({ deselected: true, row });
} else {
onRowClick(row); // Sélectionner
onRowClick(row);
}
}
}}
@ -57,7 +62,11 @@ const Table = ({
{columns.map((column, colIndex) => (
<td
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(row, column.name)

View File

@ -26,6 +26,9 @@ export const RegistrationFormStatus = {
STATUS_SEPA_TO_SEND: 8,
};
export const CURRENT_YEAR = 'current_year';
export const NEXT_YEAR = 'next_year';
export const HISTORICAL = 'historical';
export const CURRENT_YEAR_FILTER = 'current_year';
export const NEXT_YEAR_FILTER = 'next_year';
export const HISTORICAL_FILTER = 'historical';
export const PARENT_FILTER = 'parents';
export const SCHOOL_FILTER = 'school';