mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
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:
2
Front-End/.env
Normal file
2
Front-End/.env
Normal 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
3
Front-End/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
Front-End/.gitignore
vendored
Normal file
36
Front-End/.gitignore
vendored
Normal 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
6
Front-End/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"messages"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
36
Front-End/README.md
Normal file
36
Front-End/README.md
Normal 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
7
Front-End/jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Front-End/messages/en/ResponsableInputFields.json
Normal file
13
Front-End/messages/en/ResponsableInputFields.json
Normal 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"
|
||||
}
|
||||
9
Front-End/messages/en/dashboard.json
Normal file
9
Front-End/messages/en/dashboard.json
Normal 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"
|
||||
}
|
||||
5
Front-End/messages/en/homePage.json
Normal file
5
Front-End/messages/en/homePage.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"welcomeParents": "Welcome Parents",
|
||||
"pleaseLogin": "Please login to access your account",
|
||||
"loginButton": "Go to login page"
|
||||
}
|
||||
6
Front-End/messages/en/pagination.json
Normal file
6
Front-End/messages/en/pagination.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"page": "Page",
|
||||
"of": "of",
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
}
|
||||
9
Front-End/messages/en/sidebar.json
Normal file
9
Front-End/messages/en/sidebar.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"dashboard": "Dashboard",
|
||||
"students": "Students",
|
||||
"structure": "Structure",
|
||||
"planning": "Schedule",
|
||||
"grades": "Grades",
|
||||
"settings": "Settings",
|
||||
"schoolAdmin": "School Administration"
|
||||
}
|
||||
30
Front-End/messages/en/students.json
Normal file
30
Front-End/messages/en/students.json
Normal 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"
|
||||
}
|
||||
13
Front-End/messages/fr/ResponsableInputFields.json
Normal file
13
Front-End/messages/fr/ResponsableInputFields.json
Normal 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"
|
||||
}
|
||||
9
Front-End/messages/fr/dashboard.json
Normal file
9
Front-End/messages/fr/dashboard.json
Normal 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"
|
||||
}
|
||||
5
Front-End/messages/fr/homePage.json
Normal file
5
Front-End/messages/fr/homePage.json
Normal 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"
|
||||
}
|
||||
6
Front-End/messages/fr/pagination.json
Normal file
6
Front-End/messages/fr/pagination.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"page": "Page",
|
||||
"of": "sur",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant"
|
||||
}
|
||||
9
Front-End/messages/fr/sidebar.json
Normal file
9
Front-End/messages/fr/sidebar.json
Normal 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"
|
||||
}
|
||||
30
Front-End/messages/fr/students.json
Normal file
30
Front-End/messages/fr/students.json
Normal 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"
|
||||
}
|
||||
8
Front-End/next.config.mjs
Normal file
8
Front-End/next.config.mjs
Normal 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
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
36
Front-End/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
Front-End/postcss.config.js
Normal file
6
Front-End/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
Front-End/project.inlang/.gitignore
vendored
Normal file
1
Front-End/project.inlang/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
cache
|
||||
1
Front-End/project.inlang/project_id
Normal file
1
Front-End/project.inlang/project_id
Normal file
@ -0,0 +1 @@
|
||||
2ff5cbbb4bc1c6d178400871dfa342ac4f0b18e9b86cb64a1110be1ec54238c1
|
||||
12
Front-End/project.inlang/settings.json
Normal file
12
Front-End/project.inlang/settings.json
Normal 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": {}
|
||||
}
|
||||
192
Front-End/scripts/check-hardcoded-strings.js
Normal file
192
Front-End/scripts/check-hardcoded-strings.js
Normal 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);
|
||||
49
Front-End/src/app/[locale]/admin/classes/page.js
Normal file
49
Front-End/src/app/[locale]/admin/classes/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
10
Front-End/src/app/[locale]/admin/grades/page.js
Normal file
10
Front-End/src/app/[locale]/admin/grades/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
96
Front-End/src/app/[locale]/admin/layout.js
Normal file
96
Front-End/src/app/[locale]/admin/layout.js
Normal 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>© {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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
143
Front-End/src/app/[locale]/admin/page.js
Normal file
143
Front-End/src/app/[locale]/admin/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
65
Front-End/src/app/[locale]/admin/planning/page.js
Normal file
65
Front-End/src/app/[locale]/admin/planning/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
105
Front-End/src/app/[locale]/admin/settings/page.js
Normal file
105
Front-End/src/app/[locale]/admin/settings/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
159
Front-End/src/app/[locale]/admin/structure/page.js
Normal file
159
Front-End/src/app/[locale]/admin/structure/page.js
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
334
Front-End/src/app/[locale]/admin/students/page.js
Normal file
334
Front-End/src/app/[locale]/admin/students/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Front-End/src/app/[locale]/admin/teachers/page.js
Normal file
23
Front-End/src/app/[locale]/admin/teachers/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
18
Front-End/src/app/[locale]/page.js
Normal file
18
Front-End/src/app/[locale]/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
113
Front-End/src/app/[locale]/parents/editInscription/page.js
Normal file
113
Front-End/src/app/[locale]/parents/editInscription/page.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
92
Front-End/src/app/[locale]/parents/layout.js
Normal file
92
Front-End/src/app/[locale]/parents/layout.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
106
Front-End/src/app/[locale]/parents/messagerie/page.js
Normal file
106
Front-End/src/app/[locale]/parents/messagerie/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
90
Front-End/src/app/[locale]/parents/page.js
Normal file
90
Front-End/src/app/[locale]/parents/page.js
Normal 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>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
74
Front-End/src/app/[locale]/parents/settings/page.js
Normal file
74
Front-End/src/app/[locale]/parents/settings/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
124
Front-End/src/app/[locale]/users/login/page.js
Normal file
124
Front-End/src/app/[locale]/users/login/page.js
Normal 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>
|
||||
</>
|
||||
}
|
||||
};
|
||||
107
Front-End/src/app/[locale]/users/password/new/page.js
Normal file
107
Front-End/src/app/[locale]/users/password/new/page.js
Normal 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)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
144
Front-End/src/app/[locale]/users/password/reset/page.js
Normal file
144
Front-End/src/app/[locale]/users/password/reset/page.js
Normal 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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
180
Front-End/src/app/[locale]/users/subscribe/page.js
Normal file
180
Front-End/src/app/[locale]/users/subscribe/page.js
Normal 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)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
BIN
Front-End/src/app/favicon.ico
Normal file
BIN
Front-End/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
42
Front-End/src/app/favicon.svg
Normal file
42
Front-End/src/app/favicon.svg
Normal 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 |
41
Front-End/src/app/layout.js
Normal file
41
Front-End/src/app/layout.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
39
Front-End/src/app/lib/actions.js
Normal file
39
Front-End/src/app/lib/actions.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
15
Front-End/src/app/not-found.js
Normal file
15
Front-End/src/app/not-found.js
Normal 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>
|
||||
)
|
||||
}
|
||||
17
Front-End/src/components/AlertMessage.js
Normal file
17
Front-End/src/components/AlertMessage.js
Normal 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;
|
||||
29
Front-End/src/components/AlertWithModal.js
Normal file
29
Front-End/src/components/AlertWithModal.js
Normal 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;
|
||||
37
Front-End/src/components/AlphabetLinks.js
Normal file
37
Front-End/src/components/AlphabetLinks.js
Normal 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;
|
||||
27
Front-End/src/components/Button.js
Normal file
27
Front-End/src/components/Button.js
Normal 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;
|
||||
212
Front-End/src/components/Calendar.js
Normal file
212
Front-End/src/components/Calendar.js
Normal 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;
|
||||
86
Front-End/src/components/Calendar/MonthView.js
Normal file
86
Front-End/src/components/Calendar/MonthView.js
Normal 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;
|
||||
111
Front-End/src/components/Calendar/PlanningView.js
Normal file
111
Front-End/src/components/Calendar/PlanningView.js
Normal 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;
|
||||
208
Front-End/src/components/Calendar/WeekView.js
Normal file
208
Front-End/src/components/Calendar/WeekView.js
Normal 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;
|
||||
52
Front-End/src/components/Calendar/YearView.js
Normal file
52
Front-End/src/components/Calendar/YearView.js
Normal 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;
|
||||
188
Front-End/src/components/ClassForm.js
Normal file
188
Front-End/src/components/ClassForm.js
Normal 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;
|
||||
111
Front-End/src/components/ClassesSection.js
Normal file
111
Front-End/src/components/ClassesSection.js
Normal 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;
|
||||
16
Front-End/src/components/DjangoCSRFToken.js
Normal file
16
Front-End/src/components/DjangoCSRFToken.js
Normal 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" />
|
||||
);
|
||||
}
|
||||
52
Front-End/src/components/DropdownMenu.js
Normal file
52
Front-End/src/components/DropdownMenu.js
Normal 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;
|
||||
320
Front-End/src/components/EventModal.js
Normal file
320
Front-End/src/components/EventModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
37
Front-End/src/components/InputPhone.js
Normal file
37
Front-End/src/components/InputPhone.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
21
Front-End/src/components/InputText.js
Normal file
21
Front-End/src/components/InputText.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
25
Front-End/src/components/InputTextIcon.js
Normal file
25
Front-End/src/components/InputTextIcon.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
375
Front-End/src/components/Inscription/InscriptionForm.js
Normal file
375
Front-End/src/components/Inscription/InscriptionForm.js
Normal 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;
|
||||
181
Front-End/src/components/Inscription/InscriptionFormShared.js
Normal file
181
Front-End/src/components/Inscription/InscriptionFormShared.js
Normal 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>
|
||||
);
|
||||
}
|
||||
103
Front-End/src/components/Inscription/ResponsableInputFields.js
Normal file
103
Front-End/src/components/Inscription/ResponsableInputFields.js
Normal 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>
|
||||
);
|
||||
}
|
||||
9
Front-End/src/components/Loader.js
Normal file
9
Front-End/src/components/Loader.js
Normal 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;
|
||||
13
Front-End/src/components/Logo.js
Normal file
13
Front-End/src/components/Logo.js
Normal 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;
|
||||
33
Front-End/src/components/Modal.js
Normal file
33
Front-End/src/components/Modal.js
Normal 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;
|
||||
47
Front-End/src/components/Pagination.js
Normal file
47
Front-End/src/components/Pagination.js
Normal 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;
|
||||
21
Front-End/src/components/Popup.js
Normal file
21
Front-End/src/components/Popup.js
Normal 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;
|
||||
170
Front-End/src/components/ScheduleNavigation.js
Normal file
170
Front-End/src/components/ScheduleNavigation.js
Normal 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>
|
||||
);
|
||||
}
|
||||
20
Front-End/src/components/SelectChoice.js
Normal file
20
Front-End/src/components/SelectChoice.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
55
Front-End/src/components/Sidebar.js
Normal file
55
Front-End/src/components/Sidebar.js
Normal 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;
|
||||
59
Front-End/src/components/Slider.js
Normal file
59
Front-End/src/components/Slider.js
Normal 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;
|
||||
87
Front-End/src/components/SpecialitiesSection.js
Normal file
87
Front-End/src/components/SpecialitiesSection.js
Normal 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;
|
||||
50
Front-End/src/components/SpecialityForm.js
Normal file
50
Front-End/src/components/SpecialityForm.js
Normal 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;
|
||||
59
Front-End/src/components/StatusLabel.js
Normal file
59
Front-End/src/components/StatusLabel.js
Normal 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;
|
||||
10
Front-End/src/components/Tab.js
Normal file
10
Front-End/src/components/Tab.js
Normal 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;
|
||||
9
Front-End/src/components/TabContent.js
Normal file
9
Front-End/src/components/TabContent.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const TabContent = ({ children, isActive }) => (
|
||||
<div className={`${isActive ? 'block' : 'hidden'}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TabContent;
|
||||
58
Front-End/src/components/Table.js
Normal file
58
Front-End/src/components/Table.js
Normal 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;
|
||||
111
Front-End/src/components/TeacherForm.js
Normal file
111
Front-End/src/components/TeacherForm.js
Normal 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;
|
||||
94
Front-End/src/components/TeachersSection.js
Normal file
94
Front-End/src/components/TeachersSection.js
Normal 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;
|
||||
34
Front-End/src/components/ToggleView.js
Normal file
34
Front-End/src/components/ToggleView.js
Normal 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;
|
||||
93
Front-End/src/context/PlanningContext.js
Normal file
93
Front-End/src/context/PlanningContext.js
Normal 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);
|
||||
3
Front-End/src/css/tailwind.css
Normal file
3
Front-End/src/css/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
30
Front-End/src/data/mockData.js
Normal file
30
Front-End/src/data/mockData.js
Normal 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'
|
||||
}
|
||||
];
|
||||
|
||||
322
Front-End/src/data/mockFicheInscription.js
Normal file
322
Front-End/src/data/mockFicheInscription.js
Normal 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" }
|
||||
]
|
||||
}
|
||||
];
|
||||
22
Front-End/src/data/mockStudent.js
Normal file
22
Front-End/src/data/mockStudent.js
Normal 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: ""
|
||||
}]
|
||||
};
|
||||
8
Front-End/src/data/mockUsersData.js
Normal file
8
Front-End/src/data/mockUsersData.js
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export const mockUser = {
|
||||
id: 0,
|
||||
email: "fake@example.com",
|
||||
username: "fakeuser",
|
||||
password: "fakepassword",
|
||||
estConnecte: false,
|
||||
};
|
||||
BIN
Front-End/src/fonts/GeistMonoVF.woff
Normal file
BIN
Front-End/src/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
Front-End/src/fonts/GeistVF.woff
Normal file
BIN
Front-End/src/fonts/GeistVF.woff
Normal file
Binary file not shown.
26
Front-End/src/hooks/useCalendar.js
Normal file
26
Front-End/src/hooks/useCalendar.js
Normal 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
|
||||
};
|
||||
}
|
||||
29
Front-End/src/hooks/useCsrfToken.js
Normal file
29
Front-End/src/hooks/useCsrfToken.js
Normal 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;
|
||||
63
Front-End/src/hooks/useEvents.js
Normal file
63
Front-End/src/hooks/useEvents.js
Normal 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
Reference in New Issue
Block a user