chore: application prettier

This commit is contained in:
Luc SORIGNET
2025-04-15 19:37:47 +02:00
parent dd0884bbce
commit f7666c894b
174 changed files with 10609 additions and 8760 deletions

View File

@ -0,0 +1,3 @@
node_modules/
build/
dist/

6
Front-End/.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,4 +1,3 @@
{
"responsable": "Guardian",
"delete": "Delete",
@ -10,4 +9,4 @@
"profession": "Profession",
"address": "Address",
"add_responsible": "Add guardian"
}
}

View File

@ -1,10 +1,10 @@
{
"dashboard": "Dashboard",
"totalStudents": "Total Students",
"pendingRegistrations": "Pending Registration",
"reInscriptionRate": "Re-enrollment Rate",
"structureCapacity": "Structure Capacity",
"capacityRate": "Capacity Rate",
"inscriptionTrends": "Enrollment Trends",
"upcomingEvents": "Upcoming Events"
}
"dashboard": "Dashboard",
"totalStudents": "Total Students",
"pendingRegistrations": "Pending Registration",
"reInscriptionRate": "Re-enrollment Rate",
"structureCapacity": "Structure Capacity",
"capacityRate": "Capacity Rate",
"inscriptionTrends": "Enrollment Trends",
"upcomingEvents": "Upcoming Events"
}

View File

@ -1,5 +1,5 @@
{
"welcomeParents": "Welcome Parents",
"pleaseLogin": "Please login to access your account",
"loginButton": "Go to login page"
}
"welcomeParents": "Welcome Parents",
"pleaseLogin": "Please login to access your account",
"loginButton": "Go to login page"
}

View File

@ -1,6 +1,6 @@
{
"page": "Page",
"of": "of",
"previous": "Previous",
"next": "Next"
}
"page": "Page",
"of": "of",
"previous": "Previous",
"next": "Next"
}

View File

@ -1,10 +1,10 @@
{
"dashboard": "Dashboard",
"subscriptions": "Subscriptions",
"structure": "Structure",
"directory": "Directory",
"events": "Events",
"grades": "Grades",
"settings": "Settings",
"schoolAdmin": "School Administration"
}
"dashboard": "Dashboard",
"subscriptions": "Subscriptions",
"structure": "Structure",
"directory": "Directory",
"events": "Events",
"grades": "Grades",
"settings": "Settings",
"schoolAdmin": "School Administration"
}

View File

@ -1,33 +1,33 @@
{
"headerBarTitle": "Administration",
"addStudent": "New",
"allStudents": "All Students",
"pending": "Pending Registrations",
"subscribed": "Subscribed",
"archived": "Archived",
"name": "Name",
"class": "Class",
"status": "Status",
"attendance": "Attendance",
"lastEvaluation": "Last Evaluation",
"active": "Active",
"pendingStatus": "Pending",
"goodAttendance": "Good",
"averageAttendance": "Average",
"lowAttendance": "Poor",
"searchStudent": "Search for a student...",
"title": "Registration",
"information": "Information",
"no_records": "There are currently no registration records.",
"add_button": "Add",
"create_first_record": "Please click the ADD button to create your first registration record.",
"studentName":"Student name",
"studentFistName":"Student first name",
"mainContactMail":"Main contact email",
"phone":"Phone",
"lastUpdateDate":"Last update",
"classe":"Class",
"registrationFileStatus":"Registration file status",
"files":"Files",
"subscribeFiles":"Subscribe files"
}
"headerBarTitle": "Administration",
"addStudent": "New",
"allStudents": "All Students",
"pending": "Pending Registrations",
"subscribed": "Subscribed",
"archived": "Archived",
"name": "Name",
"class": "Class",
"status": "Status",
"attendance": "Attendance",
"lastEvaluation": "Last Evaluation",
"active": "Active",
"pendingStatus": "Pending",
"goodAttendance": "Good",
"averageAttendance": "Average",
"lowAttendance": "Poor",
"searchStudent": "Search for a student...",
"title": "Registration",
"information": "Information",
"no_records": "There are currently no registration records.",
"add_button": "Add",
"create_first_record": "Please click the ADD button to create your first registration record.",
"studentName": "Student name",
"studentFistName": "Student first name",
"mainContactMail": "Main contact email",
"phone": "Phone",
"lastUpdateDate": "Last update",
"classe": "Class",
"registrationFileStatus": "Registration file status",
"files": "Files",
"subscribeFiles": "Subscribe files"
}

View File

@ -1,4 +1,3 @@
{
"responsable": "Responsable",
"delete": "Supprimer",

View File

@ -1,10 +1,10 @@
{
"dashboard": "Tableau de bord",
"totalStudents": "Total des étudiants",
"pendingRegistrations": "Inscriptions en attente",
"reInscriptionRate": "Taux de réinscription",
"structureCapacity": "Capacité de la structure",
"capacityRate": "Remplissage de la structure",
"inscriptionTrends": "Tendances d'inscription",
"upcomingEvents": "Événements à venir"
}
"dashboard": "Tableau de bord",
"totalStudents": "Total des étudiants",
"pendingRegistrations": "Inscriptions en attente",
"reInscriptionRate": "Taux de réinscription",
"structureCapacity": "Capacité de la structure",
"capacityRate": "Remplissage de la structure",
"inscriptionTrends": "Tendances d'inscription",
"upcomingEvents": "Événements à venir"
}

View File

@ -1,5 +1,5 @@
{
"welcomeParents": "Bienvenue aux parents",
"pleaseLogin": "Veuillez vous connecter pour accéder à votre compte",
"loginButton": "Accéder à la page de login"
}
"welcomeParents": "Bienvenue aux parents",
"pleaseLogin": "Veuillez vous connecter pour accéder à votre compte",
"loginButton": "Accéder à la page de login"
}

View File

@ -1,6 +1,6 @@
{
"page": "Page",
"of": "sur",
"previous": "Précédent",
"next": "Suivant"
}
"page": "Page",
"of": "sur",
"previous": "Précédent",
"next": "Suivant"
}

View File

@ -1,10 +1,10 @@
{
"dashboard": "Tableau de bord",
"subscriptions": "Inscriptions",
"structure": "Structure",
"directory": "Annuaire",
"events": "Evenements",
"grades": "Notes",
"settings": "Paramètres",
"schoolAdmin": "Administration Scolaire"
}
{
"dashboard": "Tableau de bord",
"subscriptions": "Inscriptions",
"structure": "Structure",
"directory": "Annuaire",
"events": "Evenements",
"grades": "Notes",
"settings": "Paramètres",
"schoolAdmin": "Administration Scolaire"
}

View File

@ -1,33 +1,33 @@
{
"headerBarTitle":"Administration",
"addStudent": "Nouveau",
"allStudents": "Tous les élèves",
"pending": "Inscriptions en attente",
"subscribed": "Inscrits",
"archived": "Archivés",
"name": "Nom",
"class": "Classe",
"status": "Statut",
"attendance": "Assiduité",
"lastEvaluation": "Dernière évaluation",
"active": "Actif",
"pendingStatus": "En attente",
"goodAttendance": "Bonne",
"averageAttendance": "Moyenne",
"lowAttendance": "Faible",
"searchStudent": "Rechercher un élève...",
"title": "Inscription",
"information": "Information",
"no_records": "Il n'y a actuellement aucun dossier d'inscription.",
"add_button": "Ajouter",
"create_first_record": "Veuillez cliquer sur le bouton AJOUTER pour créer votre premier dossier d'inscription.",
"studentName":"Nom de l'élève",
"studentFistName":"Prénom de l'élève",
"mainContactMail":"Email de contact principal",
"phone":"Téléphone",
"lastUpdateDate":"Dernière mise à jour",
"classe":"Classe",
"registrationFileStatus":"État du dossier d'inscription",
"files":"Fichiers",
"subscribeFiles":"Fichiers d'inscription"
}
"headerBarTitle": "Administration",
"addStudent": "Nouveau",
"allStudents": "Tous les élèves",
"pending": "Inscriptions en attente",
"subscribed": "Inscrits",
"archived": "Archivés",
"name": "Nom",
"class": "Classe",
"status": "Statut",
"attendance": "Assiduité",
"lastEvaluation": "Dernière évaluation",
"active": "Actif",
"pendingStatus": "En attente",
"goodAttendance": "Bonne",
"averageAttendance": "Moyenne",
"lowAttendance": "Faible",
"searchStudent": "Rechercher un élève...",
"title": "Inscription",
"information": "Information",
"no_records": "Il n'y a actuellement aucun dossier d'inscription.",
"add_button": "Ajouter",
"create_first_record": "Veuillez cliquer sur le bouton AJOUTER pour créer votre premier dossier d'inscription.",
"studentName": "Nom de l'élève",
"studentFistName": "Prénom de l'élève",
"mainContactMail": "Email de contact principal",
"phone": "Téléphone",
"lastUpdateDate": "Dernière mise à jour",
"classe": "Classe",
"registrationFileStatus": "État du dossier d'inscription",
"files": "Fichiers",
"subscribeFiles": "Fichiers d'inscription"
}

View File

@ -1,14 +1,13 @@
import createNextIntlPlugin from 'next-intl/plugin';
import { createRequire } from "module";
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require("./package.json")
const pkg = require('./package.json');
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
output: 'standalone',
reactStrictMode: true,
experimental: {
instrumentationHook: true,
@ -16,17 +15,18 @@ const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "www.gravatar.com",
protocol: 'https',
hostname: 'www.gravatar.com',
},
],
},
env: {
NEXT_PUBLIC_APP_VERSION: pkg.version,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080",
NEXT_PUBLIC_API_URL:
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000",
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
DOCUSEAL_API_KEY: process.env.DOCUSEAL_API_KEY,
},
async rewrites() {
@ -40,7 +40,7 @@ const nextConfig = {
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy
},
];
}
},
};
export default withNextIntl(nextConfig);
export default withNextIntl(nextConfig);

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -29,7 +29,7 @@ const TAILWIND_PATTERNS = [
// États
/^(hover|focus|active|disabled|group|dark):/,
// Couleurs
/-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+$/
/-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+$/,
];
// Nouveaux patterns pour ignorer les logs et imports
@ -58,7 +58,7 @@ function isHardcodedString(str) {
// Vérifier si la chaîne fait partie d'un console.log ou d'un import
const context = str.trim();
if (CODE_PATTERNS.some(pattern => pattern.test(context))) {
if (CODE_PATTERNS.some((pattern) => pattern.test(context))) {
return false;
}
@ -68,19 +68,20 @@ function isHardcodedString(str) {
}
// Vérifier si c'est une chaîne dans un import
if (context.includes('from \'') || context.includes('from "')) {
if (context.includes("from '") || context.includes('from "')) {
return false;
}
// Vérifier si c'est une classe Tailwind
const classes = str.split(' ');
if (classes.some(cls =>
TAILWIND_PATTERNS.some(pattern => pattern.test(cls))
)) {
if (
classes.some((cls) =>
TAILWIND_PATTERNS.some((pattern) => pattern.test(cls))
)
) {
return false;
}
// Autres patterns à ignorer
const IGNORE_PATTERNS = [
/^[A-Z][A-Za-z]+$/, // Noms de composants
@ -95,7 +96,7 @@ function isHardcodedString(str) {
/^className=/, // className attributes
];
return !IGNORE_PATTERNS.some(pattern => pattern.test(str));
return !IGNORE_PATTERNS.some((pattern) => pattern.test(str));
}
async function scanFile(filePath) {
@ -106,7 +107,7 @@ async function scanFile(filePath) {
const ast = babel.parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
locations: true // Active le tracking des positions
locations: true, // Active le tracking des positions
});
traverse(ast, {
@ -140,7 +141,11 @@ async function scanDirectory(dir) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
if (
stat.isDirectory() &&
!file.startsWith('.') &&
file !== 'node_modules'
) {
Object.assign(results, await scanDirectory(filePath));
} else if (
stat.isFile() &&
@ -189,4 +194,4 @@ async function main() {
await logStringsToFile(results);
}
main().catch(console.error);
main().catch(console.error);

View File

@ -1,15 +1,21 @@
import Link from 'next/link'
import Logo from '../components/Logo'
import Link from 'next/link';
import Logo from '../components/Logo';
export default function Custom500() {
return (
<div className='flex items-center justify-center min-h-screen bg-emerald-500'>
<div className='text-center p-6 '>
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
<div className="text-center p-6 ">
<Logo className="w-32 h-32 mx-auto mb-4" />
<h2 className='text-2xl font-bold text-emerald-900 mb-4'>500 | Erreur interne</h2>
<p className='text-emerald-900 mb-4'>Une erreur interne est survenue.</p>
<Link className="text-gray-900 hover:underline" href="/">Retour Accueil</Link>
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
500 | Erreur interne
</h2>
<p className="text-emerald-900 mb-4">
Une erreur interne est survenue.
</p>
<Link className="text-gray-900 hover:underline" href="/">
Retour Accueil
</Link>
</div>
</div>
)
}
);
}

View File

@ -1,6 +1,10 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
import { fetchProfileRoles, updateProfileRoles, deleteProfileRoles } from '@/app/actions/authAction';
import {
fetchProfileRoles,
updateProfileRoles,
deleteProfileRoles,
} from '@/app/actions/authAction';
import { dissociateGuardian } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
@ -24,21 +28,23 @@ export default function Page() {
const handleProfiles = () => {
fetchProfileRoles(selectedEstablishmentId)
.then(data => {
.then((data) => {
setProfileRoles(data);
})
.catch(error => logger.error('Error fetching profileRoles:', error));
setReloadFetch(false);
.catch((error) => logger.error('Error fetching profileRoles:', error));
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));
.then((data) => {
setProfileRoles((prevState) =>
prevState.map((item) => (item.id === profileRole.id ? data : item))
);
return data;
})
.catch(error => {
.catch((error) => {
logger.error('Error editing data:', error);
throw error;
});
@ -47,10 +53,12 @@ export default function Page() {
const handleDelete = (id) => {
return deleteProfileRoles(id, csrfToken)
.then(() => {
setProfileRoles(prevState => prevState.filter(item => item.id !== id));
logger.debug("Profile deleted successfully:", id);
setProfileRoles((prevState) =>
prevState.filter((item) => item.id !== id)
);
logger.debug('Profile deleted successfully:', id);
})
.catch(error => {
.catch((error) => {
logger.error('Error deleting profile:', error);
throw error;
});
@ -59,49 +67,58 @@ export default function Page() {
const handleDissociate = (studentId, guardianId) => {
return dissociateGuardian(studentId, guardianId)
.then((response) => {
logger.debug("Guardian dissociated successfully:", guardianId);
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
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 => {
.catch((error) => {
logger.error('Error dissociating guardian:', error);
throw error;
});
};
return (
<div className='p-8'>
<div className="p-8">
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<ProfileDirectory profileRoles={profileRoles} handleActivateProfile={handleEdit} handleDeleteProfile={handleDelete} handleDissociateGuardian={handleDissociate} />
<ProfileDirectory
profileRoles={profileRoles}
handleActivateProfile={handleEdit}
handleDeleteProfile={handleDelete}
handleDissociateGuardian={handleDissociate}
/>
</div>
</div>
);

View File

@ -1,10 +1,10 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
export default function Page() {
return (
<div className='p-8'>
<h1 className='heading-section'>Statistiques</h1>
</div>
);
}
return (
<div className="p-8">
<h1 className="heading-section">Statistiques</h1>
</div>
);
}

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
import Sidebar from '@/components/Sidebar';
import { usePathname } from 'next/navigation';
@ -14,7 +14,7 @@ import {
Settings,
LogOut,
Menu,
X
X,
} from 'lucide-react';
import DropdownMenu from '@/components/DropdownMenu';
@ -26,7 +26,7 @@ import {
FE_ADMIN_DIRECTORY_URL,
FE_ADMIN_GRADES_URL,
FE_ADMIN_PLANNING_URL,
FE_ADMIN_SETTINGS_URL
FE_ADMIN_SETTINGS_URL,
} from '@/utils/Url';
import { disconnect } from '@/app/actions/authAction';
@ -38,25 +38,63 @@ import { getRightStr, RIGHTS } from '@/utils/rights';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function Layout({
children,
}) {
export default function Layout({ children }) {
const t = useTranslations('sidebar');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const { data: session } = useSession();
const { selectedEstablishmentId, setSelectedEstablishmentId, profileRole, setProfileRole, establishments, user } = useEstablishment();
const {
selectedEstablishmentId,
setSelectedEstablishmentId,
profileRole,
setProfileRole,
establishments,
user,
} = useEstablishment();
// Déplacer le reste du code ici...
const sidebarItems = {
"admin": { "id": "admin", "name": t('dashboard'), "url": FE_ADMIN_HOME_URL, "icon": LayoutDashboard },
"subscriptions": { "id": "subscriptions", "name": t('subscriptions'), "url": FE_ADMIN_SUBSCRIPTIONS_URL, "icon": FileText },
"structure": { "id": "structure", "name": t('structure'), "url": FE_ADMIN_STRUCTURE_URL, "icon": School },
"directory": { "id": "directory", "name": t('directory'), "url": FE_ADMIN_DIRECTORY_URL, "icon": Users },
"grades": { "id": "grades", "name": t('grades'), "url": FE_ADMIN_GRADES_URL, "icon": Award },
"planning": { "id": "planning", "name": t('events'), "url": FE_ADMIN_PLANNING_URL, "icon": Calendar },
"settings": { "id": "settings", "name": t('settings'), "url": FE_ADMIN_SETTINGS_URL, "icon": Settings }
admin: {
id: 'admin',
name: t('dashboard'),
url: FE_ADMIN_HOME_URL,
icon: LayoutDashboard,
},
subscriptions: {
id: 'subscriptions',
name: t('subscriptions'),
url: FE_ADMIN_SUBSCRIPTIONS_URL,
icon: FileText,
},
structure: {
id: 'structure',
name: t('structure'),
url: FE_ADMIN_STRUCTURE_URL,
icon: School,
},
directory: {
id: 'directory',
name: t('directory'),
url: FE_ADMIN_DIRECTORY_URL,
icon: Users,
},
grades: {
id: 'grades',
name: t('grades'),
url: FE_ADMIN_GRADES_URL,
icon: Award,
},
planning: {
id: 'planning',
name: t('events'),
url: FE_ADMIN_PLANNING_URL,
icon: Calendar,
},
settings: {
id: 'settings',
name: t('settings'),
url: FE_ADMIN_SETTINGS_URL,
icon: Settings,
},
};
const [isPopupVisible, setIsPopupVisible] = useState(false);
@ -66,7 +104,7 @@ export default function Layout({
const headerTitle = sidebarItems[currentPage]?.name || t('dashboard');
const softwareName = "N3WT School";
const softwareName = 'N3WT School';
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
const handleDisconnect = () => {
@ -84,13 +122,15 @@ export default function Layout({
content: (
<div className="px-4 py-2">
<div className="font-medium">{user?.email || 'Utilisateur'}</div>
<div className="text-xs text-gray-400">{getRightStr(profileRole) || ''}</div>
<div className="text-xs text-gray-400">
{getRightStr(profileRole) || ''}
</div>
</div>
)
),
},
{
type: 'separator',
content: <hr className="my-2 border-gray-200" />
content: <hr className="my-2 border-gray-200" />,
},
{
type: 'item',
@ -126,7 +166,9 @@ export default function Layout({
onEstablishmentChange={(establishmentId) => {
const parsedEstablishmentId = parseInt(establishmentId, 10);
setSelectedEstablishmentId(parsedEstablishmentId);
let roleIndex = session.user.roles.findIndex(role => role.establishment__id === parsedEstablishmentId)
let roleIndex = session.user.roles.findIndex(
(role) => role.establishment__id === parsedEstablishmentId
);
if (roleIndex === -1) {
roleIndex = 0;
}
@ -155,7 +197,9 @@ export default function Layout({
>
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
</button>
<div className="text-lg md:text-xl font-semibold">{headerTitle}</div>
<div className="text-lg md:text-xl font-semibold">
{headerTitle}
</div>
</div>
<DropdownMenu
buttonContent={
@ -175,11 +219,12 @@ export default function Layout({
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Content avec scroll si nécessaire */}
<div className="flex-1 overflow-auto p-4 md:p-6">
{children}
</div>
<div className="flex-1 overflow-auto p-4 md:p-6">{children}</div>
{/* Footer responsive */}
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
<Footer
softwareName={softwareName}
softwareVersion={softwareVersion}
/>
</div>
</div>
</div>
@ -192,5 +237,3 @@ export default function Layout({
</ProtectedRoute>
);
}

View File

@ -1,7 +1,7 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
import { Users, Clock, CalendarCheck, School, TrendingUp } from 'lucide-react';
import Loader from '@/components/Loader';
import ClasseDetails from '@/components/ClasseDetails';
import { fetchClasses } from '@/app/actions/schoolAction';
@ -11,7 +11,6 @@ import { fetchRegisterForms } from '@/app/actions/subscriptionAction';
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
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">
@ -35,10 +34,9 @@ export default function DashboardPage() {
const [upcomingEvents, setUpcomingEvents] = useState([]);
const [monthlyStats, setMonthlyStats] = useState({
inscriptions: [],
completionRate: 0
completionRate: 0,
});
const [classes, setClasses] = useState([]);
const { selectedEstablishmentId } = useEstablishment();
@ -49,35 +47,41 @@ export default function DashboardPage() {
// Fetch des classes
fetchClasses(selectedEstablishmentId)
.then(data => {
.then((data) => {
setClasses(data);
logger.info('Classes fetched:', data);
const nbMaxStudents = data.reduce((acc, classe) => acc + classe.number_of_students, 0);
const nbStudents = data.reduce((acc, classe) => acc + classe.students.length, 0);
const nbMaxStudents = data.reduce(
(acc, classe) => acc + classe.number_of_students,
0
);
const nbStudents = data.reduce(
(acc, classe) => acc + classe.students.length,
0
);
setStructureCapacity(nbMaxStudents);
setTotalStudents(nbStudents);
})
.catch(error => {
.catch((error) => {
logger.error('Error fetching classes:', error);
});
// Fetch des formulaires d'inscription
fetchRegisterForms()
.then(data => {
.then((data) => {
logger.info('Pending registrations fetched:', data);
setPendingRegistration(data.count);
})
.catch(error => {
.catch((error) => {
logger.error('Error fetching pending registrations:', error);
});
// Fetch des événements à venir
fetchUpcomingEvents()
.then(data => {
.then((data) => {
setUpcomingEvents(data);
})
.catch(error => {
.catch((error) => {
logger.error('Error fetching upcoming events:', error);
})
.finally(() => {
@ -110,9 +114,9 @@ export default function DashboardPage() {
icon={<School className="text-green-500" size={24} />}
color="emerald"
/>
<StatCard
<StatCard
title={t('capacityRate')}
value={`${(totalStudents/structureCapacity * 100).toFixed(1)}%`}
value={`${((totalStudents / structureCapacity) * 100).toFixed(1)}%`}
icon={<School className="text-orange-500" size={24} />}
color="orange"
/>
@ -122,7 +126,9 @@ export default function DashboardPage() {
<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">
<h2 className="text-lg font-semibold mb-4">{t('inscriptionTrends')}</h2>
<h2 className="text-lg font-semibold mb-4">
{t('inscriptionTrends')}
</h2>
{/* Insérer ici un composant de graphique */}
<div className="h-64 bg-gray-50 rounded flex items-center justify-center">
<TrendingUp size={48} className="text-gray-300" />
@ -140,11 +146,14 @@ export default function DashboardPage() {
<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
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

@ -1,4 +1,4 @@
'use client'
'use client';
import { PlanningProvider } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar';
import EventModal from '@/components/EventModal';
@ -19,7 +19,7 @@ export default function Page() {
recurrenceEnd: '',
customInterval: 1,
customUnit: 'days',
viewType: 'week' // Ajouter la vue semaine par défaut
viewType: 'week', // Ajouter la vue semaine par défaut
});
const initializeNewEvent = (date = new Date()) => {
@ -37,7 +37,7 @@ export default function Page() {
selectedDays: [],
recurrenceEnd: '',
customInterval: 1,
customUnit: 'days'
customUnit: 'days',
});
setIsModalOpen(true);
};
@ -62,4 +62,4 @@ export default function Page() {
</div>
</PlanningProvider>
);
}
}

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState } from 'react';
import Tab from '@/components/Tab';
import TabContent from '@/components/TabContent';
@ -85,18 +85,49 @@ export default function SettingsPage() {
<div className="mt-4">
<TabContent isActive={activeTab === 'structure'}>
<form onSubmit={handleSubmit}>
<InputText label="Email" value={email} onChange={handleEmailChange} />
<InputText label="Mot de passe" type="password" value={password} onChange={handlePasswordChange} />
<InputText label="Confirmer le mot de passe" type="password" value={confirmPassword} onChange={handleConfirmPasswordChange} />
<InputText
label="Email"
value={email}
onChange={handleEmailChange}
/>
<InputText
label="Mot de passe"
type="password"
value={password}
onChange={handlePasswordChange}
/>
<InputText
label="Confirmer le mot de passe"
type="password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
/>
<Button type="submit" primary text="Mettre à jour"></Button>
</form>
</TabContent>
<TabContent isActive={activeTab === 'smtp'}>
<form onSubmit={handleSmtpSubmit}>
<InputText label="Serveur SMTP" value={smtpServer} onChange={handleSmtpServerChange} />
<InputText label="Port SMTP" value={smtpPort} onChange={handleSmtpPortChange} />
<InputText label="Utilisateur SMTP" value={smtpUser} onChange={handleSmtpUserChange} />
<InputText label="Mot de passe SMTP" type="password" value={smtpPassword} onChange={handleSmtpPasswordChange} />
<InputText
label="Serveur SMTP"
value={smtpServer}
onChange={handleSmtpServerChange}
/>
<InputText
label="Port SMTP"
value={smtpPort}
onChange={handleSmtpPortChange}
/>
<InputText
label="Utilisateur SMTP"
value={smtpUser}
onChange={handleSmtpUserChange}
/>
<InputText
label="Mot de passe SMTP"
type="password"
value={smtpPassword}
onChange={handleSmtpPasswordChange}
/>
<Button type="submit" primary text="Mettre à jour"></Button>
</form>
</TabContent>

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
@ -6,26 +6,27 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext';
import { ClassesProvider } from '@/context/ClassesContext';
import {
createDatas,
updateDatas,
removeDatas,
fetchSpecialities,
fetchTeachers,
fetchClasses,
fetchSchedules,
fetchRegistrationDiscounts,
fetchTuitionDiscounts,
fetchRegistrationFees,
fetchTuitionFees,
fetchRegistrationPaymentPlans,
fetchTuitionPaymentPlans,
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
import {
createDatas,
updateDatas,
removeDatas,
fetchSpecialities,
fetchTeachers,
fetchClasses,
fetchSchedules,
fetchRegistrationDiscounts,
fetchTuitionDiscounts,
fetchRegistrationFees,
fetchTuitionFees,
fetchRegistrationPaymentPlans,
fetchTuitionPaymentPlans,
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction';
import { fetchProfileRoles, fetchProfiles } from '@/app/actions/authAction';
import SidebarTabs from '@/components/SidebarTabs';
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
import { fetchRegistrationTemplateMaster } from "@/app/actions/registerFileGroupAction";
import { fetchRegistrationTemplateMaster } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
@ -76,10 +77,10 @@ export default function Page() {
// Fetch data for registration file templates
fetchRegistrationTemplateMaster()
.then((data)=> {
setFichiers(data)
.then((data) => {
setFichiers(data);
})
.catch(error => logger.error('Error fetching files:', error));
.catch((error) => logger.error('Error fetching files:', error));
// Fetch data for registration payment plans
handleRegistrationPaymentPlans();
@ -94,145 +95,161 @@ export default function Page() {
handleTuitionPaymentModes();
fetchProfiles()
.then(data => {
setProfiles(data);
})
.catch(error => {
logger.error('Error fetching profileRoles:', error);
})
.then((data) => {
setProfiles(data);
})
.catch((error) => {
logger.error('Error fetching profileRoles:', error);
});
}
}, [selectedEstablishmentId]);
const handleSpecialities = () => {
fetchSpecialities(selectedEstablishmentId)
.then(data => {
.then((data) => {
setSpecialities(data);
})
.catch(error => logger.error('Error fetching specialities:', error));
.catch((error) => logger.error('Error fetching specialities:', error));
};
const handleTeachers = () => {
fetchTeachers(selectedEstablishmentId)
.then(data => {
.then((data) => {
setTeachers(data);
})
.catch(error => logger.error('Error fetching teachers:', error));
.catch((error) => logger.error('Error fetching teachers:', error));
};
const handleClasses = () => {
fetchClasses(selectedEstablishmentId)
.then(data => {
.then((data) => {
setClasses(data);
})
.catch(error => logger.error('Error fetching classes:', error));
.catch((error) => logger.error('Error fetching classes:', error));
};
const handleSchedules = () => {
fetchSchedules()
.then(data => {
.then((data) => {
setSchedules(data);
})
.catch(error => logger.error('Error fetching schedules:', error));
.catch((error) => logger.error('Error fetching schedules:', error));
};
const handleRegistrationDiscounts = () => {
fetchRegistrationDiscounts(selectedEstablishmentId)
.then(data => {
.then((data) => {
setRegistrationDiscounts(data);
})
.catch(error => logger.error('Error fetching registration discounts:', error));
.catch((error) =>
logger.error('Error fetching registration discounts:', error)
);
};
const handleTuitionDiscounts = () => {
fetchTuitionDiscounts(selectedEstablishmentId)
.then(data => {
.then((data) => {
setTuitionDiscounts(data);
})
.catch(error => logger.error('Error fetching tuition discounts:', error));
.catch((error) =>
logger.error('Error fetching tuition discounts:', error)
);
};
const handleRegistrationFees = () => {
fetchRegistrationFees(selectedEstablishmentId)
.then(data => {
.then((data) => {
setRegistrationFees(data);
})
.catch(error => logger.error('Error fetching registration fees:', error));
.catch((error) =>
logger.error('Error fetching registration fees:', error)
);
};
const handleTuitionFees = () => {
fetchTuitionFees(selectedEstablishmentId)
.then(data => {
.then((data) => {
setTuitionFees(data);
})
.catch(error => logger.error('Error fetching tuition fees', error));
.catch((error) => logger.error('Error fetching tuition fees', error));
};
const handleRegistrationPaymentPlans = () => {
fetchRegistrationPaymentPlans(selectedEstablishmentId)
.then(data => {
.then((data) => {
setRegistrationPaymentPlans(data);
})
.catch(error => logger.error('Error fetching registration payment plans:', error));
.catch((error) =>
logger.error('Error fetching registration payment plans:', error)
);
};
const handleTuitionPaymentPlans = () => {
fetchTuitionPaymentPlans(selectedEstablishmentId)
.then(data => {
.then((data) => {
setTuitionPaymentPlans(data);
})
.catch(error => logger.error('Error fetching tuition payment plans:', error));
.catch((error) =>
logger.error('Error fetching tuition payment plans:', error)
);
};
const handleRegistrationPaymentModes = () => {
fetchRegistrationPaymentModes(selectedEstablishmentId)
.then(data => {
.then((data) => {
setRegistrationPaymentModes(data);
})
.catch(error => logger.error('Error fetching registration payment modes:', error));
.catch((error) =>
logger.error('Error fetching registration payment modes:', error)
);
};
const handleTuitionPaymentModes = () => {
fetchTuitionPaymentModes(selectedEstablishmentId)
.then(data => {
.then((data) => {
setTuitionPaymentModes(data);
})
.catch(error => logger.error('Error fetching tuition payment modes:', error));
.catch((error) =>
logger.error('Error fetching tuition payment modes:', error)
);
};
const handleCreate = (url, newData, setDatas) => {
return createDatas(url, newData, csrfToken)
.then(data => {
setDatas(prevState => [...prevState, data]);
return data;
})
.catch(error => {
logger.error('Error creating data:', error);
throw error;
});
.then((data) => {
setDatas((prevState) => [...prevState, data]);
return data;
})
.catch((error) => {
logger.error('Error creating data:', error);
throw error;
});
};
const handleEdit = (url, id, updatedData, setDatas) => {
return updateDatas(url, id, updatedData, csrfToken)
.then(data => {
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
return data;
})
.catch(error => {
logger.error('Error editing data:', error);
throw error;
});
.then((data) => {
setDatas((prevState) =>
prevState.map((item) => (item.id === id ? data : item))
);
return data;
})
.catch((error) => {
logger.error('Error editing data:', error);
throw error;
});
};
const handleDelete = (url, id, setDatas) => {
return removeDatas(url, id, csrfToken)
.then(data => {
setDatas(prevState => prevState.filter(item => item.id !== id));
return data;
})
.catch(error => {
logger.error('Error deleting data:', error);
throw error;
});
.then((data) => {
setDatas((prevState) => prevState.filter((item) => item.id !== id));
return data;
})
.catch((error) => {
logger.error('Error deleting data:', error);
throw error;
});
};
const handleUpdatePlanning = (url, planningId, updatedData) => {
@ -240,19 +257,19 @@ export default function Page() {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(updatedData),
credentials: 'include'
credentials: 'include',
})
.then(response => response.json())
.then(data => {
logger.debug('Planning mis à jour avec succès :', data);
//setDatas(data);
})
.catch(error => {
logger.error('Erreur :', error);
});
.then((response) => response.json())
.then((data) => {
logger.debug('Planning mis à jour avec succès :', data);
//setDatas(data);
})
.catch((error) => {
logger.error('Erreur :', error);
});
};
const tabs = [
@ -272,7 +289,7 @@ export default function Page() {
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
)
),
},
{
id: 'Schedule',
@ -284,7 +301,7 @@ export default function Page() {
classes={classes}
/>
</ClassesProvider>
)
),
},
{
id: 'Fees',
@ -311,23 +328,27 @@ export default function Page() {
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
)
),
},
{
id: 'Files',
label: 'Documents d\'inscription',
content: <FilesGroupsManagement csrfToken={csrfToken} selectedEstablishmentId={selectedEstablishmentId} />
}
label: "Documents d'inscription",
content: (
<FilesGroupsManagement
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
/>
),
},
];
return (
<div className='p-8'>
<div className="p-8">
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<SidebarTabs tabs={tabs} />
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
@ -9,41 +9,37 @@ import { editRegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId
const router = useRouter();
const searchParams = useSearchParams();
const studentId = searchParams.get('studentId'); // Changé de codeDI à studentId
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const handleSubmit = (data) => {
editRegisterForm(studentId, data, csrfToken)
.then((result) => {
logger.debug('Success:', result);
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
})
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setFormErrors(error.details);
}
});
};
const handleSubmit = (data) => {
editRegisterForm(studentId, data, csrfToken)
.then((result) => {
logger.debug('Success:', result);
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
})
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setFormErrors(error.details);
}
});
};
return (
<InscriptionFormShared
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
errors={formErrors}
/>
);
}
return (
<InscriptionFormShared
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
errors={formErrors}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
'use client'
'use client';
import React from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import ValidateSubscription from '@/components/Inscription/ValidateSubscription';
import { sendSEPARegisterForm } from "@/app/actions/subscriptionAction"
import { sendSEPARegisterForm } from '@/app/actions/subscriptionAction';
import { useCsrfToken } from '@/context/CsrfContext';
import logger from '@/utils/logger';
import { FE_ADMIN_SUBSCRIPTIONS_URL} from '@/utils/Url';
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
export default function Page() {
const searchParams = useSearchParams();
@ -23,11 +23,11 @@ export default function Page() {
const handleAcceptRF = (data) => {
logger.debug('Mise à jour du RF avec les données:', data);
const {status, sepa_file} = data
const { status, sepa_file } = data;
const formData = new FormData();
formData.append('status', status); // Ajoute le statut
formData.append('sepa_file', sepa_file); // Ajoute le fichier SEPA
// Appeler l'API pour mettre à jour le RF
sendSEPARegisterForm(studentId, formData, csrfToken)
.then((response) => {
@ -39,7 +39,7 @@ export default function Page() {
logger.error('Erreur lors de la mise à jour du RF:', error);
});
};
return (
<ValidateSubscription
studentId={studentId}
@ -50,4 +50,4 @@ export default function Page() {
onAccept={handleAcceptRF}
/>
);
}
}

View File

@ -1,5 +1,5 @@
'use client'
import {useTranslations} from 'next-intl';
'use client';
import { useTranslations } from 'next-intl';
import React from 'react';
import Button from '@/components/Button';
import Logo from '@/components/Logo'; // Import du composant Logo

View File

@ -1,38 +1,38 @@
'use client'
import React, { useState } from 'react';
'use client';
import React, { useState } from 'react';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { useSearchParams, useRouter } from 'next/navigation';
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_PARENTS_HOME_URL} from '@/utils/Url';
import { editRegisterForm} from '@/app/actions/subscriptionAction';
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
import { editRegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
export default function Page() {
const searchParams = useSearchParams();
const idProfil = searchParams.get('id');
const studentId = searchParams.get('studentId');
const router = useRouter();
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const searchParams = useSearchParams();
const idProfil = searchParams.get('id');
const studentId = searchParams.get('studentId');
const router = useRouter();
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const handleSubmit = async (data) => {
try {
const result = await editRegisterForm(studentId, data, csrfToken);
logger.debug('Success:', result);
router.push(FE_PARENTS_HOME_URL);
} catch (error) {
logger.error('Error:', error);
}
};
const handleSubmit = async (data) => {
try {
const result = await editRegisterForm(studentId, data, csrfToken);
logger.debug('Success:', result);
router.push(FE_PARENTS_HOME_URL);
} catch (error) {
logger.error('Error:', error);
}
};
return (
<InscriptionFormShared
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
onSubmit={handleSubmit}
cancelUrl={FE_PARENTS_HOME_URL}
/>
);
}
return (
<InscriptionFormShared
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
onSubmit={handleSubmit}
cancelUrl={FE_PARENTS_HOME_URL}
/>
);
}

View File

@ -1,4 +1,4 @@
'use client'
'use client';
// src/components/Layout.js
import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu';
@ -6,7 +6,11 @@ import ProfileSelector from '@/components/ProfileSelector';
import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
import { FE_PARENTS_HOME_URL,FE_PARENTS_MESSAGERIE_URL,FE_PARENTS_SETTINGS_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import {
FE_PARENTS_HOME_URL,
FE_PARENTS_MESSAGERIE_URL,
FE_PARENTS_SETTINGS_URL,
} from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import { fetchMessages } from '@/app/actions/messagerieAction';
import ProtectedRoute from '@/components/ProtectedRoute';
import { disconnect } from '@/app/actions/authAction';
@ -18,19 +22,15 @@ import { useEstablishment } from '@/context/EstablishmentContext';
import Image from 'next/image';
import Footer from '@/components/Footer';
export default function Layout({
children,
}) {
export default function Layout({ children }) {
const router = useRouter(); // Définition de router
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isPopupVisible, setIsPopupVisible] = useState(false);
const { profileRole, user } = useEstablishment();
const softwareName = "N3WT School";
const softwareName = 'N3WT School';
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
const handleDisconnect = () => {
setIsPopupVisible(true);
};
@ -40,21 +40,29 @@ export default function Layout({
disconnect();
};
const dropdownItems = [
const dropdownItems = [
{
type: 'info',
content: (
<div className="px-4 py-2">
<div className="font-medium">{user?.email || 'Utilisateur'}</div>
<div className="text-xs text-gray-400">{getRightStr(profileRole) || ''}</div>
<div className="text-xs text-gray-400">
{getRightStr(profileRole) || ''}
</div>
</div>
)
),
},
{
type: 'separator',
content: <hr className="my-2 border-gray-200" />
content: <hr className="my-2 border-gray-200" />,
},
{
label: 'Settings',
icon: Settings,
onClick: () => {
router.push(FE_PARENTS_SETTINGS_URL);
},
},
{ label: 'Settings', icon: Settings , onClick: () => { router.push(FE_PARENTS_SETTINGS_URL); } },
{
type: 'item',
label: 'Déconnexion',
@ -63,68 +71,72 @@ const dropdownItems = [
},
];
return (
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
<ProtectedRoute requiredRight={RIGHTS.PARENT}>
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */}
<header className="h-16 bg-white border-b border-gray-200 px-4 md:px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10">
<div className="flex items-center space-x-2">
<div className="border-b border-gray-200 ">
<ProfileSelector
className="w-64 border-r"
/>
</div>
<div className="text-lg md:text-xl p-2 font-semibold">Accueil</div>
<div className="flex items-center space-x-2">
<div className="border-b border-gray-200 ">
<ProfileSelector className="w-64 border-r" />
</div>
<div className="flex items-center space-x-2 md:space-x-4">
<button
className="p-1 md:p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FE_PARENTS_HOME_URL); }} // Utilisation de router pour revenir à l'accueil parent
>
<Home className="h-5 w-5 md:h-6 md:w-6" />
</button>
<div className="text-lg md:text-xl p-2 font-semibold">Accueil</div>
</div>
<div className="flex items-center space-x-2 md:space-x-4">
<button
className="p-1 md:p-2 rounded-full hover:bg-gray-200"
onClick={() => {
router.push(FE_PARENTS_HOME_URL);
}} // Utilisation de router pour revenir à l'accueil parent
>
<Home className="h-5 w-5 md:h-6 md:w-6" />
</button>
<div className="relative">
<div className="relative">
<button
className="p-1 md:p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FE_PARENTS_MESSAGERIE_URL); }} // Utilisation de router
onClick={() => {
router.push(FE_PARENTS_MESSAGERIE_URL);
}} // Utilisation de router
>
<MessageSquare className="h-5 w-5 md:h-6 md:w-6" />
</button>
{messages.length > 0 && (
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span>
)}
</div>
<DropdownMenu
buttonContent={<Image
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full cursor-pointer"
width={32}
height={32}
/>}
items={dropdownItems}
buttonClassName=""
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
/>
{messages.length > 0 && (
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span>
)}
</div>
<DropdownMenu
buttonContent={
<Image
src={getGravatarUrl(user?.email)}
alt="Profile"
className="w-8 h-8 rounded-full cursor-pointer"
width={32}
height={32}
/>
}
items={dropdownItems}
buttonClassName=""
menuClassName="absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded shadow-lg"
/>
</div>
</header>
{/* Content */}
<div className="pt-16 md:pt-20 p-4 md:p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
<div className="pt-16 md:pt-20 p-4 md:p-8 flex-1">
{' '}
{/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
{children}
</div>
{/* Footer responsive */}
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
</div>
<Popup
visible={isPopupVisible}
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
onConfirm={confirmDisconnect}
onCancel={() => setIsPopupVisible(false)}
/>
</ProtectedRoute>
visible={isPopupVisible}
message="Êtes-vous sûr(e) de vouloir vous déconnecter ?"
onConfirm={confirmDisconnect}
onCancel={() => setIsPopupVisible(false)}
/>
</ProtectedRoute>
);
}

View File

@ -1,13 +1,25 @@
'use client'
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
import Image from 'next/image';
import { getGravatarUrl } from '@/utils/gravatar';
const contacts = [
{ id: 1, name: 'Facturation', profilePic: getGravatarUrl('facturation@n3wtschool.com') },
{ id: 2, name: 'Enseignant 1', profilePic: getGravatarUrl('enseignant@n3wtschool.com') },
{ id: 3, name: 'Contact', profilePic: getGravatarUrl('contact@n3wtschool.com') },
{
id: 1,
name: 'Facturation',
profilePic: getGravatarUrl('facturation@n3wtschool.com'),
},
{
id: 2,
name: 'Enseignant 1',
profilePic: getGravatarUrl('enseignant@n3wtschool.com'),
},
{
id: 3,
name: 'Contact',
profilePic: getGravatarUrl('contact@n3wtschool.com'),
},
];
export default function MessageriePage() {
@ -29,7 +41,14 @@ export default function MessageriePage() {
const contactMessages = messages[selectedContact.id] || [];
setMessages({
...messages,
[selectedContact.id]: [...contactMessages, { id: contactMessages.length + 1, text: newMessage, date: new Date() }],
[selectedContact.id]: [
...contactMessages,
{
id: contactMessages.length + 1,
text: newMessage,
date: new Date(),
},
],
});
setNewMessage('');
simulateContactResponse(selectedContact.id);
@ -48,14 +67,24 @@ export default function MessageriePage() {
const contactMessages = prevMessages[contactId] || [];
return {
...prevMessages,
[contactId]: [...contactMessages, { id: contactMessages.length + 2, text: 'Réponse automatique', isResponse: true, date: new Date() }],
[contactId]: [
...contactMessages,
{
id: contactMessages.length + 2,
text: 'Réponse automatique',
isResponse: true,
date: new Date(),
},
],
};
});
}, 2000);
};
return (
<div className="flex" style={{ height: 'calc(100vh - 128px )' }}> {/* Utilisation de calc pour soustraire la hauteur de l'entête */}
<div className="flex" style={{ height: 'calc(100vh - 128px )' }}>
{' '}
{/* Utilisation de calc pour soustraire la hauteur de l'entête */}
<div className="w-1/4 border-r border-gray-200 p-4 overflow-y-auto h-full ">
{contacts.map((contact) => (
<div
@ -63,27 +92,49 @@ export default function MessageriePage() {
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
onClick={() => setSelectedContact(contact)}
>
<Image src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" width={150} height={150}/>
<Image
src={contact.profilePic}
alt={`${contact.name}'s profile`}
className="w-8 h-8 rounded-full inline-block mr-2"
width={150}
height={150}
/>
{contact.name}
</div>
))}
</div>
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 h-full">
{selectedContact && (messages[selectedContact.id] || []).map((message) => (
<div
key={message.id}
className={`mb-2 p-2 rounded max-w-xs ${message.isResponse ? 'bg-gray-200 justify-self-end' : 'bg-emerald-200 justify-self-start'}`}
style={{ borderRadius: message.isResponse ? '20px 20px 0 20px' : '20px 20px 20px 0', minWidth: '25%' }}
>
<div className="flex items-center mb-1">
<img src={selectedContact.profilePic} alt={`${selectedContact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" width={150} height={150} />
<span className="text-xs text-gray-600">{selectedContact.name}</span>
<span className="text-xs text-gray-400 ml-2">{new Date(message.date).toLocaleTimeString()}</span>
{selectedContact &&
(messages[selectedContact.id] || []).map((message) => (
<div
key={message.id}
className={`mb-2 p-2 rounded max-w-xs ${message.isResponse ? 'bg-gray-200 justify-self-end' : 'bg-emerald-200 justify-self-start'}`}
style={{
borderRadius: message.isResponse
? '20px 20px 0 20px'
: '20px 20px 20px 0',
minWidth: '25%',
}}
>
<div className="flex items-center mb-1">
<img
src={selectedContact.profilePic}
alt={`${selectedContact.name}'s profile`}
className="w-8 h-8 rounded-full inline-block mr-2"
width={150}
height={150}
/>
<span className="text-xs text-gray-600">
{selectedContact.name}
</span>
<span className="text-xs text-gray-400 ml-2">
{new Date(message.date).toLocaleTimeString()}
</span>
</div>
{message.text}
</div>
{message.text}
</div>
))}
))}
<div ref={messagesEndRef} />
</div>
<div className="p-4 border-t border-gray-200 flex">

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
@ -14,35 +14,42 @@ export default function ParentHomePage() {
const [children, setChildren] = useState([]);
const [userId, setUserId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const { user, setProfileRole, selectedEstablishmentId, setSelectedEstablishmentId, establishments } = useEstablishment();
const {
user,
setProfileRole,
selectedEstablishmentId,
setSelectedEstablishmentId,
establishments,
} = useEstablishment();
const router = useRouter();
useEffect(() => {
const userIdFromSession = user.user_id;
setUserId(userIdFromSession);
console.log(selectedEstablishmentId)
fetchChildren(userIdFromSession, selectedEstablishmentId).then(data => {
setChildren(data);
});
}, [ selectedEstablishmentId]);
const userIdFromSession = user.user_id;
setUserId(userIdFromSession);
console.log(selectedEstablishmentId);
fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => {
setChildren(data);
});
}, [selectedEstablishmentId]);
const handleEstablishmentChange = (e) => {
const establishmentId = parseInt(e.target.value, 10);
setSelectedEstablishmentId(establishmentId);
const role = establishments.find(est => est.id === establishmentId)?.role_type;
const role = establishments.find(
(est) => est.id === establishmentId
)?.role_type;
setProfileRole(role);
};
function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`);
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`);
router.push(
`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`
);
}
const actionColumns = [
{ name: 'Action', transform: (row) => row.action },
];
const actionColumns = [{ name: 'Action', transform: (row) => row.action }];
// Définir les colonnes du tableau
const childrenColumns = [
@ -52,9 +59,9 @@ export default function ParentHomePage() {
name: 'Statut',
transform: (row) => (
<div className="flex justify-center items-center">
<StatusLabel status={row.status} showDropdown={false} parent/>
<StatusLabel status={row.status} showDropdown={false} parent />
</div>
)
),
},
{
name: 'Actions',
@ -71,8 +78,8 @@ export default function ParentHomePage() {
<Edit className="h-5 w-5" />
</button>
</div>
)
}
),
},
];
const itemsPerPage = 5;

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState } from 'react';
import Button from '@/components/Button';
import InputText from '@/components/InputText';
@ -61,13 +61,7 @@ export default function SettingsPage() {
required
/>
<div className="flex items-center justify-between">
<Button
type="submit"
primary
text={" Mettre à jour"}
/>
<Button type="submit" primary text={' Mettre à jour'} />
</div>
</form>
</div>

View File

@ -1,7 +1,5 @@
"use client";
'use client';
function ErrorBoundary({
error
}) {
function ErrorBoundary({ error }) {
return <>{error.message}</>;
}
}

View File

@ -1,116 +1,159 @@
'use client'
import React, { useState } from 'react'
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
'use client';
import React, { useState } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import {
FE_USERS_NEW_PASSWORD_URL,
getRedirectUrlFromRole
} from '@/utils/Url';
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
import { login } from '@/app/actions/authAction';
import { getSession } from 'next-auth/react';
import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function Page() {
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError, setUserFieldError] = useState("")
const [passwordFieldError, setPasswordFieldError] = useState("")
const { setUser } = useEstablishment();
const [isLoading, setIsLoading] = useState(false);
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState('');
const [userFieldError, setUserFieldError] = useState('');
const [passwordFieldError, setPasswordFieldError] = useState('');
const { setUser } = useEstablishment();
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
const router = useRouter();
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
function isOK(data) {
return data.errorMessage === ""
}
function isOK(data) {
return data.errorMessage === '';
}
function handleFormLogin(formData) {
setIsLoading(true);
setErrorMessage('');
login({
email: formData.get('login'),
password: formData.get('password'),
})
.then((result) => {
logger.debug('Sign In Result', result);
function handleFormLogin(formData) {
setIsLoading(true);
setErrorMessage("");
login({
email: formData.get('login'),
password: formData.get('password')
}).then(result => {
logger.debug('Sign In Result', result);
if (result.error) {
setErrorMessage(result.error);
if (result.error) {
setErrorMessage(result.error);
setIsLoading(false);
} else {
getSession()
.then((session) => {
if (!session || !session.user) {
throw new Error('Session not found');
}
const user = session.user;
logger.debug('User Session:', user);
setUser(session.user);
if (session.user.roles && session.user.roles.length > 0) {
let roleIndex = 0;
if (
session.user.roles.length > session.user.roleIndexLoginDefault
) {
roleIndex = session.user.roleIndexLoginDefault;
}
const role = session.user.roles[roleIndex].role_type;
const url = getRedirectUrlFromRole(role);
if (url) {
router.push(url);
} else {
setIsLoading(false);
setErrorMessage('Type de rôle non géré');
}
} else {
setIsLoading(false);
} else {
getSession().then(session => {
if (!session || !session.user) {
throw new Error('Session not found');
}
const user = session.user;
logger.debug('User Session:', user);
setUser(session.user);
if (session.user.roles && session.user.roles.length > 0) {
let roleIndex = 0;
if( session.user.roles.length > session.user.roleIndexLoginDefault){
roleIndex = session.user.roleIndexLoginDefault;
}
const role = session.user.roles[roleIndex].role_type;
const url = getRedirectUrlFromRole(role);
if (url) {
router.push(url);
} else {
setIsLoading(false);
setErrorMessage('Type de rôle non géré');
}
} else {
setIsLoading(false);
setErrorMessage('Aucun rôle trouvé pour le profil sélectionné.');
}
}).catch(error => {
logger.error('Erreur lors de la récupération de la session:', error);
setIsLoading(false);
setErrorMessage('Une erreur est survenue lors de la récupération de la session.');
});
}
}).catch(error => {
logger.error('Erreur lors de la connexion:', error);
setIsLoading(false);
setErrorMessage('Une erreur est survenue lors de la connexion.');
});
}
setErrorMessage(
'Aucun rôle trouvé pour le profil sélectionné.'
);
}
})
.catch((error) => {
logger.error(
'Erreur lors de la récupération de la session:',
error
);
setIsLoading(false);
setErrorMessage(
'Une erreur est survenue lors de la récupération de la session.'
);
});
}
})
.catch((error) => {
logger.error('Erreur lors de la connexion:', error);
setIsLoading(false);
setErrorMessage('Une erreur est survenue lors de la connexion.');
});
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Authentification</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); handleFormLogin(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full mb-5" />
<InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full mb-5" />
<div className="input-group mb-4">
</div>
<label className="text-red-500">{errorMessage}</label>
<label><a className="float-right mb-4" href={`${FE_USERS_NEW_PASSWORD_URL}`}>Mot de passe oublié ?</a></label>
<div className="form-group-submit mt-4">
<Button text="Se Connecter" className="w-full" primary type="submit" name="connect" />
</div>
</form>
if (isLoading === true) {
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">
Authentification
</h1>
<form
className="max-w-md mx-auto"
onSubmit={(e) => {
e.preventDefault();
handleFormLogin(new FormData(e.target));
}}
>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon
name="login"
type="text"
IconItem={User}
label="Identifiant"
placeholder="Identifiant"
errorMsg={userFieldError}
className="w-full mb-5"
/>
<InputTextIcon
name="password"
type="password"
IconItem={KeySquare}
label="Mot de passe"
placeholder="Mot de passe"
errorMsg={passwordFieldError}
className="w-full mb-5"
/>
<div className="input-group mb-4"></div>
<label className="text-red-500">{errorMessage}</label>
<label>
<a
className="float-right mb-4"
href={`${FE_USERS_NEW_PASSWORD_URL}`}
>
Mot de passe oublié ?
</a>
</label>
<div className="form-group-submit mt-4">
<Button
text="Se Connecter"
className="w-full"
primary
type="submit"
name="connect"
/>
</div>
</>
}
};
</form>
</div>
</>
);
}
}

View File

@ -1,7 +1,7 @@
'use client'
'use client';
import React, { useState } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo';
import { useSearchParams } from 'next/navigation';
import InputTextIcon from '@/components/InputTextIcon';
@ -17,80 +17,108 @@ import logger from '@/utils/logger';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError, setUserFieldError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
const csrfToken = useCsrfToken();
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState('');
const [userFieldError, setUserFieldError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
const csrfToken = useCsrfToken();
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setUserFieldError("");
setErrorMessage("");
setPopupMessage("Mot de passe réinitialisé avec succès !");
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
}, 1000); // Simule un délai de traitement
} else {
const data = {email: formData.get('email')}
sendNewPassword(data, csrfToken)
.then(data => {
logger.debug('Success:', data);
setUserFieldError("");
setErrorMessage("");
if (data.errorMessage === "") {
setPopupMessage(data.message);
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
} else {
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
}
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
}
})
.catch(error => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setUserFieldError('');
setErrorMessage('');
setPopupMessage('Mot de passe réinitialisé avec succès !');
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
}, 1000); // Simule un délai de traitement
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Nouveau Mot de passe</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="email" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
<p className="text-red-500">{errorMessage}</p>
<div className="form-group-submit mt-4">
<Button text="Réinitialiser" className="w-full" primary type="submit" name="validate" />
</div>
</form>
<br />
<div className='flex justify-center mt-2 max-w-md mx-auto'>
<Button text="Annuler" className="w-full" href={ `${FE_USERS_LOGIN_URL}`} />
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={popupConfirmAction}
onCancel={() => setPopupVisible(false)}
/>
</>
const data = { email: formData.get('email') };
sendNewPassword(data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setUserFieldError('');
setErrorMessage('');
if (data.errorMessage === '') {
setPopupMessage(data.message);
setPopupConfirmAction(() => () => setPopupVisible(false));
setPopupVisible(true);
} else {
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
}
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
}
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
}
if (isLoading === true) {
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">
Nouveau Mot de passe
</h1>
<form
className="max-w-md mx-auto"
onSubmit={(e) => {
e.preventDefault();
validate(new FormData(e.target));
}}
>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon
name="email"
type="text"
IconItem={User}
label="Identifiant"
placeholder="Identifiant"
errorMsg={userFieldError}
className="w-full"
/>
<p className="text-red-500">{errorMessage}</p>
<div className="form-group-submit mt-4">
<Button
text="Réinitialiser"
className="w-full"
primary
type="submit"
name="validate"
/>
</div>
</form>
<br />
<div className="flex justify-center mt-2 max-w-md mx-auto">
<Button
text="Annuler"
className="w-full"
href={`${FE_USERS_LOGIN_URL}`}
/>
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={popupConfirmAction}
onCancel={() => setPopupVisible(false)}
/>
</>
);
}
}

View File

@ -1,10 +1,10 @@
'use client'
'use client';
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
@ -18,113 +18,149 @@ import logger from '@/utils/logger';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const uuid = searchParams.get('uuid');
const [errorMessage, setErrorMessage] = useState("");
const [password1FieldError,setPassword1FieldError] = useState("")
const [password2FieldError,setPassword2FieldError] = useState("")
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const searchParams = useSearchParams();
const uuid = searchParams.get('uuid');
const [errorMessage, setErrorMessage] = useState('');
const [password1FieldError, setPassword1FieldError] = useState('');
const [password2FieldError, setPassword2FieldError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const router = useRouter();
const csrfToken = useCsrfToken();
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
setTimeout(() => {
setIsLoading(false);
}, 1000);
} else {
getResetPassword(uuid)
.then(data => {
logger.debug('Success:', data);
setIsLoading(true);
if(data.errorFields){
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
setIsLoading(false);
})
.catch(error => {
logger.error('Error fetching data:', error);
});
}
}, []);
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setPopupMessage("Mot de passe réinitialisé avec succès");
setPopupVisible(true);
}, 1000);
} else {
const data = {
password1: formData.get('password1'),
password2: formData.get('password2'),
}
resetPassword(uuid,data,csrfToken)
.then(data => {
logger.debug('Success:', data);
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(data.errorMessage === ""){
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
})
.catch(error => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
useEffect(() => {
if (useFakeData) {
setTimeout(() => {
setIsLoading(false);
}, 1000);
} else {
return <>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FE_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Réinitialisation du mot de passe</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); validate(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full mb-5" />
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
<label className="text-red-500">{errorMessage}</label>
<div className="form-group-submit mt-4">
<Button text="Enregistrer" className="w-full" primary type="submit" name="validate" />
</div>
</form>
<br/>
<div className="flex justify-center mt-2 max-w-md mx-auto">
<Button text="Annuler" className="w-full" href={`${FE_USERS_LOGIN_URL}`} />
</div>
</div>
</>
getResetPassword(uuid)
.then((data) => {
logger.debug('Success:', data);
setIsLoading(true);
if (data.errorFields) {
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
setIsLoading(false);
})
.catch((error) => {
logger.error('Error fetching data:', error);
});
}
}
}, []);
function validate(formData) {
if (useFakeData) {
setTimeout(() => {
setPopupMessage('Mot de passe réinitialisé avec succès');
setPopupVisible(true);
}, 1000);
} else {
const data = {
password1: formData.get('password1'),
password2: formData.get('password2'),
};
resetPassword(uuid, data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setPassword1FieldError('');
setPassword2FieldError('');
setErrorMessage('');
if (data.errorMessage === '') {
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
if (data.errorFields) {
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
}
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
}
if (isLoading === true) {
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FE_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">
Réinitialisation du mot de passe
</h1>
<form
className="max-w-md mx-auto"
onSubmit={(e) => {
e.preventDefault();
validate(new FormData(e.target));
}}
>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon
name="password1"
type="password"
IconItem={KeySquare}
label="Mot de passe"
placeholder="Mot de passe"
errorMsg={password1FieldError}
className="w-full mb-5"
/>
<InputTextIcon
name="password2"
type="password"
IconItem={KeySquare}
label="Confirmation mot de passe"
placeholder="Confirmation mot de passe"
errorMsg={password2FieldError}
className="w-full"
/>
<label className="text-red-500">{errorMessage}</label>
<div className="form-group-submit mt-4">
<Button
text="Enregistrer"
className="w-full"
primary
type="submit"
name="validate"
/>
</div>
</form>
<br />
<div className="flex justify-center mt-2 max-w-md mx-auto">
<Button
text="Annuler"
className="w-full"
href={`${FE_USERS_LOGIN_URL}`}
/>
</div>
</div>
</>
);
}
}

View File

@ -1,106 +1,155 @@
'use client'
'use client';
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'
import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Button'; // Importez le composant Button
import Popup from '@/components/Popup'; // Importez le composant Popup
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext';
import { subscribe } from '@/app/actions/authAction';
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function Page() {
const searchParams = useSearchParams();
const searchParams = useSearchParams();
const [errorMessage, setErrorMessage] = useState("");
const [userFieldError,setUserFieldError] = useState("")
const [password1FieldError,setPassword1FieldError] = useState("")
const [password2FieldError,setPassword2FieldError] = useState("")
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [errorMessage, setErrorMessage] = useState('');
const [userFieldError, setUserFieldError] = useState('');
const [password1FieldError, setPassword1FieldError] = useState('');
const [password2FieldError, setPassword2FieldError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const router = useRouter();
const csrfToken = useCsrfToken();
const establishment_id = searchParams.get('establishment_id');
const router = useRouter();
const csrfToken = useCsrfToken();
function isOK(data) {
return data.errorMessage === ""
}
const establishment_id = searchParams.get('establishment_id');
function subscribeFormSubmit(formData) {
const data ={
email: formData.get('login'),
password1: formData.get('password1'),
password2: formData.get('password2'),
establishment_id: establishment_id
function isOK(data) {
return data.errorMessage === '';
}
function subscribeFormSubmit(formData) {
const data = {
email: formData.get('login'),
password1: formData.get('password1'),
password2: formData.get('password2'),
establishment_id: establishment_id,
};
subscribe(data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setUserFieldError('');
setPassword1FieldError('');
setPassword2FieldError('');
setErrorMessage('');
if (isOK(data)) {
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if (data.errorMessage) {
setErrorMessage(data.errorMessage);
}
if (data.errorFields) {
setUserFieldError(data.errorFields.email);
setPassword1FieldError(data.errorFields.password1);
setPassword2FieldError(data.errorFields.password2);
}
}
subscribe(data,csrfToken).then(data => {
logger.debug('Success:', data);
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(isOK(data)){
setPopupMessage(data.message);
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
})
.catch(error => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
})
.catch((error) => {
logger.error('Error fetching data:', error);
error = error.errorMessage;
logger.debug(error);
});
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">Nouveau profil</h1>
<form className="max-w-md mx-auto" onSubmit={(e) => { e.preventDefault(); subscribeFormSubmit(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full mb-5" />
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full mb-5" />
<InputTextIcon name="password2" type="password" IconItem={KeySquare} label="Confirmation mot de passe" placeholder="Confirmation mot de passe" errorMsg={password2FieldError} className="w-full" />
<p className="text-red-500">{errorMessage}</p>
<div className="form-group-submit mt-4">
<Button text="Enregistrer" className="w-full" primary type="submit" name="validate" />
</div>
</form>
<br/>
<div className='flex justify-center mt-2 max-w-md mx-auto'><Button text="Annuler" className="w-full" onClick={()=>{router.push(`${FE_USERS_LOGIN_URL}`)}} /></div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FE_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
if (isLoading === true) {
return <Loader />; // Affichez le composant Loader
} else {
return (
<>
<div className="container max mx-auto p-4">
<div className="flex justify-center mb-4">
<Logo className="h-150 w-150" />
</div>
<h1 className="text-2xl font-bold text-center mb-4">
Nouveau profil
</h1>
<form
className="max-w-md mx-auto"
onSubmit={(e) => {
e.preventDefault();
subscribeFormSubmit(new FormData(e.target));
}}
>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon
name="login"
type="text"
IconItem={User}
label="Identifiant"
placeholder="Identifiant"
errorMsg={userFieldError}
className="w-full mb-5"
/>
</>
}
}
<InputTextIcon
name="password1"
type="password"
IconItem={KeySquare}
label="Mot de passe"
placeholder="Mot de passe"
errorMsg={password1FieldError}
className="w-full mb-5"
/>
<InputTextIcon
name="password2"
type="password"
IconItem={KeySquare}
label="Confirmation mot de passe"
placeholder="Confirmation mot de passe"
errorMsg={password2FieldError}
className="w-full"
/>
<p className="text-red-500">{errorMessage}</p>
<div className="form-group-submit mt-4">
<Button
text="Enregistrer"
className="w-full"
primary
type="submit"
name="validate"
/>
</div>
</form>
<br />
<div className="flex justify-center mt-2 max-w-md mx-auto">
<Button
text="Annuler"
className="w-full"
onClick={() => {
router.push(`${FE_USERS_LOGIN_URL}`);
}}
/>
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FE_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
</>
);
}
}

View File

@ -11,14 +11,13 @@ import {
} from '@/utils/Url';
import logger from '@/utils/logger';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
const error = new Error(body?.errorMessage || "Une erreur est survenue");
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
};
@ -27,43 +26,39 @@ const requestResponseHandler = async (response) => {
* Login action
*/
export const login = (data) => {
return signIn('credentials', {
redirect: false,
email: data.email,
password: data.password,
role_type: data.role_type,
})
return signIn('credentials', {
redirect: false,
email: data.email,
password: data.password,
role_type: data.role_type,
});
};
/**
* Login user with API
*/
export const getJWT = (data) =>{
const request = new Request(
`${BE_AUTH_LOGIN_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
credentials: 'include'
}
);
return fetch(request).then(requestResponseHandler)
}
export const refreshJWT = (data) =>{
const request = new Request(
`${BE_AUTH_REFRESH_JWT_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
credentials: 'include'
}
);
return fetch(request).then(requestResponseHandler)
}
export const getJWT = (data) => {
const request = new Request(`${BE_AUTH_LOGIN_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
};
export const refreshJWT = (data) => {
const request = new Request(`${BE_AUTH_REFRESH_JWT_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
};
/**
* Disconnects the user after confirming the action.
@ -75,140 +70,116 @@ export const refreshJWT = (data) =>{
* @returns {void}
*/
export const disconnect = () => {
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
signOut({ callbackUrl: FE_USERS_LOGIN_URL });
};
export const fetchProfileRoles = (establishment) => {
return fetch(`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_AUTH_PROFILES_ROLES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const updateProfileRoles = (id, data, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_ROLES_URL}/${id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};
export const deleteProfileRoles = (id, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_ROLES_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken
},
credentials: 'include'
}
);
const request = new Request(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
};
export const fetchProfiles = () => {
return fetch(`${BE_AUTH_PROFILES_URL}`)
.then(requestResponseHandler)
return fetch(`${BE_AUTH_PROFILES_URL}`).then(requestResponseHandler);
};
export const createProfile = (data, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_PROFILES_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};
export const deleteProfile = (id, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken
},
credentials: 'include'
}
);
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
};
export const updateProfile = (id, data, csrfToken) => {
const request = new Request(
`${BE_AUTH_PROFILES_URL}/${id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};
export const sendNewPassword = (data, csrfToken) => {
const request = new Request(
`${BE_AUTH_NEW_PASSWORD_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_NEW_PASSWORD_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};
export const subscribe = (data, csrfToken) => {
const request = new Request(
`${BE_AUTH_REGISTER_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_REGISTER_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};
export const resetPassword = (uuid, data, csrfToken) => {
const request = new Request(
`${BE_AUTH_RESET_PASSWORD_URL}/${uuid}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
}
);
const request = new Request(`${BE_AUTH_RESET_PASSWORD_URL}/${uuid}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
};

View File

@ -1,24 +1,20 @@
import {
BE_GESTIONMESSAGERIE_MESSAGES_URL
} from '@/utils/Url';
import { BE_GESTIONMESSAGERIE_MESSAGES_URL } from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || "Une erreur est survenue");
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
}
};
export const fetchMessages = (id) =>{
return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
}
export const fetchMessages = (id) => {
return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};

View File

@ -1,112 +1,96 @@
import { BE_PLANNING_PLANNINGS_URL,
BE_PLANNING_EVENTS_URL
} from '@/utils/Url';
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = response.status !== 204 ? await response?.json() : {};
console.log(response)
console.log(response);
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || "Une erreur est survenue");
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
}
};
const getData = (url) => {
const getData = (url) => {
return fetch(`${url}`).then(requestResponseHandler);
}
};
const createDatas = (url, newData, csrfToken) => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(newData),
credentials: 'include'
})
.then(requestResponseHandler)
credentials: 'include',
}).then(requestResponseHandler);
};
const updateDatas = (url, updatedData, csrfToken) => {
return fetch(`${url}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(updatedData),
credentials: 'include'
})
.then(requestResponseHandler)
};
const removeDatas = (url, csrfToken) => {
return fetch(`${url}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(requestResponseHandler)
return fetch(`${url}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(updatedData),
credentials: 'include',
}).then(requestResponseHandler);
};
const removeDatas = (url, csrfToken) => {
return fetch(`${url}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}).then(requestResponseHandler);
};
export const fetchPlannings = () => {
return getData(`${BE_PLANNING_PLANNINGS_URL}`)
return getData(`${BE_PLANNING_PLANNINGS_URL}`);
};
export const getPlanning = (id) => {
return getData(`${BE_PLANNING_PLANNINGS_URL}/${id}`)
return getData(`${BE_PLANNING_PLANNINGS_URL}/${id}`);
};
export const createPlanning = (newData, csrfToken) => {
return createDatas(`${BE_PLANNING_PLANNINGS_URL}`, newData, csrfToken)
}
export const updatePlanning = (id,newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, newData, csrfToken)
}
export const deletePlanning = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken)
}
export const fetchEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}`)
return createDatas(`${BE_PLANNING_PLANNINGS_URL}`, newData, csrfToken);
};
export const updatePlanning = (id, newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, newData, csrfToken);
};
export const deletePlanning = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_PLANNINGS_URL}/${id}`, csrfToken);
};
export const fetchEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}`);
};
export const getEvent = (id) => {
return getData(`${BE_PLANNING_EVENTS_URL}/${id}`)
return getData(`${BE_PLANNING_EVENTS_URL}/${id}`);
};
export const createEvent = (newData, csrfToken) => {
return createDatas(`${BE_PLANNING_EVENTS_URL}`, newData, csrfToken)
}
return createDatas(`${BE_PLANNING_EVENTS_URL}`, newData, csrfToken);
};
export const updateEvent = (id,newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, newData, csrfToken)
}
export const updateEvent = (id, newData, csrfToken) => {
return updateDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, newData, csrfToken);
};
export const deleteEvent = (id, csrfToken) => {
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken)
}
return removeDatas(`${BE_PLANNING_EVENTS_URL}/${id}`, csrfToken);
};
export const fetchUpcomingEvents = () => {
return getData(`${BE_PLANNING_EVENTS_URL}/upcoming`);
};

View File

@ -1,30 +1,33 @@
import { BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL,
FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN
import {
BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL,
FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
} from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || "Une erreur est survenue");
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
}
};
export async function fetchRegistrationFileGroups(establishment) {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
});
);
if (!response.ok) {
throw new Error('Failed to fetch file groups');
}
@ -32,15 +35,18 @@ export async function fetchRegistrationFileGroups(establishment) {
}
export async function createRegistrationFileGroup(groupData, csrfToken) {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(groupData),
credentials: 'include'
});
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(groupData),
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('Failed to create file group');
@ -50,26 +56,36 @@ export async function createRegistrationFileGroup(groupData, csrfToken) {
}
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include'
});
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
return response;
}
export const editRegistrationFileGroup = async (groupId, groupData, csrfToken) => {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(groupData),
});
export const editRegistrationFileGroup = async (
groupId,
groupData,
csrfToken
) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(groupData),
}
);
if (!response.ok) {
throw new Error('Erreur lors de la modification du groupe');
@ -79,33 +95,35 @@ export const editRegistrationFileGroup = async (groupId, groupData, csrfToken) =
};
export const fetchRegistrationFileFromGroup = async (groupId) => {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/templates`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
});
);
if (!response.ok) {
throw new Error('Erreur lors de la récupération des fichiers associés au groupe');
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
}
};
export const fetchRegistrationTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`
export const fetchRegistrationTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`;
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${id}`;
}
const request = new Request(
`${url}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const editRegistrationTemplates = (fileId, data, csrfToken) => {
@ -116,12 +134,10 @@ export const editRegistrationTemplates = (fileId, data, csrfToken) => {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
}
export const createRegistrationTemplates = (data,csrfToken) => {
}).then(requestResponseHandler);
};
export const createRegistrationTemplates = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`, {
method: 'POST',
body: JSON.stringify(data),
@ -130,73 +146,72 @@ export const createRegistrationTemplates = (data,csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
}
}).then(requestResponseHandler);
};
export const deleteRegistrationTemplates = (fileId,csrfToken) => {
export const deleteRegistrationTemplates = (fileId, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}/${fileId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
}
});
};
export const fetchRegistrationTemplateMaster = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`;
if(id){
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${id}`;
}
const request = new Request(
`${url}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const createRegistrationTemplateMaster = (data,csrfToken) => {
export const createRegistrationTemplateMaster = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type':'application/json'
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
}
}).then(requestResponseHandler);
};
export const deleteRegistrationTemplateMaster = (fileId,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
}
export const deleteRegistrationTemplateMaster = (fileId, csrfToken) => {
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
);
};
export const editRegistrationTemplateMaster = (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type':'application/json'
},
credentials: 'include',
})
.then(requestResponseHandler)
}
return fetch(
`${BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL}/${fileId}`,
{
method: 'PUT',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}
).then(requestResponseHandler);
};
export const cloneTemplate = (templateId, email, is_required) => {
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
@ -207,21 +222,19 @@ export const cloneTemplate = (templateId, email, is_required) => {
body: JSON.stringify({
templateId,
email,
is_required
})
})
.then(requestResponseHandler)
}
is_required,
}),
}).then(requestResponseHandler);
};
export const downloadTemplate = (slug) => {
return fetch(`${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
})
.then(requestResponseHandler)
}
},
}).then(requestResponseHandler);
};
export const generateToken = (email, id = null) => {
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
@ -231,4 +244,4 @@ export const generateToken = (email, id = null) => {
},
body: JSON.stringify({ user_email: email, id }),
}).then(requestResponseHandler);
};
};

View File

@ -1,127 +1,133 @@
import {
BE_SCHOOL_SPECIALITIES_URL,
BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_PLANNINGS_URL,
BE_SCHOOL_FEES_URL,
BE_SCHOOL_DISCOUNTS_URL,
BE_SCHOOL_PAYMENT_PLANS_URL,
BE_SCHOOL_PAYMENT_MODES_URL,
BE_SCHOOL_ESTABLISHMENT_URL
BE_SCHOOL_SPECIALITIES_URL,
BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_PLANNINGS_URL,
BE_SCHOOL_FEES_URL,
BE_SCHOOL_DISCOUNTS_URL,
BE_SCHOOL_PAYMENT_PLANS_URL,
BE_SCHOOL_PAYMENT_MODES_URL,
BE_SCHOOL_ESTABLISHMENT_URL,
} from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || "Une erreur est survenue");
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
}
};
export const fetchSpecialities = (establishment) => {
return fetch(`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchTeachers = (establishment) => {
return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchClasses = (establishment) => {
return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchSchedules = () => {
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
.then(requestResponseHandler)
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`).then(requestResponseHandler);
};
export const fetchRegistrationDiscounts = (establishment) => {
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchTuitionDiscounts = (establishment) => {
return fetch(`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchRegistrationFees = (establishment) => {
return fetch(`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchTuitionFees = (establishment) => {
return fetch(`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`)
.then(requestResponseHandler)
return fetch(
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchRegistrationPaymentPlans = (establishment) => {
return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`)
.then(requestResponseHandler)
}
return fetch(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchTuitionPaymentPlans = (establishment) => {
return fetch(`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`)
.then(requestResponseHandler)
}
return fetch(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchRegistrationPaymentModes = (establishment) => {
return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`)
.then(requestResponseHandler)
}
return fetch(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchTuitionPaymentModes = (establishment) => {
return fetch(`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`)
.then(requestResponseHandler)
}
return fetch(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
};
export const fetchEstablishment = (establishment) => {
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`)
.then(requestResponseHandler)
}
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`).then(
requestResponseHandler
);
};
export const createDatas = (url, newData, csrfToken) => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(newData),
credentials: 'include'
})
.then(requestResponseHandler)
credentials: 'include',
}).then(requestResponseHandler);
};
export const updateDatas = (url, id, updatedData, csrfToken) => {
return fetch(`${url}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(updatedData),
credentials: 'include'
})
.then(requestResponseHandler)
};
return fetch(`${url}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(updatedData),
credentials: 'include',
}).then(requestResponseHandler);
};
export const removeDatas = (url, id, csrfToken) => {
return fetch(`${url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(requestResponseHandler)
};
return fetch(`${url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}).then(requestResponseHandler);
};

View File

@ -1,150 +1,152 @@
import {
BE_SUBSCRIPTION_STUDENTS_URL,
BE_SUBSCRIPTION_CHILDRENS_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL
BE_SUBSCRIPTION_STUDENTS_URL,
BE_SUBSCRIPTION_CHILDRENS_URL,
BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
} from '@/utils/Url';
export const PENDING = 'pending';
export const SUBSCRIBED = 'subscribed';
export const ARCHIVED = 'archived';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || "Une erreur est survenue");
// Throw an error with the JSON body containing the form errors
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
}
export const fetchRegisterForms = (establishment, filter=PENDING, page='', pageSize='', search = '') => {
let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}`;
if (page !== '' && pageSize !== '') {
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
};
export const fetchRegisterForm = (id) =>{
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
.then(requestResponseHandler)
}
export const fetchLastGuardian = () =>{
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`)
.then(requestResponseHandler)
}
export const editRegisterForm=(id, data, csrfToken)=>{
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data),
credentials: 'include'
})
.then(requestResponseHandler)
};
export const sendSEPARegisterForm=(id, data, csrfToken)=>{
export const fetchRegisterForms = (
establishment,
filter = PENDING,
page = '',
pageSize = '',
search = ''
) => {
let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}`;
if (page !== '' && pageSize !== '') {
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};
export const fetchRegisterForm = (id) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
.then(requestResponseHandler);
};
export const fetchLastGuardian = () => {
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`).then(
requestResponseHandler
);
};
export const editRegisterForm = (id, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
method: 'PUT',
headers: {
'X-CSRFToken': csrfToken
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(data),
credentials: 'include',
}).then(requestResponseHandler);
};
export const sendSEPARegisterForm = (id, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
method: 'PUT',
headers: {
'X-CSRFToken': csrfToken,
},
body: data,
credentials: 'include'
})
.then(requestResponseHandler)
credentials: 'include',
}).then(requestResponseHandler);
};
export const createRegisterForm=(data, csrfToken)=>{
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data),
credentials: 'include'
})
.then(requestResponseHandler)
}
export const createRegisterForm = (data, csrfToken) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(data),
credentials: 'include',
}).then(requestResponseHandler);
};
export const sendRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
}
export const resendRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
}
export const archiveRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler)
}
export const fetchStudents = (establishment, id=null) => {
const url = (id)?`${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`:`${BE_SUBSCRIPTION_STUDENTS_URL}?establishment_id=${establishment}`;
const request = new Request(
url,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};
export const fetchChildren = (id, establishment) =>{
const request = new Request(
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
}
export const resendRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};
export const archiveRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
};
export const fetchStudents = (establishment, id = null) => {
const url = id
? `${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`
: `${BE_SUBSCRIPTION_STUDENTS_URL}?establishment_id=${establishment}`;
const request = new Request(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
};
export const fetchChildren = (id, establishment) => {
const request = new Request(
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
);
return fetch(request).then(requestResponseHandler);
};
export async function getRegisterFormFileTemplate(fileId) {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
});
);
if (!response.ok) {
throw new Error('Failed to fetch file template');
}
@ -152,28 +154,36 @@ export async function getRegisterFormFileTemplate(fileId) {
}
export const fetchTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/templates`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
});
);
if (!response.ok) {
throw new Error('Erreur lors de la récupération des fichiers associés au groupe');
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
}
};
export const dissociateGuardian = async (studentId, guardianId) => {
const response = await fetch(`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`, {
const response = await fetch(
`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`,
{
credentials: 'include',
method: 'PUT',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
},
});
}
);
if (!response.ok) {
throw new Error('Erreur lors de la dissociation.');
throw new Error('Erreur lors de la dissociation.');
}
return response.json();
};
};

View File

@ -1,11 +1,11 @@
import React from 'react';
import { getMessages } from 'next-intl/server';
import Providers from '@/components/Providers'
import "@/css/tailwind.css";
import Providers from '@/components/Providers';
import '@/css/tailwind.css';
import { headers } from 'next/headers';
export const metadata = {
title: "N3WT-SCHOOL",
title: 'N3WT-SCHOOL',
description: "Gestion de l'école",
icons: {
icon: [
@ -14,7 +14,7 @@ export const metadata = {
type: 'image/svg+xml',
},
{
url: '/favicon.ico', // Fallback pour les anciens navigateurs
url: '/favicon.ico', // Fallback pour les anciens navigateurs
sizes: 'any',
},
],

View File

@ -1,15 +1,21 @@
import Link from 'next/link'
import Logo from '../components/Logo'
import Link from 'next/link';
import Logo from '../components/Logo';
export default function NotFound() {
return (
<div className='flex items-center justify-center min-h-screen bg-emerald-500'>
<div className='text-center p-6 '>
<div className="flex items-center justify-center min-h-screen bg-emerald-500">
<div className="text-center p-6 ">
<Logo className="w-32 h-32 mx-auto mb-4" />
<h2 className='text-2xl font-bold text-emerald-900 mb-4'>404 | Page non trouvée</h2>
<p className='text-emerald-900 mb-4'>La ressource que vous souhaitez consulter n&apos;existe pas ou plus.</p>
<Link className="text-gray-900 hover:underline" href="/">Retour Accueil</Link>
<h2 className="text-2xl font-bold text-emerald-900 mb-4">
404 | Page non trouvée
</h2>
<p className="text-emerald-900 mb-4">
La ressource que vous souhaitez consulter n&apos;existe pas ou plus.
</p>
<Link className="text-gray-900 hover:underline" href="/">
Retour Accueil
</Link>
</div>
</div>
)
}
);
}

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react';
const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
const [formData, setFormData] = useState({
classeAssocie_id: eleve?.classeAssocie_id || null,
});
@ -17,10 +16,10 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
const handleSubmit = () => {
onSubmit({
eleve: {
...formData
...formData,
},
etat:5
});
etat: 5,
});
};
return (
@ -30,18 +29,21 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
Classes
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{classes.map(classe => (
{classes.map((classe) => (
<div key={classe.id} className="flex items-center">
<input
type="radio"
id={`classe-${classe.id}`}
name="classeAssocie_id"
value={classe.id}
checked={formData.classeAssocie_id === classe.id}
onChange={handleChange}
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
type="radio"
id={`classe-${classe.id}`}
name="classeAssocie_id"
value={classe.id}
checked={formData.classeAssocie_id === classe.id}
onChange={handleChange}
className="form-radio h-3 w-3 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 checked:h-3 checked:w-3"
/>
<label htmlFor={`classe-${classe.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
<label
htmlFor={`classe-${classe.id}`}
className="ml-2 block text-sm text-gray-900 flex items-center"
>
{classe.atmosphere_name}
</label>
</div>
@ -50,15 +52,15 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
</div>
<div className="flex justify-end mt-4 space-x-4">
<button
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(!formData.classeAssocie_id )
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(!formData.classeAssocie_id)}
onClick={handleSubmit}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
!formData.classeAssocie_id
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={!formData.classeAssocie_id}
>
Associer
Associer
</button>
</div>
</form>

View File

@ -2,11 +2,17 @@ import React from 'react';
const AlertMessage = ({ title, message, buttonText, buttonLink }) => {
return (
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<div
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
role="alert"
>
<h3 className="font-bold">{title}</h3>
<p className="mt-2">{message}</p>
<div className="alert-actions mt-4">
<a className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600" href={buttonLink}>
<a
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
href={buttonLink}
>
{buttonText} <i className="icon profile-add"></i>
</a>
</div>

View File

@ -2,28 +2,31 @@ import React, { useState } from 'react';
import Modal from '@/components/Modal';
import { UserPlus } from 'lucide-react';
const AlertWithModal = ({ title, message, buttonText}) => {
const [isOpen, setIsOpen] = useState(false);
const AlertWithModal = ({ title, message, buttonText }) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const openModal = () => {
setIsOpen(true);
};
return (
<div className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<h3 className="font-bold">{title}</h3>
<p className="mt-2">{message}</p>
<div className="alert-actions mt-4">
<button
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
onClick={openModal}
>
{buttonText} <UserPlus size={20} className="ml-2" />
</button>
</div>
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
</div>
);
return (
<div
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
role="alert"
>
<h3 className="font-bold">{title}</h3>
<p className="mt-2">{message}</p>
<div className="alert-actions mt-4">
<button
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600 flex items-center"
onClick={openModal}
>
{buttonText} <UserPlus size={20} className="ml-2" />
</button>
</div>
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
</div>
);
};
export default AlertWithModal;
export default AlertWithModal;

View File

@ -1,37 +1,39 @@
import React, { useState } from 'react';
const AlphabetPaginationNumber = ({ letter, active , onClick}) => (
<button className={`w-8 h-8 flex items-center justify-center rounded ${
active ? 'bg-emerald-500 text-white' : 'text-gray-600 bg-gray-200 hover:bg-gray-50'
}`} onClick={onClick}>
const AlphabetPaginationNumber = ({ letter, active, onClick }) => (
<button
className={`w-8 h-8 flex items-center justify-center rounded ${
active
? 'bg-emerald-500 text-white'
: 'text-gray-600 bg-gray-200 hover:bg-gray-50'
}`}
onClick={onClick}
>
{letter}
</button>
);
const AlphabetLinks = ({filter, onLetterClick }) => {
const AlphabetLinks = ({ filter, onLetterClick }) => {
const [currentLetter, setCurrentLetter] = useState(filter);
const alphabet = "*ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
const alphabet = '*ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
return (
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div className="flex items-center gap-2">
{alphabet.map((letter) => (
<AlphabetPaginationNumber
key={letter}
letter={letter}
active={currentLetter === letter }
onClick={() => {setCurrentLetter(letter);onLetterClick(letter)}}
/>
))}
{alphabet.map((letter) => (
<AlphabetPaginationNumber
key={letter}
letter={letter}
active={currentLetter === letter}
onClick={() => {
setCurrentLetter(letter);
onLetterClick(letter);
}}
/>
))}
</div>
</div>
);
};
export default AlphabetLinks;
export default AlphabetLinks;

View File

@ -1,9 +1,18 @@
import React from 'react';
import { useRouter } from 'next/navigation';
const Button = ({ text, onClick, href, className, primary, icon, disabled}) => {
const Button = ({
text,
onClick,
href,
className,
primary,
icon,
disabled,
}) => {
const router = useRouter();
const baseClass = 'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
const baseClass =
'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
const primaryClass = 'bg-emerald-500 hover:bg-emerald-600';
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
const buttonClass = `${baseClass} ${primary && !disabled ? primaryClass : secondaryClass} ${className}`;

View File

@ -6,25 +6,43 @@ import YearView from '@/components/Calendar/YearView';
import PlanningView from '@/components/Calendar/PlanningView';
import ToggleView from '@/components/ToggleView';
import { ChevronLeft, ChevronRight, Plus, ChevronDown } from 'lucide-react';
import { format, addWeeks, addMonths, addYears, subWeeks, subMonths, subYears, getWeek, setMonth, setYear } from 'date-fns';
import {
format,
addWeeks,
addMonths,
addYears,
subWeeks,
subMonths,
subYears,
getWeek,
setMonth,
setYear,
} from 'date-fns';
import { fr } from 'date-fns/locale';
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
import logger from '@/utils/logger';
const Calendar = ({ onDateClick, onEventClick }) => {
const { currentDate, setCurrentDate, viewType, setViewType, events, hiddenSchedules } = usePlanning();
const {
currentDate,
setCurrentDate,
viewType,
setViewType,
events,
hiddenSchedules,
} = usePlanning();
const [visibleEvents, setVisibleEvents] = useState([]);
const [showDatePicker, setShowDatePicker] = useState(false);
// Ajouter ces fonctions pour la gestion des mois et années
const months = Array.from({ length: 12 }, (_, i) => ({
value: i,
label: format(new Date(2024, i, 1), 'MMMM', { locale: fr })
label: format(new Date(2024, i, 1), 'MMMM', { locale: fr }),
}));
const years = Array.from({ length: 10 }, (_, i) => ({
value: new Date().getFullYear() - 5 + i,
label: new Date().getFullYear() - 5 + i
label: new Date().getFullYear() - 5 + i,
}));
const handleMonthSelect = (monthIndex) => {
@ -39,7 +57,9 @@ const Calendar = ({ onDateClick, onEventClick }) => {
useEffect(() => {
// S'assurer que le filtrage est fait au niveau parent
const filtered = events?.filter(event => !hiddenSchedules.includes(event.planning));
const filtered = events?.filter(
(event) => !hiddenSchedules.includes(event.planning)
);
setVisibleEvents(filtered);
logger.debug('Events filtrés:', filtered); // Debug
}, [events, hiddenSchedules]);
@ -78,7 +98,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
>
Aujourd&apos;hui
</button>
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
<button
onClick={() => navigateDate('prev')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronLeft className="w-5 h-5" />
</button>
@ -89,7 +112,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
className="flex items-center gap-1 px-2 py-1 hover:bg-gray-100 rounded-md"
>
<h2 className="text-xl font-semibold">
{format(currentDate, viewType === 'year' ? 'yyyy' : 'MMMM yyyy', { locale: fr })}
{format(
currentDate,
viewType === 'year' ? 'yyyy' : 'MMMM yyyy',
{ locale: fr }
)}
</h2>
<ChevronDown className="w-4 h-4" />
</button>
@ -129,7 +156,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
)}
</div>
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
<button
onClick={() => navigateDate('next')}
className="p-2 hover:bg-gray-100 rounded-full"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
@ -168,7 +198,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
transition={{ duration: 0.2 }}
className="h-full flex flex-col"
>
<WeekView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
<WeekView
onDateClick={onDateClick}
onEventClick={onEventClick}
events={visibleEvents}
/>
</motion.div>
)}
{viewType === 'month' && (
@ -179,7 +213,11 @@ const Calendar = ({ onDateClick, onEventClick }) => {
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<MonthView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
<MonthView
onDateClick={onDateClick}
onEventClick={onEventClick}
events={visibleEvents}
/>
</motion.div>
)}
{viewType === 'year' && (
@ -201,7 +239,10 @@ const Calendar = ({ onDateClick, onEventClick }) => {
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<PlanningView onEventClick={onEventClick} events={visibleEvents} />
<PlanningView
onEventClick={onEventClick}
events={visibleEvents}
/>
</motion.div>
)}
</AnimatePresence>
@ -210,4 +251,4 @@ const Calendar = ({ onDateClick, onEventClick }) => {
);
};
export default Calendar;
export default Calendar;

View File

@ -1,6 +1,15 @@
import React from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
import {
format,
startOfWeek,
endOfWeek,
eachDayOfInterval,
startOfMonth,
endOfMonth,
isSameMonth,
isToday,
} from 'date-fns';
import { fr } from 'date-fns/locale';
import { getEventsForDate } from '@/utils/events';
@ -35,7 +44,8 @@ const MonthView = ({ onDateClick, onEventClick }) => {
onClick={() => handleDayClick(day)}
>
<div className="flex justify-between items-center mb-1">
<span className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
<span
className={`text-sm font-medium rounded-full w-7 h-7 flex items-center justify-center
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
${!isCurrentMonth ? 'text-gray-400' : ''}`}
>
@ -50,7 +60,7 @@ const MonthView = ({ onDateClick, onEventClick }) => {
style={{
backgroundColor: `${event.color}15`,
color: event.color,
borderLeft: `2px solid ${event.color}`
borderLeft: `2px solid ${event.color}`,
}}
onClick={(e) => {
e.stopPropagation();
@ -69,8 +79,11 @@ const MonthView = ({ onDateClick, onEventClick }) => {
<div className="h-full flex flex-col border border-gray-200 rounded-lg bg-white">
{/* En-tête des jours de la semaine */}
<div className="grid grid-cols-7 border-b">
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500">
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
<div
key={day}
className="p-2 text-center text-sm font-medium text-gray-500"
>
{day}
</div>
))}
@ -83,4 +96,4 @@ const MonthView = ({ onDateClick, onEventClick }) => {
);
};
export default MonthView;
export default MonthView;

View File

@ -15,17 +15,20 @@ const PlanningView = ({ events, onEventClick }) => {
// Sinon, créer une entrée pour chaque jour
const days = eachDayOfInterval({ start, end });
return days.map(day => ({
return days.map((day) => ({
...event,
displayDate: day,
isMultiDay: true
isMultiDay: true,
}));
};
// Aplatir tous les événements en incluant les événements sur plusieurs jours
const flattenedEvents = events
.flatMap(splitEventByDays)
.sort((a, b) => new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start));
.sort(
(a, b) =>
new Date(a.displayDate || a.start) - new Date(b.displayDate || b.start)
);
return (
<div className="bg-white h-full overflow-auto">
@ -58,26 +61,29 @@ const PlanningView = ({ events, onEventClick }) => {
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
<div className="flex items-center gap-1">
<span className="font-extrabold">{format(start, 'd')}</span>
<span className="font-semibold">{format(start, 'MMM', { locale: fr }).toLowerCase()}</span>
<span className="font-semibold">{format(start, 'EEE', { locale: fr })}</span>
<span className="font-semibold">
{format(start, 'MMM', { locale: fr }).toLowerCase()}
</span>
<span className="font-semibold">
{format(start, 'EEE', { locale: fr })}
</span>
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
<div className="flex items-center">
<div
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: event.color }}
/>
{isMultiDay
? (isSameDay(start, new Date(event.start))
? "À partir de "
: isSameDay(start, end)
? "Jusqu'à "
: "Toute la journée")
: ""
}
{format(new Date(event.start), 'HH:mm')}
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
{isMultiDay
? isSameDay(start, new Date(event.start))
? 'À partir de '
: isSameDay(start, end)
? "Jusqu'à "
: 'Toute la journée'
: ''}
{format(new Date(event.start), 'HH:mm')}
{!isMultiDay && ` - ${format(end, 'HH:mm')}`}
</div>
</td>
<td className="py-3 px-4">
@ -108,4 +114,4 @@ const PlanningView = ({ events, onEventClick }) => {
);
};
export default PlanningView;
export default PlanningView;

View File

@ -1,6 +1,12 @@
import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format, startOfWeek, addDays, differenceInMinutes, isSameDay } from 'date-fns';
import {
format,
startOfWeek,
addDays,
differenceInMinutes,
isSameDay,
} from 'date-fns';
import { fr } from 'date-fns/locale';
import { getWeekEvents } from '@/utils/events';
import { isToday } from 'date-fns';
@ -16,7 +22,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
// Maintenant on peut utiliser weekDays
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
const isCurrentWeek = weekDays.some((day) => isSameDay(day, new Date()));
// Mettre à jour la position de la ligne toutes les minutes
useEffect(() => {
@ -61,7 +67,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
return dayEvents.filter(otherEvent => {
return dayEvents.filter((otherEvent) => {
if (otherEvent.id === event.id) return false;
const otherStart = new Date(otherEvent.start);
const otherEnd = new Date(otherEvent.end);
@ -77,7 +83,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
// Trouver les événements qui se chevauchent
const overlappingEvents = findOverlappingEvents(event, dayEvents);
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
const eventIndex = overlappingEvents.findIndex((e) => e.id > event.id) + 1;
const totalOverlapping = overlappingEvents.length + 1;
// Calculer la largeur et la position horizontale
@ -93,7 +99,7 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
borderLeft: `3px solid ${event.color}`,
borderRadius: '0.25rem',
zIndex: 1,
transform: `translateY(${startMinutes}rem)`
transform: `translateY(${startMinutes}rem)`,
};
};
@ -111,14 +117,24 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
}}
>
<div className="p-1">
<div className="font-semibold text-xs truncate" style={{ color: event.color }}>
<div
className="font-semibold text-xs truncate"
style={{ color: event.color }}
>
{event.title}
</div>
<div className="text-xs" style={{ color: event.color, opacity: 0.75 }}>
{format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
<div
className="text-xs"
style={{ color: event.color, opacity: 0.75 }}
>
{format(new Date(event.start), 'HH:mm')} -{' '}
{format(new Date(event.end), 'HH:mm')}
</div>
{event.location && (
<div className="text-xs truncate" style={{ color: event.color, opacity: 0.75 }}>
<div
className="text-xs truncate"
style={{ color: event.color, opacity: 0.75 }}
>
{event.location}
</div>
)}
@ -130,7 +146,10 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* En-tête des jours */}
<div className="grid gap-[1px] bg-gray-100 pr-[17px]" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
<div
className="grid gap-[1px] bg-gray-100 pr-[17px]"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
>
<div className="bg-white h-14"></div>
{weekDays.map((day) => (
<div
@ -142,8 +161,10 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
<div className="text-xs font-medium text-gray-500">
{format(day, 'EEEE', { locale: fr })}
</div>
<div className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}>
<div
className={`text-sm font-semibold inline-block rounded-full w-7 h-7 leading-7
${isToday(day) ? 'bg-emerald-500 text-white' : ''}`}
>
{format(day, 'd', { locale: fr })}
</div>
</div>
@ -158,17 +179,17 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
className="absolute left-0 right-0 z-10 border-emerald-500 border pointer-events-none"
style={{
top: getCurrentTimePosition(),
}}
>
<div
className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500"
/>
<div className="absolute -left-2 -top-1 w-2 h-2 rounded-full bg-emerald-500" />
</div>
)}
<div className="grid gap-[1px] bg-gray-100" style={{ gridTemplateColumns: "2.5rem repeat(7, 1fr)" }}>
{timeSlots.map(hour => (
<div
className="grid gap-[1px] bg-gray-100"
style={{ gridTemplateColumns: '2.5rem repeat(7, 1fr)' }}
>
{timeSlots.map((hour) => (
<React.Fragment key={hour}>
<div className="h-20 p-1 text-right text-sm text-gray-500 bg-gray-100 font-medium">
{`${hour.toString().padStart(2, '0')}:00`}
@ -188,11 +209,15 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
onDateClick(date);
}}
>
<div className="flex gap-1"> {/* Ajout de gap-1 */}
{dayEvents.filter(event => {
const eventStart = new Date(event.start);
return eventStart.getHours() === hour;
}).map(event => renderEventInCell(event, dayEvents))}
<div className="flex gap-1">
{' '}
{/* Ajout de gap-1 */}
{dayEvents
.filter((event) => {
const eventStart = new Date(event.start);
return eventStart.getHours() === hour;
})
.map((event) => renderEventInCell(event, dayEvents))}
</div>
</div>
);
@ -205,4 +230,4 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
);
};
export default WeekView;
export default WeekView;

View File

@ -37,7 +37,7 @@ const YearView = ({ onDateClick }) => {
return (
<div className="grid grid-cols-4 gap-4 p-4">
{months.map(month => (
{months.map((month) => (
<MonthCard
key={month.getTime()}
month={month}
@ -49,4 +49,4 @@ const YearView = ({ onDateClick }) => {
);
};
export default YearView;
export default YearView;

View File

@ -1,13 +1,24 @@
import React from 'react';
const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = () => null, labelAttenuated = () => false, horizontal }) => {
const CheckBox = ({
item,
formData,
handleChange,
fieldName,
itemLabelFunc = () => null,
labelAttenuated = () => false,
horizontal,
}) => {
const isChecked = formData[fieldName].includes(parseInt(item.id));
const isAttenuated = labelAttenuated(item) && !isChecked;
return (
<div key={item.id} className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}>
<div
key={item.id}
className={`flex ${horizontal ? 'flex-col items-center' : 'flex-row items-center'}`}
>
{horizontal && (
<label
htmlFor={`${fieldName}-${item.id}`}
<label
htmlFor={`${fieldName}-${item.id}`}
className={`block text-sm text-center mb-1 ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
>
{itemLabelFunc(item)}
@ -24,8 +35,8 @@ const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = ()
style={{ borderRadius: '6px', outline: 'none', boxShadow: 'none' }}
/>
{!horizontal && (
<label
htmlFor={`${fieldName}-${item.id}`}
<label
htmlFor={`${fieldName}-${item.id}`}
className={`block text-sm ${isAttenuated ? 'text-gray-300' : 'font-bold text-emerald-600'}`}
>
{itemLabelFunc(item)}
@ -35,4 +46,4 @@ const CheckBox = ({ item, formData, handleChange, fieldName, itemLabelFunc = ()
);
};
export default CheckBox;
export default CheckBox;

View File

@ -11,7 +11,7 @@ const CheckBoxList = ({
className,
itemLabelFunc = (item) => item.name,
labelAttenuated = () => false,
horizontal = false // Ajouter l'option horizontal
horizontal = false, // Ajouter l'option horizontal
}) => {
return (
<div className={`mb-4 w-full ${className}`}>
@ -19,8 +19,10 @@ const CheckBoxList = ({
{Icon && <Icon className="w-5 h-5 mr-2" />}
{label}
</label>
<div className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}>
{items.map(item => (
<div
className={`mt-2 grid ${horizontal ? 'grid-cols-6 gap-2' : 'grid-cols-1 gap-4'}`}
>
{items.map((item) => (
<CheckBox
key={`${fieldName}-${item.id}`}
item={item}

View File

@ -5,7 +5,7 @@ import { GraduationCap } from 'lucide-react';
const ClasseDetails = ({ classe }) => {
if (!classe) return null;
const nombreElevesInscrits = classe?.eleves?.length||0;
const nombreElevesInscrits = classe?.eleves?.length || 0;
const capaciteTotale = classe.number_of_students;
const pourcentage = Math.round((nombreElevesInscrits / capaciteTotale) * 100);
@ -42,14 +42,12 @@ const ClasseDetails = ({ classe }) => {
{nombreElevesInscrits}/{capaciteTotale}
</span>
<div className="w-32 bg-gray-200 rounded-full h-6 shadow-inner">
<div
<div
className={`h-full rounded-full ${getColor(pourcentage)}`}
style={{ width: `${pourcentage}%` }}
></div>
</div>
<span className="ml-4 font-bold text-gray-700">
{pourcentage}%
</span>
<span className="ml-4 font-bold text-gray-700">{pourcentage}%</span>
</div>
</div>
</div>
@ -60,7 +58,7 @@ const ClasseDetails = ({ classe }) => {
columns={[
{ name: 'NOM', transform: (row) => row.name },
{ name: 'PRENOM', transform: (row) => row.first_name },
{ name: 'AGE', transform: (row) => `${row.age}` }
{ name: 'AGE', transform: (row) => `${row.age}` },
]}
data={classe.students}
/>

View File

@ -2,8 +2,8 @@ import React from 'react';
const LevelLabel = ({ label, index }) => {
return (
<div
key={index}
<div
key={index}
className={`ml-2 px-3 py-1 rounded-md shadow-sm ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
} border border-gray-200 text-gray-700`}

View File

@ -2,13 +2,15 @@ import React from 'react';
const TeacherLabel = ({ nom, prenom, index }) => {
return (
<div
key={index}
<div
key={index}
className={`ml-2 px-3 py-1 rounded-md shadow-sm ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
} border border-gray-200 text-gray-700`}
>
<span className="font-bold">{nom} {prenom}</span>
<span className="font-bold">
{nom} {prenom}
</span>
</div>
);
};

View File

@ -2,9 +2,17 @@ import React, { useState, useEffect } from 'react';
import { Check } from 'lucide-react';
import Popup from '@/components/Popup';
const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, paymentPlanId, resetModifiedDates }) => {
const DateTab = ({
dates,
activeTab,
handleDateChange,
handleEdit,
type,
paymentPlanId,
resetModifiedDates,
}) => {
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [popupMessage, setPopupMessage] = useState('');
const [modifiedDates, setModifiedDates] = useState({});
useEffect(() => {
@ -16,22 +24,24 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
const submit = (updatedData) => {
const dataWithType = {
...updatedData,
type: type
type: type,
};
handleEdit(paymentPlanId, dataWithType)
.then(() => {
setPopupMessage(`Mise à jour de la date d'échéance effectuée avec succès`);
setPopupMessage(
`Mise à jour de la date d'échéance effectuée avec succès`
);
setPopupVisible(true);
setModifiedDates({});
})
.catch(error => {
.catch((error) => {
console.error(error);
});
};
const handleDateChangeWithModification = (tab, index, value) => {
handleDateChange(tab, index, value);
setModifiedDates(prev => ({ ...prev, [`${tab}-${index}`]: true }));
setModifiedDates((prev) => ({ ...prev, [`${tab}-${index}`]: true }));
};
return (
@ -39,17 +49,30 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
<div className="flex flex-col space-y-3">
{dates[activeTab]?.map((date, index) => (
<div key={index} className="flex items-center space-x-3">
<span className="text-emerald-700 font-semibold">Échéance {index + 1}</span>
<span className="text-emerald-700 font-semibold">
Échéance {index + 1}
</span>
<input
type="date"
value={date}
onChange={(e) => handleDateChangeWithModification(activeTab, index, e.target.value)}
onChange={(e) =>
handleDateChangeWithModification(
activeTab,
index,
e.target.value
)
}
className="p-2 border border-emerald-300 rounded focus:outline-none focus:ring-2 focus:ring-emerald-500 cursor-pointer"
/>
{modifiedDates[`${activeTab}-${index}`] && (
<button
type="button"
onClick={() => submit({ frequency: dates[activeTab].length, due_dates: dates[activeTab] })}
onClick={() =>
submit({
frequency: dates[activeTab].length,
due_dates: dates[activeTab],
})
}
className="text-emerald-500 hover:text-emerald-800"
>
<Check className="w-5 h-5" />
@ -71,4 +94,4 @@ const DateTab = ({ dates, activeTab, handleDateChange, handleEdit, type, payment
);
};
export default DateTab;
export default DateTab;

View File

@ -2,15 +2,15 @@ import React, { useEffect } from 'react';
import { useCookies } from 'react-cookie';
export default function DjangoCSRFToken({ csrfToken }) {
const [cookies, setCookie] = useCookies(['csrftoken']);
const [cookies, setCookie] = useCookies(['csrftoken']);
useEffect(() => {
if (csrfToken && csrfToken !== cookies.csrftoken) {
setCookie('csrftoken', csrfToken, { path: '/' });
}
}, [csrfToken, cookies.csrftoken, setCookie]);
useEffect(() => {
if (csrfToken && csrfToken !== cookies.csrftoken) {
setCookie('csrftoken', csrfToken, { path: '/' });
}
}, [csrfToken, cookies.csrftoken, setCookie]);
return (
<input type="hidden" value={cookies.csrftoken} name="csrfmiddlewaretoken" />
);
return (
<input type="hidden" value={cookies.csrftoken} name="csrfmiddlewaretoken" />
);
}

View File

@ -14,4 +14,4 @@ const DocusealBuilder = ({ onSave, onSend, ...props }) => {
return <OriginalDocusealBuilder {...props} />;
};
export default DocusealBuilder;
export default DocusealBuilder;

View File

@ -4,7 +4,6 @@ import { Upload } from 'lucide-react';
export default function DraggableFileUpload({ fileName, onFileSelect }) {
const [dragActive, setDragActive] = useState(false);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
@ -39,12 +38,23 @@ export default function DraggableFileUpload({ fileName, onFileSelect }) {
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<input
type="file"
onChange={handleFileChange}
className="hidden"
id="fileInput"
/>
<label
htmlFor="fileInput"
className="cursor-pointer flex flex-col items-center"
>
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
<p className="text-center">
{fileName ||
'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}
</p>
</label>
</div>
</div>
);
}
}

View File

@ -2,13 +2,23 @@
import { useRouter } from 'next/navigation';
import React, { useState, useEffect, useRef } from 'react';
const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dropdownOpen: propDropdownOpen, setDropdownOpen: propSetDropdownOpen }) => {
const DropdownMenu = ({
buttonContent,
items,
buttonClassName,
menuClassName,
dropdownOpen: propDropdownOpen,
setDropdownOpen: propSetDropdownOpen,
}) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const menuRef = useRef(null);
const router = useRouter();
const isControlled = propDropdownOpen !== undefined && propSetDropdownOpen !== undefined;
const isControlled =
propDropdownOpen !== undefined && propSetDropdownOpen !== undefined;
const actualDropdownOpen = isControlled ? propDropdownOpen : dropdownOpen;
const actualSetDropdownOpen = isControlled ? propSetDropdownOpen : setDropdownOpen;
const actualSetDropdownOpen = isControlled
? propSetDropdownOpen
: setDropdownOpen;
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
@ -52,7 +62,10 @@ const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dr
return (
<div className="relative" ref={menuRef}>
<button className={buttonClassName} onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}>
<button
className={buttonClassName}
onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}
>
{buttonContent}
</button>
{actualDropdownOpen && (
@ -64,4 +77,4 @@ const DropdownMenu = ({ buttonContent, items, buttonClassName, menuClassName, dr
);
};
export default DropdownMenu;
export default DropdownMenu;

View File

@ -2,16 +2,22 @@ import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning();
export default function EventModal({
isOpen,
onClose,
eventData,
setEventData,
}) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
usePlanning();
// S'assurer que planning est défini lors du premier rendu
React.useEffect(() => {
if (!eventData.planning && schedules.length > 0) {
setEventData(prev => ({
setEventData((prev) => ({
...prev,
planning: schedules[0].id,
color: schedules[0].color
color: schedules[0].color,
}));
}
}, [schedules, eventData.planning]);
@ -23,7 +29,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
{ value: 'daily', label: 'Quotidienne' },
{ value: 'weekly', label: 'Hebdomadaire' },
{ value: 'monthly', label: 'Mensuelle' },
{ value: 'custom', label: 'Personnalisée' } // Nouvelle option
{ value: 'custom', label: 'Personnalisée' }, // Nouvelle option
];
const daysOfWeek = [
@ -33,7 +39,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
{ value: 4, label: 'Jeu' },
{ value: 5, label: 'Ven' },
{ value: 6, label: 'Sam' },
{ value: 0, label: 'Dim' }
{ value: 0, label: 'Dim' },
];
const handleSubmit = (e) => {
@ -44,27 +50,30 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
return;
}
const selectedSchedule = schedules.find(s => s.id === eventData.planning);
const selectedSchedule = schedules.find((s) => s.id === eventData.planning);
if (eventData.id) {
handleUpdateEvent(eventData.id, {
...eventData,
planning: eventData.planning, // S'assurer que planning est bien défini
color: eventData.color || selectedSchedule?.color
color: eventData.color || selectedSchedule?.color,
});
} else {
addEvent({
...eventData,
id: `event-${Date.now()}`,
planning: eventData.planning, // S'assurer que planning est bien défini
color: eventData.color || selectedSchedule?.color
color: eventData.color || selectedSchedule?.color,
});
}
onClose();
};
const handleDelete = () => {
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
if (
eventData.id &&
confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')
) {
handleDeleteEvent(eventData.id);
onClose();
}
@ -74,7 +83,7 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold mb-4">
{eventData.id ? 'Modifier l\'événement' : 'Nouvel événement'}
{eventData.id ? "Modifier l'événement" : 'Nouvel événement'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
@ -86,7 +95,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<input
type="text"
value={eventData.title || ''}
onChange={(e) => setEventData({ ...eventData, title: e.target.value })}
onChange={(e) =>
setEventData({ ...eventData, title: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
@ -99,7 +110,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
</label>
<textarea
value={eventData.description || ''}
onChange={(e) => setEventData({ ...eventData, description: e.target.value })}
onChange={(e) =>
setEventData({ ...eventData, description: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
rows="3"
/>
@ -113,17 +126,19 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<select
value={eventData.planning || schedules[0]?.id}
onChange={(e) => {
const selectedSchedule = schedules.find(s => s.id === e.target.value);
const selectedSchedule = schedules.find(
(s) => s.id === e.target.value
);
setEventData({
...eventData,
planning: e.target.value,
color: selectedSchedule?.color || '#10b981'
color: selectedSchedule?.color || '#10b981',
});
}}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
>
{schedules.map(schedule => (
{schedules.map((schedule) => (
<option key={schedule.id} value={schedule.id}>
{schedule.name}
</option>
@ -138,8 +153,14 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
</label>
<input
type="color"
value={eventData.color || schedules.find(s => s.id === eventData.planning)?.color || '#10b981'}
onChange={(e) => setEventData({ ...eventData, color: e.target.value })}
value={
eventData.color ||
schedules.find((s) => s.id === eventData.planning)?.color ||
'#10b981'
}
onChange={(e) =>
setEventData({ ...eventData, color: e.target.value })
}
className="w-full h-10 p-1 rounded border"
/>
</div>
@ -151,10 +172,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
</label>
<select
value={eventData.recurrence || 'none'}
onChange={(e) => setEventData({ ...eventData, recurrence: e.target.value })}
onChange={(e) =>
setEventData({ ...eventData, recurrence: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
{recurrenceOptions.map(option => (
{recurrenceOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
@ -174,18 +197,22 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
type="number"
min="1"
value={eventData.customInterval || 1}
onChange={(e) => setEventData({
...eventData,
customInterval: parseInt(e.target.value) || 1
})}
onChange={(e) =>
setEventData({
...eventData,
customInterval: parseInt(e.target.value) || 1,
})
}
className="w-20 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
<select
value={eventData.customUnit || 'days'}
onChange={(e) => setEventData({
...eventData,
customUnit: e.target.value
})}
onChange={(e) =>
setEventData({
...eventData,
customUnit: e.target.value,
})
}
className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
<option value="days">Jours</option>
@ -204,14 +231,14 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
Jours de répétition
</label>
<div className="flex gap-2 flex-wrap">
{daysOfWeek.map(day => (
{daysOfWeek.map((day) => (
<button
key={day.value}
type="button"
onClick={() => {
const days = eventData.selectedDays || [];
const newDays = days.includes(day.value)
? days.filter(d => d !== day.value)
? days.filter((d) => d !== day.value)
: [...days, day.value];
setEventData({ ...eventData, selectedDays: newDays });
}}
@ -237,7 +264,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<input
type="date"
value={eventData.recurrenceEnd || ''}
onChange={(e) => setEventData({ ...eventData, recurrenceEnd: e.target.value })}
onChange={(e) =>
setEventData({ ...eventData, recurrenceEnd: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
</div>
@ -252,7 +281,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<input
type="datetime-local"
value={format(new Date(eventData.start), "yyyy-MM-dd'T'HH:mm")}
onChange={(e) => setEventData({ ...eventData, start: new Date(e.target.value).toISOString() })}
onChange={(e) =>
setEventData({
...eventData,
start: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
@ -264,7 +298,12 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<input
type="datetime-local"
value={format(new Date(eventData.end), "yyyy-MM-dd'T'HH:mm")}
onChange={(e) => setEventData({ ...eventData, end: new Date(e.target.value).toISOString() })}
onChange={(e) =>
setEventData({
...eventData,
end: new Date(e.target.value).toISOString(),
})
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
@ -279,7 +318,9 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
<input
type="text"
value={eventData.location || ''}
onChange={(e) => setEventData({ ...eventData, location: e.target.value })}
onChange={(e) =>
setEventData({ ...eventData, location: e.target.value })
}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
/>
</div>
@ -317,4 +358,4 @@ export default function EventModal({ isOpen, onClose, eventData, setEventData })
</div>
</div>
);
}
}

View File

@ -8,14 +8,14 @@ const FileStatusLabel = ({ status }) => {
return {
label: 'En attente',
className: 'bg-green-50 text-green-600',
icon: <Check size={16} className="text-green-600" />
icon: <Check size={16} className="text-green-600" />,
};
case 'pending':
default:
return {
label: 'En attente',
className: 'bg-orange-50 text-orange-600',
icon: <Clock size={16} className="text-orange-600" />
icon: <Clock size={16} className="text-orange-600" />,
};
}
};
@ -23,7 +23,9 @@ const FileStatusLabel = ({ status }) => {
const { label, className, icon } = getStatusConfig();
return (
<div className={`flex items-center justify-center gap-2 px-3 py-1 rounded-md text-sm font-medium ${className}`}>
<div
className={`flex items-center justify-center gap-2 px-3 py-1 rounded-md text-sm font-medium ${className}`}
>
{icon}
<span>{label}</span>
</div>

View File

@ -15,7 +15,9 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => {
fetchRegistrationFileGroups(selectedEstablishmentId).then(data => setGroups(data));
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
setGroups(data)
);
if (fileToEdit) {
setFileName(fileToEdit.name || '');
@ -35,7 +37,7 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
groupId: selectedGroup || null
groupId: selectedGroup || null,
});
setFile(null);
setFileName('');
@ -50,7 +52,7 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ''));
}}
/>
<div className="flex mt-2">
@ -70,8 +72,8 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
/>
<button
onClick={handleUpload}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== "" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName === ""}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== '' ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName === ''}
>
Ajouter
</button>
@ -91,11 +93,13 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) {
className="w-full border rounded p-2"
>
<option value="">Aucun groupe</option>
{groups.map(group => (
<option key={group.id} value={group.id}>{group.name}</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</div>
</div>
);
}
}

View File

@ -1,17 +1,19 @@
import Logo from '@/components/Logo';
export default function Footer ({softwareName, softwareVersion}) {
return (
export default function Footer({ softwareName, softwareVersion }) {
return (
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
<div className="text-sm font-light">
<span>&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.</span>
</div>
<div className="text-sm font-light flex items-center justify-between">
<div className="text-sm font-light mr-4">{softwareName} - {softwareVersion}</div>
<Logo className="w-8 h-8" />
<div className="text-sm font-light">
<span>
&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.
</span>
</div>
<div className="text-sm font-light flex items-center justify-between">
<div className="text-sm font-light mr-4">
{softwareName} - {softwareVersion}
</div>
<Logo className="w-8 h-8" />
</div>
</footer>
)
);
}

View File

@ -2,31 +2,37 @@ import React from 'react';
import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';
export default function InputPhone({ name, label, value, onChange, errorMsg, className, required }) {
return (
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
export default function InputPhone({
name,
label,
value,
onChange,
errorMsg,
className,
required,
}) {
return (
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
inputProps={{
name: name,
required: required,
}}
className="!w-full mt-1 !h-[38px]"
containerClassName="!w-full !h-[36px] !flex !items-center !rounded-md"
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md !outline-none items-center !border !border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500` }
buttonClassName="!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0"
/>
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
inputProps={{
name: name,
required: required,
}}
className="!w-full mt-1 !h-[38px]"
containerClassName="!w-full !h-[36px] !flex !items-center !rounded-md"
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md !outline-none items-center !border !border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}
buttonClassName="!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0"
/>
{errorMsg && (
<p className="mt-2 text-sm text-red-600">{errorMsg}</p>
)}
</div>
);
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
);
}

View File

@ -1,25 +1,40 @@
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder, className, required}) {
return (
<>
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<input
type={type}
id={name}
placeholder={placeholder}
name={name}
value={value}
onChange={onChange}
className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md"
required={required}
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
)
}
export default function InputText({
name,
type,
label,
value,
onChange,
errorMsg,
placeholder,
className,
required,
}) {
return (
<>
<div className={`${className}`}>
<label
htmlFor={name}
className="block text-sm font-medium text-gray-700"
>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div
className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}
>
<input
type={type}
id={name}
placeholder={placeholder}
name={name}
value={value}
onChange={onChange}
className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md"
required={required}
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
);
}

View File

@ -1,24 +1,41 @@
export default function InputTextIcon({name, type, IconItem, label, value, onChange, errorMsg, placeholder, className}) {
return (
<>
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
{IconItem && <IconItem />}
</span>
<input
type={type}
id={name}
placeholder={placeholder}
name={name}
value={value}
onChange={onChange}
className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none"
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
)
}
export default function InputTextIcon({
name,
type,
IconItem,
label,
value,
onChange,
errorMsg,
placeholder,
className,
}) {
return (
<>
<div className={`${className}`}>
<label
htmlFor={name}
className="block text-sm font-medium text-gray-700"
>
{label}
</label>
<div
className={`mt-1 flex items-stretch border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}
>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
{IconItem && <IconItem />}
</span>
<input
type={type}
id={name}
placeholder={placeholder}
name={name}
value={value}
onChange={onChange}
className="flex-1 px-3 py-2 block w-full rounded-r-md sm:text-sm border-none focus:ring-0 outline-none"
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
);
}

View File

@ -1,7 +1,15 @@
import React from 'react';
import { Palette } from 'lucide-react';
const InputTextWithColorIcon = ({ name, textValue, colorValue, onTextChange, onColorChange, placeholder, errorMsg }) => {
const InputTextWithColorIcon = ({
name,
textValue,
colorValue,
onTextChange,
onColorChange,
placeholder,
errorMsg,
}) => {
return (
<div className="flex items-center space-x-2">
<input
@ -31,4 +39,4 @@ const InputTextWithColorIcon = ({ name, textValue, colorValue, onTextChange, onC
);
};
export default InputTextWithColorIcon;
export default InputTextWithColorIcon;

View File

@ -2,17 +2,19 @@ import React from 'react';
import Table from '@/components/Table';
export default function FilesToSign({ fileTemplates, columns }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à remplir</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">
Fichiers à remplir
</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}

View File

@ -2,17 +2,19 @@ import React from 'react';
import Table from '@/components/Table';
export default function FilesToUpload({ fileTemplates, columns }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à uploader</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">
Fichiers à uploader
</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -3,22 +3,29 @@ import React, { useState, useEffect } from 'react';
import Loader from '@/components/Loader';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { fetchRegisterForm, fetchTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
import { downloadTemplate,
createRegistrationTemplates,
editRegistrationTemplates,
deleteRegistrationTemplates
import {
fetchRegisterForm,
fetchTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import {
downloadTemplate,
createRegistrationTemplates,
editRegistrationTemplates,
deleteRegistrationTemplates,
} from '@/app/actions/registerFileGroupAction';
import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/components/DraggableFileUpload';
import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel';
import logger from '@/utils/logger';
import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm';
import StudentInfoForm, {
validateStudentInfo,
} from '@/components/Inscription/StudentInfoForm';
import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react';
@ -31,440 +38,498 @@ import { DocusealForm } from '@docuseal/react';
* @param {object} errors - Erreurs de validation du formulaire
*/
export default function InscriptionFormShared({
studentId,
csrfToken,
selectedEstablishmentId,
onSubmit,
cancelUrl,
errors = {} // Nouvelle prop pour les erreurs
studentId,
csrfToken,
selectedEstablishmentId,
onSubmit,
cancelUrl,
errors = {}, // Nouvelle prop pour les erreurs
}) {
// États pour gérer les données du formulaire
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({
id: '',
last_name: '',
first_name: '',
address: '',
birth_date: '',
birth_place: '',
birth_postal_code: '',
nationality: '',
attending_physician: '',
level: '',
registration_payment: '',
tuition_payment: ''
// États pour gérer les données du formulaire
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({
id: '',
last_name: '',
first_name: '',
address: '',
birth_date: '',
birth_place: '',
birth_postal_code: '',
nationality: '',
attending_physician: '',
level: '',
registration_payment: '',
tuition_payment: '',
});
const [guardians, setGuardians] = useState([]);
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileGroup, setFileGroup] = useState(null);
const [fileName, setFileName] = useState('');
const [file, setFile] = useState('');
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
if (currentPage === 1) {
const isValid = validateStudentInfo(formData);
return isValid;
}
return true;
};
// Chargement initial des données
// Mettre à jour les données quand initialData change
useEffect(() => {
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
logger.debug(data);
setFormData({
id: data?.student?.id || '',
last_name: data?.student?.last_name || '',
first_name: data?.student?.first_name || '',
address: data?.student?.address || '',
birth_date: data?.student?.birth_date || '',
birth_place: data?.student?.birth_place || '',
birth_postal_code: data?.student?.birth_postal_code || '',
nationality: data?.student?.nationality || '',
attending_physician: data?.student?.attending_physician || '',
level: data?.student?.level || '',
registration_payment: data?.registration_payment || '',
tuition_payment: data?.tuition_payment || '',
totalRegistrationFees: data?.totalRegistrationFees,
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setIsLoading(false);
}
}, [studentId]);
useEffect(() => {
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
setFileTemplates(data);
});
}, []);
const [guardians, setGuardians] = useState([]);
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
const [tuitionPaymentModes, setTuitionPaymentModes] = useState([]);
// Fetch data for tuition payment modes
handleTuitionPaymentModes();
}
}, [selectedEstablishmentId]);
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileGroup, setFileGroup] = useState(null);
const [fileName, setFileName] = useState("");
const [file, setFile] = useState("");
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
if (currentPage === 1) {
const isValid = validateStudentInfo(formData);
return isValid;
}
return true;
};
// Chargement initial des données
// Mettre à jour les données quand initialData change
useEffect(() => {
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
logger.debug(data);
setFormData({
id: data?.student?.id || '',
last_name: data?.student?.last_name || '',
first_name: data?.student?.first_name || '',
address: data?.student?.address || '',
birth_date: data?.student?.birth_date || '',
birth_place: data?.student?.birth_place || '',
birth_postal_code: data?.student?.birth_postal_code || '',
nationality: data?.student?.nationality || '',
attending_physician: data?.student?.attending_physician || '',
level: data?.student?.level || '',
registration_payment: data?.registration_payment || '',
tuition_payment: data?.tuition_payment || '',
totalRegistrationFees: data?.totalRegistrationFees,
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setIsLoading(false);
}
}, [studentId]);
useEffect(() => {
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
setFileTemplates(data);
})
}, []);
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
// Fetch data for tuition payment modes
handleTuitionPaymentModes();
}
}, [selectedEstablishmentId]);
const handleRegistrationPaymentModes = () => {
fetchRegistrationPaymentModes(selectedEstablishmentId)
.then(data => {
const activePaymentModes = data.filter(mode => mode.is_active === true);
setRegistrationPaymentModes(activePaymentModes);
})
.catch(error => logger.error('Error fetching registration payment modes:', error));
};
const handleTuitionPaymentModes = () => {
fetchTuitionPaymentModes(selectedEstablishmentId)
.then(data => {
const activePaymentModes = data.filter(mode => mode.is_active === true);
setTuitionPaymentModes(activePaymentModes);
})
.catch(error => logger.error('Error fetching tuition payment modes:', error));
};
// Fonctions de gestion du formulaire et des fichiers
const updateFormField = (field, value) => {
setFormData(prev => ({...prev, [field]: value}));
};
// Gestion du téléversement de fichiers
const handleFileUpload = async (file, fileName) => {
if (!file || !currentTemplateId || !formData.id) {
logger.error('Missing required data for upload');
return;
}
const data = new FormData();
data.append('file', file);
data.append('name', fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try {
const response = await createRegistrationTemplates(data, csrfToken);
if (response) {
setUploadedFiles(prev => {
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
return [...newFiles, {
name: fileName,
template: currentTemplateId,
file: response.file
}];
});
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
setUploadedFiles(data.registration_files || []);
});
}
}
} catch (error) {
logger.error('Error uploading file:', error);
}
};
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (templateId) => {
return uploadedFiles.find(template =>
template.template === templateId
const handleRegistrationPaymentModes = () => {
fetchRegistrationPaymentModes(selectedEstablishmentId)
.then((data) => {
const activePaymentModes = data.filter(
(mode) => mode.is_active === true
);
};
setRegistrationPaymentModes(activePaymentModes);
})
.catch((error) =>
logger.error('Error fetching registration payment modes:', error)
);
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find(file => parseInt(file.template) === templateId);
};
const handleTuitionPaymentModes = () => {
fetchTuitionPaymentModes(selectedEstablishmentId)
.then((data) => {
const activePaymentModes = data.filter(
(mode) => mode.is_active === true
);
setTuitionPaymentModes(activePaymentModes);
})
.catch((error) =>
logger.error('Error fetching tuition payment modes:', error)
);
};
// Suppression d'un fichier
const handleDeleteFile = async (templateId) => {
const fileToDelete = getUploadedFile(templateId);
if (!fileToDelete) return;
// Fonctions de gestion du formulaire et des fichiers
const updateFormField = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
try {
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
} catch (error) {
logger.error('Error deleting file:', error);
}
};
// Gestion du téléversement de fichiers
const handleFileUpload = async (file, fileName) => {
if (!file || !currentTemplateId || !formData.id) {
logger.error('Missing required data for upload');
return;
}
// Soumission du formulaire
const handleSubmit = (e) => {
e.preventDefault();
const data ={
student: {
...formData,
guardians
const data = new FormData();
data.append('file', file);
data.append('name', fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try {
const response = await createRegistrationTemplates(data, csrfToken);
if (response) {
setUploadedFiles((prev) => {
const newFiles = prev.filter(
(f) => parseInt(f.template) !== currentTemplateId
);
return [
...newFiles,
{
name: fileName,
template: currentTemplateId,
file: response.file,
},
establishment: selectedEstablishmentId,
status:3,
tuition_payment:formData.tuition_payment,
registration_payment:formData.registration_payment
];
});
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
setUploadedFiles(data.registration_files || []);
});
}
onSubmit(data);
};
}
} catch (error) {
logger.error('Error uploading file:', error);
}
};
// Soumission du formulaire
const handleSave = (e) => {
e.preventDefault();
const data ={
student: {
...formData,
guardians
},
establishment: selectedEstablishmentId
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (templateId) => {
return uploadedFiles.find((template) => template.template === templateId);
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find((file) => parseInt(file.template) === templateId);
};
// Suppression d'un fichier
const handleDeleteFile = async (templateId) => {
const fileToDelete = getUploadedFile(templateId);
if (!fileToDelete) return;
try {
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
setUploadedFiles((prev) =>
prev.filter((f) => parseInt(f.template) !== templateId)
);
} catch (error) {
logger.error('Error deleting file:', error);
}
};
// Soumission du formulaire
const handleSubmit = (e) => {
e.preventDefault();
const data = {
student: {
...formData,
guardians,
},
establishment: selectedEstablishmentId,
status: 3,
tuition_payment: formData.tuition_payment,
registration_payment: formData.registration_payment,
};
onSubmit(data);
};
// Soumission du formulaire
const handleSave = (e) => {
e.preventDefault();
const data = {
student: {
...formData,
guardians,
},
establishment: selectedEstablishmentId,
};
onSubmit(data);
};
const handleNextPage = () => {
setCurrentPage(currentPage + 1);
};
const handlePreviousPage = () => {
setCurrentPage(currentPage - 1);
};
const requiredFileTemplates = fileTemplates;
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{
name: 'Fichier à Remplir',
transform: (row) => (row.is_required ? 'Oui' : 'Non'),
},
{
name: 'Fichier de référence',
transform: (row) =>
row.file && (
<div className="flex items-center justify-center gap-2">
{' '}
<a
href={`${BASE_URL}${row.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Download size={16} />
</a>{' '}
</div>
),
},
{
name: 'Statut',
transform: (row) =>
row.is_required && (
<FileStatusLabel
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
/>
),
},
{
name: 'Actions',
transform: (row) => {
if (!row.is_required) return null;
const uploadedFile = getUploadedFile(row.id);
if (uploadedFile) {
return (
<div className="flex items-center justify-center gap-2">
<a
href={`${BASE_URL}${uploadedFile.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Eye size={16} />
</a>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDeleteFile(row.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
);
}
onSubmit(data);
};
const handleNextPage = () => {
setCurrentPage(currentPage + 1);
};
const handlePreviousPage = () => {
setCurrentPage(currentPage - 1);
};
return (
<button
className="text-emerald-500 hover:text-emerald-700"
type="button"
onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}
>
<Upload size={16} />
</button>
);
},
},
];
const requiredFileTemplates = fileTemplates;
// Affichage du loader pendant le chargement
if (isLoading) return <Loader />;
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Fichier de référence', transform: (row) => row.file && <div className="flex items-center justify-center gap-2"> <a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
<Download size={16} />
</a> </div>},
{ name: 'Statut', transform: (row) =>
row.is_required && (
<FileStatusLabel
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
/>
)
},
{ name: 'Actions', transform: (row) => {
if (!row.is_required) return null;
// Rendu du composant
return (
<div className="max-w-4xl mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken} />
{/* Page 1 : Informations de l'élève et Responsables */}
{currentPage === 1 && (
<StudentInfoForm
formData={formData}
updateFormField={updateFormField}
guardians={guardians}
setGuardians={setGuardians}
registrationPaymentModes={registrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
errors={errors}
/>
)}
const uploadedFile = getUploadedFile(row.id);
{/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name ||
'Document sans nom'}
</h2>
<p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description ||
'Aucune description disponible pour ce document.'}
</p>
</div>
if (uploadedFile) {
return (
<div className="flex items-center justify-center gap-2">
<a
href={`${BASE_URL}${uploadedFile.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Eye size={16} />
</a>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDeleteFile(row.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
);
}
{/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === '' ? (
<DocusealForm
id="docusealForm"
src={
'https://docuseal.com/s/' +
requiredFileTemplates[currentPage - 2].slug
}
withDownloadButton={false}
onComplete={() => {
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data))
.then((response) => response.blob())
.then((blob) => {
const file = new File(
[blob],
`${requiredFileTemplates[currentPage - 2].name}.pdf`,
{ type: blob.type }
);
const updateData = new FormData();
updateData.append('file', file);
return (
<button
className="text-emerald-500 hover:text-emerald-700"
type="button"
onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}
>
<Upload size={16} />
</button>
);
}},
];
// Affichage du loader pendant le chargement
if (isLoading) return <Loader />;
// Rendu du composant
return (
<div className="max-w-4xl mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken}/>
{/* Page 1 : Informations de l'élève et Responsables */}
{currentPage === 1 && (
<StudentInfoForm
formData={formData}
updateFormField={updateFormField}
guardians={guardians}
setGuardians={setGuardians}
registrationPaymentModes={registrationPaymentModes}
tuitionPaymentModes={tuitionPaymentModes}
errors={errors}
/>
)}
{/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name || "Document sans nom"}
</h2>
<p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."}
</p>
</div>
{/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === "" ? (
<DocusealForm
id="docusealForm"
src={"https://docuseal.com/s/" + requiredFileTemplates[currentPage - 2].slug}
withDownloadButton={false}
onComplete={() => {
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data))
.then((response) => response.blob())
.then((blob) => {
const file = new File([blob], `${requiredFileTemplates[currentPage - 2].name}.pdf`, { type: blob.type });
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationTemplates(requiredFileTemplates[currentPage - 2].id, updateData, csrfToken);
})
.then((data) => {
logger.debug("EDIT TEMPLATE : ", data);
})
.catch((error) => {
logger.error("error editing template : ", error);
});
}}
/>
) : (
<iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
border: 'none',
}}
/>
)}
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && (
<>
<FilesToUpload
fileTemplates={fileTemplates.filter(template => !template.is_required)}
columns={columns}
/>
</>
)}
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
<Button
text="Sauvegarder"
onClick={handleSave}
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-orange-500 text-white hover:bg-orange-600"
primary
name="Save"
/>
{currentPage > 1 && (
<Button text="Précédent" onClick={(e) => { e.preventDefault(); handlePreviousPage(); }} />
)}
{currentPage < requiredFileTemplates.length + 2 && (
<Button
text="Suivant"
onClick={(e) => { e.preventDefault(); handleNextPage(); }}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
!isCurrentPageValid()
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={!isCurrentPageValid()}
primary
name="Next"
/>
)}
{currentPage === requiredFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary />
)}
</div>
</form>
{fileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
return editRegistrationTemplates(
requiredFileTemplates[currentPage - 2].id,
updateData,
csrfToken
);
})
.then((data) => {
logger.debug('EDIT TEMPLATE : ', data);
})
.catch((error) => {
logger.error('error editing template : ', error);
});
}}
/>
) : (
<iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
border: 'none',
}}
/>
)}
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && (
<>
<FilesToUpload
fileTemplates={fileTemplates.filter(
(template) => !template.is_required
)}
columns={columns}
/>
</>
)}
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
<Button
text="Sauvegarder"
onClick={handleSave}
className="px-4 py-2 rounded-md shadow-sm focus:outline-none bg-orange-500 text-white hover:bg-orange-600"
primary
name="Save"
/>
{currentPage > 1 && (
<Button
text="Précédent"
onClick={(e) => {
e.preventDefault();
handlePreviousPage();
}}
/>
)}
{currentPage < requiredFileTemplates.length + 2 && (
<Button
text="Suivant"
onClick={(e) => {
e.preventDefault();
handleNextPage();
}}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
!isCurrentPageValid()
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={!isCurrentPageValid()}
primary
name="Next"
/>
)}
{currentPage === requiredFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary />
)}
</div>
);
}
</form>
{fileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
)}
</div>
);
}

View File

@ -1,34 +1,48 @@
import React from 'react';
import SelectChoice from '@/components/SelectChoice';
export default function PaymentMethodSelector({ formData, title, name, updateFormField, selected, paymentModes, paymentModesOptions, amount, getError }) {
//console.log(paymentModes)
//console.log(selected)
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre */}
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">{title}</h2>
export default function PaymentMethodSelector({
formData,
title,
name,
updateFormField,
selected,
paymentModes,
paymentModesOptions,
amount,
getError,
}) {
//console.log(paymentModes)
//console.log(selected)
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre */}
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
{title}
</h2>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong> {amount}
</p>
</div>
{/* Section d'information */}
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<p className="text-gray-700 text-sm mb-2">
<strong className="text-gray-900">Montant :</strong> {amount}
</p>
</div>
<SelectChoice
name={name}
label="Mode de Paiement"
placeHolder="Sélectionner un mode de paiement"
selected={selected || ''}
callback={(e) => updateFormField(name, e.target.value)}
choices={paymentModes.map((mode) => ({
value: mode.mode,
label: paymentModesOptions.find(option => option.id === mode.mode)?.name || 'Mode inconnu'
}))}
required
errorMsg={getError('payment_method')}
/>
</div>
);
}
<SelectChoice
name={name}
label="Mode de Paiement"
placeHolder="Sélectionner un mode de paiement"
selected={selected || ''}
callback={(e) => updateFormField(name, e.target.value)}
choices={paymentModes.map((mode) => ({
value: mode.mode,
label:
paymentModesOptions.find((option) => option.id === mode.mode)
?.name || 'Mode inconnu',
}))}
required
errorMsg={getError('payment_method')}
/>
</div>
);
}

View File

@ -5,112 +5,137 @@ import React from 'react';
import { useTranslations } from 'next-intl';
import { Trash2, Plus } from 'lucide-react';
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) {
const t = useTranslations('ResponsableInputFields');
export default function ResponsableInputFields({
guardians,
onGuardiansChange,
addGuardian,
deleteGuardian,
errors = [],
}) {
const t = useTranslations('ResponsableInputFields');
const getError = (index, field) => {
return errors[index]?.[field]?.[0];
};
const getError = (index, field) => {
return errors[index]?.[field]?.[0];
};
return (
<div className="space-y-8">
{guardians.map((item, index) => (
<div className="p-6 bg-gray-50 rounded-lg shadow-sm" key={index}>
<div className='flex justify-between items-center mb-4'>
<h3 className='text-xl font-bold'>{t('responsable')} {index+1}</h3>
{guardians.length > 1 && (
<Trash2
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
onClick={() => deleteGuardian(index)}
/>
)}
</div>
return (
<div className="space-y-8">
{guardians.map((item, index) => (
<div className="p-6 bg-gray-50 rounded-lg shadow-sm" key={index}>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold">
{t('responsable')} {index + 1}
</h3>
{guardians.length > 1 && (
<Trash2
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
onClick={() => deleteGuardian(index)}
/>
)}
</div>
<input type="hidden" name="idResponsable" value={item.id} />
<input type="hidden" name="idResponsable" value={item.id} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<InputText
name="nomResponsable"
type="text"
label={t('lastname')}
value={item.last_name}
onChange={(event) => {
onGuardiansChange(item.id, 'last_name', event.target.value);
}}
errorMsg={getError(index, 'last_name')}
required
/>
<InputText
name="prenomResponsable"
type="text"
label={t('firstname')}
value={item.first_name}
onChange={(event) => {
onGuardiansChange(item.id, 'first_name', event.target.value);
}}
errorMsg={getError(index, 'first_name')}
required
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
<InputText
name="nomResponsable"
type="text"
label={t('lastname')}
value={item.last_name}
onChange={(event) => {onGuardiansChange(item.id, "last_name", event.target.value)}}
errorMsg={getError(index, 'last_name')}
required
/>
<InputText
name="prenomResponsable"
type="text"
label={t('firstname')}
value={item.first_name}
onChange={(event) => {onGuardiansChange(item.id, "first_name", event.target.value)}}
errorMsg={getError(index, 'first_name')}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<InputText
name="mailResponsable"
type="email"
label={t('email')}
value={item.associated_profile_email}
onChange={(event) => {
onGuardiansChange(
item.id,
'associated_profile_email',
event.target.value
);
}}
required
errorMsg={getError(index, 'email')}
/>
<InputPhone
name="telephoneResponsable"
label={t('phone')}
value={item.phone}
onChange={(event) => {
onGuardiansChange(item.id, 'phone', event);
}}
required
errorMsg={getError(index, 'phone')}
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
<InputText
name="mailResponsable"
type="email"
label={t('email')}
value={item.associated_profile_email}
onChange={(event) => {onGuardiansChange(item.id, "associated_profile_email", event.target.value)}}
required
errorMsg={getError(index, 'email')}
/>
<InputPhone
name="telephoneResponsable"
label={t('phone')}
value={item.phone}
onChange={(event) => {onGuardiansChange(item.id, "phone", event)}}
required
errorMsg={getError(index, 'phone')}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<InputText
name="dateNaissanceResponsable"
type="date"
label={t('birthdate')}
value={item.birth_date}
onChange={(event) => {
onGuardiansChange(item.id, 'birth_date', event.target.value);
}}
required
errorMsg={getError(index, 'birth_date')}
/>
<InputText
name="professionResponsable"
type="text"
label={t('profession')}
value={item.profession}
onChange={(event) => {
onGuardiansChange(item.id, 'profession', event.target.value);
}}
required
errorMsg={getError(index, 'profession')}
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
<InputText
name="dateNaissanceResponsable"
type="date"
label={t('birthdate')}
value={item.birth_date}
onChange={(event) => {onGuardiansChange(item.id, "birth_date", event.target.value)}}
required
errorMsg={getError(index, 'birth_date')}
/>
<InputText
name="professionResponsable"
type="text"
label={t('profession')}
value={item.profession}
onChange={(event) => {onGuardiansChange(item.id, "profession", event.target.value)}}
required
errorMsg={getError(index, 'profession')}
/>
</div>
<div className='grid grid-cols-1 gap-4'>
<InputText
name="adresseResponsable"
type="text"
label={t('address')}
value={item.address}
onChange={(event) => {onGuardiansChange(item.id, "address", event.target.value)}}
required
errorMsg={getError(index, 'address')}
/>
</div>
</div>
))}
<div className="flex justify-center">
<Plus
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
onClick={(e) => addGuardian(e)}
/>
</div>
<div className="grid grid-cols-1 gap-4">
<InputText
name="adresseResponsable"
type="text"
label={t('address')}
value={item.address}
onChange={(event) => {
onGuardiansChange(item.id, 'address', event.target.value);
}}
required
errorMsg={getError(index, 'address')}
/>
</div>
</div>
);
}
))}
<div className="flex justify-center">
<Plus
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
onClick={(e) => addGuardian(e)}
/>
</div>
</div>
);
}

View File

@ -5,176 +5,190 @@ import ResponsableInputFields from '@/components/Inscription/ResponsableInputFie
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
const levels = [
{ value:'1', label: 'TPS - Très Petite Section'},
{ value:'2', label: 'PS - Petite Section'},
{ value:'3', label: 'MS - Moyenne Section'},
{ value:'4', label: 'GS - Grande Section'},
{ value: '1', label: 'TPS - Très Petite Section' },
{ value: '2', label: 'PS - Petite Section' },
{ value: '3', label: 'MS - Moyenne Section' },
{ value: '4', label: 'GS - Grande Section' },
];
const paymentModesOptions = [
{ id: 1, name: 'Prélèvement SEPA' },
{ id: 2, name: 'Virement' },
{ id: 3, name: 'Chèque' },
{ id: 4, name: 'Espèce' },
];
{ id: 1, name: 'Prélèvement SEPA' },
{ id: 2, name: 'Virement' },
{ id: 3, name: 'Chèque' },
{ id: 4, name: 'Espèce' },
];
// Fonction de validation pour vérifier les champs requis
export function validateStudentInfo(formData) {
const requiredFields = [
'last_name',
'first_name',
'nationality',
'birth_date',
'birth_place',
'birth_postal_code',
'address',
'attending_physician',
'level',
];
const requiredFields = [
'last_name',
'first_name',
'nationality',
'birth_date',
'birth_place',
'birth_postal_code',
'address',
'attending_physician',
'level',
];
const isValid = requiredFields.every((field) => {
const value = formData[field];
return typeof value === 'string' ? value.trim() !== '' : Boolean(value);
});
const isValid = requiredFields.every((field) => {
const value = formData[field];
return typeof value === 'string' ? value.trim() !== '' : Boolean(value);
});
return isValid;
return isValid;
}
export default function StudentInfoForm({ formData, updateFormField, guardians, setGuardians, registrationPaymentModes, tuitionPaymentModes, errors }) {
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
export default function StudentInfoForm({
formData,
updateFormField,
guardians,
setGuardians,
registrationPaymentModes,
tuitionPaymentModes,
errors,
}) {
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
return (
<>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Informations de l&apos;élève</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputText
name="last_name"
label="Nom"
value={formData.last_name}
onChange={(e) => updateFormField('last_name', e.target.value)}
required
errorMsg={getError('last_name')}
/>
<InputText
name="first_name"
label="Prénom"
value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required
/>
<InputText
name="nationality"
label="Nationalité"
value={formData.nationality}
required
onChange={(e) => updateFormField('nationality', e.target.value)}
/>
<InputText
name="birth_date"
type="date"
label="Date de Naissance"
value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)}
required
errorMsg={getError('birth_date')}
/>
<InputText
name="birth_place"
label="Lieu de Naissance"
value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)}
required
errorMsg={getError('birth_place')}
/>
<InputText
name="birth_postal_code"
label="Code Postal de Naissance"
value={formData.birth_postal_code}
onChange={(e) => updateFormField('birth_postal_code', e.target.value)}
required
errorMsg={getError('birth_postal_code')}
/>
<div className="md:col-span-2">
<InputText
name="address"
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
required
errorMsg={getError('address')}
/>
</div>
<InputText
name="attending_physician"
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) => updateFormField('attending_physician', e.target.value)}
required
errorMsg={getError('attending_physician')}
/>
<SelectChoice
name="level"
label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)}
choices={levels}
required
errorMsg={getError('level')}
/>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
<ResponsableInputFields
guardians={guardians}
onGuardiansChange={(id, field, value) => {
const updatedGuardians = guardians.map(resp =>
resp.id === id ? { ...resp, [field]: value } : resp
);
setGuardians(updatedGuardians);
}}
addGuardian={(e) => {
e.preventDefault();
setGuardians([...guardians, { id: Date.now() }]);
}}
deleteGuardian={(index) => {
const newArray = [...guardians];
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/>
</div>
<PaymentMethodSelector
formData={formData}
title="Frais d'inscription"
name="registration_payment"
updateFormField={updateFormField}
selected={formData.registration_payment}
paymentModes={registrationPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalRegistrationFees}
getError={getError}
return (
<>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">
Informations de l&apos;élève
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputText
name="last_name"
label="Nom"
value={formData.last_name}
onChange={(e) => updateFormField('last_name', e.target.value)}
required
errorMsg={getError('last_name')}
/>
<InputText
name="first_name"
label="Prénom"
value={formData.first_name}
onChange={(e) => updateFormField('first_name', e.target.value)}
errorMsg={getError('first_name')}
required
/>
<InputText
name="nationality"
label="Nationalité"
value={formData.nationality}
required
onChange={(e) => updateFormField('nationality', e.target.value)}
/>
<InputText
name="birth_date"
type="date"
label="Date de Naissance"
value={formData.birth_date}
onChange={(e) => updateFormField('birth_date', e.target.value)}
required
errorMsg={getError('birth_date')}
/>
<InputText
name="birth_place"
label="Lieu de Naissance"
value={formData.birth_place}
onChange={(e) => updateFormField('birth_place', e.target.value)}
required
errorMsg={getError('birth_place')}
/>
<InputText
name="birth_postal_code"
label="Code Postal de Naissance"
value={formData.birth_postal_code}
onChange={(e) =>
updateFormField('birth_postal_code', e.target.value)
}
required
errorMsg={getError('birth_postal_code')}
/>
<div className="md:col-span-2">
<InputText
name="address"
label="Adresse"
value={formData.address}
onChange={(e) => updateFormField('address', e.target.value)}
required
errorMsg={getError('address')}
/>
</div>
<InputText
name="attending_physician"
label="Médecin Traitant"
value={formData.attending_physician}
onChange={(e) =>
updateFormField('attending_physician', e.target.value)
}
required
errorMsg={getError('attending_physician')}
/>
<SelectChoice
name="level"
label="Niveau"
placeHolder="Sélectionner un niveau"
selected={formData.level}
callback={(e) => updateFormField('level', e.target.value)}
choices={levels}
required
errorMsg={getError('level')}
/>
</div>
</div>
<PaymentMethodSelector
formData={formData}
title="Frais de scolarité"
name="tuition_payment"
updateFormField={updateFormField}
selected={formData.tuition_payment}
paymentModes={tuitionPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalTuitionFees}
getError={getError}
/>
</>
);
}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
<ResponsableInputFields
guardians={guardians}
onGuardiansChange={(id, field, value) => {
const updatedGuardians = guardians.map((resp) =>
resp.id === id ? { ...resp, [field]: value } : resp
);
setGuardians(updatedGuardians);
}}
addGuardian={(e) => {
e.preventDefault();
setGuardians([...guardians, { id: Date.now() }]);
}}
deleteGuardian={(index) => {
const newArray = [...guardians];
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/>
</div>
<PaymentMethodSelector
formData={formData}
title="Frais d'inscription"
name="registration_payment"
updateFormField={updateFormField}
selected={formData.registration_payment}
paymentModes={registrationPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalRegistrationFees}
getError={getError}
/>
<PaymentMethodSelector
formData={formData}
title="Frais de scolarité"
name="tuition_payment"
updateFormField={updateFormField}
selected={formData.tuition_payment}
paymentModes={tuitionPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalTuitionFees}
getError={getError}
/>
</>
);
}

View File

@ -1,4 +1,4 @@
'use client'
'use client';
import React, { useState, useEffect } from 'react';
import { DocusealBuilder } from '@docuseal/react';
import Button from '@/components/Button';
@ -7,7 +7,14 @@ import { generateToken } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger';
import { GraduationCap, CloudUpload } from 'lucide-react';
export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) {
export default function ValidateSubscription({
studentId,
firstName,
lastName,
paymentMode,
file,
onAccept,
}) {
const [token, setToken] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState('');
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
@ -20,7 +27,9 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
.then((data) => {
setToken(data.token);
})
.catch((error) => logger.error('Erreur lors de la génération du token:', error));
.catch((error) =>
logger.error('Erreur lors de la génération du token:', error)
);
}
}, [isSepa]);
@ -32,23 +41,23 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
const handleAccept = () => {
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
if (!file) {
logger.error('Aucun fichier sélectionné pour le champ SEPA.');
return;
}
const data = {
status: 7,
status: 7,
sepa_file: file,
};
// Appeler la fonction passée par le parent pour mettre à jour le RF
onAccept(data);
};
const handleRefuse = () => {
logger.debug('Dossier refusé pour l\'étudiant:', studentId);
logger.debug("Dossier refusé pour l'étudiant:", studentId);
// Logique pour refuser l'inscription
};
@ -75,10 +84,14 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
</div>
<div>
<h1 className="text-3xl font-bold text-gray-800">
Dossier scolaire de <span className="text-emerald-600">{firstName} {lastName}</span>
Dossier scolaire de{' '}
<span className="text-emerald-600">
{firstName} {lastName}
</span>
</h1>
<p className="text-sm text-gray-500 italic">
Année scolaire {new Date().getFullYear()}-{new Date().getFullYear() + 1}
Année scolaire {new Date().getFullYear()}-
{new Date().getFullYear() + 1}
</p>
</div>
</div>
@ -101,45 +114,54 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
{currentPage === 2 && isSepa && (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">Sélection du mandat de pélèvement SEPA</h3>
<h3 className="text-lg font-semibold mb-4">
Sélection du mandat de pélèvement SEPA
</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
}}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}
}}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
</label>
</div>
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
}}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" />{' '}
{/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}
}}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">
Déposez votre fichier ici
</p>
<p className="text-sm text-gray-500 mt-2">
ou cliquez pour sélectionner un fichier PDF
</p>
</label>
</div>
{uploadedFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800"><span className="font-semibold">{uploadedFileName}</span></p>
</div>
)}
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{uploadedFileName}</span>
</p>
</div>
)}
</div>
)}
@ -172,4 +194,4 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
</div>
</div>
);
}
}

View File

@ -5,7 +5,12 @@ import logoImage from '@/img/logo_min.svg'; // Assurez-vous que le chemin vers l
const Logo = ({ className }) => {
return (
<div className={`max-w-[150px] ${className}`}>
<Image src={logoImage} alt="Logo" style={{ width: 'auto', height: 'auto'}} priority />
<Image
src={logoImage}
alt="Logo"
style={{ width: 'auto', height: 'auto' }}
priority
/>
</div>
);
};

View File

@ -1,36 +1,55 @@
import * as Dialog from '@radix-ui/react-dialog';
const Modal = ({ isOpen, setIsOpen, title, ContentComponent, modalClassName }) => {
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<Dialog.Content className="fixed inset-0 flex items-center justify-center p-4">
<div className={`inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 ${modalClassName ? modalClassName : 'min-w-[500px] m-12 w-max max-h-[80vh] h-full'}`}>
<div className="flex justify-between items-start mb-4">
<Dialog.Title className="text-xl font-medium text-gray-900">
{title}
</Dialog.Title>
<Dialog.Close asChild>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-500 ml-4 focus:outline-none"
>
<span className="sr-only">Fermer</span>
<svg className="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</Dialog.Close>
</div>
<div className="w-full h-full">
<ContentComponent />
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
const Modal = ({
isOpen,
setIsOpen,
title,
ContentComponent,
modalClassName,
}) => {
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<Dialog.Content className="fixed inset-0 flex items-center justify-center p-4">
<div
className={`inline-block bg-white rounded-lg px-6 py-5 text-left shadow-xl transform transition-all sm:my-8 ${modalClassName ? modalClassName : 'min-w-[500px] m-12 w-max max-h-[80vh] h-full'}`}
>
<div className="flex justify-between items-start mb-4">
<Dialog.Title className="text-xl font-medium text-gray-900">
{title}
</Dialog.Title>
<Dialog.Close asChild>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-500 ml-4 focus:outline-none"
>
<span className="sr-only">Fermer</span>
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</Dialog.Close>
</div>
<div className="w-full h-full">
<ContentComponent />
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
};
export default Modal;

View File

@ -1,15 +1,26 @@
import React, { useState, useEffect, useRef } from 'react';
import { Check, ChevronDown } from 'lucide-react';
const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg }) => {
const MultiSelect = ({
name,
label,
options,
selectedOptions,
onChange,
errorMsg,
}) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef(null);
const handleSelect = (option) => {
const isSelected = selectedOptions.some(selected => selected.id === option.id);
const isSelected = selectedOptions.some(
(selected) => selected.id === option.id
);
let newSelectedOptions;
if (isSelected) {
newSelectedOptions = selectedOptions.filter(selected => selected.id !== option.id);
newSelectedOptions = selectedOptions.filter(
(selected) => selected.id !== option.id
);
} else {
newSelectedOptions = [...selectedOptions, option];
}
@ -39,8 +50,11 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
>
{selectedOptions.length > 0 ? (
<div className="flex flex-wrap gap-1 justify-center items-center">
{selectedOptions.map(option => (
<span key={option.id} className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm">
{selectedOptions.map((option) => (
<span
key={option.id}
className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm"
>
{option.name}
</span>
))}
@ -52,18 +66,24 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
</button>
{isOpen && (
<ul className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{options.map(option => (
{options.map((option) => (
<li
key={option.id}
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
selectedOptions.some(selected => selected.id === option.id) ? 'text-white bg-emerald-600' : 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
selectedOptions.some((selected) => selected.id === option.id)
? 'text-white bg-emerald-600'
: 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
}`}
onClick={() => handleSelect(option)}
>
<span className={`block truncate ${selectedOptions.some(selected => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}>
<span
className={`block truncate ${selectedOptions.some((selected) => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}
>
{option.name}
</span>
{selectedOptions.some(selected => selected.id === option.id) && (
{selectedOptions.some(
(selected) => selected.id === option.id
) && (
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-white">
<Check className="h-5 w-5" />
</span>
@ -78,4 +98,4 @@ const MultiSelect = ({ name, label, options, selectedOptions, onChange, errorMsg
);
};
export default MultiSelect;
export default MultiSelect;

View File

@ -1,45 +1,57 @@
import React from 'react';
import {useTranslations} from 'next-intl';
import { useTranslations } from 'next-intl';
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const t = useTranslations('pagination');
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div className="text-sm text-gray-600">{t('page')} {currentPage} {t('of')} {pages.length}</div>
<div className="text-sm text-gray-600">
{t('page')} {currentPage} {t('of')} {pages.length}
</div>
<div className="flex items-center gap-2">
{currentPage > 1 && (
<PaginationButton text={t('previous')} onClick={() => onPageChange(currentPage - 1)}/>
)}
{pages.map((page) => (
<PaginationNumber
key={page}
number={page}
active={page === currentPage}
onClick={() => onPageChange(page)}
/>
))}
{currentPage < totalPages && (
<PaginationButton text={t('next')} onClick={() => onPageChange(currentPage + 1)} />
)}
{currentPage > 1 && (
<PaginationButton
text={t('previous')}
onClick={() => onPageChange(currentPage - 1)}
/>
)}
{pages.map((page) => (
<PaginationNumber
key={page}
number={page}
active={page === currentPage}
onClick={() => onPageChange(page)}
/>
))}
{currentPage < totalPages && (
<PaginationButton
text={t('next')}
onClick={() => onPageChange(currentPage + 1)}
/>
)}
</div>
</div>
);
};
const PaginationButton = ({ text , onClick}) => (
<button className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded" onClick={onClick}>
const PaginationButton = ({ text, onClick }) => (
<button
className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded"
onClick={onClick}
>
{text}
</button>
);
const PaginationNumber = ({ number, active , onClick}) => (
<button className={`w-8 h-8 flex items-center justify-center rounded ${
active ? 'bg-emerald-500 text-white' : 'text-gray-600 hover:bg-gray-50'
}`} onClick={onClick}>
const PaginationNumber = ({ number, active, onClick }) => (
<button
className={`w-8 h-8 flex items-center justify-center rounded ${
active ? 'bg-emerald-500 text-white' : 'text-gray-600 hover:bg-gray-50'
}`}
onClick={onClick}
>
{number}
</button>
);

View File

@ -8,12 +8,19 @@ const paymentModesOptions = [
{ id: 4, name: 'Espèce' },
];
const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }) => {
const PaymentModeSelector = ({
paymentModes,
setPaymentModes,
handleEdit,
type,
}) => {
const [activePaymentModes, setActivePaymentModes] = useState([]);
useEffect(() => {
// Initialiser activePaymentModes avec les modes dont is_active est à true
const activeModes = paymentModes.filter(mode => mode.is_active).map(mode => mode.mode);
const activeModes = paymentModes
.filter((mode) => mode.is_active)
.map((mode) => mode.mode);
setActivePaymentModes(activeModes);
}, [paymentModes]);
@ -24,9 +31,12 @@ const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }
: [...prevActiveModes, modeId];
// Mettre à jour le mode de paiement dans le backend
const updatedMode = paymentModes.find(mode => mode.mode === modeId);
const updatedMode = paymentModes.find((mode) => mode.mode === modeId);
if (updatedMode) {
handleEdit(updatedMode.id, { ...updatedMode, is_active: !updatedMode.is_active });
handleEdit(updatedMode.id, {
...updatedMode,
is_active: !updatedMode.is_active,
});
}
return newActiveModes;
@ -59,4 +69,4 @@ const PaymentModeSelector = ({ paymentModes, setPaymentModes, handleEdit, type }
);
};
export default PaymentModeSelector;
export default PaymentModeSelector;

View File

@ -12,24 +12,33 @@ const paymentPlansOptions = [
{ id: 3, name: '12 fois', frequency: 12 },
];
const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }) => {
const PaymentPlanSelector = ({
paymentPlans,
setPaymentPlans,
handleEdit,
type,
}) => {
const [dates, setDates] = useState({});
const [selectedFrequency, setSelectedFrequency] = useState(null);
const [activeFrequencies, setActiveFrequencies] = useState([]);
const [defaultDay, setDefaultDay] = useState('-');
const [isDefaultDayModified, setIsDefaultDayModified] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [popupMessage, setPopupMessage] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const [resetModifiedDates, setResetModifiedDates] = useState(false);
useEffect(() => {
if (paymentPlans && paymentPlans.length > 0) {
const activePlans = paymentPlans.filter(plan => plan.is_active);
const frequencies = activePlans.map(plan => {
const paymentPlanOption = paymentPlansOptions.find(p => p.frequency === plan.frequency);
return paymentPlanOption ? paymentPlanOption.id : null;
}).filter(id => id !== null);
const activePlans = paymentPlans.filter((plan) => plan.is_active);
const frequencies = activePlans
.map((plan) => {
const paymentPlanOption = paymentPlansOptions.find(
(p) => p.frequency === plan.frequency
);
return paymentPlanOption ? paymentPlanOption.id : null;
})
.filter((id) => id !== null);
setActiveFrequencies(frequencies);
if (activePlans.length > 0) {
@ -38,8 +47,10 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
}
const initialDates = {};
paymentPlans.forEach(plan => {
const paymentPlanOption = paymentPlansOptions.find(p => p.frequency === plan.frequency);
paymentPlans.forEach((plan) => {
const paymentPlanOption = paymentPlansOptions.find(
(p) => p.frequency === plan.frequency
);
if (paymentPlanOption) {
initialDates[paymentPlanOption.id] = plan.due_dates;
}
@ -55,8 +66,8 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
const updateDefaultDay = () => {
const currentDates = dates[selectedFrequency];
if (currentDates && currentDates.length > 0) {
const days = currentDates.map(date => new Date(date).getDate());
const allSameDay = days.every(day => day === days[0]);
const days = currentDates.map((date) => new Date(date).getDate());
const allSameDay = days.every((day) => day === days[0]);
if (allSameDay) {
setDefaultDay(days[0]);
} else {
@ -69,32 +80,44 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
};
const handleActivationChange = (value) => {
const selectedPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === value)?.frequency);
const selectedPlan = paymentPlans.find(
(plan) =>
plan.frequency ===
paymentPlansOptions.find((p) => p.id === value)?.frequency
);
if (!selectedPlan) return;
const updatedData = {
...selectedPlan,
is_active: !selectedPlan.is_active
is_active: !selectedPlan.is_active,
};
handleEdit(selectedPlan.id, updatedData)
.then(() => {
setPaymentPlans(prevPlans => prevPlans.map(plan =>
plan.id === selectedPlan.id ? { ...plan, is_active: updatedData.is_active } : plan
));
setActiveFrequencies(prevFrequencies => {
setPaymentPlans((prevPlans) =>
prevPlans.map((plan) =>
plan.id === selectedPlan.id
? { ...plan, is_active: updatedData.is_active }
: plan
)
);
setActiveFrequencies((prevFrequencies) => {
if (updatedData.is_active) {
setPopupMessage(`L'option de paiement en ${paymentPlansOptions.find(p => p.id === value).name} a été activée.`);
setPopupMessage(
`L'option de paiement en ${paymentPlansOptions.find((p) => p.id === value).name} a été activée.`
);
setPopupVisible(true);
return [...prevFrequencies, value];
} else {
setPopupMessage(`L'option de paiement en ${paymentPlansOptions.find(p => p.id === value).name} a été désactivée.`);
setPopupMessage(
`L'option de paiement en ${paymentPlansOptions.find((p) => p.id === value).name} a été désactivée.`
);
setPopupVisible(true);
return prevFrequencies.filter(item => item !== value);
return prevFrequencies.filter((item) => item !== value);
}
});
})
.catch(error => {
.catch((error) => {
console.error(error);
});
};
@ -106,18 +129,21 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
} else {
setSelectedFrequency(value);
if (!dates[value]) {
const frequencyValue = paymentPlansOptions.find(plan => plan.id === value)?.frequency || 1;
const newDates = Array(frequencyValue).fill('').map((_, index) => {
const newDate = new Date();
newDate.setDate(defaultDay);
if (value === 1) {
newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois
} else {
newDate.setMonth(newDate.getMonth() + index);
}
return newDate.toISOString().split('T')[0];
});
setDates(prevDates => ({ ...prevDates, [value]: newDates }));
const frequencyValue =
paymentPlansOptions.find((plan) => plan.id === value)?.frequency || 1;
const newDates = Array(frequencyValue)
.fill('')
.map((_, index) => {
const newDate = new Date();
newDate.setDate(defaultDay);
if (value === 1) {
newDate.setMonth(newDate.getMonth() + index * 4); // Espacer de 4 mois pour le paiement en 3 fois
} else {
newDate.setMonth(newDate.getMonth() + index);
}
return newDate.toISOString().split('T')[0];
});
setDates((prevDates) => ({ ...prevDates, [value]: newDates }));
}
}
};
@ -154,30 +180,39 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
setTimeout(() => setResetModifiedDates(false), 0);
// Mettre à jour les dates d'échéance en fonction du jour sélectionné
const updatedDates = dates[selectedFrequency].map(date => {
const updatedDates = dates[selectedFrequency].map((date) => {
const newDate = new Date(date);
newDate.setDate(day);
return newDate.toISOString().split('T')[0];
});
setDates(prevDates => ({ ...prevDates, [selectedFrequency]: updatedDates }));
setDates((prevDates) => ({
...prevDates,
[selectedFrequency]: updatedDates,
}));
};
const handleSubmitDefaultDay = () => {
const selectedPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === selectedFrequency)?.frequency);
const selectedPlan = paymentPlans.find(
(plan) =>
plan.frequency ===
paymentPlansOptions.find((p) => p.id === selectedFrequency)?.frequency
);
if (!selectedPlan) return;
const updatedData = {
...selectedPlan,
due_dates: dates[selectedFrequency]
due_dates: dates[selectedFrequency],
};
handleEdit(selectedPlan.id, updatedData)
.then(() => {
setPopupMessage(`Mise à jour des dates d'échéances effectuée avec succès`);
.then(() => {
setPopupMessage(
`Mise à jour des dates d'échéances effectuée avec succès`
);
setPopupVisible(true);
setIsDefaultDayModified(false);
})
.catch(error => {
.catch((error) => {
console.error(error);
});
};
@ -190,7 +225,9 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
const renderCell = (row, column) => {
switch (column) {
case 'OPTIONS':
return <span className="text-sm font-medium text-gray-900">{row.name}</span>;
return (
<span className="text-sm font-medium text-gray-900">{row.name}</span>
);
case 'ACTIONS':
return (
<button
@ -199,9 +236,17 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
e.stopPropagation();
handleActivationChange(row.id);
}}
className={activeFrequencies.includes(row.id) ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
className={
activeFrequencies.includes(row.id)
? 'text-emerald-500 hover:text-emerald-700'
: 'text-orange-500 hover:text-orange-700'
}
>
{activeFrequencies.includes(row.id) ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
{activeFrequencies.includes(row.id) ? (
<Eye className="w-5 h-5" />
) : (
<EyeOff className="w-5 h-5" />
)}
</button>
);
default:
@ -209,7 +254,11 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
}
};
const selectedPaymentPlan = paymentPlans.find(plan => plan.frequency === paymentPlansOptions.find(p => p.id === selectedFrequency)?.frequency);
const selectedPaymentPlan = paymentPlans.find(
(plan) =>
plan.frequency ===
paymentPlansOptions.find((p) => p.id === selectedFrequency)?.frequency
);
return (
<div className="space-y-4">
@ -275,4 +324,4 @@ const PaymentPlanSelector = ({ paymentPlans, setPaymentPlans, handleEdit, type }
);
};
export default PaymentPlanSelector;
export default PaymentPlanSelector;

View File

@ -1,7 +1,12 @@
import { formatPhoneNumber } from "@/utils/Telephone";
import { formatPhoneNumber } from '@/utils/Telephone';
export function PhoneLabel({phoneNumber}){
return (
<a className="text-sm font-semibold text-gray-800" href={"tel:"+phoneNumber}>{formatPhoneNumber(phoneNumber)}</a>
);
}
export function PhoneLabel({ phoneNumber }) {
return (
<a
className="text-sm font-semibold text-gray-800"
href={'tel:' + phoneNumber}
>
{formatPhoneNumber(phoneNumber)}
</a>
);
}

View File

@ -1,7 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
const Popup = ({
visible,
message,
onConfirm,
onCancel,
uniqueConfirmButton = false,
}) => {
if (!visible) return null;
// Vérifier si le message est une chaîne de caractères
@ -13,17 +19,15 @@ const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = fa
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full">
<div className="mb-4">
{isStringMessage ? (
// Afficher le message sous forme de lignes si c'est une chaîne
messageLines.map((line, index) => (
<p key={index} className="text-gray-700">
{line}
</p>
))
) : (
// Sinon, afficher directement le contenu React
message
)}
{isStringMessage
? // Afficher le message sous forme de lignes si c'est une chaîne
messageLines.map((line, index) => (
<p key={index} className="text-gray-700">
{line}
</p>
))
: // Sinon, afficher directement le contenu React
message}
</div>
<div className="flex justify-end space-x-2">
{!uniqueConfirmButton && (

View File

@ -1,5 +1,13 @@
import React, { useState } from 'react';
import { Trash2, Eye, EyeOff, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
import {
Trash2,
Eye,
EyeOff,
ToggleLeft,
ToggleRight,
Info,
XCircle,
} from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import StatusLabel from '@/components/StatusLabel';
@ -32,14 +40,23 @@ 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 = ({
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 [popupMessage, setPopupMessage] = useState("");
const [popupMessage, setPopupMessage] = useState('');
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState("");
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const [visibleTooltipId, setVisibleTooltipId] = useState(null);
@ -49,18 +66,24 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
const handleTooltipHide = () => {
setVisibleTooltipId(null); // Cacher toutes les tooltips
}
};
const handleConfirmActivateProfile = (profileRole) => {
setConfirmPopupMessage(`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`);
setConfirmPopupMessage(
`Êtes-vous sûr de vouloir ${profileRole.is_active ? 'désactiver' : 'activer'} ce profil ?`
);
setConfirmPopupOnConfirm(() => () => {
handleActivateProfile(profileRole)
.then(() => {
setPopupMessage(`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`);
setPopupMessage(
`Le profil a été ${profileRole.is_active ? 'désactivé' : 'activé'} avec succès.`
);
setPopupVisible(true);
})
.catch(error => {
setPopupMessage(`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`);
.catch((error) => {
setPopupMessage(
`Erreur lors de la ${profileRole.is_active ? 'désactivation' : 'activation'} du profil.`
);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
@ -69,15 +92,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
};
const handleConfirmDeleteProfile = (id) => {
setConfirmPopupMessage("Êtes-vous sûr de vouloir supprimer ce profil ?");
setConfirmPopupMessage('Êtes-vous sûr de vouloir supprimer ce profil ?');
setConfirmPopupOnConfirm(() => () => {
handleDeleteProfile(id)
.then(() => {
setPopupMessage("Le profil a été supprimé avec succès.");
setPopupMessage('Le profil a été supprimé avec succès.');
setPopupVisible(true);
})
.catch(error => {
setPopupMessage("Erreur lors de la suppression du profil.");
.catch((error) => {
setPopupMessage('Erreur lors de la suppression du profil.');
setPopupVisible(true);
});
setConfirmPopupVisible(false);
@ -93,11 +116,11 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
setConfirmPopupOnConfirm(() => () => {
handleDissociateGuardian(student.id, profileRole.associated_person?.id)
.then(() => {
setPopupMessage("Le responsable a été dissocié avec succès.");
setPopupMessage('Le responsable a été dissocié avec succès.');
setPopupVisible(true);
})
.catch(error => {
setPopupMessage("Erreur lors de la dissociation du responsable.");
.catch((error) => {
setPopupMessage('Erreur lors de la dissociation du responsable.');
setPopupVisible(true);
});
setConfirmPopupVisible(false);
@ -108,11 +131,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
const parentColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
{ name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{
name: 'Rôle',
transform: (row) => (
<span
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
>
{roleTypeToLabel(row.role_type)}
</span>
)
),
},
{
name: 'Utilisateur',
@ -121,47 +148,53 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<span>{row.associated_person?.guardian_name}</span>
{row.associated_person && (
<div
className="relative group"
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
onMouseLeave={handleTooltipHide} // Cacher la tooltip
>
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
{row.associated_person?.students?.length || 0}
</div>
</button>
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
<div
className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2"
>
<div className="mb-2">
<strong>Elève(s) associé(s):</strong>
<div className="flex flex-col justify-center space-y-2 mt-4">
{row.associated_person?.students?.map(student => (
<div key={student.student_name} className="flex justify-between items-center">
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
{student.student_name}
</span>
<div className="flex items-center space-x-2">
<StatusLabel status={student.registration_status} showDropdown={false} />
<button
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
onClick={() => handleConfirmDissociateGuardian(row, student)}
>
<XCircle className="w-5 h-5" />
<span className="text-sm">Dissocier</span>
</button>
className="relative group"
onMouseEnter={() => handleTooltipVisibility(row.id)} // Afficher la tooltip pour cette ligne
onMouseLeave={handleTooltipHide} // Cacher la tooltip
>
<button className="relative text-blue-500 hover:text-blue-700 flex items-center justify-center">
<div className="w-6 h-6 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center font-bold">
{row.associated_person?.students?.length || 0}
</div>
</button>
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
<div className="mb-2">
<strong>Elève(s) associé(s):</strong>
<div className="flex flex-col justify-center space-y-2 mt-4">
{row.associated_person?.students?.map((student) => (
<div
key={student.student_name}
className="flex justify-between items-center"
>
<span className="px-2 py-1 rounded-full text-gray-800 whitespace-nowrap inline-block min-w-0 max-w-fit">
{student.student_name}
</span>
<div className="flex items-center space-x-2">
<StatusLabel
status={student.registration_status}
showDropdown={false}
/>
<button
className="text-red-500 hover:text-red-700 flex items-center space-x-1"
onClick={() =>
handleConfirmDissociateGuardian(row, student)
}
>
<XCircle className="w-5 h-5" />
<span className="text-sm">Dissocier</span>
</button>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
)
),
},
{
name: 'Actions',
@ -169,10 +202,18 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<div className="flex justify-center space-x-2">
<button
type="button"
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
className={
row.is_active
? 'text-emerald-500 hover:text-emerald-700'
: 'text-orange-500 hover:text-orange-700'
}
onClick={() => handleConfirmActivateProfile(row)}
>
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
{row.is_active ? (
<ToggleRight className="w-5 h-5 " />
) : (
<ToggleLeft className="w-5 h-5" />
)}
</button>
<button
type="button"
@ -182,20 +223,26 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<Trash2 className="w-5 h-5" />
</button>
</div>
)
}
),
},
];
const schoolAdminColumns = [
{ name: 'Identifiant', transform: (row) => row.associated_profile_email },
{ name: 'Mise à jour', transform: (row) => row.updated_date_formatted },
{ name: 'Rôle', transform: (row) => (
<span className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}>
{
name: 'Rôle',
transform: (row) => (
<span
className={`px-2 py-1 rounded-full font-bold ${roleTypeToBadgeClass(row.role_type)}`}
>
{roleTypeToLabel(row.role_type)}
</span>
)
),
},
{ name: 'Utilisateur', transform: (row) => (
{
name: 'Utilisateur',
transform: (row) => (
<div className="flex items-center justify-center space-x-2 relative">
<span>{row.associated_person?.teacher_name}</span>
{row.associated_person && (
@ -210,14 +257,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
</div>
</button>
{visibleTooltipId === row.id && ( // Afficher uniquement si l'ID correspond
<div
className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2"
>
<div className="fixed z-50 w-96 p-4 bg-white border border-gray-200 rounded shadow-lg -translate-x-1/2">
<div className="mb-2">
<strong>Classes associées:</strong>
<div className="flex flex-wrap justify-center space-x-2 mt-4">
{row.associated_person?.classes?.map(classe => (
<span key={classe.id} className="px-2 py-1 rounded-full bg-gray-200 text-gray-800">
{row.associated_person?.classes?.map((classe) => (
<span
key={classe.id}
className="px-2 py-1 rounded-full bg-gray-200 text-gray-800"
>
{classe.name}
</span>
))}
@ -226,9 +274,15 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<div>
<strong>Spécialités:</strong>
<div className="flex flex-wrap justify-center space-x-2 mt-4">
{row.associated_person?.specialities?.map(speciality => (
<SpecialityItem key={speciality.name} speciality={speciality} isDraggable={false} />
))}
{row.associated_person?.specialities?.map(
(speciality) => (
<SpecialityItem
key={speciality.name}
speciality={speciality}
isDraggable={false}
/>
)
)}
</div>
</div>
</div>
@ -236,7 +290,7 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
</div>
)}
</div>
)
),
},
{
name: 'Actions',
@ -244,10 +298,18 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<div className="flex justify-center space-x-2">
<button
type="button"
className={row.is_active ? 'text-emerald-500 hover:text-emerald-700' : 'text-orange-500 hover:text-orange-700'}
className={
row.is_active
? 'text-emerald-500 hover:text-emerald-700'
: 'text-orange-500 hover:text-orange-700'
}
onClick={() => handleConfirmActivateProfile(row)}
>
{row.is_active ? <ToggleRight className="w-5 h-5 " /> : <ToggleLeft className="w-5 h-5" />}
{row.is_active ? (
<ToggleRight className="w-5 h-5 " />
) : (
<ToggleLeft className="w-5 h-5" />
)}
</button>
<button
type="button"
@ -257,8 +319,8 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
<Trash2 className="w-5 h-5" />
</button>
</div>
)
}
),
},
];
return (
@ -268,20 +330,14 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
{parentProfiles.length === 0 ? (
<div>Aucun profil trouvé</div>
) : (
<Table
data={parentProfiles}
columns={parentColumns}
/>
<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}
/>
<Table data={schoolAdminProfiles} columns={schoolAdminColumns} />
)}
</div>
</div>
@ -301,4 +357,4 @@ const ProfileDirectory = ({ profileRoles, handleActivateProfile, handleDeletePro
);
};
export default ProfileDirectory;
export default ProfileDirectory;

View File

@ -5,12 +5,19 @@ import { getRightStr } from '@/utils/rights';
import { ChevronDown } from 'lucide-react'; // Import de l'icône
const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
const { establishments, selectedEstablishmentId, setSelectedEstablishmentId, setProfileRole } = useEstablishment();
const {
establishments,
selectedEstablishmentId,
setSelectedEstablishmentId,
setProfileRole,
} = useEstablishment();
const [dropdownOpen, setDropdownOpen] = useState(false);
const handleEstablishmentChange = (establishmentId) => {
setSelectedEstablishmentId(establishmentId);
const role = establishments.find(est => est.id === establishmentId)?.role_type;
const role = establishments.find(
(est) => est.id === establishmentId
)?.role_type;
setProfileRole(role);
if (onEstablishmentChange) {
@ -20,11 +27,17 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
};
// Si on a pas de rôle ou un seul rôle, on n'affiche pas le sélecteur
if (!establishments || establishments.length === 0 || establishments.length === 1) {
if (
!establishments ||
establishments.length === 0 ||
establishments.length === 1
) {
return null;
}
const selectedEstablishment = establishments.find(est => est.id === selectedEstablishmentId);
const selectedEstablishment = establishments.find(
(est) => est.id === selectedEstablishmentId
);
return (
<div className={`relative ${className}`}>
@ -32,7 +45,9 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
buttonContent={
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white">
<div className="flex-1">
<div className="font-bold text-left">{getRightStr(selectedEstablishment?.role_type) || ''}</div>
<div className="font-bold text-left">
{getRightStr(selectedEstablishment?.role_type) || ''}
</div>
<div className="text-sm text-gray-500 text-left">
{selectedEstablishment?.name || 'Sélectionnez un établissement'}
</div>
@ -45,11 +60,13 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
/>
</div>
}
items={establishments.map(establishment => ({
items={establishments.map((establishment) => ({
type: 'item',
label: (
<div className="text-left">
<div className="font-bold">{getRightStr(establishment.role_type)}</div>
<div className="font-bold">
{getRightStr(establishment.role_type)}
</div>
<div className="text-sm text-gray-500">{establishment.name}</div>
</div>
),
@ -64,4 +81,4 @@ const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
);
};
export default ProfileSelector;
export default ProfileSelector;

View File

@ -1,157 +1,186 @@
import React, { useState, useEffect } from 'react';
const Step = ({ number, title, isActive, isValid, isCompleted, onClick }) => {
return (
<div className="flex-shrink-0 flex justify-center relative mx-4">
<div className={`
return (
<div className="flex-shrink-0 flex justify-center relative mx-4">
<div
className={`
w-8 h-8 rounded-full
flex items-center justify-center
text-sm font-semibold
${isCompleted
${
isCompleted
? 'bg-emerald-600 text-white'
: isActive
? 'bg-emerald-600 text-white'
: 'bg-gray-200 text-gray-600'
? 'bg-emerald-600 text-white'
: 'bg-gray-200 text-gray-600'
}
`}>
{isCompleted ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
) : (
number
)}
</div>
<div className="absolute top-12 left-1/2 -translate-x-1/2">
<span className={`
`}
>
{isCompleted ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
) : (
number
)}
</div>
<div className="absolute top-12 left-1/2 -translate-x-1/2">
<span
className={`
text-xs font-medium w-20 text-center block break-words
${isActive ? 'text-emerald-600' : 'text-gray-500'}
`}>
{title}
</span>
</div>
</div>
);
`}
>
{title}
</span>
</div>
</div>
);
};
const SpacerStep = ({ isCompleted }) => {
return (
<div className={`flex-1 h-0.5 ${isCompleted ? 'bg-emerald-600' : 'bg-gray-200'}`} />
);
return (
<div
className={`flex-1 h-0.5 ${isCompleted ? 'bg-emerald-600' : 'bg-gray-200'}`}
/>
);
};
const Dots = () => {
return (
<div className="text-gray-500 relative flex items-center mx-4">
<span>...</span>
<div className="absolute top-8 left-1/2 -translate-x-1/2">
<span className="text-xs font-medium w-20 text-center block">...</span>
</div>
</div>
);
return (
<div className="text-gray-500 relative flex items-center mx-4">
<span>...</span>
<div className="absolute top-8 left-1/2 -translate-x-1/2">
<span className="text-xs font-medium w-20 text-center block">...</span>
</div>
</div>
);
};
const ProgressStep = ({ steps, stepTitles, currentStep, setStep, isStepValid }) => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [visibleSteps, setVisibleSteps] = useState(steps);
const ProgressStep = ({
steps,
stepTitles,
currentStep,
setStep,
isStepValid,
}) => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [visibleSteps, setVisibleSteps] = useState(steps);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const calculateVisibleSteps = () => {
const minWidth = 150; // Largeur minimale estimée par étape
const maxVisibleSteps = Math.floor(windowWidth / minWidth);
useEffect(() => {
const calculateVisibleSteps = () => {
const minWidth = 150; // Largeur minimale estimée par étape
const maxVisibleSteps = Math.floor(windowWidth / minWidth);
if (maxVisibleSteps >= steps.length) {
setVisibleSteps(steps);
return;
}
if (maxVisibleSteps >= steps.length) {
setVisibleSteps(steps);
return;
}
if (maxVisibleSteps < 4) {
// Garder seulement première, dernière et courante
let filtered = [steps[0]];
if (currentStep > 1 && currentStep < steps.length) {
filtered.push('...');
filtered.push(steps[currentStep - 1]);
}
if (currentStep < steps.length) {
filtered.push('...');
}
filtered.push(steps[steps.length - 1]);
setVisibleSteps(filtered);
} else {
// Garder première, dernière, courante et quelques étapes adjacentes
let filtered = [steps[0]];
if (currentStep > 2) filtered.push('...');
if (currentStep > 1 && currentStep < steps.length) {
filtered.push(steps[currentStep - 1]);
}
if (currentStep < steps.length - 1) filtered.push('...');
filtered.push(steps[steps.length - 1]);
setVisibleSteps(filtered);
}
};
calculateVisibleSteps();
}, [windowWidth, currentStep, steps]);
const handleStepClick = (stepIndex) => {
// Vérifie si on peut naviguer vers l'étape (toutes les étapes précédentes doivent être valides)
const canNavigate = Array.from({ length: stepIndex }, (_, i) => i + 1)
.every(step => isStepValid(step));
if (canNavigate) {
setStep(stepIndex + 1);
if (maxVisibleSteps < 4) {
// Garder seulement première, dernière et courante
let filtered = [steps[0]];
if (currentStep > 1 && currentStep < steps.length) {
filtered.push('...');
filtered.push(steps[currentStep - 1]);
}
if (currentStep < steps.length) {
filtered.push('...');
}
filtered.push(steps[steps.length - 1]);
setVisibleSteps(filtered);
} else {
// Garder première, dernière, courante et quelques étapes adjacentes
let filtered = [steps[0]];
if (currentStep > 2) filtered.push('...');
if (currentStep > 1 && currentStep < steps.length) {
filtered.push(steps[currentStep - 1]);
}
if (currentStep < steps.length - 1) filtered.push('...');
filtered.push(steps[steps.length - 1]);
setVisibleSteps(filtered);
}
};
return (
<div className="w-full py-6">
<div className="flex items-center min-h-[100px]">
{visibleSteps.map((step, index) => {
if (step === '...') {
return (
<div key={`dots-${index}`} className="flex-1 flex items-center justify-center">
<Dots />
{index !== visibleSteps.length - 1 && <SpacerStep isCompleted={false} />}
</div>
);
}
calculateVisibleSteps();
}, [windowWidth, currentStep, steps]);
const originalIndex = steps.indexOf(step);
return (
<div
key={index}
className={`
const handleStepClick = (stepIndex) => {
// Vérifie si on peut naviguer vers l'étape (toutes les étapes précédentes doivent être valides)
const canNavigate = Array.from(
{ length: stepIndex },
(_, i) => i + 1
).every((step) => isStepValid(step));
if (canNavigate) {
setStep(stepIndex + 1);
}
};
return (
<div className="w-full py-6">
<div className="flex items-center min-h-[100px]">
{visibleSteps.map((step, index) => {
if (step === '...') {
return (
<div
key={`dots-${index}`}
className="flex-1 flex items-center justify-center"
>
<Dots />
{index !== visibleSteps.length - 1 && (
<SpacerStep isCompleted={false} />
)}
</div>
);
}
const originalIndex = steps.indexOf(step);
return (
<div
key={index}
className={`
flex-1 relative
${Array.from({ length: originalIndex + 1 }, (_, i) => i + 1).every(s => isStepValid(s)) ? 'cursor-pointer' : 'cursor-not-allowed'}
${Array.from({ length: originalIndex + 1 }, (_, i) => i + 1).every((s) => isStepValid(s)) ? 'cursor-pointer' : 'cursor-not-allowed'}
`}
onClick={() => handleStepClick(originalIndex)}
>
<div className="flex items-center">
<div className="w-full flex items-center">
<Step
number={originalIndex + 1}
title={stepTitles ? stepTitles[originalIndex + 1] : step}
isActive={currentStep === originalIndex + 1}
isCompleted={currentStep > originalIndex + 1}
isValid={isStepValid(originalIndex + 1)}
/>
{index !== visibleSteps.length - 1 && (
<SpacerStep isCompleted={currentStep > originalIndex + 1} />
)}
</div>
</div>
</div>
);
})}
onClick={() => handleStepClick(originalIndex)}
>
<div className="flex items-center">
<div className="w-full flex items-center">
<Step
number={originalIndex + 1}
title={stepTitles ? stepTitles[originalIndex + 1] : step}
isActive={currentStep === originalIndex + 1}
isCompleted={currentStep > originalIndex + 1}
isValid={isStepValid(originalIndex + 1)}
/>
{index !== visibleSteps.length - 1 && (
<SpacerStep isCompleted={currentStep > originalIndex + 1} />
)}
</div>
</div>
</div>
</div>
);
);
})}
</div>
</div>
);
};
export default ProgressStep;

View File

@ -4,37 +4,33 @@ import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_USERS_LOGIN_URL, getRedirectUrlFromRole } from '@/utils/Url';
const ProtectedRoute = ({ children, requiredRight }) => {
const { user, profileRole } = useEstablishment();
const router = useRouter();
let hasRequiredRight = false;
if(requiredRight && Array.isArray(requiredRight) ){
if (requiredRight && Array.isArray(requiredRight)) {
// Vérifier si l'utilisateur a le droit requis
hasRequiredRight = requiredRight.some((right) => profileRole === right);
}else{
hasRequiredRight = (profileRole === requiredRight);
} else {
hasRequiredRight = profileRole === requiredRight;
}
// Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight
useEffect(() => {
if(user){
if (user) {
// Vérifier si l'utilisateur a le droit requis mais pas le bon role on le redirige la page d'accueil associé au role
if (!hasRequiredRight) {
const redirectUrl = getRedirectUrlFromRole(profileRole);
router.push(`${redirectUrl}`);
}
}else{
} else {
// User non authentifié
router.push(`${FE_USERS_LOGIN_URL}`);
}
}, [profileRole]);
// Autoriser l'affichage si authentifié et rôle correct
return hasRequiredRight ? children : null;
};
export default ProtectedRoute;
export default ProtectedRoute;

View File

@ -1,13 +1,12 @@
'use client'
'use client';
import { SessionProvider } from "next-auth/react"
import { CsrfProvider } from '@/context/CsrfContext'
import { NextIntlClientProvider } from 'next-intl'
import { SessionProvider } from 'next-auth/react';
import { CsrfProvider } from '@/context/CsrfContext';
import { NextIntlClientProvider } from 'next-intl';
import { EstablishmentProvider } from '@/context/EstablishmentContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
export default function Providers({ children, messages, locale, session }) {
if (!locale) {
console.error('Locale non définie dans Providers');
@ -25,5 +24,5 @@ export default function Providers({ children, messages, locale, session }) {
</CsrfProvider>
</DndProvider>
</SessionProvider>
)
}
);
}

View File

@ -1,10 +1,17 @@
import React from 'react';
const RadioList = ({ items, formData, handleChange, fieldName, icon: Icon, className }) => {
const RadioList = ({
items,
formData,
handleChange,
fieldName,
icon: Icon,
className,
}) => {
return (
<div className={`mb-4 ${className}`}>
<div className="grid grid-cols-1 gap-4">
{items.map(item => (
{items.map((item) => (
<div key={item.id} className="flex items-center">
<input
key={`${item.id}-${Math.random()}`}

View File

@ -3,12 +3,23 @@ import { usePlanning } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
export default function ScheduleNavigation() {
const { schedules, selectedSchedule, setSelectedSchedule, hiddenSchedules, toggleScheduleVisibility, addSchedule, updateSchedule } = usePlanning();
const {
schedules,
selectedSchedule,
setSelectedSchedule,
hiddenSchedules,
toggleScheduleVisibility,
addSchedule,
updateSchedule,
} = usePlanning();
const [editingId, setEditingId] = useState(null);
const [editedName, setEditedName] = useState('');
const [editedColor, setEditedColor] = useState('');
const [isAddingNew, setIsAddingNew] = useState(false);
const [newSchedule, setNewSchedule] = useState({ name: '', color: '#10b981' });
const [newSchedule, setNewSchedule] = useState({
name: '',
color: '#10b981',
});
const handleEdit = (schedule) => {
setEditingId(schedule.id);
@ -19,9 +30,9 @@ export default function ScheduleNavigation() {
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find(s => s.id === editingId),
...schedules.find((s) => s.id === editingId),
name: editedName,
color: editedColor
color: editedColor,
});
setEditingId(null);
}
@ -31,7 +42,7 @@ export default function ScheduleNavigation() {
if (newSchedule.name) {
addSchedule({
id: `schedule-${Date.now()}`,
...newSchedule
...newSchedule,
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981' });
@ -55,7 +66,9 @@ export default function ScheduleNavigation() {
<input
type="text"
value={newSchedule.name}
onChange={(e) => setNewSchedule(prev => ({ ...prev, name: e.target.value }))}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full p-1 mb-2 border rounded"
placeholder="Nom du planning"
/>
@ -64,7 +77,9 @@ export default function ScheduleNavigation() {
<input
type="color"
value={newSchedule.color}
onChange={(e) => setNewSchedule(prev => ({ ...prev, color: e.target.value }))}
onChange={(e) =>
setNewSchedule((prev) => ({ ...prev, color: e.target.value }))
}
className="w-8 h-8"
/>
</div>
@ -86,11 +101,13 @@ export default function ScheduleNavigation() {
)}
<ul className="space-y-2">
{schedules.map(schedule => (
{schedules.map((schedule) => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id ? 'bg-gray-100' : 'hover:bg-gray-50'
selectedSchedule === schedule.id
? 'bg-gray-100'
: 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
@ -135,7 +152,13 @@ export default function ScheduleNavigation() {
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span className={hiddenSchedules.includes(schedule.id) ? 'text-gray-400' : ''}>
<span
className={
hiddenSchedules.includes(schedule.id)
? 'text-gray-400'
: ''
}
>
{schedule.name}
</span>
</div>
@ -167,4 +190,4 @@ export default function ScheduleNavigation() {
</ul>
</nav>
);
}
}

View File

@ -13,4 +13,4 @@ const SectionTitle = ({ title }) => {
);
};
export default SectionTitle;
export default SectionTitle;

View File

@ -1,17 +1,34 @@
export default function SelectChoice({ type, name, label, required, placeHolder, choices, callback, selected, errorMsg, IconItem, disabled = false }) {
export default function SelectChoice({
type,
name,
label,
required,
placeHolder,
choices,
callback,
selected,
errorMsg,
IconItem,
disabled = false,
}) {
return (
<>
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
<label
htmlFor={name}
className="block text-sm font-medium text-gray-700"
>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}>
{IconItem &&
<div
className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} ${disabled ? '' : 'hover:border-gray-400 focus-within:border-gray-500'}`}
>
{IconItem && (
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
{<IconItem />}
</span>
}
)}
<select
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${disabled ? 'bg-gray-100' : ''}`}
type={type}
@ -33,4 +50,4 @@ export default function SelectChoice({ type, name, label, required, placeHolder,
</div>
</>
);
}
}

Some files were not shown because too many files have changed in this diff Show More