mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Création nouveau style / pagination profils annuaires
This commit is contained in:
@ -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}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user