feat: gestion des no data dans les table [#33]

This commit is contained in:
N3WT DE COMPET
2025-05-17 14:35:33 +02:00
parent 3990d75e52
commit 2888f8dcce
14 changed files with 258 additions and 103 deletions

View File

@ -1,4 +1,4 @@
{
"presets": ["next/babel"],
"plugins": []
}
}

View File

@ -41,4 +41,4 @@
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14"
}
}
}

View File

@ -17,6 +17,7 @@ import { dissociateGuardian } from '@/app/actions/subscriptionAction';
import { useCsrfToken } from '@/context/CsrfContext';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import logger from '@/utils/logger';
import AlertMessage from '@/components/AlertMessage';
const roleTypeToLabel = (roleType) => {
switch (roleType) {
@ -191,7 +192,7 @@ export default function Page() {
setProfileRolesParent(profilesRoles);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
count === 0 ? count : Math.ceil(count / page_size);
setTotalProfilesParent(count);
setTotalProfilesParentPages(calculatedTotalPages);
}
@ -204,7 +205,7 @@ export default function Page() {
setProfileRolesSchool(profilesRoles);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
count === 0 ? count : Math.ceil(count / page_size);
setTotalProfilesSchool(count);
setTotalProfilesSchoolPages(calculatedTotalPages);
}
@ -505,7 +506,7 @@ export default function Page() {
id: 'parent',
label: 'Parents',
content: (
<div className="h-full overflow-y-auto">
<div className="h-full overflow-y-auto p-4">
<Table
key={`parent-${currentProfilesParentPage}`}
data={profileRolesParent}
@ -514,6 +515,13 @@ export default function Page() {
currentPage={currentProfilesParentPage}
totalPages={totalProfilesParentPages}
onPageChange={handlePageChange}
emptyMessage={
<AlertMessage
type="info"
title="Aucun profil PARENT enregistré"
message="Un profil Parent est ajouté lors de la création d'un nouveau dossier d'inscription."
/>
}
/>
</div>
),
@ -522,7 +530,7 @@ export default function Page() {
id: 'school',
label: 'École',
content: (
<div className="h-full overflow-y-auto">
<div className="h-full overflow-y-auto p-4">
<Table
key={`school-${currentProfilesSchoolPage}`}
data={profileRolesSchool}
@ -531,6 +539,13 @@ export default function Page() {
currentPage={currentProfilesSchoolPage}
totalPages={totalProfilesSchoolPages}
onPageChange={handlePageChange}
emptyMessage={
<AlertMessage
type="info"
title="Aucun profil ECOLE enregistré"
message="Un profil ECOLE est ajouté lors de la création d'un nouvel enseignant."
/>
}
/>
</div>
),

View File

@ -14,7 +14,6 @@ import {
fetchSpecialities,
fetchTeachers,
fetchClasses,
fetchSchedules,
fetchRegistrationDiscounts,
fetchTuitionDiscounts,
fetchRegistrationFees,
@ -267,69 +266,77 @@ export default function Page() {
id: 'Configuration',
label: 'Classes',
content: (
<StructureManagement
specialities={specialities}
setSpecialities={setSpecialities}
teachers={teachers}
setTeachers={setTeachers}
classes={classes}
setClasses={setClasses}
profiles={profiles}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
<div className="h-full overflow-y-auto p-4">
<StructureManagement
specialities={specialities}
setSpecialities={setSpecialities}
teachers={teachers}
setTeachers={setTeachers}
classes={classes}
setClasses={setClasses}
profiles={profiles}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div>
),
},
{
id: 'Schedule',
label: 'Emploi du temps',
content: (
<ClassesProvider>
<ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning}
classes={classes}
specialities={specialities}
teachers={teachers}
/>
</ClassesProvider>
<div className="h-full overflow-y-auto p-4">
<ClassesProvider>
<ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning}
classes={classes}
specialities={specialities}
teachers={teachers}
/>
</ClassesProvider>
</div>
),
},
{
id: 'Fees',
label: 'Tarifs',
content: (
<FeesManagement
registrationDiscounts={registrationDiscounts}
setRegistrationDiscounts={setRegistrationDiscounts}
tuitionDiscounts={tuitionDiscounts}
setTuitionDiscounts={setTuitionDiscounts}
registrationFees={registrationFees}
setRegistrationFees={setRegistrationFees}
tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees}
registrationPaymentPlans={registrationPaymentPlans}
setRegistrationPaymentPlans={setRegistrationPaymentPlans}
tuitionPaymentPlans={tuitionPaymentPlans}
setTuitionPaymentPlans={setTuitionPaymentPlans}
registrationPaymentModes={registrationPaymentModes}
setRegistrationPaymentModes={setRegistrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
setTuitionPaymentModes={setTuitionPaymentModes}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
<div className="h-full overflow-y-auto p-4">
<FeesManagement
registrationDiscounts={registrationDiscounts}
setRegistrationDiscounts={setRegistrationDiscounts}
tuitionDiscounts={tuitionDiscounts}
setTuitionDiscounts={setTuitionDiscounts}
registrationFees={registrationFees}
setRegistrationFees={setRegistrationFees}
tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees}
registrationPaymentPlans={registrationPaymentPlans}
setRegistrationPaymentPlans={setRegistrationPaymentPlans}
tuitionPaymentPlans={tuitionPaymentPlans}
setTuitionPaymentPlans={setTuitionPaymentPlans}
registrationPaymentModes={registrationPaymentModes}
setRegistrationPaymentModes={setRegistrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
setTuitionPaymentModes={setTuitionPaymentModes}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div>
),
},
{
id: 'Files',
label: 'Documents',
content: (
<FilesGroupsManagement
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
/>
<div className="h-full overflow-y-auto p-4">
<FilesGroupsManagement
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
/>
</div>
),
},
];

View File

@ -51,6 +51,7 @@ import {
NEXT_YEAR_FILTER,
HISTORICAL_FILTER,
} from '@/utils/constants';
import AlertMessage from '@/components/AlertMessage';
export default function Page({ params: { locale } }) {
const t = useTranslations('subscriptions');
@ -136,7 +137,7 @@ export default function Page({ params: { locale } }) {
setRegistrationFormsDataCurrentYear(registerForms);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
count === 0 ? count : Math.ceil(count / page_size);
setTotalCurrentYear(count);
setTotalCurrentSchoolYearPages(calculatedTotalPages);
}
@ -156,7 +157,7 @@ export default function Page({ params: { locale } }) {
setRegistrationFormsDataNextYear(registerForms);
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
count === 0 ? count : Math.ceil(count / page_size);
setTotalNextYear(count);
setTotalNextSchoolYearPages(calculatedTotalPages);
}
@ -177,7 +178,7 @@ export default function Page({ params: { locale } }) {
}
const calculatedTotalPages =
count === 0 ? 1 : Math.ceil(count / page_size);
count === 0 ? count : Math.ceil(count / page_size);
setTotalHistorical(count);
setTotalHistoricalPages(calculatedTotalPages);
}
@ -692,6 +693,33 @@ export default function Page({ params: { locale } }) {
},
];
let emptyMessage;
if (activeTab === CURRENT_YEAR_FILTER) {
emptyMessage = (
<AlertMessage
type="warning"
title="Aucun dossier d'inscription pour l'année en cours"
message="Veuillez procéder à la création d'un nouveau dossier d'inscription pour l'année scolaire en cours."
/>
);
} else if (activeTab === NEXT_YEAR_FILTER) {
emptyMessage = (
<AlertMessage
type="info"
title="Aucun dossier d'inscription pour l'année prochaine"
message="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
/>
);
} else if (activeTab === HISTORICAL_FILTER) {
emptyMessage = (
<AlertMessage
type="info"
title="Aucun dossier d'inscription historique"
message="Aucun dossier archivé n'est disponible pour les années précédentes."
/>
);
}
if (isLoading) {
return <Loader />;
}
@ -802,6 +830,7 @@ export default function Page({ params: { locale } }) {
: totalHistoricalPages
}
onPageChange={handlePageChange}
emptyMessage={emptyMessage}
/>
</div>
</React.Fragment>

View File

@ -81,7 +81,7 @@ export default function FilesToUpload({
},
{
name: 'Priorité',
transform: (row) => (
transform: (row) =>
row.is_required ? (
<span className="flex items-center justify-center">
<span className="px-2 py-1 rounded-full bg-red-100 text-red-600 text-xs font-semibold">
@ -94,8 +94,7 @@ export default function FilesToUpload({
Optionnel
</span>
</span>
)
),
),
},
{
name: 'Statut',

View File

@ -16,6 +16,7 @@ import { FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL } from '@/utils/Url';
import { usePlanning } from '@/context/PlanningContext';
import { useClasses } from '@/context/ClassesContext';
import { useRouter } from 'next/navigation';
import AlertMessage from '@/components/AlertMessage';
const ItemTypes = {
TEACHER: 'teacher',
@ -544,6 +545,13 @@ const ClassesSection = ({
data={newClass ? [newClass, ...classes] : classes}
columns={columns}
renderCell={renderClassCell}
emptyMessage={
<AlertMessage
type="warning"
title="Aucune classe enregistrée"
message="Veuillez procéder à la création d'une nouvelle classe."
/>
}
/>
<Popup
visible={popupVisible}

View File

@ -9,6 +9,7 @@ import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'
import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import AlertMessage from '@/components/AlertMessage';
const SpecialitiesSection = ({
specialities,
@ -255,6 +256,13 @@ const SpecialitiesSection = ({
data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns}
renderCell={renderSpecialityCell}
emptyMessage={
<AlertMessage
type="warning"
title="Aucune spécialité enregistrée"
message="Veuillez procéder à la création d'une nouvelle spécialité."
/>
}
/>
<Popup
visible={popupVisible}

View File

@ -12,6 +12,7 @@ import TeacherItem from './TeacherItem';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
import SectionHeader from '@/components/SectionHeader';
import AlertMessage from '@/components/AlertMessage';
const ItemTypes = {
SPECIALITY: 'speciality',
@ -579,6 +580,13 @@ const TeachersSection = ({
data={newTeacher ? [newTeacher, ...teachers] : teachers}
columns={columns}
renderCell={renderTeacherCell}
emptyMessage={
<AlertMessage
type="warning"
title="Aucun enseignant enregistré"
message="Veuillez procéder à la création d'un nouvel enseignant."
/>
}
/>
<Popup
visible={popupVisible}

View File

@ -30,6 +30,7 @@ import SectionHeader from '@/components/SectionHeader';
import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage';
export default function FilesGroupsManagement({
csrfToken,
@ -542,7 +543,17 @@ export default function FilesGroupsManagement({
buttonOpeningModal={true}
onClick={() => setIsGroupModalOpen(true)}
/>
<Table data={groups} columns={columnsGroups} />
<Table
data={groups}
columns={columnsGroups}
emptyMessage={
<AlertMessage
type="warning"
title="Aucun dossier d'inscription enregistré"
message="Veuillez procéder à la création d'un nouveau dossier d'inscription"
/>
}
/>
</div>
{/* Section Fichiers */}
@ -558,7 +569,17 @@ export default function FilesGroupsManagement({
setIsEditing(false);
}}
/>
<Table data={filteredFiles} columns={columnsFiles} />
<Table
data={filteredFiles}
columns={columnsFiles}
emptyMessage={
<AlertMessage
type="warning"
title="Aucun formulaire enregistré"
message="Veuillez procéder à la création d'un nouveau formulaire à signer"
/>
}
/>
</div>
{/* Section Pièces à fournir */}

View File

@ -10,6 +10,7 @@ import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader';
import ToggleSwitch from '@/components/ToggleSwitch';
import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage';
export default function ParentFilesSection({
parentFiles,
@ -49,7 +50,7 @@ export default function ParentFilesSection({
const handleSaveDocument = () => {
if (!formData.name) {
showNotification(
'Veuillez saisir un nom de document pour valider l\'opération',
"Veuillez saisir un nom de document pour valider l'opération",
'error',
'Erreur'
);
@ -57,7 +58,7 @@ export default function ParentFilesSection({
}
if (selectedGroups.length === 0) {
showNotification(
'Veuillez sélectionner au moins un dossier d\'inscription pour valider l\'opération',
"Veuillez sélectionner au moins un dossier d'inscription pour valider l'opération",
'error',
'Erreur'
);
@ -267,7 +268,11 @@ export default function ParentFilesSection({
setRemovePopupOnConfirm(() => () => {
handleRemoveDocument(document.id)
.then(() => {
showNotification('Le document "${document.name}" a été correctement supprimé.', 'success', 'Succès');
showNotification(
'Le document "${document.name}" a été correctement supprimé.',
'success',
'Succès'
);
setRemovePopupVisible(false);
})
.catch((error) => {
@ -334,6 +339,13 @@ export default function ParentFilesSection({
editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles
}
columns={columnsRequiredDocuments}
emptyMessage={
<AlertMessage
type="warning"
title="Aucune pièce à fournir enregistrée"
message="Veuillez procéder à la création de nouvelles pièces à fournir par les parents"
/>
}
/>
<Popup
visible={removePopupVisible}

View File

@ -7,6 +7,7 @@ import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext';
import AlertMessage from '@/components/AlertMessage';
const DiscountsSection = ({
discounts,
@ -344,6 +345,25 @@ const DiscountsSection = ({
{ name: 'ACTIONS', label: 'Actions' },
];
let emptyMessage;
if (type === 0) {
emptyMessage = (
<AlertMessage
type="info"
title="Aucune réduction enregistrée"
message="Aucune réduction sur les frais d'inscription n'a été enregistrée"
/>
);
} else {
emptyMessage = (
<AlertMessage
type="info"
title="Aucune réduction enregistrée"
message="Aucune réduction sur les frais de scolarité n'a été enregistrée"
/>
);
}
return (
<div className="space-y-4">
{!subscriptionMode && (
@ -361,6 +381,7 @@ const DiscountsSection = ({
columns={columns}
renderCell={renderDiscountCell}
defaultTheme="bg-yellow-50"
emptyMessage={emptyMessage}
/>
<Popup
visible={popupVisible}

View File

@ -7,11 +7,11 @@ import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext';
import AlertMessage from '@/components/AlertMessage';
const FeesSection = ({
fees,
setFees,
discounts,
handleCreate,
handleEdit,
handleDelete,
@ -317,6 +317,25 @@ const FeesSection = ({
{ name: 'ACTIONS', label: 'Actions' },
];
let emptyMessage;
if (type === 0) {
emptyMessage = (
<AlertMessage
type="warning"
title="Aucun frais d'inscription enregistré"
message="Veuillez procéder à la création de nouveaux frais d'inscription"
/>
);
} else {
emptyMessage = (
<AlertMessage
type="warning"
title="Aucun frais de scolarité enregistré"
message="Veuillez procéder à la création de nouveaux frais de scolarité"
/>
);
}
return (
<div className="space-y-4">
{!subscriptionMode && (
@ -332,6 +351,7 @@ const FeesSection = ({
data={newFee ? [newFee, ...fees] : fees}
columns={columns}
renderCell={renderFeeCell}
emptyMessage={emptyMessage}
/>
<Popup
isOpen={popupVisible}

View File

@ -14,6 +14,7 @@ const Table = ({
selectedRows,
isSelectable = false,
defaultTheme = 'bg-emerald-50', // Blanc cassé pour les lignes paires
emptyMessage = null,
}) => {
const handlePageChange = (newPage) => {
onPageChange(newPage);
@ -35,49 +36,55 @@ const Table = ({
</tr>
</thead>
<tbody>
{data?.map((row, rowIndex) => (
<tr
key={rowIndex}
className={`
${isSelectable ? 'cursor-pointer' : ''}
${
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) {
if (selectedRows?.includes(row.id)) {
onRowClick({ deselected: true, row });
} else {
onRowClick(row);
}
}
}}
>
{columns.map((column, colIndex) => (
<td
key={colIndex}
className={`py-2 px-4 border-b border-gray-300 text-center text-sm ${
{data && data.length > 0 ? (
data.map((row, rowIndex) => (
<tr
key={rowIndex}
className={`
${isSelectable ? 'cursor-pointer' : ''}
${
selectedRows?.includes(row.id)
? 'text-white'
: 'text-gray-700'
}`}
>
{renderCell
? renderCell(row, column.name)
: column.transform(row)}
</td>
))}
? 'bg-emerald-200 text-white'
: rowIndex % 2 === 0
? `${defaultTheme}`
: 'bg-stone-50'
}
${isSelectable ? 'hover:bg-emerald-100' : ''}
`}
onClick={() => {
if (isSelectable && onRowClick) {
if (selectedRows?.includes(row.id)) {
onRowClick({ deselected: true, row });
} else {
onRowClick(row);
}
}
}}
>
{columns.map((column, colIndex) => (
<td
key={colIndex}
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)
: column.transform(row)}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length}>{emptyMessage}</td>
</tr>
))}
)}
</tbody>
</table>
{itemsPerPage > 0 && (
{itemsPerPage > 0 && data && data.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}