chore: Initial Commit

feat: Gestion des inscriptions [#1]
feat(frontend): Création des vues pour le paramétrage de l'école [#2]
feat: Gestion du login [#6]
fix: Correction lors de la migration des modèle [#8]
feat: Révision du menu principal [#9]
feat: Ajout d'un footer [#10]
feat: Création des dockers compose pour les environnements de
développement et de production [#12]
doc(ci): Mise en place de Husky et d'un suivi de version automatique [#14]
This commit is contained in:
Luc SORIGNET
2024-11-18 10:02:58 +01:00
committed by N3WT DE COMPET
commit af0cd1c840
228 changed files with 22694 additions and 0 deletions

2
Front-End/.env Normal file
View File

@ -0,0 +1,2 @@
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_USE_FAKE_DATA='false'

3
Front-End/.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
Front-End/.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

6
Front-End/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": [
"messages"
],
"i18n-ally.keystyle": "nested"
}

36
Front-End/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

7
Front-End/jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,13 @@
{
"responsable": "Guardian",
"delete": "Delete",
"lastname": "Last name",
"firstname": "First name",
"email": "Email",
"phone": "Phone",
"birthdate": "Birth date",
"profession": "Profession",
"address": "Address",
"add_responsible": "Add guardian"
}

View File

@ -0,0 +1,9 @@
{
"dashboard": "Dashboard",
"totalStudents": "Total Students",
"averageInscriptionTime": "Average Registration Time",
"reInscriptionRate": "Re-enrollment Rate",
"structureCapacity": "Structure Capacity",
"inscriptionTrends": "Enrollment Trends",
"upcomingEvents": "Upcoming Events"
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"dashboard": "Dashboard",
"students": "Students",
"structure": "Structure",
"planning": "Schedule",
"grades": "Grades",
"settings": "Settings",
"schoolAdmin": "School Administration"
}

View File

@ -0,0 +1,30 @@
{
"headerBarTitle": "Administration",
"addStudent": "New",
"allStudents": "All Students",
"pending": "Pending Registrations",
"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",
"registrationFileStatus":"Registration file status",
"files":"Files"
}

View File

@ -0,0 +1,13 @@
{
"responsable": "Responsable",
"delete": "Supprimer",
"lastname": "Nom",
"firstname": "Prénom",
"email": "Email",
"phone": "Téléphone",
"birthdate": "Date de naissance",
"profession": "Profession",
"address": "Adresse",
"add_responsible": "Ajouter un responsable"
}

View File

@ -0,0 +1,9 @@
{
"dashboard": "Tableau de bord",
"totalStudents": "Total des étudiants",
"averageInscriptionTime": "Temps moyen d'inscription",
"reInscriptionRate": "Taux de réinscription",
"structureCapacity": "Remplissage de la structure",
"inscriptionTrends": "Tendances d'inscription",
"upcomingEvents": "Événements à venir"
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
{
"dashboard": "Tableau de bord",
"students": "Élèves",
"structure": "Structure",
"planning": "Emploi du temps",
"grades": "Notes",
"settings": "Paramètres",
"schoolAdmin": "Administration Scolaire"
}

View File

@ -0,0 +1,30 @@
{
"headerBarTitle":"Administration",
"addStudent": "Nouveau",
"allStudents": "Tous les élèves",
"pending": "Inscriptions en attente",
"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",
"registrationFileStatus":"État du dossier d'inscription",
"files":"Fichiers"
}

View File

@ -0,0 +1,8 @@
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);

5642
Front-End/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
Front-End/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "n3wt-school-front-end",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"check-strings": "node scripts/check-hardcoded-strings.js"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9",
"date-fns": "^4.1.0",
"framer-motion": "^11.11.11",
"ics": "^3.8.1",
"lucide-react": "^0.453.0",
"next": "14.2.11",
"next-intl": "^3.24.0",
"react": "^18",
"react-cookie": "^7.2.0",
"react-dom": "^18",
"react-phone-number-input": "^3.4.8",
"react-tooltip": "^5.28.0"
},
"devDependencies": {
"@babel/parser": "^7.26.2",
"@babel/traverse": "^7.25.9",
"autoprefixer": "^10.4.20",
"eslint": "^8",
"eslint-config-next": "14.2.11",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
Front-End/project.inlang/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
2ff5cbbb4bc1c6d178400871dfa342ac4f0b18e9b86cb64a1110be1ec54238c1

View File

@ -0,0 +1,12 @@
{
// official schema ensures that your project file is valid
"$schema": "https://inlang.com/schema/project-settings",
// the "source" language tag that is used in your project
"sourceLanguageTag": "fr",
// all the language tags you want to support in your project
"languageTags": ["fr", "en"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-json@4/dist/index.js"
], // or use another storage module: https://inlang.com/c/plugins (i18next, json, inlang message format)
"settings": {}
}

View File

@ -0,0 +1,192 @@
// scripts/check-hardcoded-strings.js
const fs = require('fs');
const path = require('path');
const babel = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// Patterns pour les classes Tailwind
const TAILWIND_PATTERNS = [
// Layout
/^(container|flex|grid|block|inline|hidden)/,
// Spacing
/^(m|p|mt|mb|ml|mr|mx|my|pt|pb|pl|pr|px|py)-[0-9]+/,
// Sizing
/^(w|h|min-w|min-h|max-w|max-h)-[0-9]+/,
// Typography
/^(text|font|leading)-/,
// Backgrounds
/^bg-/,
// Borders
/^(border|rounded|divide)-/,
// Effects
/^(shadow|opacity|transition)-/,
// Interactivity
/^(cursor|pointer-events|select|resize)-/,
// Layout
/^(z|top|right|bottom|left|float|clear)-/,
// Flexbox & Grid
/^(justify|items|content|self|place|gap)-/,
// É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]+$/
];
// Nouveaux patterns pour ignorer les logs et imports
const CODE_PATTERNS = [
// Console et debug avec contenu
/console\.(log|error|warn|info|debug)\(['"].*['"]/,
/^console\.(log|error|warn|info|debug)\(/,
/^debugger/,
// Imports et requires avec leurs chaînes
/^import\s+.*\s+from\s+['"].*['"]/,
/^import\s+['"].*['"]/,
/^require\(['"].*['"]\)/,
/^from\s+['"].*['"]/,
/^@/,
// Autres cas courants
/^process\.env\./,
/^module\.exports/,
/^export\s+/,
];
function isHardcodedString(str) {
// Ignorer les chaînes vides ou trop courtes
if (!str || str.length <= 1) return false;
// 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))) {
return false;
}
// Vérifier si c'est une chaîne dans un console.log
if (context.includes('console.log(')) {
return false;
}
// Vérifier si c'est une chaîne dans un import
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))
)) {
return false;
}
// Autres patterns à ignorer
const IGNORE_PATTERNS = [
/^[A-Z][A-Za-z]+$/, // Noms de composants
/^@/, // Imports
/^[./]/, // Chemins de fichiers
/^[0-9]+$/, // Nombres
/^style=/, // Props style
/^id=/, // Props id
/^data-/, // Data attributes
/^aria-/, // Aria attributes
/^role=/, // Role attributes
/^className=/, // className attributes
];
return !IGNORE_PATTERNS.some(pattern => pattern.test(str));
}
async function scanFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const hardcodedStrings = new Map(); // Utiliser Map pour stocker string -> ligne
try {
const ast = babel.parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
locations: true // Active le tracking des positions
});
traverse(ast, {
StringLiteral(path) {
const value = path.node.value;
const line = path.node.loc.start.line;
if (isHardcodedString(value)) {
hardcodedStrings.set(value, line);
}
},
JSXText(path) {
const value = path.node.value.trim();
const line = path.node.loc.start.line;
if (isHardcodedString(value)) {
hardcodedStrings.set(value, line);
}
},
});
} catch (error) {
console.error(`Erreur dans le fichier ${filePath}:`, error);
}
return Array.from(hardcodedStrings.entries());
}
async function scanDirectory(dir) {
const results = {};
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
Object.assign(results, await scanDirectory(filePath));
} else if (
stat.isFile() &&
(file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.tsx'))
) {
const strings = await scanFile(filePath);
if (strings.length > 0) {
results[filePath] = strings;
}
}
}
return results;
}
async function logStringsToFile(results) {
const outputPath = path.join(process.cwd(), 'hardcoded-strings-report.md');
let content = '# Rapport des chaînes en dur\n\n';
let totalStrings = 0;
for (const [file, strings] of Object.entries(results)) {
if (strings.length > 0) {
const relativePath = path.relative(process.cwd(), file);
content += `\n## ${relativePath}\n\n`;
strings.forEach(([str, line]) => {
totalStrings++;
content += `- Ligne ${line}: \`${str}\`\n`;
content += ` → [Voir dans le code](${relativePath}:${line}:1)\n\n`;
});
}
}
content += `\n## Résumé\n`;
content += `- Total des chaînes trouvées: ${totalStrings}\n`;
content += `- Date du scan: ${new Date().toLocaleString('fr-FR')}\n`;
fs.writeFileSync(outputPath, content, 'utf8');
console.log(`\nRapport généré: ${outputPath}`);
}
// Modifier la fonction main existante
async function main() {
const rootDir = process.cwd();
const results = await scanDirectory(path.join(rootDir, 'src'));
await logStringsToFile(results);
}
main().catch(console.error);

View File

@ -0,0 +1,49 @@
'use client'
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import Button from '@/components/Button';
const columns = [
{ name: 'Nom', transform: (row) => row.Nom },
{ name: 'Niveau', transform: (row) => row.Niveau },
{ name: 'Effectif', transform: (row) => row.Effectif },
];
export default function Page() {
const [classes, setClasses] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
fetchClasses();
}, [currentPage]);
const fetchClasses = async () => {
const fakeData = {
classes: [
{ Nom: 'Classe A', Niveau: '1ère année', Effectif: 30 },
{ Nom: 'Classe B', Niveau: '2ème année', Effectif: 25 },
{ Nom: 'Classe C', Niveau: '3ème année', Effectif: 28 },
],
totalPages: 3
};
setClasses(fakeData.classes);
setTotalPages(fakeData.totalPages);
};
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handleCreateClass = () => {
console.log('Créer une nouvelle classe');
};
return (
<div className='p-8'>
<h1 className='heading-section'>Gestion des Classes</h1>
<Button text="Créer une nouvelle classe" onClick={handleCreateClass} primary />
<Table data={classes} columns={columns} itemsPerPage={5} />
</div>
);
}

View File

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

View File

@ -0,0 +1,96 @@
'use client'
// src/components/Layout.js
import React from 'react';
import Sidebar from '@/components/Sidebar';
import { usePathname } from 'next/navigation';
import {useTranslations} from 'next-intl';
import {
Users,
Building,
Home,
Calendar,
Settings,
FileText,
LogOut
} from 'lucide-react';
import DropdownMenu from '@/components/DropdownMenu';
import Logo from '@/components/Logo';
import {
FR_ADMIN_HOME_URL,
FR_ADMIN_STUDENT_URL,
FR_ADMIN_STRUCTURE_URL,
FR_ADMIN_GRADES_URL,
FR_ADMIN_PLANNING_URL,
FR_ADMIN_SETTINGS_URL
} from '@/utils/Url';
import { disconnect } from '@/app/lib/actions';
export default function Layout({
children,
}) {
const t = useTranslations('sidebar');
const sidebarItems = {
"admin": { "id": "admin", "name": t('dashboard'), "url": FR_ADMIN_HOME_URL, "icon": Home },
"students": { "id": "students", "name": t('students'), "url": FR_ADMIN_STUDENT_URL, "icon": Users },
"structure": { "id": "structure", "name": t('structure'), "url": FR_ADMIN_STRUCTURE_URL, "icon": Building },
"grades": { "id": "grades", "name": t('grades'), "url": FR_ADMIN_GRADES_URL, "icon": FileText },
"planning": { "id": "planning", "name": t('planning'), "url": FR_ADMIN_PLANNING_URL, "icon": Calendar },
"settings": { "id": "settings", "name": t('settings'), "url": FR_ADMIN_SETTINGS_URL, "icon": Settings }
};
const pathname = usePathname();
const currentPage = pathname.split('/').pop();
const headerTitle = sidebarItems[currentPage]?.name || t('dashboard');
const softwareName = "N3WT School";
const softwareVersion = "v1.0.0";
const dropdownItems = [
{
label: 'Déconnexion',
onClick: disconnect,
icon: LogOut,
},
];
return (
<>
<div className="flex min-h-screen bg-gray-50">
<Sidebar currentPage={currentPage} items={Object.values(sidebarItems)} className="h-full" />
<div className="flex flex-col flex-1">
{/* Header - h-16 = 64px */}
<header className="h-16 bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between z-10">
<div className="text-xl font-semibold">{headerTitle}</div>
<DropdownMenu
buttonContent={<img src="https://i.pravatar.cc/32" alt="Profile" className="w-8 h-8 rounded-full cursor-pointer" />}
items={dropdownItems}
buttonClassName=""
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded shadow-lg"
/>
</header>
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Content avec scroll si nécessaire */}
<div className="flex-1 overflow-auto">
{children}
</div>
{/* Footer - h-16 = 64px */}
<footer className="h-16 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between">
<div>
<span>&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.</span>
<div>{softwareName} - {softwareVersion}</div>
</div>
<Logo className="w-8 h-8" />
</footer>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,143 @@
'use client'
import React, { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react';
import Loader from '@/components/Loader';
// Composant StatCard pour afficher une statistique
const StatCard = ({ title, value, icon, change, color = "blue" }) => (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<div className="flex justify-between items-start">
<div>
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
<p className="text-2xl font-semibold mt-1">{value}</p>
{change && (
<p className={`text-sm ${change > 0 ? 'text-green-500' : 'text-red-500'}`}>
{change > 0 ? '+' : ''}{change}% depuis le mois dernier
</p>
)}
</div>
<div className={`p-3 rounded-full bg-${color}-100`}>
{icon}
</div>
</div>
</div>
);
// Composant EventCard pour afficher les événements
const EventCard = ({ title, date, description, type }) => (
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
<div className="flex items-center gap-3">
<CalendarCheck className="text-blue-500" size={20} />
<div>
<h4 className="font-medium">{title}</h4>
<p className="text-sm text-gray-500">{date}</p>
<p className="text-sm mt-1">{description}</p>
</div>
</div>
</div>
);
export default function DashboardPage() {
const t = useTranslations('dashboard');
const [isLoading, setIsLoading] = useState(true);
const [stats, setStats] = useState({
totalStudents: 0,
averageInscriptionTime: 0,
reInscriptionRate: 0,
structureCapacity: 0,
upcomingEvents: [],
monthlyStats: {
inscriptions: [],
completionRate: 0
}
});
useEffect(() => {
// Simulation de chargement des données
setTimeout(() => {
setStats({
totalStudents: 245,
averageInscriptionTime: 3.5,
reInscriptionRate: 85,
structureCapacity: 300,
upcomingEvents: [
{
title: "Réunion de rentrée",
date: "2024-09-01",
description: "Présentation de l'année scolaire",
type: "meeting"
},
{
title: "Date limite inscriptions",
date: "2024-08-15",
description: "Clôture des inscriptions",
type: "deadline"
}
],
monthlyStats: {
inscriptions: [150, 180, 210, 245],
completionRate: 78
}
});
setIsLoading(false);
}, 1000);
}, []);
if (isLoading) return <Loader />;
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">{t('dashboard')}</h1>
{/* Statistiques principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard
title={t('totalStudents')}
value={stats.totalStudents}
icon={<Users className="text-blue-500" size={24} />}
change={12}
/>
<StatCard
title={t('averageInscriptionTime')}
value={`${stats.averageInscriptionTime} jours`}
icon={<Clock className="text-green-500" size={24} />}
color="green"
/>
<StatCard
title={t('reInscriptionRate')}
value={`${stats.reInscriptionRate}%`}
icon={<UserCheck className="text-purple-500" size={24} />}
color="purple"
/>
<StatCard
title={t('structureCapacity')}
value={`${(stats.totalStudents/stats.structureCapacity * 100).toFixed(1)}%`}
icon={<School className="text-orange-500" size={24} />}
color="orange"
/>
</div>
{/* Événements et KPIs */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-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>
{/* 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" />
</div>
</div>
{/* Événements à venir */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
{stats.upcomingEvents.map((event, index) => (
<EventCard key={index} {...event} />
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,65 @@
'use client'
import { PlanningProvider } from '@/context/PlanningContext';
import Calendar from '@/components/Calendar';
import EventModal from '@/components/EventModal';
import ScheduleNavigation from '@/components/ScheduleNavigation';
import { useState } from 'react';
export default function Page() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [eventData, setEventData] = useState({
title: '',
description: '',
start: '',
end: '',
location: '',
scheduleId: '', // Enlever la valeur par défaut ici
recurrence: 'none',
selectedDays: [],
recurrenceEnd: '',
customInterval: 1,
customUnit: 'days',
viewType: 'week' // Ajouter la vue semaine par défaut
});
const initializeNewEvent = (date = new Date()) => {
// S'assurer que date est un objet Date valide
const eventDate = date instanceof Date ? date : new Date();
setEventData({
title: '',
description: '',
start: eventDate.toISOString(),
end: new Date(eventDate.getTime() + 2 * 60 * 60 * 1000).toISOString(),
location: '',
scheduleId: '', // Ne pas définir de valeur par défaut ici non plus
recurrence: 'none',
selectedDays: [],
recurrenceEnd: '',
customInterval: 1,
customUnit: 'days'
});
setIsModalOpen(true);
};
return (
<PlanningProvider>
<div className="flex h-full overflow-hidden">
<ScheduleNavigation />
<Calendar
onDateClick={initializeNewEvent}
onEventClick={(event) => {
setEventData(event);
setIsModalOpen(true);
}}
/>
<EventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
eventData={eventData}
setEventData={setEventData}
/>
</div>
</PlanningProvider>
);
}

View File

@ -0,0 +1,105 @@
'use client'
import React, { useState } from 'react';
import Tab from '@/components/Tab';
import TabContent from '@/components/TabContent';
import Button from '@/components/Button';
import InputText from '@/components/InputText';
export default function SettingsPage() {
const [activeTab, setActiveTab] = useState('structure');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [smtpServer, setSmtpServer] = useState('');
const [smtpPort, setSmtpPort] = useState('');
const [smtpUser, setSmtpUser] = useState('');
const [smtpPassword, setSmtpPassword] = useState('');
const handleTabClick = (tab) => {
setActiveTab(tab);
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
};
const handleConfirmPasswordChange = (e) => {
setConfirmPassword(e.target.value);
};
const handleSmtpServerChange = (e) => {
setSmtpServer(e.target.value);
};
const handleSmtpPortChange = (e) => {
setSmtpPort(e.target.value);
};
const handleSmtpUserChange = (e) => {
setSmtpUser(e.target.value);
};
const handleSmtpPasswordChange = (e) => {
setSmtpPassword(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (password !== confirmPassword) {
alert('Les mots de passe ne correspondent pas');
return;
}
// Logique pour mettre à jour l'email et le mot de passe
console.log('Email:', email);
console.log('Password:', password);
};
const handleSmtpSubmit = (e) => {
e.preventDefault();
// Logique pour mettre à jour les paramètres SMTP
console.log('SMTP Server:', smtpServer);
console.log('SMTP Port:', smtpPort);
console.log('SMTP User:', smtpUser);
console.log('SMTP Password:', smtpPassword);
};
return (
<div className="p-8">
<div className="flex space-x-4 mb-4">
<Tab
text="Informations de la structure"
active={activeTab === 'structure'}
onClick={() => handleTabClick('structure')}
/>
<Tab
text="Paramètres SMTP"
active={activeTab === 'smtp'}
onClick={() => handleTabClick('smtp')}
/>
</div>
<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} />
<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} />
<Button type="submit" primary text="Mettre à jour"></Button>
</form>
</TabContent>
</div>
</div>
);
}

View File

@ -0,0 +1,159 @@
'use client'
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import SpecialitiesSection from '@/components/SpecialitiesSection'
import ClassesSection from '@/components/ClassesSection'
import TeachersSection from '@/components/TeachersSection';
import { User, School } from 'lucide-react'
import { BK_GESTIONINSCRIPTION_SPECIALITES_URL,
BK_GESTIONINSCRIPTION_CLASSES_URL,
BK_GESTIONINSCRIPTION_SPECIALITE_URL,
BK_GESTIONINSCRIPTION_CLASSE_URL,
BK_GESTIONINSCRIPTION_TEACHERS_URL,
BK_GESTIONINSCRIPTION_TEACHER_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken';
export default function Page() {
const [specialities, setSpecialities] = useState([]);
const [classes, setClasses] = useState([]);
const [teachers, setTeachers] = useState([]);
const csrfToken = useCsrfToken();
useEffect(() => {
// Fetch data for specialities
fetchSpecialities();
// Fetch data for teachers
fetchTeachers();
// Fetch data for classes
fetchClasses();
}, []);
const fetchSpecialities = () => {
fetch(`${BK_GESTIONINSCRIPTION_SPECIALITES_URL}`)
.then(response => response.json())
.then(data => {
setSpecialities(data);
})
.catch(error => {
console.error('Error fetching specialities:', error);
});
};
const fetchTeachers = () => {
fetch(`${BK_GESTIONINSCRIPTION_TEACHERS_URL}`)
.then(response => response.json())
.then(data => {
setTeachers(data);
})
.catch(error => {
console.error('Error fetching teachers:', error);
});
};
const fetchClasses = () => {
fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`)
.then(response => response.json())
.then(data => {
setClasses(data);
})
.catch(error => {
console.error('Error fetching classes:', error);
});
};
const handleCreate = (url, newData, setDatas) => {
console.log('SEND POST :', JSON.stringify(newData));
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(newData),
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Succes :', data);
setDatas(prevState => [...prevState, data]);
})
.catch(error => {
console.error('Erreur :', error);
});
};
const handleEdit = (url, id, updatedData, setDatas) => {
fetch(`${url}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(updatedData),
credentials: 'include'
})
.then(response => response.json())
.then(data => {
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
})
.catch(error => {
console.error('Erreur :', error);
});
};
const handleDelete = (url, id, setDatas) => {
fetch(`${url}/${id}`, {
method:'DELETE',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
setDatas(prevState => prevState.filter(item => item.id !== id));
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
};
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, newData, setSpecialities)}
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, updatedData, setSpecialities)}
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_SPECIALITE_URL}`, id, setSpecialities)}
/>
<TeachersSection
teachers={teachers}
specialities={specialities}
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, newData, setTeachers)}
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, updatedData, setTeachers)}
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_TEACHER_URL}`, id, setTeachers)}
/>
<ClassesSection
classes={classes}
specialities={specialities}
teachers={teachers}
handleCreate={(newData) => handleCreate(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, newData, setClasses)}
handleEdit={(id, updatedData) => handleEdit(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, updatedData, setClasses)}
handleDelete={(id) => handleDelete(`${BK_GESTIONINSCRIPTION_CLASSE_URL}`, id, setClasses)}
/>
</div>
);
};

View File

@ -0,0 +1,91 @@
'use client'
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { FR_ADMIN_STUDENT_URL,
BK_GESTIONINSCRIPTION_ELEVE_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
import { mockStudent } from '@/data/mockStudent';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const idProfil = searchParams.get('id');
const idEleve = searchParams.get('idEleve'); // Changé de codeDI à idEleve
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
setInitialData(mockStudent);
setIsLoading(false);
} else {
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`) // Utilisation de idEleve au lieu de codeDI
.then(response => response.json())
.then(data => {
console.log('Fetched data:', data); // Pour le débogage
const formattedData = {
id: data.id,
nom: data.nom,
prenom: data.prenom,
adresse: data.adresse,
dateNaissance: data.dateNaissance,
lieuNaissance: data.lieuNaissance,
codePostalNaissance: data.codePostalNaissance,
nationalite: data.nationalite,
medecinTraitant: data.medecinTraitant,
niveau: data.niveau,
responsables: data.responsables || []
};
setInitialData(formattedData);
setIsLoading(false);
})
.catch(error => {
console.error('Error fetching student data:', error);
setIsLoading(false);
});
}
}, [idEleve]); // Dépendance changée à idEleve
const handleSubmit = async (data) => {
if (useFakeData) {
console.log('Fake submit:', data);
return;
}
try {
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, { // Utilisation de idEleve
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
});
const result = await response.json();
console.log('Success:', result);
// Redirection après succès
window.location.href = FR_ADMIN_STUDENT_URL;
} catch (error) {
console.error('Error:', error);
alert('Une erreur est survenue lors de la mise à jour des données');
}
};
return (
<InscriptionFormShared
initialData={initialData}
csrfToken={csrfToken}
onSubmit={handleSubmit}
cancelUrl={FR_ADMIN_STUDENT_URL}
isLoading={isLoading}
/>
);
}

View File

@ -0,0 +1,334 @@
'use client'
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import {mockFicheInscription} from '@/data/mockFicheInscription';
import Tab from '@/components/Tab';
import { useTranslations } from 'next-intl';
import StatusLabel from '@/components/StatusLabel';
import { Search } from 'lucide-react';
import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import AlertWithModal from '@/components/AlertWithModal';
import Button from '@/components/Button';
import DropdownMenu from "@/components/DropdownMenu";
import { swapFormatDate } from '@/utils/Date';
import { formatPhoneNumber } from '@/utils/Telephone';
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm'
import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, BK_GESTIONINSCRIPTION_SEND_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, BK_GESTIONINSCRIPTION_ARCHIVE_URL } from '@/utils/Url';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page({ params: { locale } }) {
const t = useTranslations('students');
const [ficheInscriptions, setFicheInscriptions] = useState([]);
const [ficheInscriptionsData, setFicheInscriptionsData] = useState([]);
const [fichesInscriptionsDataArchivees, setFicheInscriptionsDataArchivees] = useState([]);
// const [filter, setFilter] = useState('*');
const [searchTerm, setSearchTerm] = useState('');
const [alertPage, setAlertPage] = useState(false);
const [mailSent, setMailSent] = useState(false);
const [ficheArchivee, setFicheArchivee] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [popup, setPopup] = useState({ visible: false, message: '', onConfirm: null });
const [activeTab, setActiveTab] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalStudents, setTotalStudents] = useState(0);
const [totalArchives, setTotalArchives] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
}
// Modifier la fonction fetchData pour inclure le terme de recherche
const fetchData = (page, pageSize, search = '') => {
const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/all?page=${page}&page_size=${pageSize}&search=${search}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
setIsLoading(false);
if (data) {
const { fichesInscriptions, count } = data;
setFicheInscriptionsData(fichesInscriptions);
const calculatedTotalPages = Math.ceil(count / pageSize);
setTotalStudents(count);
setTotalPages(calculatedTotalPages);
}
})
.catch(error => {
console.error('Error fetching data:', error);
setIsLoading(false);
});
};
const fetchDataArchived = () => {
fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/archived`, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
setIsLoading(false);
if (data) {
const { fichesInscriptions, count } = data;
setTotalArchives(count);
setFicheInscriptionsDataArchivees(fichesInscriptions);
}
console.log('Success ARCHIVED:', data);
})
.catch(error => {
console.error('Error fetching data:', error);
setIsLoading(false);
});
};
useEffect(() => {
const fetchDataAndSetState = () => {
if (!useFakeData) {
fetchData(currentPage, itemsPerPage, searchTerm);
fetchDataArchived();
} else {
setTimeout(() => {
setFicheInscriptionsData(mockFicheInscription);
setIsLoading(false);
}, 1000);
}
setFicheArchivee(false);
setMailSent(false);
};
fetchDataAndSetState();
}, [mailSent, ficheArchivee, currentPage, itemsPerPage]);
// Modifier le useEffect pour la recherche
useEffect(() => {
const timeoutId = setTimeout(() => {
fetchData(currentPage, itemsPerPage, searchTerm);
}, 500); // Debounce la recherche
return () => clearTimeout(timeoutId);
}, [searchTerm, currentPage, itemsPerPage]);
const archiveFicheInscription = (id, nom, prenom) => {
setPopup({
visible: true,
message: `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
onConfirm: () => {
const url = `${BK_GESTIONINSCRIPTION_ARCHIVE_URL}/${id}`;
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('Success:', data);
setFicheInscriptions(ficheInscriptions.filter(fiche => fiche.id !== id));
setFicheArchivee(true);
alert("Le dossier d'inscription a été correctement archivé");
})
.catch(error => {
console.error('Error archiving data:', error);
alert("Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur.");
});
}
});
};
const sendConfirmFicheInscription = (id, nom, prenom) => {
setPopup({
visible: true,
message: `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
onConfirm: () => {
const url = `${BK_GESTIONINSCRIPTION_SEND_URL}/${id}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('Success:', data);
setMailSent(true);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
});
};
const updateStatusAction = (id, newStatus) => {
console.log('Edit fiche inscription with id:', id);
};
const handleLetterClick = (letter) => {
setFilter(letter);
};
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici
};
const columns = [
{ name: t('studentName'), transform: (row) => row.eleve.nom },
{ name: t('studentFistName'), transform: (row) => row.eleve.prenom },
{ name: t('mainContactMail'), transform: (row) => row.eleve.responsables[0].mail },
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.eleve.responsables[0].telephone) },
{ name: t('lastUpdateDate'), transform: (row) => swapFormatDate(row.dateMAJ, "DD-MM-YYYY hh:mm:ss", "DD/MM/YYYY hh:mm") },
{ name: t('registrationFileStatus'), transform: (row) => <StatusLabel etat={row.etat} onChange={(newStatus) => updateStatusAction(row.eleve.id, newStatus)} /> },
{ name: t('files'), transform: (row) => (
<ul>
{row.fichiers?.map((fichier, fileIndex) => (
<li key={fileIndex} className="flex items-center gap-2">
<FileText size={16} />
<a href={fichier.url}>{fichier.nom}</a>
</li>
))}
</ul>
) },
{ name: 'Actions', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
...(row.etat === 1 ? [{
label: (
<>
<Send size={16} className="mr-2" /> Envoyer
</>
),
onClick: () => sendConfirmFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom),
}] : []),
...(row.etat === 1 ? [{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?idEleve=${row.eleve.id}&id=1`,
}] : []),
...(row.etat === 2 ? [{
label: (
<>
<Edit size={16} className="mr-2" /> Modifier
</>
),
onClick: () => window.location.href = `${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?idEleve=${row.eleve.id}&id=1`,
}] : []),
...(row.etat !== 6 ? [{
label: (
<>
<Trash2 size={16} className="mr-2 text-red-700" /> Archiver
</>
),
onClick: () => archiveFicheInscription(row.eleve.id, row.eleve.nom, row.eleve.prenom),
}] : []),
]}
buttonClassName="text-gray-400 hover:text-gray-600"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
/>
) },
];
if (isLoading) {
return <Loader />;
} else {
if (ficheInscriptions.length === 0 && fichesInscriptionsDataArchivees.length === 0 && alertPage) {
return (
<div className='p-8'>
<AlertWithModal
title={t("information")}
message={t("no_records") + " " + t("create_first_record")}
buttonText={t("add_button")}
/>
</div>
);
} else {
return (
<div className='p-8'>
<div className="border-b border-gray-200 mb-6">
<div className="flex gap-8">
<Tab
text={<>
{t('allStudents')}
<span className="ml-2 text-sm text-gray-400">({totalStudents})</span>
</>}
active={activeTab === 'all'}
onClick={() => setActiveTab('all')}
/>
<Tab
text={<>
{t('pending')}
<span className="ml-2 text-sm text-gray-400">({12})</span>
</>}
active={activeTab === 'pending'}
onClick={() => setActiveTab('pending')}
/>
<Tab
text={<>
{t('archived')}
<span className="ml-2 text-sm text-gray-400">({totalArchives})</span>
</>}
active={activeTab === 'archived'}
onClick={() => setActiveTab('archived')}
/>
<Button text={t("addStudent")} primary onClick={openModal} icon={<UserPlus size={20} />} />
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
title={"Création d'un nouveau dossier d'inscription"}
ContentComponent={InscriptionForm}
/>
</div>
</div>
<div className="flex justify-between items-center mb-6">
<div className="relative flex-grow mr-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder={t('searchStudent')}
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md"
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
</div>
<Table
key={`${currentPage}-${searchTerm}`}
data={(activeTab === 'all' || activeTab === 'pending') ? ficheInscriptionsData : fichesInscriptionsDataArchivees}
columns={columns}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
<Popup
visible={popup.visible}
message={popup.message}
onConfirm={() => {
popup.onConfirm();
setPopup({ ...popup, visible: false });
}}
onCancel={() => setPopup({ ...popup, visible: false })}
/>
</div>
);
}
}
}

View File

@ -0,0 +1,23 @@
'use client'
import React, { useState, useEffect } from 'react';
import Button from '@/components/Button';
import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react';
import Modal from '@/components/Modal';
export default function Page() {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
}
return (
<div className='p-8'>
<Button text={"addTeacher"} primary onClick={openModal} icon={<UserPlus size={20} />} />
<Modal isOpen={isOpen} setIsOpen={setIsOpen} />
</div>
);
}

View File

@ -0,0 +1,18 @@
'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
export default function Home() {
const t = useTranslations('homePage');
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<Logo className="mb-4" /> {/* Ajout du logo */}
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
<p className="text-lg mb-8">{t('pleaseLogin')}</p>
<Button text={t('loginButton')} primary href="/users/login" />
</div>
);
}

View File

@ -0,0 +1,113 @@
'use client'
import React, { useState, useEffect } from 'react';
import InscriptionFormShared from '@/components/Inscription/InscriptionFormShared';
import { useSearchParams, redirect, useRouter } from 'next/navigation';
import useCsrfToken from '@/hooks/useCsrfToken';
import { FR_PARENTS_HOME_URL,
BK_GESTIONINSCRIPTION_ELEVE_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL,
BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL } from '@/utils/Url';
import { mockStudent } from '@/data/mockStudent';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
export default function Page() {
const searchParams = useSearchParams();
const idProfil = searchParams.get('id');
const idEleve = searchParams.get('idEleve');
const router = useRouter();
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const csrfToken = useCsrfToken();
const [currentProfil, setCurrentProfil] = useState("");
const [lastIdResponsable, setLastIdResponsable] = useState(1);
useEffect(() => {
if (!idEleve || !idProfil) {
console.error('Missing idEleve or idProfil');
return;
}
if (useFakeData) {
setInitialData(mockStudent);
setLastIdResponsable(999);
setIsLoading(false);
} else {
Promise.all([
// Fetch eleve data
fetch(`${BK_GESTIONINSCRIPTION_ELEVE_URL}/${idEleve}`),
// Fetch last responsable ID
fetch(BK_GESTIONINSCRIPTION_RECUPEREDERNIER_RESPONSABLE_URL)
])
.then(async ([eleveResponse, responsableResponse]) => {
const eleveData = await eleveResponse.json();
const responsableData = await responsableResponse.json();
const formattedData = {
id: eleveData.id,
nom: eleveData.nom,
prenom: eleveData.prenom,
adresse: eleveData.adresse,
dateNaissance: eleveData.dateNaissance,
lieuNaissance: eleveData.lieuNaissance,
codePostalNaissance: eleveData.codePostalNaissance,
nationalite: eleveData.nationalite,
medecinTraitant: eleveData.medecinTraitant,
niveau: eleveData.niveau,
responsables: eleveData.responsables || []
};
setInitialData(formattedData);
setLastIdResponsable(responsableData.lastid);
let profils = eleveData.profils;
const currentProf = profils.find(profil => profil.id === idProfil);
if (currentProf) {
setCurrentProfil(currentProf);
}
})
.catch(error => {
console.error('Error fetching data:', error);
})
.finally(() => {
setIsLoading(false);
});
}
}, [idEleve, idProfil]);
const handleSubmit = async (data) => {
if (useFakeData) {
console.log('Fake submit:', data);
return;
}
try {
const response = await fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${idEleve}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify(data),
});
const result = await response.json();
console.log('Success:', result);
router.push(FR_PARENTS_HOME_URL);
} catch (error) {
console.error('Error:', error);
}
};
return (
<InscriptionFormShared
initialData={initialData}
csrfToken={csrfToken}
onSubmit={handleSubmit}
cancelUrl={FR_PARENTS_HOME_URL}
isLoading={isLoading}
/>
);
}

View File

@ -0,0 +1,92 @@
'use client'
// src/components/Layout.js
import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu';
import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { Bell, 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 { FR_PARENTS_HOME_URL,FR_PARENTS_MESSAGERIE_URL,FR_PARENTS_SETTINGS_URL, BK_GESTIONINSCRIPTION_MESSAGES_URL } from '@/utils/Url'; // Ajout de l'importation de l'URL de la page d'accueil parent
import useLocalStorage from '@/hooks/useLocalStorage';
export default function Layout({
children,
}) {
const router = useRouter(); // Définition de router
const [messages, setMessages] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ;
useEffect(() => {
setUserId(userId);
fetch(`${BK_GESTIONINSCRIPTION_MESSAGES_URL}/${userId}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
if (data) {
setMessages(data);
}
console.log('Success :', data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}, []);
return (
<>
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */}
<header className="bg-white border-b border-gray-200 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">
<Logo className="h-8 w-8" /> {/* Utilisation du composant Logo */}
<div className="text-xl font-semibold">Accueil</div>
</div>
<div className="flex items-center space-x-4">
<button
className="p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FR_PARENTS_HOME_URL); }} // Utilisation de router pour revenir à l'accueil parent
>
<Home />
</button>
<div className="relative">
<button
className="p-2 rounded-full hover:bg-gray-200"
onClick={() => { router.push(FR_PARENTS_MESSAGERIE_URL); }} // Utilisation de router
>
<MessageSquare />
</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={<User />}
items={[
{ label: 'Se déconnecter', icon: LogOut, onClick: () => {} },
{ label: 'Settings', icon: Settings , onClick: () => { router.push(FR_PARENTS_SETTINGS_URL); } }
]}
buttonClassName="p-2 rounded-full hover:bg-gray-200"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg"
/>
</div>
</header>
{/* Content */}
<div className="pt-20 p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
{children}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,106 @@
'use client'
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
const contacts = [
{ id: 1, name: 'Facturation', profilePic: 'https://i.pravatar.cc/32' },
{ id: 2, name: 'Enseignant 1', profilePic: 'https://i.pravatar.cc/32' },
{ id: 3, name: 'Contact', profilePic: 'https://i.pravatar.cc/32' },
];
export default function MessageriePage() {
const [selectedContact, setSelectedContact] = useState(null);
const [messages, setMessages] = useState({});
const [newMessage, setNewMessage] = useState('');
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = () => {
if (newMessage.trim() && selectedContact) {
const contactMessages = messages[selectedContact.id] || [];
setMessages({
...messages,
[selectedContact.id]: [...contactMessages, { id: contactMessages.length + 1, text: newMessage, date: new Date() }],
});
setNewMessage('');
simulateContactResponse(selectedContact.id);
}
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
const simulateContactResponse = (contactId) => {
setTimeout(() => {
setMessages((prevMessages) => {
const contactMessages = prevMessages[contactId] || [];
return {
...prevMessages,
[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="w-1/4 border-r border-gray-200 p-4 overflow-y-auto h-full ">
{contacts.map((contact) => (
<div
key={contact.id}
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
onClick={() => setSelectedContact(contact)}
>
<img src={contact.profilePic} alt={`${contact.name}'s profile`} className="w-8 h-8 rounded-full inline-block mr-2" />
{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" />
<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>
))}
<div ref={messagesEndRef} />
</div>
<div className="p-4 border-t border-gray-200 flex">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="w-full p-2 border border-gray-300 rounded"
placeholder="Écrire un message..."
onKeyDown={handleKeyPress}
/>
<button
onClick={handleSendMessage}
className="p-2 bg-emerald-500 text-white rounded mr-2"
>
<SendHorizontal />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,90 @@
'use client'
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import { Edit } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import useLocalStorage from '@/hooks/useLocalStorage';
import { BK_GESTIONINSCRIPTION_ENFANTS_URL , FR_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
export default function ParentHomePage() {
const [actions, setActions] = useState([]);
const [children, setChildren] = useState([]);
const [userId, setUserId] = useLocalStorage("userId", '') ;
const router = useRouter();
useEffect(() => {
if (!userId) return;
const fetchActions = async () => {
const response = await fetch('/api/actions');
const data = await response.json();
setActions(data);
};
const fetchEleves = async () => {
const response = await fetch(`${BK_GESTIONINSCRIPTION_ENFANTS_URL}/${userId}`);
const data = await response.json();
console.log(data);
setChildren(data);
};
fetchEleves();
}, [userId]);
function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
console.log(`Edit dossier for eleve id: ${eleveId}`);
router.push(`${FR_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&idEleve=${eleveId}`);
}
const actionColumns = [
{ name: 'Action', transform: (row) => row.action },
];
const getShadowColor = (etat) => {
switch (etat) {
case 1:
return 'shadow-blue-500'; // Couleur d'ombre plus visible
case 2:
return 'shadow-orange-500'; // Couleur d'ombre plus visible
case 3:
return 'shadow-purple-500'; // Couleur d'ombre plus visible
case 4:
return 'shadow-red-500'; // Couleur d'ombre plus visible
case 5:
return 'shadow-green-500'; // Couleur d'ombre plus visible
case 6:
return 'shadow-red-500'; // Couleur d'ombre plus visible
default:
return 'shadow-green-500'; // Couleur d'ombre plus visible
}
};
return (
<div>
<div>
<h2 className="text-xl font-semibold mb-4">Dernières actions à effectuer</h2>
<Table data={actions} columns={actionColumns} itemsPerPage={5} />
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Enfants</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{children.map((child) => (
<div key={child.eleve.id} className={`border p-4 rounded shadow ${getShadowColor(child.etat)}`}>
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">{child.eleve.nom} {child.eleve.prenom}</h3>
<Edit className="cursor-pointer" onClick={() => handleEdit(child.eleve.id)} />
</div>
<StatusLabel etat={child.etat } showDropdown={false}/>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,74 @@
'use client'
import React, { useState } from 'react';
import Button from '@/components/Button';
import InputText from '@/components/InputText';
export default function SettingsPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
};
const handleConfirmPasswordChange = (e) => {
setConfirmPassword(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (password !== confirmPassword) {
alert('Les mots de passe ne correspondent pas');
return;
}
// Logique pour mettre à jour l'email et le mot de passe
console.log('Email:', email);
console.log('Password:', password);
};
return (
<div className="p-4">
<h2 className="text-xl mb-4">Paramètres du compte</h2>
<form onSubmit={handleSubmit}>
<InputText
type="email"
id="email"
label="Email"
value={email}
onChange={handleEmailChange}
required
/>
<InputText
type="password"
id="password"
label="Nouveau mot de passe"
value={password}
onChange={handlePasswordChange}
required
/>
<InputText
type="password"
id="confirmPassword"
label="Confirmer le mot de passe"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
required
/>
<div className="flex items-center justify-between">
<Button
type="submit"
primary
text={" Mettre à jour"}
/>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,124 @@
'use client'
import React, { useState, useEffect } from 'react'
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
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 { BK_LOGIN_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, FR_PARENTS_HOME_URL, FR_USERS_NEW_PASSWORD_URL, FR_USERS_SUBSCRIBE_URL } from '@/utils/Url';
import useLocalStorage from '@/hooks/useLocalStorage';
import useCsrfToken from '@/hooks/useCsrfToken';
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 [passwordFieldError,setPasswordFieldError] = useState("")
const [isLoading, setIsLoading] = useState(false);
const [userId, setUserId] = useLocalStorage("userId", '') ;
const router = useRouter();
const csrfToken = useCsrfToken();
function isOK(data) {
return data.errorMessage === ""
}
function handleFormLogin(formData) {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: "",
profil: "fakeProfileId"
};
setUserFieldError("")
setPasswordFieldError("")
setErrorMessage("")
if(isOK(data)){
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
router.push(`${FR_ADMIN_STUDENT_EDIT_SUBSCRIBE}?id=${data.profil}`);
} else {
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPasswordFieldError(data.errorFields.password);
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
}
} else {
const request = new Request(
`${BK_LOGIN_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify( {
email: formData.get('login'),
password: formData.get('password'),
}),
credentials: 'include',
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("")
setPasswordFieldError("")
setErrorMessage("")
if(isOK(data)){
localStorage.setItem('userId', data.profil); // Stocker l'identifiant de l'utilisateur
router.push(`${FR_PARENTS_HOME_URL}`);
} else {
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPasswordFieldError(data.errorFields.password);
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.message;
console.log(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 text-emerald-900 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" />
<InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full" />
<div className="input-group mb-4">
</div>
<label className="text-red-500">{errorMessage}</label>
<label><a className="float-right text-emerald-900" href={`${FR_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

@ -0,0 +1,107 @@
'use client'
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
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 } from 'lucide-react'; // Importez directement les icônes nécessaires
import { BK_NEW_PASSWORD_URL,FR_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
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();
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 request = new Request(
`${BK_NEW_PASSWORD_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify({
email: formData.get('email')
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('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 => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(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={ `${FR_USERS_LOGIN_URL}`} />
</div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={popupConfirmAction}
onCancel={() => setPopupVisible(false)}
/>
</>
}
}

View File

@ -0,0 +1,144 @@
'use client'
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
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';
import { BK_RESET_PASSWORD_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import useCsrfToken from '@/hooks/useCsrfToken';
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(true);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
setTimeout(() => {
setIsLoading(false);
}, 1000);
} else {
const url= `${BK_RESET_PASSWORD_URL}/${uuid}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('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 => {
console.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 request = new Request(
`${BK_RESET_PASSWORD_URL}/${uuid}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
password1: formData.get('password1'),
password2: formData.get('password2'),
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('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 => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
}
if (isLoading === true) {
return <Loader /> // Affichez le composant Loader
} else {
return <>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FR_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" />
<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={`${FR_USERS_LOGIN_URL}`} />
</div>
</div>
</>
}
}

View File

@ -0,0 +1,180 @@
'use client'
// src/app/pages/subscribe.js
import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import Logo from '@/components/Logo';
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 { BK_REGISTER_URL, FR_USERS_LOGIN_URL } from '@/utils/Url';
import useCsrfToken from '@/hooks/useCsrfToken';
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 [password1FieldError,setPassword1FieldError] = useState("")
const [password2FieldError,setPassword2FieldError] = useState("")
const [isLoading, setIsLoading] = useState(true);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
setIsLoading(false);
} else {
const url= `${BK_REGISTER_URL}`;
fetch(url, {
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json())
.then(data => {
console.log('Success:', data);
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
setIsLoading(true);
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
if(data.errorMessage){
setErrorMessage(data.errorMessage)
}
setIsLoading(false);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
}, []);
function isOK(data) {
return data.errorMessage === ""
}
function suscribe(formData) {
if (useFakeData) {
// Simuler une réponse réussie
const data = {
errorFields: {},
errorMessage: ""
};
setUserFieldError("")
setPassword1FieldError("")
setPassword2FieldError("")
setErrorMessage("")
if(isOK(data)){
setPopupMessage("Votre compte a été créé avec succès");
setPopupVisible(true);
} else {
if(data.errorMessage){
setErrorMessage(data.errorMessage);
}
if(data.errorFields){
setUserFieldError(data.errorFields.email)
setPassword1FieldError(data.errorFields.password1)
setPassword2FieldError(data.errorFields.password2)
}
}
} else {
const request = new Request(
`${BK_REGISTER_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
email: formData.get('login'),
password1: formData.get('password1'),
password2: formData.get('password2'),
}),
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('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 => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(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(); suscribe(new FormData(e.target)); }}>
<DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full" />
<InputTextIcon name="password1" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={password1FieldError} className="w-full" />
<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(`${FR_USERS_LOGIN_URL}`)}} /></div>
</div>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => {
setPopupVisible(false);
router.push(`${FR_USERS_LOGIN_URL}`);
}}
onCancel={() => setPopupVisible(false)}
/>
</>
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

@ -0,0 +1,42 @@
<svg width="565" height="609" viewBox="0 0 565 609" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M170.999 374.501C167.799 383.301 172.332 402.834 174.999 411.501C189.998 452.002 218.216 463.317 223 464C237 466 238.5 464 246.5 459.5C248.1 451.9 238.333 445 234 443C224 439.8 213.833 431 210 427C246.4 422.6 255.833 416.167 256 413.5C246.8 395.9 213.5 396.834 198 399.501C181.2 372.301 172.999 371.501 170.999 374.501Z" fill="#003625"/>
<path d="M166 84.5L208 94.5L206 112.5C203 121 196.9 138.5 196.5 140.5C196.1 142.5 174 145.333 163 146.5L112 166.5C96.3333 177.833 64.7 200.8 63.5 202C62.3 203.2 42.3333 237.5 32.5 254.5L23 320L21 389.5C29.8333 416.333 47.7 470.6 48.5 473C49.5 476 66.9995 505.5 65.9995 505.5C65.1995 505.5 92.6662 529.5 106.5 541.5C116.666 549.667 137.4 566.2 139 567C140.6 567.8 156.333 574.667 164 578L221 584.5H247.5L291.5 572.5L341 546L388 490.5L397 475L411.5 447.5L414.5 425L406.5 400L386.5 377.5L366 371L367.5 381L375 402L377 419L367.5 440.5L360.5 456.5L337 475C327.166 480.167 307.5 489.9 307.5 487.5C307.5 484.5 269 494 264 494.5C259 495 231 493 227.5 490.5C224.7 488.5 183 471.667 162.5 463.5L104 282L199 314C219.5 315.5 261.2 318.5 264 318.5C266.8 318.5 275.5 315.5 279.5 314L276 307.5L272.5 299L264 289.5C266.833 288.167 273.9 285.5 279.5 285.5C285.1 285.5 281.833 279.5 279.5 276.5L272.5 266.5L236 261L227.5 248.5V227L264 185L279.5 157.5L288.5 142.5L295 130L307.5 120.5L328 112.5L392.5 103.5L457.5 87.5H476L481 84.5V73L476 60.5L470.5 50C467 47.6667 459.5 42.4 457.5 40C455.5 37.6 449.666 33 447 31H435.5L425.5 19.5L416 16L406 11H392.5L377 16H366.5L314 19.5L264 25.5L199 55L166 84.5Z" fill="#10B981"/>
<path d="M215 304C195.4 294 147.167 248.5 125.5 227L119.5 230.5C116.5 235.333 110.5 245.3 110.5 246.5C110.5 247.7 109.833 257 109.5 261.5C112 266.167 117.5 276.4 119.5 280C122 284.5 131.5 284.5 142 290C152.5 295.5 171 297 173.5 298C176 299 192 311 194.5 313C196.5 314.6 200.667 313.667 202.5 313L215 304Z" fill="#059669"/>
<path d="M33.5 381.5C32 386.167 29 396.9 29 402.5V415V427.5C41.1667 449.167 65.9 493 67.5 495C69.5 497.5 84 521 87.5 524.5C91 528 105 542.5 107.5 546C109.5 548.8 124.667 560.833 132 566.5L184.5 575C281.7 595 343 545 361.5 517.5L359.5 511C275.1 571.8 199.333 562.333 172 550C178.4 550 184.333 546.333 186.5 544.5C83.3 509.3 41.5 421.167 33.5 381.5Z" fill="#059669"/>
<path d="M346 184C315.2 203.2 291.5 245.333 283.5 264C283.5 259.599 239.5 254.166 217.5 252C232.064 267.745 261.219 267.834 273.898 267.992C273.587 267.653 273.741 267.591 274.5 268C274.304 267.997 274.103 267.995 273.898 267.992C274.473 268.621 276.642 270.201 279.5 271.5C283.9 273.5 279.5 278 274.5 276C272.1 276 265.5 279.333 262.5 281H258C250 283.8 229 272.167 219.5 266C167.1 228.8 137.333 222.5 129 224L145.5 214C149.5 210.4 184.5 230.5 201.5 241C205.9 203.4 251 177.333 273 169C251.4 135 283 116.167 301.5 111C308.7 107.4 404.5 96.1665 451.5 90.9997C464.3 86.5997 465.5 92.833 464.5 96.4997C444.9 129.7 377.334 168.666 346 184Z" fill="white"/>
<path d="M299 33.9991C400.2 9.99913 455.167 48.3325 470 70.4991C471 68.3328 473.1 63.7 473.5 62.5C474 61 467 48.5 466 47C465 45.5 459 43 457.5 42C456 41 449.5 37.5 448 36C446.5 34.5 437 32.4987 436 32.4987C435 32.4987 430 28.4987 428.5 28.4987C427 28.4987 424 22.5 422.5 20.9987C421 19.4974 411.5 15 410.5 14C409.5 13 391.5 13 389 13C387 13 381.167 15.9991 378.5 17.4987C374.833 17.9987 366.9 18.6987 364.5 17.4987C361.5 15.9987 334 17.4987 331.5 17.4987C329 17.4987 300 20.9987 299 20.9987C298 20.9987 273 27.4987 270 28.4987C267.6 29.2987 249.667 31.4987 241 32.4987L188.5 65.9987V70.4991C223.3 46.4995 254 40.499 265 40.4987C250.2 48.8987 242.833 65.332 241 72.4987C255 53.2987 285.5 38.8323 299 33.9991Z" fill="white"/>
<path d="M273 295.001C268.2 296.601 269.667 300.667 271 302.501C259.001 296.9 259 286.168 260.5 281.502C273.5 276.502 274.5 275.501 275 272.502C275.4 270.102 257.833 270.502 249 271.002C239.8 271.802 224.5 259.335 218 253.002C224 253.002 245.833 256.001 256 257.5C264.8 257.5 278.333 260.833 284 262.5C303.2 218.9 334.667 191 348 182.5C447.6 126.099 466.833 98.9995 464 92.5C466 88.5 454.833 90.8333 449 92.5C379 113.7 318.167 118 296.5 117.5L318.5 107C338.899 107 428.333 82.3333 470.5 70C469.7 44.8 439.834 32.5 425.001 29.5C407.001 11.5 385.834 16 377.501 20.5C341.901 19.3 323.334 21 318.5 22C226.1 32.8 185.333 67.1667 176.5 83C206.1 80.2 220.167 85.8333 223.5 89C213.5 90.6 209.667 99.3333 209 103.5C206.2 108.3 201.5 129.5 199.5 139.5L188.501 145.5C96.9026 141.9 49.001 215.668 36.5 253.002C15.7 329.003 27.8333 401.334 36.5 428C74.9 542.799 158.167 574.5 195 576C276.6 586.8 339.333 541.167 360.5 517L358.5 510C389.7 488.4 403.167 450 406 433.5C410.799 391.9 382.666 377.833 368 376C397.999 417.999 367.834 457.833 349.001 472.5C289.226 524.9 200.428 504 163.5 487C43.5 421 68.1667 314.834 95.5 270.002C95.9 203.203 141 199.834 163.5 206.5C140.7 210.9 125 227 120 234.5C97.2 262.5 125.833 278.5 143 283.001C145 281.8 167.5 290.5 178.5 295.001C179.7 293.001 193 304.501 199.5 310.501L202 309.501C207.6 295.503 218.667 301.334 223.5 306L246 348.5C248.8 360.1 242.167 362 238.5 361.5C225.3 361.9 210.667 337.667 205 325.5L206.5 350C207.7 360 202.333 363.167 199.5 363.5C185.9 363.5 180.833 340.167 180 328.5C182 313.7 173.167 311.667 168.5 312.5C156.1 322.899 153 357.5 153 373.5C157 445.899 206.667 474.333 231 479.5C311.8 500.7 354.334 451.333 365.501 424C369.101 377.2 332.334 364.167 313.5 363.5C329.899 351.1 348.667 350.667 356.001 352C422.401 360 433.001 425.667 430.001 457.5C409.201 575.899 293.667 607.833 238.5 609C96.1 601.4 31.1676 490.5 16.5015 436C-33.0985 282.8 46.1681 182.833 92.0015 152C124.001 130.4 159.668 126.333 173.501 127C185.501 127.8 188.501 122 188.501 119C192.901 106.2 165.668 105.667 151.501 107C134.701 109 134.501 98.1667 136.501 92.5C202.101 14.1 314.501 5.49998 362.501 11C404.501 -15 432.667 12.5 441.501 29.5C481.101 33.1 489.667 60 489.001 73C486.201 106.2 465.834 129.5 456.001 137C449.201 145 419.167 165.333 405.001 174.5L344.001 209L344.501 211C356.501 219 371.501 242.334 377.501 253.002C386.301 251.402 402.167 254.335 409.001 256.002C419.001 260.002 427.167 267.002 430.001 270.002V275.002C429.601 276.202 426.501 277.168 425.001 277.502C422.601 278.702 407.334 273.668 400.001 271.002H398.001V272.502L409.001 281.502C415.801 287.102 420.501 295.835 422.001 299.501C424.001 305.102 422.167 308.501 421.001 309.501C418.201 312.702 414.501 311.501 413.001 310.501L386.001 284.502L385.001 284.002L392.001 303.001C394.001 307.001 394.167 315.335 394.001 319.001C391.201 331.001 384.167 327.001 381.001 323.501L365.501 286.501L365.001 310.501C363.801 318.901 358.501 322.335 356.001 323.001C351.201 323.001 349.334 320.335 349.001 319.001C347.401 316.202 346.667 301.168 346.501 294.001C344.101 286.801 334.834 281.001 330.501 279.001C322.501 274.201 302.167 274.667 293.001 275.501C293.001 279.901 292.334 282.334 292.001 283.001L273 295.001Z" fill="#003625"/>
<path d="M255 296.5C239.8 285.3 233.667 289.5 232.5 293C226.1 301.8 235.167 312.667 240.5 317C251.3 327 272.667 334.167 282 336.5C295.6 337.3 296 327.833 294.5 323C290.1 316.2 266.333 302.5 255 296.5Z" fill="#003625"/>
<path d="M283.499 263.5C278.299 258.7 255.999 254.5 245.499 253C241.899 249.401 294.666 208.834 321.499 189L303.499 185.5C331.899 183.5 362.333 153.334 373.999 138.5C417.499 142 460.999 87.5004 462.999 90.5004C464.599 92.9004 464.333 95.1671 463.999 96.0004C448.799 127.2 382.666 165 351.499 180C321.499 194.8 293.666 241.834 283.499 263.5Z" fill="#EEEDE8"/>
<path d="M201.5 147L198.5 139C191.7 143.8 184.5 143.833 182 144.5C141.6 139.3 104.5 164.5 91 176.5C37.8 218.1 21.6667 289.833 21 320.5C36.6 234.1 88.8333 186.5 112 173.5C135.2 156.3 171.667 153 187 153.5C189.8 153.5 197.833 149.167 201.5 147Z" fill="white"/>
<path d="M331.248 39C354.848 39.4 364.414 59.5 366.248 69.5C364.081 68.8333 359.848 67.6 360.248 68C360.648 68.4 359.415 72.1667 358.748 74C352.748 89.2 336.582 93 329.248 93C311.248 92.2 303.081 77.3333 301.248 70C298.448 46 320.081 39.3333 331.248 39Z" fill="#003625"/>
<path d="M310.749 63.4997C316.749 48.2997 331.582 50.1664 338.249 52.9997C325.849 55.7994 323.748 58.4998 324.248 59.5C329.448 70.3 323.748 73.3333 320.249 73.5C311.849 73.5 310.415 66.8331 310.749 63.4997Z" fill="white"/>
<circle cx="226" cy="107" r="10" fill="#003625"/>
<circle cx="226" cy="107" r="10" fill="#003625"/>
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
<circle cx="209" cy="147" r="10" fill="#003625"/>
<circle cx="209" cy="147" r="10" fill="#003625"/>
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,41 @@
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
import "@/css/tailwind.css";
export const metadata = {
title: "N3WT-SCHOOL",
description: "Gestion de l'école",
icons: {
icon: [
{
url: '/favicon.svg',
type: 'image/svg+xml',
},
{
url: '/favicon.ico', // Fallback pour les anciens navigateurs
sizes: 'any',
},
],
},
};
export default async function RootLayout({ children, params: {locale}}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@ -0,0 +1,39 @@
import {
BK_LOGIN_URL,
FR_USERS_LOGIN_URL ,
FR_ADMIN_HOME_URL,
FR_ADMIN_STUDENT_URL,
FR_ADMIN_CLASSES_URL,
FR_ADMIN_GRADES_URL,
FR_ADMIN_PLANNING_URL,
FR_ADMIN_TEACHERS_URL,
FR_ADMIN_SETTINGS_URL
} from '@/utils/Url';
import {mockUser} from "@/data/mockUsersData";
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
/**
* Disconnects the user after confirming the action.
* If `NEXT_PUBLIC_USE_FAKE_DATA` environment variable is set to 'true', it will log a fake disconnect and redirect to the login URL.
* Otherwise, it will send a PUT request to the backend to update the user profile and then redirect to the login URL.
*
* @function
* @name disconnect
* @returns {void}
*/
export function disconnect () {
if (confirm("\nÊtes-vous sûr(e) de vouloir vous déconnecter ?")) {
if (useFakeData) {
console.log('Fake disconnect:', mockUser);
router.push(`${FR_USERS_LOGIN_URL}`);
} else {
console.log('Fake disconnect:', mockUser);
router.push(`${FR_USERS_LOGIN_URL}`);
}
}
};

View File

@ -0,0 +1,15 @@
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 '>
<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'existe pas ou plus.</p>
<Link className="text-gray-900 hover:underline" href="/">Retour Accueil</Link>
</div>
</div>
)
}

View File

@ -0,0 +1,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">
<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}>
{buttonText} <i className="icon profile-add"></i>
</a>
</div>
</div>
);
};
export default AlertMessage;

View File

@ -0,0 +1,29 @@
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 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>
);
};
export default AlertWithModal;

View File

@ -0,0 +1,37 @@
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}>
{letter}
</button>
);
const AlphabetLinks = ({filter, onLetterClick }) => {
const [currentLetter, setCurrentLetter] = useState(filter);
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)}}
/>
))}
</div>
</div>
);
};
export default AlphabetLinks;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { useRouter } from 'next/navigation';
const Button = ({ text, onClick, href, className, primary, icon }) => {
const router = useRouter();
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 ? primaryClass : secondaryClass} ${className}`;
const handleClick = (e) => {
if (href) {
router.push(href);
} else if (onClick) {
onClick(e);
}
};
return (
<button className={buttonClass} onClick={handleClick}>
{icon && <span className="mr-2">{icon}</span>}
{text}
</button>
);
};
export default Button;

View File

@ -0,0 +1,212 @@
import React, { useEffect, useState } from 'react';
import { usePlanning } from '@/context/PlanningContext';
import WeekView from '@/components/Calendar/WeekView';
import MonthView from '@/components/Calendar/MonthView';
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 { fr } from 'date-fns/locale';
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
const Calendar = ({ onDateClick, onEventClick }) => {
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 })
}));
const years = Array.from({ length: 10 }, (_, i) => ({
value: new Date().getFullYear() - 5 + i,
label: new Date().getFullYear() - 5 + i
}));
const handleMonthSelect = (monthIndex) => {
setCurrentDate(setMonth(currentDate, monthIndex));
setShowDatePicker(false);
};
const handleYearSelect = (year) => {
setCurrentDate(setYear(currentDate, year));
setShowDatePicker(false);
};
useEffect(() => {
// S'assurer que le filtrage est fait au niveau parent
const filtered = events.filter(event => !hiddenSchedules.includes(event.scheduleId));
setVisibleEvents(filtered);
console.log('Events filtrés:', filtered); // Debug
}, [events, hiddenSchedules]);
const navigateDate = (direction) => {
const getNewDate = () => {
switch (viewType) {
case 'week':
return direction === 'next'
? addWeeks(currentDate, 1)
: subWeeks(currentDate, 1);
case 'month':
return direction === 'next'
? addMonths(currentDate, 1)
: subMonths(currentDate, 1);
case 'year':
return direction === 'next'
? addYears(currentDate, 1)
: subYears(currentDate, 1);
default:
return currentDate;
}
};
setCurrentDate(getNewDate());
};
return (
<div className="flex-1 flex h-full flex-col">
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
{/* Navigation à gauche */}
<div className="flex items-center gap-4">
<button
onClick={() => setCurrentDate(new Date())}
className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
Aujourd'hui
</button>
<button onClick={() => navigateDate('prev')} className="p-2 hover:bg-gray-100 rounded-full">
<ChevronLeft className="w-5 h-5" />
</button>
{/* Menu déroulant pour le mois/année */}
<div className="relative">
<button
onClick={() => setShowDatePicker(!showDatePicker)}
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 })}
</h2>
<ChevronDown className="w-4 h-4" />
</button>
{/* Menu de sélection du mois/année */}
{showDatePicker && (
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
{viewType !== 'year' && (
<div className="p-2 border-b">
<div className="grid grid-cols-3 gap-1">
{months.map((month) => (
<button
key={month.value}
onClick={() => handleMonthSelect(month.value)}
className="p-2 text-sm hover:bg-gray-100 rounded-md"
>
{month.label}
</button>
))}
</div>
</div>
)}
<div className="p-2">
<div className="grid grid-cols-3 gap-1">
{years.map((year) => (
<button
key={year.value}
onClick={() => handleYearSelect(year.value)}
className="p-2 text-sm hover:bg-gray-100 rounded-md"
>
{year.label}
</button>
))}
</div>
</div>
</div>
)}
</div>
<button onClick={() => navigateDate('next')} className="p-2 hover:bg-gray-100 rounded-full">
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Numéro de semaine au centre */}
{viewType === 'week' && (
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
<span>Semaine</span>
<span className="px-2 py-1 bg-gray-100 rounded-md">
{getWeek(currentDate, { weekStartsOn: 1 })}
</span>
</div>
)}
{/* Contrôles à droite */}
<div className="flex items-center gap-4">
<ToggleView viewType={viewType} setViewType={setViewType} />
<button
onClick={onDateClick}
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
{/* Contenu scrollable */}
<div className="flex-1 max-h-[calc(100vh-192px)] overflow-hidden">
<AnimatePresence mode="wait">
{viewType === 'week' && (
<motion.div
key="week"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="h-full flex flex-col"
>
<WeekView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
</motion.div>
)}
{viewType === 'month' && (
<motion.div
key="month"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<MonthView onDateClick={onDateClick} onEventClick={onEventClick} events={visibleEvents} />
</motion.div>
)}
{viewType === 'year' && (
<motion.div
key="year"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<YearView onDateClick={onDateClick} events={visibleEvents} />
</motion.div>
)}
{viewType === 'planning' && (
<motion.div
key="planning"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
<PlanningView onEventClick={onEventClick} events={visibleEvents} />
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
export default Calendar;

View File

@ -0,0 +1,86 @@
import React from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format, startOfWeek, endOfWeek, eachDayOfInterval, startOfMonth, endOfMonth, isSameMonth, isToday } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getEventsForDate } from '@/utils/events';
const MonthView = ({ onDateClick, onEventClick }) => {
const { currentDate, setViewType, setCurrentDate, events } = usePlanning();
// Obtenir tous les jours du mois actuel
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const startDate = startOfWeek(monthStart, { locale: fr, weekStartsOn: 1 });
const endDate = endOfWeek(monthEnd, { locale: fr, weekStartsOn: 1 });
const days = eachDayOfInterval({ start: startDate, end: endDate });
const handleDayClick = (day) => {
setCurrentDate(day); // Met à jour la date courante
setViewType('week'); // Change la vue en mode semaine
};
const renderDay = (day) => {
const isCurrentMonth = isSameMonth(day, currentDate);
const dayEvents = getEventsForDate(day, events);
const isCurrentDay = isToday(day);
return (
<div
key={day.toString()}
className={`p-2 overflow-y-auto relative flex flex-col
${!isCurrentMonth ? 'bg-gray-100 text-gray-400' : ''}
${isCurrentDay ? 'bg-emerald-50' : ''}
hover:bg-gray-100 cursor-pointer border-b border-r`}
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
${isCurrentDay ? 'bg-emerald-500 text-white' : ''}
${!isCurrentMonth ? 'text-gray-400' : ''}`}
>
{format(day, 'd')}
</span>
</div>
<div className="space-y-1 flex-1">
{dayEvents.map((event, index) => (
<div
key={event.id}
className="text-xs p-1 rounded truncate cursor-pointer"
style={{
backgroundColor: `${event.color}15`,
color: event.color,
borderLeft: `2px solid ${event.color}`
}}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
{event.title}
</div>
))}
</div>
</div>
);
};
return (
<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">
{day}
</div>
))}
</div>
{/* Grille des jours */}
<div className="flex-1 grid grid-cols-7 grid-rows-[repeat(6,1fr)]">
{days.map((day) => renderDay(day))}
</div>
</div>
);
};
export default MonthView;

View File

@ -0,0 +1,111 @@
import React from 'react';
import { format, isSameDay, eachDayOfInterval } from 'date-fns';
import { fr } from 'date-fns/locale';
const PlanningView = ({ events, onEventClick }) => {
// Fonction pour diviser un événement en jours
const splitEventByDays = (event) => {
const start = new Date(event.start);
const end = new Date(event.end);
// Si même jour, retourner l'événement tel quel
if (isSameDay(start, end)) {
return [event];
}
// Sinon, créer une entrée pour chaque jour
const days = eachDayOfInterval({ start, end });
return days.map(day => ({
...event,
displayDate: day,
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));
return (
<div className="bg-white h-full overflow-auto">
<table className="w-full border-collapse">
<thead className="bg-gray-50 sticky top-0 z-10">
<tr>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Date
</th>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Horaires
</th>
<th className="py-3 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b">
Événement
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{flattenedEvents.map((event, index) => {
const start = new Date(event.displayDate || event.start);
const end = new Date(event.end);
const isMultiDay = event.isMultiDay;
return (
<tr
key={`${event.id}-${index}`}
className="hover:bg-gray-50 cursor-pointer"
onClick={() => onEventClick(event)}
>
<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>
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-900 whitespace-nowrap">
<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')}`}
</div>
</td>
<td className="py-3 px-4">
<div className="flex items-center">
<div>
<div className="text-sm font-medium text-gray-900">
{event.title}
</div>
{event.description && (
<div className="text-sm text-gray-500">
{event.description}
</div>
)}
{event.location && (
<div className="text-sm text-gray-500">
{event.location}
</div>
)}
</div>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default PlanningView;

View File

@ -0,0 +1,208 @@
import React, { useEffect, useState, useRef } from 'react';
import { usePlanning } from '@/context/PlanningContext';
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';
const WeekView = ({ onDateClick, onEventClick, events }) => {
const { currentDate } = usePlanning();
const [currentTime, setCurrentTime] = useState(new Date());
const scrollContainerRef = useRef(null); // Ajouter cette référence
// Déplacer ces déclarations avant leur utilisation
const timeSlots = Array.from({ length: 24 }, (_, i) => i);
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
// Maintenant on peut utiliser weekDays
const isCurrentWeek = weekDays.some(day => isSameDay(day, new Date()));
// Mettre à jour la position de la ligne toutes les minutes
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(new Date());
}, 60000);
return () => clearInterval(interval);
}, []);
// Modifier l'useEffect pour l'auto-scroll
useEffect(() => {
if (scrollContainerRef.current && isCurrentWeek) {
const currentHour = new Date().getHours();
const scrollPosition = currentHour * 80;
// Ajout d'un délai pour laisser le temps au DOM de se mettre à jour
setTimeout(() => {
scrollContainerRef.current.scrollTop = scrollPosition - 200;
}, 0);
}
}, [currentDate, isCurrentWeek]); // Ajout de currentDate dans les dépendances
// Calculer la position de la ligne de temps
const getCurrentTimePosition = () => {
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
return `${(hours + minutes / 60) * 5}rem`;
};
// Utiliser les événements déjà filtrés passés en props
const weekEventsMap = weekDays.reduce((acc, day) => {
acc[format(day, 'yyyy-MM-dd')] = getWeekEvents(day, events);
return acc;
}, {});
const isWeekend = (date) => {
const day = date.getDay();
return day === 0 || day === 6;
};
const findOverlappingEvents = (event, dayEvents) => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
return dayEvents.filter(otherEvent => {
if (otherEvent.id === event.id) return false;
const otherStart = new Date(otherEvent.start);
const otherEnd = new Date(otherEvent.end);
return !(otherEnd <= eventStart || otherStart >= eventEnd);
});
};
const calculateEventStyle = (event, dayEvents) => {
const start = new Date(event.start);
const end = new Date(event.end);
const startMinutes = (start.getMinutes() / 60) * 5;
const duration = ((end - start) / (1000 * 60 * 60)) * 5;
// Trouver les événements qui se chevauchent
const overlappingEvents = findOverlappingEvents(event, dayEvents);
const eventIndex = overlappingEvents.findIndex(e => e.id > event.id) + 1;
const totalOverlapping = overlappingEvents.length + 1;
// Calculer la largeur et la position horizontale
const width = `calc((100% / ${totalOverlapping}) - 4px)`;
const left = `calc((100% / ${totalOverlapping}) * ${eventIndex})`;
return {
height: `${duration}rem`,
position: 'absolute',
width,
left,
backgroundColor: `${event.color}15`,
borderLeft: `3px solid ${event.color}`,
borderRadius: '0.25rem',
zIndex: 1,
transform: `translateY(${startMinutes}rem)`
};
};
const renderEventInCell = (event, dayEvents) => {
const eventStyle = calculateEventStyle(event, dayEvents);
return (
<div
key={event.id}
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
style={eventStyle}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
<div className="p-1">
<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>
{event.location && (
<div className="text-xs truncate" style={{ color: event.color, opacity: 0.75 }}>
{event.location}
</div>
)}
</div>
</div>
);
};
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="bg-white h-14"></div>
{weekDays.map((day) => (
<div
key={day}
className={`p-2 text-center border-b
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100 border-x border-emerald-600' : ''}`}
>
<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' : ''}`}>
{format(day, 'd', { locale: fr })}
</div>
</div>
))}
</div>
{/* Grille horaire */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
{/* Ligne de temps actuelle */}
{isCurrentWeek && (
<div
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>
)}
<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`}
</div>
{weekDays.map((day) => {
const dayKey = format(day, 'yyyy-MM-dd');
const dayEvents = weekEventsMap[dayKey] || [];
return (
<div
key={`${hour}-${day}`}
className={`h-20 relative border-b border-gray-100
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
onClick={() => {
const date = new Date(day);
date.setHours(hour);
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>
</div>
);
})}
</React.Fragment>
))}
</div>
</div>
</div>
);
};
export default WeekView;

View File

@ -0,0 +1,52 @@
import React from 'react';
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import { getMonthEventCount } from '@/utils/events';
import { isSameMonth } from 'date-fns';
const MonthCard = ({ month, eventCount, onClick }) => (
<div
className={`bg-white p-4 rounded shadow hover:shadow-lg cursor-pointer
${isSameMonth(month, new Date()) ? 'ring-2 ring-emerald-500' : ''}`}
onClick={onClick}
>
<h3 className="font-medium text-center mb-2">
{format(month, 'MMMM', { locale: fr })}
</h3>
<div className="text-center text-sm">
<span className="inline-flex items-center justify-center bg-emerald-100 text-emerald-800 px-2 py-1 rounded-full">
{eventCount} événements
</span>
</div>
</div>
);
const YearView = ({ onDateClick }) => {
const { currentDate, events, setViewType, setCurrentDate } = usePlanning();
const months = Array.from(
{ length: 12 },
(_, i) => new Date(currentDate.getFullYear(), i, 1)
);
const handleMonthClick = (month) => {
setCurrentDate(month);
setViewType('month');
};
return (
<div className="grid grid-cols-4 gap-4 p-4">
{months.map(month => (
<MonthCard
key={month.getTime()}
month={month}
eventCount={getMonthEventCount(month, events)}
onClick={() => handleMonthClick(month)}
/>
))}
</div>
);
};
export default YearView;

View File

@ -0,0 +1,188 @@
import React, { useState } from 'react';
import Slider from '@/components/Slider'
const ClassForm = ({ classe, onSubmit, isNew, specialities, teachers }) => {
const [formData, setFormData] = useState({
nom_ambiance: classe.nom_ambiance || '',
tranche_age: classe.tranche_age || [3, 6],
nombre_eleves: classe.nombre_eleves || '',
langue_enseignement: classe.langue_enseignement || 'Français',
annee_scolaire: classe.annee_scolaire || '2024-2025',
specialites_ids: classe.specialites_ids || [],
enseignant_principal_id: classe.enseignant_principal_id || null,
});
const handleChange = (e) => {
const { name, value, type } = e.target;
const newValue = type === 'radio' ? parseInt(value) : value;
setFormData((prevState) => ({
...prevState,
[name]: newValue,
}));
};
const handleSliderChange = (value) => {
console.log('update value : ', value)
setFormData(prevFormData => ({
...prevFormData,
tranche_age: value
}));
};
const handleNumberChange = (e) => {
const { name, value } = e.target;
setFormData(prevFormData => ({
...prevFormData,
[name]: Number(value)
}));
};
const handleSubmit = () => {
onSubmit(formData, isNew);
};
const handleSpecialityChange = (id) => {
setFormData(prevFormData => {
const specialites_ids = prevFormData.specialites_ids.includes(id)
? prevFormData.specialites_ids.filter(specialityId => specialityId !== id)
: [...prevFormData.specialites_ids, id];
return { ...prevFormData, specialites_ids };
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Nom d'ambiance
</label>
<input
type="text"
placeholder="Nom de l'ambiance"
name="nom_ambiance"
value={formData.nom_ambiance}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Tranche d'âge
</label>
<Slider
min={3}
max={12}
value={formData.tranche_age}
onChange={handleSliderChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Nombre d'élèves
</label>
<input
type="number"
name="nombre_eleves"
value={formData.nombre_eleves}
onChange={handleNumberChange}
min="1"
max="40"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Langue d'enseignement
</label>
<select
name="langue_enseignement"
value={formData.langue_enseignement}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
>
<option value="Français">Français</option>
<option value="Anglais">Anglais</option>
<option value="Espagnol">Espagnol</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Année scolaire
</label>
<input
type="text"
name="annee_scolaire"
value={formData.annee_scolaire}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Spécialités
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{specialities.map(speciality => (
<div key={speciality.id} className="flex items-center">
<input
type="checkbox"
id={`speciality-${speciality.id}`}
name="specialites"
value={speciality.id}
checked={formData.specialites_ids.includes(speciality.id)}
onChange={() => handleSpecialityChange(speciality.id)}
className="h-4 w-4 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500"
/>
<label htmlFor={`speciality-${speciality.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
{speciality.nom}
<div
className="w-4 h-4 rounded-full ml-2"
style={{ backgroundColor: speciality.codeCouleur }}
title={speciality.codeCouleur}
></div>
</label>
</div>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Enseignant principal
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{teachers.map(teacher => (
<div key={teacher.id} className="flex items-center">
<input
type="radio"
id={`teacher-${teacher.id}`}
name="enseignant_principal_id"
value={teacher.id}
checked={formData.enseignant_principal_id === teacher.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={`speciality-${teacher.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
{teacher.nom} {teacher.prenom}
</label>
</div>
))}
</div>
</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.nom_ambiance || !formData.nombre_eleves)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(!formData.nom_ambiance || !formData.nombre_eleves)}
>
Soumettre
</button>
</div>
</form>
);
};
export default ClassForm;

View File

@ -0,0 +1,111 @@
import { School, Trash2, MoreVertical, Edit3, Plus, ZoomIn } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal';
import ClassForm from '@/components/ClassForm';
const ClassesSection = ({ classes, specialities, teachers, handleCreate, handleEdit, handleDelete }) => {
const [isOpen, setIsOpen] = useState(false);
const [editingClass, setEditingClass] = useState(null);
const openEditModal = (classe) => {
setIsOpen(true);
setEditingClass(classe);
}
const closeEditModal = () => {
setIsOpen(false);
setEditingClass(null);
};
const handleModalSubmit = (updatedData) => {
if (editingClass) {
handleEdit(editingClass.id, updatedData);
} else {
handleCreate(updatedData);
}
closeEditModal();
};
const handleInspect = (data) => {
console.log('inspect classe : ', data)
}
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-8xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<School className="w-8 h-8 mr-2" />
Classes
</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div>
<div className="bg-white rounded-lg border border-gray-200 max-w-8xl ml-0">
<Table
columns={[
{ name: 'AMBIANCE', transform: (row) => row.nom_ambiance },
{ name: 'AGE', transform: (row) => `${row.tranche_age[0]} - ${row.tranche_age[1]} ans` },
{ name: 'NOMBRE D\'ELEVES', transform: (row) => row.nombre_eleves },
{ name: 'LANGUE D\'ENSEIGNEMENT', transform: (row) => row.langue_enseignement },
{ name: 'ANNEE SCOLAIRE', transform: (row) => row.annee_scolaire },
{
name: 'SPECIALITES',
transform: (row) => (
<div key={row.id} className="flex justify-center items-center space-x-2">
{row.specialites.map(specialite => (
<span
key={specialite.id}
className="w-4 h-4 rounded-full"
style={{ backgroundColor: specialite.codeCouleur }}
title={specialite.nom}
></span>
))}
</div>
)
},
{
name: 'ENSEIGNANT PRINCIPAL',
transform: (row) => {
return row.enseignant_principal
? `${row.enseignant_principal.nom || ''} ${row.enseignant_principal.prenom || ''}`
: <i>Non assigné</i>;
}
},
{ name: 'ACTIONS', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
{ label: 'Inspecter', icon: ZoomIn, onClick: () => handleInspect(row) },
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
]
}
buttonClassName="text-gray-400 hover:text-gray-600"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
/>
)}
]}
data={classes}
/>
</div>
{isOpen && (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
title={editingClass ? "Modification de la classe" : "Création d'une nouvelle classe"} ContentComponent={() => (
<ClassForm classe={editingClass || {}} onSubmit={handleModalSubmit} isNew={!editingClass} specialities={specialities} teachers={teachers} />
)}
/>
)}
</div>
);
};
export default ClassesSection;

View File

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

View File

@ -0,0 +1,52 @@
// Composant générique pour les menus dropdown
import { useRouter } from 'next/navigation';
import React, { useState, useEffect, useRef } from 'react';
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 actualDropdownOpen = isControlled ? propDropdownOpen : dropdownOpen;
const actualSetDropdownOpen = isControlled ? propSetDropdownOpen : setDropdownOpen;
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
actualSetDropdownOpen(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={menuRef}>
<button className={buttonClassName} onClick={() => actualSetDropdownOpen(!actualDropdownOpen)}>
{buttonContent}
</button>
{actualDropdownOpen && (
<div className={menuClassName}>
{items.map((item, index) => (
<button
key={index}
className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
onClick={() => {
item.onClick();
actualSetDropdownOpen(false);
}}
>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="flex items-center justify-center">{item.label}</span>
</button>
))}
</div>
)}
</div>
);
};
export default DropdownMenu;

View File

@ -0,0 +1,320 @@
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
export default function EventModal({ isOpen, onClose, eventData, setEventData }) {
const { addEvent, updateEvent, deleteEvent, schedules } = usePlanning();
if (!isOpen) return null;
const recurrenceOptions = [
{ value: 'none', label: 'Aucune' },
{ value: 'daily', label: 'Quotidienne' },
{ value: 'weekly', label: 'Hebdomadaire' },
{ value: 'monthly', label: 'Mensuelle' },
{ value: 'custom', label: 'Personnalisée' } // Nouvelle option
];
const daysOfWeek = [
{ value: 1, label: 'Lun' },
{ value: 2, label: 'Mar' },
{ value: 3, label: 'Mer' },
{ value: 4, label: 'Jeu' },
{ value: 5, label: 'Ven' },
{ value: 6, label: 'Sam' },
{ value: 0, label: 'Dim' }
];
// S'assurer que scheduleId est défini lors du premier rendu
React.useEffect(() => {
if (!eventData.scheduleId && schedules.length > 0) {
setEventData(prev => ({
...prev,
scheduleId: schedules[0].id,
color: schedules[0].color
}));
}
}, [schedules, eventData.scheduleId]);
const handleSubmit = (e) => {
e.preventDefault();
if (!eventData.scheduleId) {
alert('Veuillez sélectionner un planning');
return;
}
const selectedSchedule = schedules.find(s => s.id === eventData.scheduleId);
if (eventData.id) {
updateEvent(eventData.id, {
...eventData,
scheduleId: eventData.scheduleId, // S'assurer que scheduleId est bien défini
color: eventData.color || selectedSchedule?.color
});
} else {
addEvent({
...eventData,
id: `event-${Date.now()}`,
scheduleId: eventData.scheduleId, // S'assurer que scheduleId est bien défini
color: eventData.color || selectedSchedule?.color
});
}
onClose();
};
const handleDelete = () => {
if (eventData.id && confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
deleteEvent(eventData.id);
onClose();
}
};
return (
<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'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Titre */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Titre
</label>
<input
type="text"
value={eventData.title || ''}
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
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={eventData.description || ''}
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"
/>
</div>
{/* Planning */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Planning
</label>
<select
value={eventData.scheduleId || schedules[0]?.id}
onChange={(e) => {
const selectedSchedule = schedules.find(s => s.id === e.target.value);
setEventData({
...eventData,
scheduleId: e.target.value,
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 => (
<option key={schedule.id} value={schedule.id}>
{schedule.name}
</option>
))}
</select>
</div>
{/* Couleur */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Couleur
</label>
<input
type="color"
value={eventData.color || schedules.find(s => s.id === eventData.scheduleId)?.color || '#10b981'}
onChange={(e) => setEventData({ ...eventData, color: e.target.value })}
className="w-full h-10 p-1 rounded border"
/>
</div>
{/* Récurrence */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Récurrence
</label>
<select
value={eventData.recurrence || 'none'}
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 => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{/* Paramètres de récurrence personnalisée */}
{eventData.recurrence === 'custom' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Répéter tous les
</label>
<div className="flex gap-2">
<input
type="number"
min="1"
value={eventData.customInterval || 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
})}
className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
<option value="days">Jours</option>
<option value="weeks">Semaines</option>
<option value="months">Mois</option>
</select>
</div>
</div>
</div>
)}
{/* Jours de la semaine (pour récurrence hebdomadaire) */}
{eventData.recurrence === 'weekly' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Jours de répétition
</label>
<div className="flex gap-2 flex-wrap">
{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, day.value];
setEventData({ ...eventData, selectedDays: newDays });
}}
className={`px-3 py-1 rounded-full text-sm ${
(eventData.selectedDays || []).includes(day.value)
? 'bg-emerald-100 text-emerald-800'
: 'bg-gray-100 text-gray-600'
}`}
>
{day.label}
</button>
))}
</div>
</div>
)}
{/* Date de fin de récurrence */}
{eventData.recurrence !== 'none' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin de récurrence
</label>
<input
type="date"
value={eventData.recurrenceEnd || ''}
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>
)}
{/* Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Début
</label>
<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() })}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fin
</label>
<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() })}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
</div>
{/* Lieu */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lieu
</label>
<input
type="text"
value={eventData.location || ''}
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>
{/* Boutons */}
<div className="flex justify-between gap-2 mt-6">
<div>
{eventData.id && (
<button
type="button"
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded"
>
Supprimer
</button>
)}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Annuler
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
{eventData.id ? 'Modifier' : 'Créer'}
</button>
</div>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react';
import { isValidPhoneNumber } from 'react-phone-number-input';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className }) {
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleChange = (e) => {
const newValue = e.target.value;
onChange(newValue);
};
return (
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<div className={`flex items-center border-2 border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500 h-8`}>
<input
type="tel"
name={name}
ref={inputRef}
className="flex-1 pl-2 block w-full sm:text-sm focus:ring-0 h-full rounded-md border-none outline-none"
value={typeof value === 'string' ? value : ''}
onChange={handleChange}
placeholder={placeholder}
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
)
}

View File

@ -0,0 +1,21 @@
export default function InputText({name, type, label, value, onChange, errorMsg, placeholder,className}) {
return (
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</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"
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
)
}

View File

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

View File

@ -0,0 +1,375 @@
import { useState, useEffect } from 'react';
import {BK_GESTIONINSCRIPTION_ELEVES_URL,
BK_PROFILE_URL,
BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken';
import { useSearchParams, redirect, useRouter } from 'next/navigation'
const InscriptionForm = () => {
const [step, setStep] = useState(1);
const [eleveNom, setEleveNom] = useState('');
const [elevePrenom, setElevePrenom] = useState('');
const [responsableEmail, setResponsableEmail] = useState('');
const [responsableType, setResponsableType] = useState('new');
const [selectedEleve, setSelectedEleve] = useState(null);
const [existingResponsables, setExistingResponsables] = useState([]);
const [allEleves, setAllEleves] = useState([]);
const [selectedResponsables, setSelectedResponsables] = useState([]);
const csrfToken = useCsrfToken();
const router = useRouter();
useEffect(() => {
const request = new Request(
`${BK_GESTIONINSCRIPTION_ELEVES_URL}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
fetch(request).then(response => response.json())
.then(data => {
console.log('Success:', data);
setAllEleves(data);
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.message;
console.log(error);
});
}, []);
const nextStep = () => {
if (step < 3) {
setStep(step + 1);
}
};
const prevStep = () => {
if (step > 1) {
setStep(step - 1);
}
};
const handleEleveSelection = (eleve) => {
setSelectedEleve(eleve);
setExistingResponsables(eleve.responsables);
};
const handleResponsableSelection = (id) => {
setSelectedResponsables((prevSelectedResponsables) => {
const newSelectedResponsables = new Set(prevSelectedResponsables);
if (newSelectedResponsables.has(id)) {
newSelectedResponsables.delete(id);
} else {
newSelectedResponsables.add(id);
}
return Array.from(newSelectedResponsables);
});
};
const resetResponsableEmail = () => {
setResponsableEmail('');
};
const submit = function(){
if (selectedResponsables.length !== 0) {
const selectedResponsablesIds = selectedResponsables.map(responsableId => responsableId)
const data = {
eleve: {
nom: eleveNom,
prenom: elevePrenom,
},
idResponsables: selectedResponsablesIds
};
console.log(data);
const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`;
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data),
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
// Ajouter vérifications sur le retour de la commande (saisies incorrecte, ...)
window.location.reload()
})
.catch((error) => {
console.error('Error:', error);
});
}
else {
// Création d'un profil associé à l'adresse mail du responsable saisie
// Le profil est inactif
const request = new Request(
`${BK_PROFILE_URL}`,
{
method:'POST',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include',
body: JSON.stringify( {
email: responsableEmail,
password: 'Provisoire01!',
username: responsableEmail,
is_active: 0, // On rend le profil inactif : impossible de s'y connecter dans la fenêtre du login tant qu'il ne s'est pas inscrit
droit:1
}),
}
);
fetch(request).then(response => response.json())
.then(response => {
console.log('Success:', response);
if (response.id) {
let idProfil = response.id;
const data = {
eleve: {
nom: eleveNom,
prenom: elevePrenom,
responsables: [
{
mail: responsableEmail,
//telephone: telephoneResponsable,
profilAssocie: idProfil // Association entre le reponsable de l'élève et le profil créé par défaut précédemment
}
],
freres: []
}
};
const url = `${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}`;
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data),
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
// Ajouter vérifications sur le retour de la commande (saisies incorrecte, ...)
window.location.reload()
})
.catch((error) => {
console.error('Error:', error);
});
}
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
}
}
return (
<div className="p-6 w-full max-w-4xl bg-white rounded-xl shadow-md space-y-4">
{step === 1 && (
<div>
<h2 className="text-2xl font-bold mb-4">Nouvel élève</h2>
<input
type="text"
placeholder="Nom de l'élève"
value={eleveNom}
onChange={(e) => setEleveNom(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
<input
type="text"
placeholder="Prénom de l'élève"
value={elevePrenom}
onChange={(e) => setElevePrenom(e.target.value)}
className="mt-4 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
<div className="flex justify-between mt-4">
{step > 1 && (
<button
onClick={prevStep}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
>
Précédent
</button>
)}
</div>
</div>
)}
{step === 2 && (
<div className="mt-6">
<h2 className="text-2xl font-bold mb-4">Responsable(s)</h2>
<div className="flex flex-col space-y-4">
<label className="flex items-center space-x-3">
<input
type="radio"
name="responsableType"
value="new"
checked={responsableType === 'new'}
onChange={() => setResponsableType('new')}
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"
/>
<span className="text-gray-900">Nouveau Responsable</span>
</label>
<label className="flex items-center space-x-3">
<input
type="radio"
name="responsableType"
value="existing"
checked={responsableType === 'existing'}
onChange={() => {
setResponsableType('existing');
resetResponsableEmail();
}}
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"
/>
<span className="text-gray-900">Responsable Existant</span>
</label>
</div>
{responsableType === 'new' && (
<div className="mt-4">
<input
type="email"
placeholder="Email du responsable"
value={responsableEmail}
onChange={(e) => setResponsableEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
)}
{responsableType === 'existing' && (
<div className="mt-4">
<div className="mt-4" style={{ maxHeight: '300px', overflowY: 'auto' }}>
<table className="min-w-full bg-white border">
<thead>
<tr>
<th className="px-4 py-2 border">Nom</th>
<th className="px-4 py-2 border">Prénom</th>
</tr>
</thead>
<tbody>
{allEleves.map((eleve, index) => (
<tr
key={eleve.id}
className={`cursor-pointer ${selectedEleve && selectedEleve.id === eleve.id ? 'bg-emerald-600 text-white' : index % 2 === 0 ? 'bg-emerald-100' : ''}`}
onClick={() => handleEleveSelection(eleve)}
>
<td className="px-4 py-2 border">{eleve.nom}</td>
<td className="px-4 py-2 border">{eleve.prenom}</td>
</tr>
))}
</tbody>
</table>
</div>
{selectedEleve && (
<div className="mt-4">
<h3 className="font-bold">Responsables associés à {selectedEleve.nom} {selectedEleve.prenom} :</h3>
{existingResponsables.map((responsable) => (
<div key={responsable.id}>
<label className="flex items-center space-x-3 mt-2">
<input
type="checkbox"
checked={selectedResponsables.includes(responsable.id)}
className="form-checkbox h-5 w-5 text-emerald-600"
onChange={() => handleResponsableSelection(responsable.id)}
/>
<span className="text-gray-900">
{responsable.nom && responsable.prenom ? `${responsable.nom} ${responsable.prenom}` : `adresse mail : ${responsable.mail}`}
</span>
</label>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
{step === 3 && (
<div>
<h2 className="text-2xl font-bold mb-4">Récapitulatif</h2>
<div className="mb-4">
<h3 className="text-xl font-semibold">Élève</h3>
<p>Nom : {eleveNom}</p>
<p>Prénom : {elevePrenom}</p>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold">Responsable(s)</h3>
{responsableType === 'new' && (
<p>Email du nouveau responsable : {responsableEmail}</p>
)}
{responsableType === 'existing' && selectedEleve && (
<div>
<h4 className="font-bold">Responsables associés à {selectedEleve.nom} {selectedEleve.prenom} :</h4>
<ul className="list-disc ml-6">
{existingResponsables.filter(responsable => selectedResponsables.includes(responsable.id)).map((responsable) => (
<li key={responsable.id}>
{responsable.nom && responsable.prenom ? `${responsable.nom} ${responsable.prenom}` : responsable.mail}
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
<div className="flex justify-end mt-4 space-x-4">
{step > 1 && (
<button
onClick={prevStep}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md shadow-sm hover:bg-gray-400 focus:outline-none"
>
Précédent
</button>
)}
{step < 3 ? (
<button
onClick={nextStep}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(step === 1 && (!eleveNom || !elevePrenom)) ||
(step === 2 && (responsableType === 'new' && !responsableEmail || responsableType === 'existing' && (!selectedEleve || selectedResponsables.length === 0)))
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(step === 1 && (!eleveNom || !elevePrenom)) ||
(step === 2 && (responsableType === 'new' && !responsableEmail || responsableType === 'existing' && (!selectedEleve || selectedResponsables.length === 0)))} // Désactive le bouton "Suivant" selon les conditions spécifiées
>
Suivant
</button>
) : (
<>
<DjangoCSRFToken csrfToken={csrfToken} />
<button
onClick={submit}
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
>
Soumettre
</button>
</>
)}
</div>
</div>
);
}
export default InscriptionForm;

View File

@ -0,0 +1,181 @@
import React, { useState, useEffect } from 'react';
import InputText from '@/components/InputText';
import SelectChoice from '@/components/SelectChoice';
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
import Loader from '@/components/Loader';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
const niveaux = [
{ 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'},
];
export default function InscriptionFormShared({
initialData,
csrfToken,
onSubmit,
cancelUrl,
isLoading = false
}) {
const [formData, setFormData] = useState(() => ({
id: initialData?.id || '',
nom: initialData?.nom || '',
prenom: initialData?.prenom || '',
adresse: initialData?.adresse || '',
dateNaissance: initialData?.dateNaissance || '',
lieuNaissance: initialData?.lieuNaissance || '',
codePostalNaissance: initialData?.codePostalNaissance || '',
nationalite: initialData?.nationalite || '',
medecinTraitant: initialData?.medecinTraitant || '',
niveau: initialData?.niveau || ''
}));
const [responsables, setReponsables] = useState(() =>
initialData?.responsables || []
);
// Mettre à jour les données quand initialData change
useEffect(() => {
if (initialData) {
setFormData({
id: initialData.id || '',
nom: initialData.nom || '',
prenom: initialData.prenom || '',
adresse: initialData.adresse || '',
dateNaissance: initialData.dateNaissance || '',
lieuNaissance: initialData.lieuNaissance || '',
codePostalNaissance: initialData.codePostalNaissance || '',
nationalite: initialData.nationalite || '',
medecinTraitant: initialData.medecinTraitant || '',
niveau: initialData.niveau || ''
});
setReponsables(initialData.responsables || []);
}
}, [initialData]);
const updateFormField = (field, value) => {
setFormData(prev => ({...prev, [field]: value}));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
eleve: {
...formData,
responsables
}
});
};
if (isLoading) return <Loader />;
return (
<div className="max-w-4xl mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken}/>
{/* Section Élève */}
<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'élève</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputText
name="nom"
label="Nom"
value={formData.nom}
onChange={(e) => updateFormField('nom', e.target.value)}
required
/>
<InputText
name="prenom"
label="Prénom"
value={formData.prenom}
onChange={(e) => updateFormField('prenom', e.target.value)}
required
/>
<InputText
name="nationalite"
label="Nationalité"
value={formData.nationalite}
onChange={(e) => updateFormField('nationalite', e.target.value)}
/>
<InputText
name="dateNaissance"
type="date"
label="Date de Naissance"
value={formData.dateNaissance}
onChange={(e) => updateFormField('dateNaissance', e.target.value)}
required
/>
<InputText
name="lieuNaissance"
label="Lieu de Naissance"
value={formData.lieuNaissance}
onChange={(e) => updateFormField('lieuNaissance', e.target.value)}
/>
<InputText
name="codePostalNaissance"
label="Code Postal de Naissance"
value={formData.codePostalNaissance}
onChange={(e) => updateFormField('codePostalNaissance', e.target.value)}
/>
<div className="md:col-span-2">
<InputText
name="adresse"
label="Adresse"
value={formData.adresse}
onChange={(e) => updateFormField('adresse', e.target.value)}
/>
</div>
<InputText
name="medecinTraitant"
label="Médecin Traitant"
value={formData.medecinTraitant}
onChange={(e) => updateFormField('medecinTraitant', e.target.value)}
/>
<SelectChoice
name="niveau"
label="Niveau"
selected={formData.niveau}
callback={(e) => updateFormField('niveau', e.target.value)}
choices={niveaux}
required
/>
</div>
</div>
{/* Section Responsables */}
<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
responsables={responsables}
onResponsablesChange={(id, field, value) => {
const updatedResponsables = responsables.map(resp =>
resp.id === id ? { ...resp, [field]: value } : resp
);
setReponsables(updatedResponsables);
}}
addResponsible={(e) => {
e.preventDefault();
setReponsables([...responsables, { id: Date.now() }]);
}}
deleteResponsable={(index) => {
const newArray = [...responsables];
newArray.splice(index, 1);
setReponsables(newArray);
}}
/>
</div>
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
<Button href={cancelUrl} text="Annuler" />
<Button type="submit" text="Valider" primary />
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,103 @@
import InputText from '@/components/InputText';
import InputPhone from '@/components/InputPhone';
import Button from '@/components/Button';
import React from 'react';
import { useTranslations } from 'next-intl';
import 'react-phone-number-input/style.css'
export default function ResponsableInputFields({responsables, onResponsablesChange, addResponsible, deleteResponsable}) {
const t = useTranslations('ResponsableInputFields');
return (
<div className="space-y-8">
{responsables.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>
{responsables.length > 1 && (
<Button
text={t('delete')}
onClick={() => deleteResponsable(index)}
className="w-32"
/>
)}
</div>
<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.nom}
onChange={(event) => {onResponsablesChange(item.id, "nom", event.target.value)}}
/>
<InputText
name="prenomResponsable"
type="text"
label={t('firstname')}
value={item.prenom}
onChange={(event) => {onResponsablesChange(item.id, "prenom", event.target.value)}}
/>
</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.mail}
onChange={(event) => {onResponsablesChange(item.id, "mail", event.target.value)}}
/>
<InputPhone
name="telephoneResponsable"
label={t('phone')}
value={item.telephone}
onChange={(event) => {onResponsablesChange(item.id, "telephone", event)}}
/>
</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.dateNaissance}
onChange={(event) => {onResponsablesChange(item.id, "dateNaissance", event.target.value)}}
/>
<InputText
name="professionResponsable"
type="text"
label={t('profession')}
value={item.profession}
onChange={(event) => {onResponsablesChange(item.id, "profession", event.target.value)}}
/>
</div>
<div className='grid grid-cols-1 gap-4'>
<InputText
name="adresseResponsable"
type="text"
label={t('address')}
value={item.adresse}
onChange={(event) => {onResponsablesChange(item.id, "adresse", event.target.value)}}
/>
</div>
</div>
))}
<div className="flex justify-center">
<Button
text={t('add_responsible')}
onClick={(e) => addResponsible(e)}
primary
icon={<i className="icon profile-add" />}
type="button"
className="w-64"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
const Loader = () => {
return (
<div className="flex justify-center items-center h-screen">
<div className="w-9 h-9 border-4 border-t-4 border-t-emerald-500 border-gray-200 rounded-full animate-spin"></div>
</div>
);
};
export default Loader;

View File

@ -0,0 +1,13 @@
import React from 'react';
import Image from 'next/image';
import logoImage from '@/img/logo_min.svg'; // Assurez-vous que le chemin vers l'image du logo est correct
const Logo = ({ className }) => {
return (
<div className={className}>
<Image src={logoImage} alt="Logo" width={150} height={150} />
</div>
);
};
export default Logo;

View File

@ -0,0 +1,33 @@
import * as Dialog from '@radix-ui/react-dialog';
const Modal = ({ isOpen, setIsOpen, title, ContentComponent }) => {
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">
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full sm:p-6">
<Dialog.Title className="text-lg font-medium text-gray-900">
{title}
</Dialog.Title>
<div className="mt-2">
<ContentComponent />
</div>
<div className="mt-4 flex justify-end">
<Dialog.Close asChild>
<button
className="inline-flex justify-center px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
onClick={() => setIsOpen(false)}
>
Fermer
</button>
</Dialog.Close>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
};
export default Modal;

View File

@ -0,0 +1,47 @@
import React from 'react';
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="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)} />
)}
</div>
</div>
);
};
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}>
{number}
</button>
);
export default Pagination;

View File

@ -0,0 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel }) => {
if (!visible) return null;
return ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-md shadow-md">
<p className="mb-4">{message}</p>
<div className="flex justify-end gap-4">
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}>Confirmer</button>
</div>
</div>
</div>,
document.body
);
};
export default Popup;

View File

@ -0,0 +1,170 @@
import { useState } from 'react';
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 [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 handleEdit = (schedule) => {
setEditingId(schedule.id);
setEditedName(schedule.name);
setEditedColor(schedule.color);
};
const handleSave = () => {
if (editingId) {
updateSchedule(editingId, {
...schedules.find(s => s.id === editingId),
name: editedName,
color: editedColor
});
setEditingId(null);
}
};
const handleAddNew = () => {
if (newSchedule.name) {
addSchedule({
id: `schedule-${Date.now()}`,
...newSchedule
});
setIsAddingNew(false);
setNewSchedule({ name: '', color: '#10b981' });
}
};
return (
<nav className="w-64 border-r p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">Plannings</h2>
<button
onClick={() => setIsAddingNew(true)}
className="p-1 hover:bg-gray-100 rounded"
>
<Plus className="w-4 h-4" />
</button>
</div>
{isAddingNew && (
<div className="mb-4 p-2 border rounded">
<input
type="text"
value={newSchedule.name}
onChange={(e) => setNewSchedule(prev => ({ ...prev, name: e.target.value }))}
className="w-full p-1 mb-2 border rounded"
placeholder="Nom du planning"
/>
<div className="flex gap-2 items-center mb-2">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={newSchedule.color}
onChange={(e) => setNewSchedule(prev => ({ ...prev, color: e.target.value }))}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setIsAddingNew(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleAddNew}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
)}
<ul className="space-y-2">
{schedules.map(schedule => (
<li
key={schedule.id}
className={`p-2 rounded ${
selectedSchedule === schedule.id ? 'bg-gray-100' : 'hover:bg-gray-50'
}`}
>
{editingId === schedule.id ? (
<div className="space-y-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className="w-full p-1 border rounded"
/>
<div className="flex gap-2 items-center">
<label className="text-sm">Couleur:</label>
<input
type="color"
value={editedColor}
onChange={(e) => setEditedColor(e.target.value)}
className="w-8 h-8"
/>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setEditingId(null)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleSave}
className="p-1 hover:bg-gray-100 rounded"
>
<Check className="w-4 h-4" />
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => setSelectedSchedule(schedule.id)}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: schedule.color }}
/>
<span className={hiddenSchedules.includes(schedule.id) ? 'text-gray-400' : ''}>
{schedule.name}
</span>
</div>
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la propagation du clic
toggleScheduleVisibility(schedule.id);
}}
className="p-1 hover:bg-gray-100 rounded"
>
{hiddenSchedules.includes(schedule.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(schedule)}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@ -0,0 +1,20 @@
export default function SelectChoice({type, name, label, choices, callback, selected, error }) {
return (
<>
<div className="mb-4">
<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
<select
className={`mt-1 block w-full px-3 py-2 text-base border ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md`}
type={type}
id={name}
name={name}
value={selected}
onChange={callback}
>
{choices.map(({ value, label }, index) => <option key={value} value={value}>{label}</option>)}
</select>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</div>
</>
)
}

View File

@ -0,0 +1,55 @@
'use client'
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
<div
onClick={onClick}
className={`flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer ${
active ? 'bg-emerald-50 text-emerald-600' : 'text-gray-600 hover:bg-gray-50'
}`}
>
<Icon size={20} />
<span>{text}</span>
</div>
);
function Sidebar({ currentPage, items }) {
const router = useRouter();
const [selectedItem, setSelectedItem] = useState(currentPage);
useEffect(() => {
setSelectedItem(currentPage);
}, [currentPage]);
const handleItemClick = (url) => {
setSelectedItem(url);
router.push(url);
};
return <>
{/* Sidebar */}
<div className="w-64 bg-white border-r border-gray-200 py-6 px-4">
<div className="flex items-center mb-8 px-2">
<div className="text-xl font-semibold">Collège Saint-Joseph</div>
</div>
<nav className="space-y-1">
{
items.map((item) => (
<SidebarItem
key={item.id}
icon={item.icon}
text={item.name}
active={item.id === selectedItem}
url={item.url}
onClick={() => handleItemClick(item.url)}
/>
))
}
</nav>
</div>
</>
}
export default Sidebar;

View File

@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'tailwindcss/tailwind.css';
const Slider = ({ min, max, value, onChange }) => {
const handleMinChange = (event) => {
const newMin = Number(event.target.value);
if (newMin < value[1]) {
onChange([newMin, value[1]]);
} else {
onChange([newMin, newMin + 1]);
}
};
const handleMaxChange = (event) => {
const newMax = Number(event.target.value);
if (newMax > value[0]) {
onChange([value[0], newMax]);
} else {
onChange([newMax - 1, newMax]);
}
};
return (
<div className="space-y-4">
<div className="flex items-center space-x-2 w-1/2">
<span className="text-emerald-600">{value[0]}</span>
<input
type="range"
min={min}
max={max}
value={value[0]}
onChange={handleMinChange}
className="w-full h-2 bg-emerald-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-opacity-50"
/>
</div>
<div className="flex items-center space-x-2 w-1/2">
<span className="text-emerald-600">{value[1]}</span>
<input
type="range"
min={value[0] + 1}
max={max}
value={value[1]}
onChange={handleMaxChange}
className="w-full h-2 bg-emerald-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-opacity-50"
/>
</div>
</div>
);
};
Slider.propTypes = {
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
onChange: PropTypes.func.isRequired
};
export default Slider;

View File

@ -0,0 +1,87 @@
import { BookOpen, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal';
import SpecialityForm from '@/components/SpecialityForm';
const SpecialitiesSection = ({ specialities, handleCreate, handleEdit, handleDelete }) => {
const [isOpen, setIsOpen] = useState(false);
const [editingSpeciality, setEditingSpeciality] = useState(null);
const openEditModal = (speciality) => {
setIsOpen(true);
setEditingSpeciality(speciality);
}
const closeEditModal = () => {
setIsOpen(false);
setEditingSpeciality(null);
};
const handleModalSubmit = (updatedData) => {
if (editingSpeciality) {
handleEdit(editingSpeciality.id, updatedData);
} else {
handleCreate(updatedData);
}
closeEditModal();
};
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-4xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<BookOpen className="w-8 h-8 mr-2" />
Spécialités
</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div>
<div className="bg-white rounded-lg border border-gray-200 max-w-4xl ml-0">
<Table
columns={[
{ name: 'NOM', transform: (row) => row.nom.toUpperCase() },
{ name: 'CODE', transform: (row) => (
<div
className="w-4 h-4 rounded-full mx-auto"
style={{ backgroundColor: row.codeCouleur }}
title={row.codeCouleur}
></div>
)},
{ name: 'DATE DE CREATION', transform: (row) => row.dateCreation_formattee },
{ name: 'ACTIONS', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
]
}
buttonClassName="text-gray-400 hover:text-gray-600"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
/>
)}
]}
data={specialities}
/>
</div>
{isOpen && (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
title={editingSpeciality ? "Modification de la spécialité" : "Création d'une nouvelle spécialité"} ContentComponent={() => (
<SpecialityForm speciality={editingSpeciality || {}} onSubmit={handleModalSubmit} isNew={!editingSpeciality} />
)}
/>
)}
</div>
);
};
export default SpecialitiesSection;

View File

@ -0,0 +1,50 @@
import { useState } from 'react';
const SpecialityForm = ({ speciality = {}, onSubmit, isNew }) => {
const [nom, setNom] = useState(speciality.nom || '');
const [codeCouleur, setCodeCouleur] = useState(speciality.codeCouleur || '#FFFFFF');
const handleSubmit = () => {
const updatedData = {
nom,
codeCouleur,
};
onSubmit(updatedData, isNew);
};
return (
<div className="p-4">
<div>
<input
type="text"
placeholder="Nom de la spécialité"
value={nom}
onChange={(e) => setNom(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700">
Code couleur de la spécialité
</label>
<input
type="color"
value={codeCouleur}
onChange={(e) => setCodeCouleur(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 h-10 w-10 p-0 cursor-pointer"
style={{ appearance: 'none', borderRadius: '0' }}
/>
</div>
<div className="flex justify-end mt-4 space-x-4">
<button
onClick={handleSubmit}
className="px-4 py-2 bg-emerald-500 text-white rounded-md shadow-sm hover:bg-emerald-600 focus:outline-none"
>
Soumettre
</button>
</div>
</div>
);
};
export default SpecialityForm;

View File

@ -0,0 +1,59 @@
import { useState } from 'react';
import { ChevronUp } from 'lucide-react';
import DropdownMenu from './DropdownMenu';
const StatusLabel = ({ etat, onChange, showDropdown = true }) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const statusOptions = [
{ value: 1, label: 'Créé' },
{ value: 2, label: 'Envoyé' },
{ value: 3, label: 'En Validation' },
{ value: 4, label: 'A Relancer' },
{ value: 5, label: 'Validé' },
{ value: 6, label: 'Archivé' },
];
const currentStatus = statusOptions.find(option => option.value === etat);
return (
<>
{showDropdown ? (
<DropdownMenu
buttonContent={
<>
{currentStatus ? currentStatus.label : 'Statut inconnu'}
<ChevronUp size={16} className={`transform transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : 'rotate-90'}`} />
</>
}
items={statusOptions.map(option => ({
label: option.label,
onClick: () => onChange(option.value),
}))}
buttonClassName={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${
etat === 1 && 'bg-blue-50 text-blue-600' ||
etat === 2 && 'bg-orange-50 text-orange-600' ||
etat === 3 && 'bg-purple-50 text-purple-600' ||
etat === 4 && 'bg-red-50 text-red-600' ||
etat === 5 && 'bg-green-50 text-green-600' ||
etat === 6 && 'bg-red-50 text-red-600'
}`}
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10"
dropdownOpen={dropdownOpen}
setDropdownOpen={setDropdownOpen}
/>
) : (
<div className={`w-[150px] flex items-center justify-center gap-2 px-2 py-2 rounded-md text-sm text-center font-medium ${
etat === 1 && 'bg-blue-50 text-blue-600' ||
etat === 2 && 'bg-orange-50 text-orange-600' ||
etat === 3 && 'bg-purple-50 text-purple-600' ||
etat === 4 && 'bg-red-50 text-red-600' ||
etat === 5 && 'bg-green-50 text-green-600' ||
etat === 6 && 'bg-red-50 text-red-600'
}`}>
{currentStatus ? currentStatus.label : 'Statut inconnu'}
</div>
)}
</>
);
};
export default StatusLabel;

View File

@ -0,0 +1,10 @@
const Tab = ({ text, active, count, onClick}) => (
<button
onClick={onClick}
className={`pb-4 px-2 relative ${
active ? 'text-emerald-600 border-b-2 border-emerald-600' : 'text-gray-600'
}`}>
{text}
</button>
);
export default Tab;

View File

@ -0,0 +1,9 @@
import React from 'react';
const TabContent = ({ children, isActive }) => (
<div className={`${isActive ? 'block' : 'hidden'}`}>
{children}
</div>
);
export default TabContent;

View File

@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange }) => {
const handlePageChange = (newPage) => {
onPageChange(newPage);
};
return (
<div className="bg-white rounded-lg border border-gray-200">
<table className="min-w-full bg-white">
<thead>
<tr>
{columns.map((column, index) => (
<th key={index} className="py-2 px-4 border-b border-gray-200 bg-gray-100 text-center text-sm font-semibold text-gray-600">
{column.name}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex} className={` ${rowIndex % 2 === 0 ? 'bg-emerald-50' : ''}`}>
{columns.map((column, colIndex) => (
<td key={colIndex} className="py-2 px-4 border-b border-gray-200 text-center text-sm text-gray-700">
{renderCell ? renderCell(row, column.name) : column.transform(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
{itemsPerPage > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
)}
</div>
);
};
Table.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
transform: PropTypes.func.isRequired,
})).isRequired,
renderCell: PropTypes.func,
itemsPerPage: PropTypes.number,
currentPage: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
export default Table;

View File

@ -0,0 +1,111 @@
import React, { useState } from 'react';
const TeacherForm = ({ teacher, onSubmit, isNew, specialities }) => {
const [formData, setFormData] = useState({
nom: teacher.nom || '',
prenom: teacher.prenom || '',
mail: teacher.mail || '',
specialite_id: teacher.specialite_id || 1,
classes: teacher.classes || []
});
const handleChange = (e) => {
const { name, value, type } = e.target;
const newValue = type === 'radio' ? parseInt(value) : value;
setFormData((prevState) => ({
...prevState,
[name]: newValue,
}));
};
const handleSubmit = () => {
onSubmit(formData, isNew);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Nom
</label>
<input
type="text"
placeholder="Nom de l'enseignant"
name="nom"
value={formData.nom}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Prénom
</label>
<input
type="text"
placeholder="Prénom de l'enseignant"
name="prenom"
value={formData.prenom}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Adresse email
</label>
<input
type="text"
placeholder="email de l'enseignant"
name="mail"
value={formData.mail}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 italic"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Spécialités
</label>
<div className="mt-2 grid grid-cols-1 gap-4">
{specialities.map(speciality => (
<div key={speciality.id} className="flex items-center">
<input
type="radio"
id={`speciality-${speciality.id}`}
name="specialite_id"
value={speciality.id}
checked={formData.specialite_id === speciality.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={`speciality-${speciality.id}`} className="ml-2 block text-sm text-gray-900 flex items-center">
{speciality.nom}
<div
className="w-4 h-4 rounded-full ml-2"
style={{ backgroundColor: speciality.codeCouleur }}
title={speciality.codeCouleur}
></div>
</label>
</div>
))}
</div>
</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.nom || !formData.prenom || !formData.mail)
? "bg-gray-300 text-gray-700 cursor-not-allowed"
: "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
disabled={(!formData.nom || !formData.prenom || !formData.mail)}
>
Soumettre
</button>
</div>
</form>
);
};
export default TeacherForm;

View File

@ -0,0 +1,94 @@
import { School, Trash2, MoreVertical, Edit3, Plus } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import DropdownMenu from '@/components/DropdownMenu';
import Modal from '@/components/Modal';
import TeacherForm from '@/components/TeacherForm';
const TeachersSection = ({ teachers, handleCreate, handleEdit, handleDelete, specialities }) => {
const [isOpen, setIsOpen] = useState(false);
const [editingTeacher, setEditingTeacher] = useState(null);
const openEditModal = (teacher) => {
setIsOpen(true);
setEditingTeacher(teacher);
}
const closeEditModal = () => {
setIsOpen(false);
setEditingTeacher(null);
};
const handleModalSubmit = (updatedData) => {
if (editingTeacher) {
handleEdit(editingTeacher.id, updatedData);
} else {
handleCreate(updatedData);
}
closeEditModal();
};
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-4 max-w-7xl ml-0">
<h2 className="text-3xl text-gray-800 flex items-center">
<School className="w-8 h-8 mr-2" />
Enseignants
</h2>
<button
onClick={() => openEditModal(null)} // ouvrir le modal pour créer une nouvelle spécialité
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div>
<div className="bg-white rounded-lg border border-gray-200 max-w-7xl ml-0">
<Table
columns={[
{ name: 'NOM', transform: (row) => row.nom },
{ name: 'PRENOM', transform: (row) => row.prenom },
{ name: 'MAIL', transform: (row) => row.mail },
{ name: 'SPECIALITE',
transform: (row) => (
<div key={row.id} className="flex justify-center items-center space-x-2">
<span
key={row.specialite.id}
className="w-4 h-4 rounded-full"
style={{ backgroundColor: row.specialite.codeCouleur }}
title={row.specialite.nom}
></span>
</div>
)
},
{ name: 'ACTIONS', transform: (row) => (
<DropdownMenu
buttonContent={<MoreVertical size={20} className="text-gray-400 hover:text-gray-600" />}
items={[
{ label: 'Modifier', icon:Edit3, onClick: () => openEditModal(row) },
{ label: 'Supprimer', icon: Trash2, onClick: () => handleDelete(row.id) }
]
}
buttonClassName="text-gray-400 hover:text-gray-600"
menuClassName="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10 flex flex-col items-center"
/>
)}
// { name: 'SPECIALITE', transform: (row) => row.specialite_id },
// { name: 'CLASSES', transform: (row) => row.classe },
]}
data={teachers}
/>
</div>
{isOpen && (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
title={editingTeacher ? "Modification de l'enseignant" : "Création d'un nouvel enseignant"} ContentComponent={() => (
<TeacherForm teacher={editingTeacher || {}} onSubmit={handleModalSubmit} isNew={!editingTeacher} specialities={specialities} />
)}
/>
)}
</div>
);
};
export default TeachersSection;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { CalendarDays, Calendar, CalendarRange, List } from 'lucide-react';
const ToggleView = ({ viewType, setViewType }) => {
const views = [
{ type: 'week', label: 'Semaine', icon: CalendarDays },
{ type: 'month', label: 'Mois', icon: Calendar },
{ type: 'year', label: 'Année', icon: CalendarRange },
{ type: 'planning', label: 'Planning', icon: List }
];
return (
<div className="bg-gray-100 p-1 rounded-lg flex gap-1">
{views.map(({ type, label, icon: Icon }) => (
<button
key={type}
onClick={() => setViewType(type)}
className={`
flex items-center gap-2 px-3 py-1.5 rounded-md transition-all
${viewType === type
? 'bg-emerald-600 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-200'
}
`}
>
<Icon className="w-4 h-4" />
<span className="text-sm font-medium">{label}</span>
</button>
))}
</div>
);
};
export default ToggleView;

View File

@ -0,0 +1,93 @@
import { createContext, useContext, useState } from 'react';
import { mockEvents, mockSchedules } from '@/data/mockData';
/**
* Contexte de planification pour gérer l'état global du planning
* Fournit des fonctionnalités pour :
* - Gestion des événements (ajout, modification, suppression)
* - Gestion des emplois du temps (ajout, modification, suppression)
* - Gestion de la visibilité des emplois du temps
* - Contrôle de la vue du calendrier (date courante, type de vue)
* - Stockage de l'état des événements et des emplois du temps
*/
const PlanningContext = createContext();
export function PlanningProvider({ children }) {
const [events, setEvents] = useState(mockEvents);
const [schedules, setSchedules] = useState(mockSchedules);
const [selectedSchedule, setSelectedSchedule] = useState(mockSchedules[0].id);
const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
const [hiddenSchedules, setHiddenSchedules] = useState([]);
const addEvent = (newEvent) => {
setEvents((prevEvents) => [...prevEvents, newEvent]);
};
const updateEvent = (id, updatedEvent) => {
setEvents((prevEvents) =>
prevEvents.map((event) => (event.id === id ? updatedEvent : event))
);
};
const deleteEvent = (id) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
};
const addSchedule = (newSchedule) => {
setSchedules((prevSchedules) => [...prevSchedules, newSchedule]);
};
const updateSchedule = (id, updatedSchedule) => {
setSchedules((prevSchedules) =>
prevSchedules.map((schedule) =>
schedule.id === id ? updatedSchedule : schedule
)
);
};
const deleteSchedule = (id) => {
setSchedules((prevSchedules) =>
prevSchedules.filter((schedule) => schedule.id !== id)
);
};
const toggleScheduleVisibility = (scheduleId) => {
setHiddenSchedules((prev) => {
const isHidden = prev.includes(scheduleId);
const newHiddenSchedules = isHidden
? prev.filter((id) => id !== scheduleId)
: [...prev, scheduleId];
return newHiddenSchedules;
});
};
const value = {
events,
setEvents,
schedules,
setSchedules,
selectedSchedule,
setSelectedSchedule,
addEvent,
updateEvent,
deleteEvent,
addSchedule,
updateSchedule,
deleteSchedule,
currentDate,
setCurrentDate,
viewType,
setViewType,
hiddenSchedules,
toggleScheduleVisibility
};
return (
<PlanningContext.Provider value={value}>
{children}
</PlanningContext.Provider>
);
}
export const usePlanning = () => useContext(PlanningContext);

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,30 @@
export const mockSchedules = [
{ id: 'default', name: 'Planning principal', color: '#10b981' },
{ id: 'secondary', name: 'Planning secondaire', color: '#3b82f6' },
{ id: 'special', name: 'Événements spéciaux', color: '#ef4444' },
{ id: 'exam', name: 'Planning examens', color: '#f59e0b' }
];
export const mockEvents = [
{
id: 'event-1',
title: 'Cours de Mathématiques',
description: 'Cours de mathématiques avancées',
start: '2024-02-20T08:00:00',
end: '2024-02-20T10:00:00',
scheduleId: 'default',
location: 'Salle A101',
color: '#10b981'
},
{
id: 'event-2',
title: 'Examen Physique',
description: 'Examen final de physique',
start: '2024-02-21T14:00:00',
end: '2024-02-21T16:00:00',
scheduleId: 'exam',
location: 'Amphithéâtre B',
color: '#f59e0b'
}
];

View File

@ -0,0 +1,322 @@
export const mockFicheInscription = [
{
"eleve": {
"nom": "Dupont",
"prenom": "Jean",
"responsables":[ {
"mail": "responsable@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2023-10-01",
"etat": 1,
"fichiers": [
{ "url": "/path/to/file1", "nom": "file1.pdf" },
{ "url": "/path/to/file2", "nom": "file2.pdf" }
]
},
{
"eleve": {
"nom": "Martin",
"prenom": "Paul",
"responsables":[ {
"mail": "paul.martin@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2023-09-15",
"etat": 2,
"fichiers": [
{ "url": "/path/to/file3", "nom": "file3.pdf" },
{ "url": "/path/to/file4", "nom": "file4.pdf" }
]
},
{
"eleve": {
"nom": "Bernard",
"prenom": "Alice",
"responsables":[ {
"mail": "alice.bernard@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2023-08-20",
"etat": 0,
"fichiers": [
{ "url": "/path/to/file5", "nom": "file5.pdf" },
{ "url": "/path/to/file6", "nom": "file6.pdf" }
]
},
{
"eleve": {
"nom": "Lefevre",
"prenom": "Marie",
"responsables":[ {
"mail": "marie.lefevre@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2023-07-10",
"etat": 3,
"fichiers": [
{ "url": "/path/to/file7", "nom": "file7.pdf" },
{ "url": "/path/to/file8", "nom": "file8.pdf" }
]
},
{
"eleve": {
"nom": "Moreau",
"prenom": "Luc",
"responsables":[ {
"mail": "luc.moreau@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2023-06-05",
"etat": 1,
"fichiers": [
{ "url": "/path/to/file9", "nom": "file9.pdf" },
{ "url": "/path/to/file10", "nom": "file10.pdf" }
]
},
{
"eleve": {
"nom": "Simon",
"prenom": "Julie",
"responsables":[ {
"mail": "julie.simon@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2023-05-15",
"etat": 2,
"fichiers": [
{ "url": "/path/to/file11", "nom": "file11.pdf" },
{ "url": "/path/to/file12", "nom": "file12.pdf" }
]
},
{
"eleve": {
"nom": "Laurent",
"prenom": "Pierre",
"responsables":[ {
"mail": "pierre.laurent@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2023-04-10",
"etat": 0,
"fichiers": [
{ "url": "/path/to/file13", "nom": "file13.pdf" },
{ "url": "/path/to/file14", "nom": "file14.pdf" }
]
},
{
"eleve": {
"nom": "Roux",
"prenom": "Sophie",
"responsables":[ {
"mail": "sophie.roux@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2023-03-20",
"etat": 3,
"fichiers": [
{ "url": "/path/to/file15", "nom": "file15.pdf" },
{ "url": "/path/to/file16", "nom": "file16.pdf" }
]
},
{
"eleve": {
"nom": "David",
"prenom": "Antoine",
"responsables":[ {
"mail": "antoine.david@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2023-02-25",
"etat": 1,
"fichiers": [
{ "url": "/path/to/file17", "nom": "file17.pdf" },
{ "url": "/path/to/file18", "nom": "file18.pdf" }
]
},
{
"eleve": {
"nom": "Gauthier",
"prenom": "Emma",
"responsables":[ {
"mail": "emma.gauthier@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2023-01-15",
"etat": 2,
"fichiers": [
{ "url": "/path/to/file19", "nom": "file19.pdf" },
{ "url": "/path/to/file20", "nom": "file20.pdf" }
]
},
{
"eleve": {
"nom": "Garcia",
"prenom": "Lucas",
"responsables":[ {
"mail": "lucas.garcia@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2022-12-10",
"etat": 0,
"fichiers": [
{ "url": "/path/to/file21", "nom": "file21.pdf" },
{ "url": "/path/to/file22", "nom": "file22.pdf" }
]
},
{
"eleve": {
"nom": "Perrin",
"prenom": "Chloe",
"responsables":[ {
"mail": "chloe.perrin@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2022-11-05",
"etat": 3,
"fichiers": [
{ "url": "/path/to/file23", "nom": "file23.pdf" },
{ "url": "/path/to/file24", "nom": "file24.pdf" }
]
},
{
"eleve": {
"nom": "Robin",
"prenom": "Nathan",
"responsables":[ {
"mail": "nathan.robin@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2022-10-15",
"etat": 1,
"fichiers": [
{ "url": "/path/to/file25", "nom": "file25.pdf" },
{ "url": "/path/to/file26", "nom": "file26.pdf" }
]
},
{
"eleve": {
"nom": "Richard",
"prenom": "Lea",
"responsables":[ {
"mail": "lea.richard@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2022-09-10",
"etat": 2,
"fichiers": [
{ "url": "/path/to/file27", "nom": "file27.pdf" },
{ "url": "/path/to/file28", "nom": "file28.pdf" }
]
},
{
"eleve": {
"nom": "Petit",
"prenom": "Tom",
"responsables":[ {
"mail": "tom.petit@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2022-08-05",
"etat": 0,
"fichiers": [
{ "url": "/path/to/file29", "nom": "file29.pdf" },
{ "url": "/path/to/file30", "nom": "file30.pdf" }
]
},
{
"eleve": {
"nom": "Durand",
"prenom": "Sarah",
"responsables":[ {
"mail": "sarah.durand@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2022-07-10",
"etat": 3,
"fichiers": [
{ "url": "/path/to/file31", "nom": "file31.pdf" },
{ "url": "/path/to/file32", "nom": "file32.pdf" }
]
},
{
"eleve": {
"nom": "Lemoine",
"prenom": "Maxime",
"responsables":[ {
"mail": "maxime.lemoine@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2022-06-05",
"etat": 1,
"fichiers": [
{ "url": "/path/to/file33", "nom": "file33.pdf" },
{ "url": "/path/to/file34", "nom": "file34.pdf" }
]
},
{
"eleve": {
"nom": "Marchand",
"prenom": "Elodie",
"responsables":[ {
"mail": "elodie.marchand@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2022-05-15",
"etat": 2,
"fichiers": [
{ "url": "/path/to/file35", "nom": "file35.pdf" },
{ "url": "/path/to/file36", "nom": "file36.pdf" }
]
},
{
"eleve": {
"nom": "Blanc",
"prenom": "Louis",
"responsables":[ {
"mail": "louis.blanc@example.com",
"telephone": "0123456789"
}]
},
"dateMAJ": "2022-04-10",
"etat": 0,
"fichiers": [
{ "url": "/path/to/file37", "nom": "file37.pdf" },
{ "url": "/path/to/file38", "nom": "file38.pdf" }
]
},
{
"eleve": {
"nom": "Fontaine",
"prenom": "Camille",
"responsables":[ {
"mail": "camille.fontaine@example.com",
"telephone": "0987654321"
}]
},
"dateMAJ": "2022-03-20",
"etat": 3,
"fichiers": [
{ "url": "/path/to/file39", "nom": "file39.pdf" },
{ "url": "/path/to/file40", "nom": "file40.pdf" }
]
}
];

View File

@ -0,0 +1,22 @@
export const mockStudent = {
id: 0,
nom: "Fake",
prenom: "Student",
nationalite: "FakeNationality",
dateNaissance: "2000-01-01",
codePostalNaissance: "00000",
lieuNaissance: "FakeCity",
adresse: "123 Fake Street",
medecinTraitant: "Dr. Fake",
niveau: "1",
responsables: [{
id: 1,
nom: "Fake Responsable",
prenom: "",
mail: "",
telephone: "",
dateNaissance: "",
profession: "",
adresse: ""
}]
};

View File

@ -0,0 +1,8 @@
export const mockUser = {
id: 0,
email: "fake@example.com",
username: "fakeuser",
password: "fakepassword",
estConnecte: false,
};

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,26 @@
import { useState } from 'react';
import { addDays, addMonths, addYears } from 'date-fns';
export function useCalendar() {
const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week');
const navigateDate = (direction) => {
const newDate = new Date(currentDate);
const operations = {
week: (date) => direction === 'prev' ? addDays(date, -7) : addDays(date, 7),
month: (date) => direction === 'prev' ? addMonths(date, -1) : addMonths(date, 1),
year: (date) => direction === 'prev' ? addYears(date, -1) : addYears(date, 1)
};
setCurrentDate(operations[viewType](newDate));
};
return {
currentDate,
setCurrentDate,
viewType,
setViewType,
navigateDate
};
}

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
import { BK_GET_CSRF } from '@/utils/Url';
const useCsrfToken = () => {
const [token, setToken] = useState('');
useEffect(() => {
fetch(`${BK_GET_CSRF}`, {
method: 'GET',
credentials: 'include' // Inclut les cookies dans la requête
})
.then(response => response.json())
.then(data => {
if (data) {
if(data.csrfToken != token) {
setToken(data.csrfToken);
console.log('------------> CSRF Token reçu:', data.csrfToken);
}
}
})
.catch(error => {
console.error('Error fetching CSRF token:', error);
});
}, []);
return token;
};
export default useCsrfToken;

View File

@ -0,0 +1,63 @@
import { useState } from 'react';
export function useSchedules() {
const [schedules, setSchedules] = useState([
{ id: 'default', name: 'Planning principal', color: '#10b981' },
{ id: 'secondary', name: 'Planning secondaire', color: '#3b82f6' },
{ id: 'special', name: 'Événements spéciaux', color: '#ef4444' },
{ id: 'exam', name: 'Planning examens', color: '#f59e0b' }
]);
const addSchedule = (newSchedule) => {
setSchedules(prev => [...prev, {
...newSchedule,
id: `schedule-${Date.now()}`
}]);
};
const updateSchedule = (id, updates) => {
setSchedules(prev => prev.map(schedule =>
schedule.id === id ? { ...schedule, ...updates } : schedule
));
};
const deleteSchedule = (id) => {
setSchedules(prev => prev.filter(schedule => schedule.id !== id));
};
return {
schedules,
addSchedule,
updateSchedule,
deleteSchedule
};
}
export function useEvents(initialEvents = []) {
const [events, setEvents] = useState(initialEvents);
const addEvent = (newEvent) => {
setEvents(prev => [...prev, {
...newEvent,
id: `event-${Date.now()}`
}]);
};
const updateEvent = (id, updates) => {
setEvents(prev => prev.map(event =>
event.id === id ? { ...event, ...updates } : event
));
};
const deleteEvent = (id) => {
setEvents(prev => prev.filter(event => event.id !== id));
};
return {
events,
setEvents,
addEvent,
updateEvent,
deleteEvent
};
}

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