2 Commits

Author SHA1 Message Date
616eaccd0e chore(release): 0.0.2 2025-06-01 13:28:30 +02:00
b780e8b4ff fix: ajout des urls prod et demo
# Message de commit (conventionnel) :
#  Construction automatique depuis une branche aux formats:
#  - <type>-<ticket-id>-ma_super_description
#  - <type>-ma_super_description
# <type>(<scope>): <description> [#<ticket-id>]
# ex : feat(frontend): ajout de la gestion des utilisateurs dans le dashboard [#1]
#
# Types:
# - feat: Nouvelle fonctionnalité
# - fix: Correction de bug
# - docs: Documentation
# - style: Changements esthétiques
# - refactor: Refactorisation
# - test: Ajout/modification de tests
# - chore: Tâches diverses (e.g., mise à jour des dépendances)
#
# Scope: Optionnel (ex. backend, frontend, api, ci )
#
# Ticket ID: Référence à un ticket ou une issue (ex. [#123])
2025-06-01 13:28:28 +02:00
65 changed files with 255 additions and 2159 deletions

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
.env .env
node_modules/ node_modules/
hardcoded-strings-report.md hardcoded-strings-report.md
backend.env

View File

@ -1 +1 @@
#node scripts/prepare-commit-msg.js "$1" "$2" node scripts/prepare-commit-msg.js "$1" "$2"

View File

@ -37,7 +37,7 @@
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<img src="{{URL_DJANGO}}/static/img/logo_min.svg" alt="Logo N3wt School" class="logo" /> <img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
<h1>Confirmation de souscription</h1> <h1>Confirmation de souscription</h1>
</div> </div>
<div class="content"> <div class="content">

View File

@ -1 +1 @@
__version__ = "0.0.3" __version__ = "0.0.2"

View File

@ -66,7 +66,6 @@ urllib3==2.2.3
vine==5.1.0 vine==5.1.0
wcwidth==0.2.13 wcwidth==0.2.13
webencodings==0.5.1 webencodings==0.5.1
watchfiles
xhtml2pdf==0.2.16 xhtml2pdf==0.2.16
channels==4.0.0 channels==4.0.0
channels-redis==4.1.0 channels-redis==4.1.0

View File

@ -1,6 +1,5 @@
import subprocess import subprocess
import os import os
from watchfiles import run_process
def run_command(command): def run_command(command):
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -12,7 +11,6 @@ def run_command(command):
return process.returncode return process.returncode
test_mode = os.getenv('test_mode', 'false').lower() == 'true' test_mode = os.getenv('test_mode', 'false').lower() == 'true'
watch_mode = os.getenv('DJANGO_WATCH', 'false').lower() == 'true'
commands = [ commands = [
["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "collectstatic", "--noinput"],
@ -34,17 +32,6 @@ test_commands = [
["python", "manage.py", "init_mock_datas"] ["python", "manage.py", "init_mock_datas"]
] ]
def run_daphne():
try:
result = subprocess.run([
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
])
return result.returncode
except KeyboardInterrupt:
print("Arrêt de Daphne (KeyboardInterrupt)")
return 0
if __name__ == "__main__":
for command in commands: for command in commands:
if run_command(command) != 0: if run_command(command) != 0:
exit(1) exit(1)
@ -54,35 +41,14 @@ if __name__ == "__main__":
# if run_command(test_command) != 0: # if run_command(test_command) != 0:
# exit(1) # exit(1)
if watch_mode: # Lancer les processus en parallèle
celery_worker = subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"])
celery_beat = subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
try:
run_process(
'.',
target=run_daphne
)
except KeyboardInterrupt:
print("Arrêt demandé (KeyboardInterrupt)")
finally:
celery_worker.terminate()
celery_beat.terminate()
celery_worker.wait()
celery_beat.wait()
else:
processes = [ processes = [
subprocess.Popen([ subprocess.Popen(["daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"]),
"daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"
]),
subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]), subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]),
subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"]) subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
] ]
try:
for process in processes: # Attendre la fin des processus
process.wait()
except KeyboardInterrupt:
print("Arrêt demandé (KeyboardInterrupt)")
for process in processes:
process.terminate()
for process in processes: for process in processes:
process.wait() process.wait()

View File

@ -2,13 +2,6 @@
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier. Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
### Corrections de bugs
* Ajout d'un '/' en fin d'URL ([67cea2f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67cea2f1c6edae8eed5e024c79b1e19d08788d4c))
### 0.0.2 (2025-06-01) ### 0.0.2 (2025-06-01)
@ -168,7 +161,7 @@ Toutes les modifications notables apportées à ce projet seront documentées da
* ajout de credential include dans get CSRF ([c161fa7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c161fa7e7568437ba501a565ad53192b9cb3b6f3)) * ajout de credential include dans get CSRF ([c161fa7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c161fa7e7568437ba501a565ad53192b9cb3b6f3))
* Ajout de l'établissement dans la requête KPI récupérant les ([ada2a44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ada2a44c3ec9ba45462bd7e78984dfa38008e231)) * Ajout de l'établissement dans la requête KPI récupérant les ([ada2a44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ada2a44c3ec9ba45462bd7e78984dfa38008e231))
* Ajout des niveaux scolaires dans le back [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([05542df](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05542dfc40649fd194ee551f0298f1535753f219)) * Ajout des niveaux scolaires dans le back [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([05542df](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05542dfc40649fd194ee551f0298f1535753f219))
* ajout des urls prod et demo ([043d93d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/043d93dcc476e5eb3962fdbe0f6a81b937122647)) * ajout des urls prod et demo ([b780e8b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b780e8b4ff4b5e6bbbccf1c77a56136c0c4affcb)), closes [#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1) [#123](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/123)
* Ajout du % ou € en mode édition de réduction ([f2628bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2628bb45a14da42d014e42b1521820ffeedfb33)) * Ajout du % ou € en mode édition de réduction ([f2628bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2628bb45a14da42d014e42b1521820ffeedfb33))
* Ajout du controle sur le format des dates ([e538ac3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e538ac3d56294d4e647a38d730168ea567c76f04)) * Ajout du controle sur le format des dates ([e538ac3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e538ac3d56294d4e647a38d730168ea567c76f04))
* Ajout du mode Visu ([e1c6073](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1c607308c12cf75695e9d4593dc27ebe74e6a4f)) * Ajout du mode Visu ([e1c6073](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1c607308c12cf75695e9d4593dc27ebe74e6a4f))

View File

@ -1,12 +1,12 @@
{ {
"name": "n3wt-school-front-end", "name": "n3wt-school-front-end",
"version": "0.0.3", "version": "0.0.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n3wt-school-front-end", "name": "n3wt-school-front-end",
"version": "0.0.3", "version": "0.0.1",
"dependencies": { "dependencies": {
"@docuseal/react": "^1.0.56", "@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
@ -29,7 +29,6 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.62.0",
"react-international-phone": "^4.5.0", "react-international-phone": "^4.5.0",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-tooltip": "^5.28.0" "react-tooltip": "^5.28.0"
@ -8835,21 +8834,6 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-international-phone": { "node_modules/react-international-phone": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz", "resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",
@ -17176,12 +17160,6 @@
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
} }
}, },
"react-hook-form": {
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"requires": {}
},
"react-international-phone": { "react-international-phone": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz", "resolved": "https://registry.npmjs.org/react-international-phone/-/react-international-phone-4.5.0.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "n3wt-school-front-end", "name": "n3wt-school-front-end",
"version": "0.0.3", "version": "0.0.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -35,20 +35,19 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.62.0",
"react-international-phone": "^4.5.0", "react-international-phone": "^4.5.0",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-tooltip": "^5.28.0" "react-tooltip": "^5.28.0"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.2.11", "eslint-config-next": "14.2.11",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.14" "tailwindcss": "^3.4.14"
} }

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import AcademicResults from '@/components/Grades/AcademicResults'; import AcademicResults from '@/components/Grades/AcademicResults';
import Attendance from '@/components/Grades/Attendance'; import Attendance from '@/components/Grades/Attendance';
import Remarks from '@/components/Grades/Remarks'; import Remarks from '@/components/Grades/Remarks';
@ -9,7 +9,7 @@ import Homeworks from '@/components/Grades/Homeworks';
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations'; import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
import Orientation from '@/components/Grades/Orientation'; import Orientation from '@/components/Grades/Orientation';
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle'; import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { import {
FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL, FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
@ -29,7 +29,7 @@ import { useClasses } from '@/context/ClassesContext';
import { Award, FileText } from 'lucide-react'; import { Award, FileText } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart'; import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import GradeView from '@/components/Grades/GradeView'; import GradeView from '@/components/Grades/GradeView';
import { import {
fetchStudentCompetencies, fetchStudentCompetencies,

View File

@ -2,9 +2,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Tab from '@/components/Tab'; import Tab from '@/components/Tab';
import TabContent from '@/components/TabContent'; import TabContent from '@/components/TabContent';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { import {
fetchSmtpSettings, fetchSmtpSettings,

View File

@ -8,9 +8,9 @@ import { fetchClasse } from '@/app/actions/schoolAction';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useClasses } from '@/context/ClassesContext'; import { useClasses } from '@/context/ClassesContext';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
import { import {
fetchAbsences, fetchAbsences,
createAbsences, createAbsences,

View File

@ -2,17 +2,17 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { User, Mail } from 'lucide-react'; import { User, Mail } from 'lucide-react';
import InputTextIcon from '@/components/Form/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import ToggleSwitch from '@/components/Form/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FeesSection from '@/components/Structure/Tarification/FeesSection'; import FeesSection from '@/components/Structure/Tarification/FeesSection';
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection'; import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
import SectionTitle from '@/components/SectionTitle'; import SectionTitle from '@/components/SectionTitle';
import InputPhone from '@/components/Form/InputPhone'; import InputPhone from '@/components/InputPhone';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
import RadioList from '@/components/Form/RadioList'; import RadioList from '@/components/RadioList';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date'; import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
import logger from '@/utils/logger'; import logger from '@/utils/logger';

View File

@ -40,8 +40,8 @@ import {
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { PhoneLabel } from '@/components/Form/PhoneLabel'; import { PhoneLabel } from '@/components/PhoneLabel';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/FileUpload';
import FilesModal from '@/components/Inscription/FilesModal'; import FilesModal from '@/components/Inscription/FilesModal';
import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date'; import { getCurrentSchoolYear, getNextSchoolYear } from '@/utils/Date';
@ -250,12 +250,7 @@ export default function Page({ params: { locale } }) {
}, 500); // Debounce la recherche }, 500); // Debounce la recherche
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
}, [ }, [searchTerm, selectedEstablishmentId, currentSchoolYearPage, itemsPerPage]);
searchTerm,
selectedEstablishmentId,
currentSchoolYearPage,
itemsPerPage,
]);
/** /**
* UseEffect to update page count of tab * UseEffect to update page count of tab

View File

@ -1,10 +1,8 @@
'use client'; 'use client';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import React from 'react'; import React from 'react';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import Logo from '@/components/Logo'; // Import du composant Logo import Logo from '@/components/Logo'; // Import du composant Logo
import FormRenderer from '@/components/Form/FormRenderer';
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
export default function Home() { export default function Home() {
const t = useTranslations('homePage'); const t = useTranslations('homePage');
@ -15,7 +13,6 @@ export default function Home() {
<h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1> <h1 className="text-4xl font-bold mb-4">{t('welcomeParents')}</h1>
<p className="text-lg mb-8">{t('pleaseLogin')}</p> <p className="text-lg mb-8">{t('pleaseLogin')}</p>
<Button text={t('loginButton')} primary href="/users/login" /> <Button text={t('loginButton')} primary href="/users/login" />
<FormTemplateBuilder />
</div> </div>
); );
} }

View File

@ -11,7 +11,7 @@ import {
CalendarDays, CalendarDays,
} from 'lucide-react'; } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel'; import StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url'; import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
import { import {
fetchChildren, fetchChildren,

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useNotification } from '@/context/NotificationContext'; import { useNotification } from '@/context/NotificationContext';

View File

@ -3,9 +3,9 @@ import React, { useState } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import InputTextIcon from '@/components/Form/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; // Importez le composant Loader import Loader from '@/components/Loader'; // Importez le composant Loader
import Button from '@/components/Form/Button'; // Importez le composant Button import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url'; import { FE_USERS_NEW_PASSWORD_URL, getRedirectUrlFromRole } from '@/utils/Url';
import { login } from '@/app/actions/authAction'; import { login } from '@/app/actions/authAction';
@ -35,7 +35,11 @@ export default function Page() {
logger.debug('Sign In Result', result); logger.debug('Sign In Result', result);
if (result.error) { if (result.error) {
showNotification(result.error, 'error', 'Erreur'); showNotification(
result.error,
'error',
'Erreur'
);
setIsLoading(false); setIsLoading(false);
} else { } else {
// On initialise le contexte establishement avec la session // On initialise le contexte establishement avec la session
@ -46,7 +50,11 @@ export default function Page() {
if (url) { if (url) {
router.push(url); router.push(url);
} else { } else {
showNotification('Type de rôle non géré', 'error', 'Erreur'); showNotification(
'Type de rôle non géré',
'error',
'Erreur'
);
} }
}); });
setIsLoading(false); setIsLoading(false);

View File

@ -3,9 +3,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import InputTextIcon from '@/components/Form/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
@ -25,13 +25,25 @@ export default function Page() {
.then((data) => { .then((data) => {
logger.debug('Success:', data); logger.debug('Success:', data);
if (data.message !== '') { if (data.message !== '') {
showNotification(data.message, 'success', 'Succès'); showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`); router.push(`${FE_USERS_LOGIN_URL}`);
} else { } else {
if (data.errorMessage) { if (data.errorMessage) {
showNotification(data.errorMessage, 'error', 'Erreur'); showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) { } else if (data.errorFields) {
showNotification(data.errorFields.email, 'error', 'Erreur'); showNotification(
data.errorFields.email,
'error',
'Erreur'
);
} }
} }
setIsLoading(false); setIsLoading(false);

View File

@ -5,9 +5,9 @@ import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/Form/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { KeySquare } from 'lucide-react'; import { KeySquare } from 'lucide-react';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
@ -33,12 +33,21 @@ export default function Page() {
resetPassword(uuid, data, csrfToken) resetPassword(uuid, data, csrfToken)
.then((data) => { .then((data) => {
if (data.message !== '') { if (data.message !== '') {
logger.debug('Success:', data); logger.debug('Success:', data);
showNotification(data.message, 'success', 'Succès'); showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`); router.push(`${FE_USERS_LOGIN_URL}`);
} else { } else {
if (data.errorMessage) { if (data.errorMessage) {
showNotification(data.errorMessage, 'error', 'Erreur'); showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) { } else if (data.errorFields) {
showNotification( showNotification(
data.errorFields.password1 || data.errorFields.password2, data.errorFields.password1 || data.errorFields.password2,

View File

@ -4,9 +4,9 @@ import React, { useState, useEffect } from 'react';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import InputTextIcon from '@/components/Form/InputTextIcon'; import InputTextIcon from '@/components/InputTextIcon';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import { User, KeySquare } from 'lucide-react'; import { User, KeySquare } from 'lucide-react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
@ -36,16 +36,22 @@ export default function Page() {
.then((data) => { .then((data) => {
logger.debug('Success:', data); logger.debug('Success:', data);
if (data.message !== '') { if (data.message !== '') {
showNotification(data.message, 'success', 'Succès'); showNotification(
data.message,
'success',
'Succès'
);
router.push(`${FE_USERS_LOGIN_URL}`); router.push(`${FE_USERS_LOGIN_URL}`);
} else { } else {
if (data.errorMessage) { if (data.errorMessage) {
showNotification(data.errorMessage, 'error', 'Erreur'); showNotification(
data.errorMessage,
'error',
'Erreur'
);
} else if (data.errorFields) { } else if (data.errorFields) {
showNotification( showNotification(
data.errorFields.email || data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
data.errorFields.password1 ||
data.errorFields.password2,
'error', 'error',
'Erreur' 'Erreur'
); );

View File

@ -9,10 +9,10 @@ import { useEstablishment } from '@/context/EstablishmentContext';
import AlertMessage from '@/components/AlertMessage'; import AlertMessage from '@/components/AlertMessage';
import RecipientInput from '@/components/RecipientInput'; import RecipientInput from '@/components/RecipientInput';
import { useRouter } from 'next/navigation'; // Ajoute cette ligne import { useRouter } from 'next/navigation'; // Ajoute cette ligne
import WisiwigTextArea from '@/components/Form/WisiwigTextArea'; import WisiwigTextArea from '@/components/WisiwigTextArea';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
export default function EmailSender({ csrfToken }) { export default function EmailSender({ csrfToken }) {
const [recipients, setRecipients] = useState([]); const [recipients, setRecipients] = useState([]);

View File

@ -7,7 +7,7 @@ const CheckBox = ({
handleChange, handleChange,
fieldName, fieldName,
itemLabelFunc = () => null, itemLabelFunc = () => null,
horizontal = false, horizontal,
}) => { }) => {
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne // Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
const isChecked = Array.isArray(formData[fieldName]) const isChecked = Array.isArray(formData[fieldName])
@ -22,7 +22,7 @@ const CheckBox = ({
{horizontal && ( {horizontal && (
<label <label
htmlFor={`${fieldName}-${item.id}`} htmlFor={`${fieldName}-${item.id}`}
className="block text-sm text-center mb-1 font-medium text-gray-700 cursor-pointer" className="block text-sm text-center mb-1 font-medium text-gray-700"
> >
{itemLabelFunc(item)} {itemLabelFunc(item)}
</label> </label>
@ -40,7 +40,7 @@ const CheckBox = ({
{!horizontal && ( {!horizontal && (
<label <label
htmlFor={`${fieldName}-${item.id}`} htmlFor={`${fieldName}-${item.id}`}
className="block text-sm font-medium text-gray-700 cursor-pointer" className="block text-sm text-center mb-1 font-medium text-gray-700"
> >
{itemLabelFunc(item)} {itemLabelFunc(item)}
</label> </label>

View File

@ -1,589 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import InputTextIcon from './InputTextIcon';
import SelectChoice from './SelectChoice';
import Button from './Button';
import IconSelector from './IconSelector';
import * as LucideIcons from 'lucide-react';
import { FIELD_TYPES } from './FormTypes';
export default function AddFieldModal({
isOpen,
onClose,
onSubmit,
editingField = null,
editingIndex = -1,
}) {
const isEditing = editingIndex >= 0;
const [currentField, setCurrentField] = useState({
id: '',
label: '',
type: 'text',
required: false,
icon: '',
options: [],
text: '',
placeholder: '',
acceptTypes: '',
maxSize: 5, // 5MB par défaut
checked: false,
validation: {
pattern: '',
minLength: '',
maxLength: '',
},
});
const [showIconPicker, setShowIconPicker] = useState(false);
const [newOption, setNewOption] = useState('');
const { control, handleSubmit, reset, setValue } = useForm();
// Mettre à jour l'état et les valeurs du formulaire lorsque editingField change
useEffect(() => {
if (isOpen) {
const defaultValues = editingField || {
id: '',
label: '',
type: 'text',
required: false,
icon: '',
options: [],
text: '',
placeholder: '',
acceptTypes: '',
maxSize: 5,
checked: false,
validation: {
pattern: '',
minLength: '',
maxLength: '',
},
};
setCurrentField(defaultValues);
// Réinitialiser le formulaire avec les valeurs de l'élément à éditer
reset({
type: defaultValues.type,
label: defaultValues.label,
placeholder: defaultValues.placeholder,
required: defaultValues.required,
icon: defaultValues.icon,
text: defaultValues.text,
acceptTypes: defaultValues.acceptTypes,
maxSize: defaultValues.maxSize,
checked: defaultValues.checked,
validation: defaultValues.validation,
});
}
}, [isOpen, editingField, reset]);
// Ajouter une option au select
const addOption = () => {
if (newOption.trim()) {
setCurrentField({
...currentField,
options: [...currentField.options, newOption.trim()],
});
setNewOption('');
}
};
// Supprimer une option du select
const removeOption = (index) => {
const newOptions = currentField.options.filter((_, i) => i !== index);
setCurrentField({ ...currentField, options: newOptions });
};
// Sélectionner une icône
const selectIcon = (iconName) => {
setCurrentField({ ...currentField, icon: iconName });
// Mettre à jour la valeur dans le formulaire
const iconField = control._fields.icon;
if (iconField && iconField.onChange) {
iconField.onChange(iconName);
}
};
const handleFieldSubmit = (data) => {
onSubmit(data, currentField, editingIndex);
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold">
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
</h3>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-xl"
>
</button>
</div>
<form onSubmit={handleSubmit(handleFieldSubmit)} className="space-y-4">
<Controller
name="type"
control={control}
defaultValue={currentField.type}
render={({ field: { onChange, value } }) => (
<SelectChoice
label="Type de champ"
name="type"
selected={value}
callback={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
type: e.target.value,
});
}}
choices={FIELD_TYPES}
placeHolder="Sélectionner un type"
required
/>
)}
/>
{![
'paragraph',
'heading1',
'heading2',
'heading3',
'heading4',
'heading5',
'heading6',
].includes(currentField.type) && (
<>
<Controller
name="label"
control={control}
defaultValue={currentField.label}
render={({ field: { onChange, value } }) => (
<InputTextIcon
label="Label du champ"
name="label"
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
label: e.target.value,
});
}}
required
/>
)}
/>
<Controller
name="placeholder"
control={control}
defaultValue={currentField.placeholder}
render={({ field: { onChange, value } }) => (
<InputTextIcon
label="Placeholder (optionnel)"
name="placeholder"
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
placeholder: e.target.value,
});
}}
/>
)}
/>
<div className="flex items-center">
<Controller
name="required"
control={control}
defaultValue={currentField.required}
render={({ field: { onChange, value } }) => (
<input
type="checkbox"
id="required"
checked={value}
onChange={(e) => {
onChange(e.target.checked);
setCurrentField({
...currentField,
required: e.target.checked,
});
}}
className="mr-2"
/>
)}
/>
<label htmlFor="required">Champ obligatoire</label>
</div>
{(currentField.type === 'text' ||
currentField.type === 'email' ||
currentField.type === 'date') && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Icône (optionnel)
</label>
<Controller
name="icon"
control={control}
defaultValue={currentField.icon}
render={({ field: { onChange } }) => (
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-2 p-3 border border-gray-300 rounded-md bg-gray-50">
{currentField.icon &&
LucideIcons[currentField.icon] ? (
<>
{React.createElement(
LucideIcons[currentField.icon],
{
size: 20,
className: 'text-gray-600',
}
)}
<span className="text-sm text-gray-700">
{currentField.icon}
</span>
</>
) : (
<span className="text-sm text-gray-500">
Aucune icône sélectionnée
</span>
)}
</div>
<Button
type="button"
text="Choisir"
onClick={(e) => {
e.preventDefault();
setShowIconPicker(true);
}}
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
/>
{currentField.icon && (
<Button
type="button"
text="✕"
onClick={() => {
onChange('');
setCurrentField({ ...currentField, icon: '' });
}}
className="px-2 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
/>
)}
</div>
)}
/>
</div>
)}
</>
)}
{[
'paragraph',
'heading1',
'heading2',
'heading3',
'heading4',
'heading5',
'heading6',
].includes(currentField.type) && (
<Controller
name="text"
control={control}
defaultValue={currentField.text}
render={({ field: { onChange, value } }) => (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{currentField.type === 'paragraph'
? 'Texte du paragraphe'
: 'Texte du titre'}
</label>
<textarea
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
text: e.target.value,
});
}}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
required
/>
</div>
)}
/>
)}
{currentField.type === 'select' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Options de la liste
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newOption}
onChange={(e) => setNewOption(e.target.value)}
placeholder="Nouvelle option"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyPress={(e) =>
e.key === 'Enter' && (e.preventDefault(), addOption())
}
/>
<Button
type="button"
text="Ajouter"
onClick={addOption}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
/>
</div>
<div className="space-y-1">
{currentField.options.map((option, index) => (
<div
key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded"
>
<span>{option}</span>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
</button>
</div>
))}
</div>
</div>
)}
{currentField.type === 'radio' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Options des boutons radio
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newOption}
onChange={(e) => setNewOption(e.target.value)}
placeholder="Nouvelle option"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyPress={(e) =>
e.key === 'Enter' && (e.preventDefault(), addOption())
}
/>
<Button
type="button"
text="Ajouter"
onClick={addOption}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
/>
</div>
<div className="space-y-1">
{currentField.options.map((option, index) => (
<div
key={index}
className="flex items-center justify-between bg-gray-50 p-2 rounded"
>
<span>{option}</span>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
</button>
</div>
))}
</div>
</div>
)}
{currentField.type === 'phone' && (
<Controller
name="validation.pattern"
control={control}
defaultValue={currentField.validation?.pattern || ''}
render={({ field: { onChange, value } }) => (
<InputTextIcon
label="Format de téléphone (optionnel, exemple: ^\\+?[0-9]{10,15}$)"
name="phonePattern"
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
validation: {
...currentField.validation,
pattern: e.target.value,
},
});
}}
/>
)}
/>
)}
{currentField.type === 'file' && (
<>
<Controller
name="acceptTypes"
control={control}
defaultValue={currentField.acceptTypes || ''}
render={({ field: { onChange, value } }) => (
<InputTextIcon
label="Types de fichiers acceptés (ex: .pdf,.jpg,.png)"
name="acceptTypes"
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
acceptTypes: e.target.value,
});
}}
/>
)}
/>
<Controller
name="maxSize"
control={control}
defaultValue={currentField.maxSize || 5}
render={({ field: { onChange, value } }) => (
<InputTextIcon
label="Taille maximale (MB)"
name="maxSize"
type="number"
value={value}
onChange={(e) => {
onChange(e.target.value);
setCurrentField({
...currentField,
maxSize: parseInt(e.target.value) || 5,
});
}}
/>
)}
/>
</>
)}
{currentField.type === 'checkbox' && (
<>
<div className="flex items-center mt-2">
<Controller
name="checked"
control={control}
defaultValue={currentField.checked || false}
render={({ field: { onChange, value } }) => (
<input
type="checkbox"
id="defaultChecked"
checked={value}
onChange={(e) => {
onChange(e.target.checked);
setCurrentField({
...currentField,
checked: e.target.checked,
});
}}
className="mr-2"
/>
)}
/>
<label htmlFor="defaultChecked">Coché par défaut</label>
</div>
<div className="flex items-center mt-2">
<Controller
name="horizontal"
control={control}
defaultValue={currentField.horizontal || false}
render={({ field: { onChange, value } }) => (
<input
type="checkbox"
id="horizontal"
checked={value}
onChange={(e) => {
onChange(e.target.checked);
setCurrentField({
...currentField,
horizontal: e.target.checked,
});
}}
className="mr-2"
/>
)}
/>
<label htmlFor="horizontal">Label au-dessus (horizontal)</label>
</div>
</>
)}
{currentField.type === 'toggle' && (
<div className="flex items-center mt-2">
<Controller
name="checked"
control={control}
defaultValue={currentField.checked || false}
render={({ field: { onChange, value } }) => (
<input
type="checkbox"
id="defaultToggled"
checked={value}
onChange={(e) => {
onChange(e.target.checked);
setCurrentField({
...currentField,
checked: e.target.checked,
});
}}
className="mr-2"
/>
)}
/>
<label htmlFor="defaultToggled">Activé par défaut</label>
</div>
)}
<div className="flex gap-2 mt-6">
<Button
type="submit"
text={isEditing ? 'Modifier' : 'Ajouter'}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
/>
<Button
type="button"
text="Annuler"
onClick={onClose}
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
/>
</div>
</form>
{/* Sélecteur d'icônes - déplacé en dehors du formulaire */}
<IconSelector
isOpen={showIconPicker}
onClose={() => setShowIconPicker(false)}
onSelect={selectIcon}
selectedIcon={currentField.icon}
/>
</div>
</div>
);
}

View File

@ -1,443 +0,0 @@
import logger from '@/utils/logger';
import { useForm, Controller } from 'react-hook-form';
import SelectChoice from './SelectChoice';
import InputTextIcon from './InputTextIcon';
import * as LucideIcons from 'lucide-react';
import Button from './Button';
import DjangoCSRFToken from '../DjangoCSRFToken';
import WisiwigTextArea from './WisiwigTextArea';
import RadioList from './RadioList';
import CheckBox from './CheckBox';
import ToggleSwitch from './ToggleSwitch';
import InputPhone from './InputPhone';
import FileUpload from './FileUpload';
/*
* Récupère une icône Lucide par son nom.
*/
export function getIcon(name) {
if (Object.keys(LucideIcons).includes(name)) {
const Icon = LucideIcons[name];
return Icon ?? null;
} else {
return null;
}
}
const formConfigTest = {
id: 0,
title: 'Mon formulaire dynamique',
submitLabel: 'Envoyer',
fields: [
{ id: 'name', label: 'Nom', type: 'text', required: true },
{ id: 'email', label: 'Email', type: 'email' },
{
id: 'email2',
label: 'Email',
type: 'text',
icon: 'Mail',
},
{
id: 'role',
label: 'Rôle',
type: 'select',
options: ['Admin', 'Utilisateur', 'Invité'],
required: true,
},
{
type: 'paragraph',
text: "Bonjour, Bienvenue dans ce formulaire d'inscription haha",
},
{
id: 'birthdate',
label: 'Date de naissance',
type: 'date',
icon: 'Calendar',
},
{
id: 'textarea',
label: 'toto',
type: 'textarea',
},
],
};
export default function FormRenderer({
formConfig = formConfigTest,
csrfToken,
onFormSubmit = (data) => {
alert(JSON.stringify(data, null, 2));
}, // Callback de soumission personnalisé (optionnel)
}) {
const {
handleSubmit,
control,
formState: { errors },
reset,
} = useForm();
// Fonction utilitaire pour envoyer les données au backend
const sendFormDataToBackend = async (formData) => {
try {
// Cette fonction peut être remplacée par votre propre implémentation
// Exemple avec fetch:
const response = await fetch('/api/submit-form', {
method: 'POST',
body: formData,
// Les en-têtes sont automatiquement définis pour FormData
});
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const result = await response.json();
logger.debug('Envoi réussi:', result);
return result;
} catch (error) {
logger.error("Erreur lors de l'envoi:", error);
throw error;
}
};
const onSubmit = async (data) => {
logger.debug('=== DÉBUT onSubmit ===');
logger.debug('Réponses :', data);
try {
// Vérifier si nous avons des fichiers dans les données
const hasFiles = Object.keys(data).some((key) => {
return (
data[key] instanceof FileList ||
(data[key] && data[key][0] instanceof File)
);
});
if (hasFiles) {
// Utiliser FormData pour l'envoi de fichiers
const formData = new FormData();
// Ajouter l'ID du formulaire
formData.append('formId', formConfig.id.toString());
// Traiter chaque champ et ses valeurs
Object.keys(data).forEach((key) => {
const value = data[key];
if (
value instanceof FileList ||
(value && value[0] instanceof File)
) {
// Gérer les champs de type fichier
if (value.length > 0) {
for (let i = 0; i < value.length; i++) {
formData.append(`files.${key}`, value[i]);
}
}
} else {
// Gérer les autres types de champs
formData.append(
`data.${key}`,
value !== undefined ? value.toString() : ''
);
}
});
if (onFormSubmit) {
// Utiliser le callback personnalisé si fourni
await onFormSubmit(formData, true);
} else {
// Sinon, utiliser la fonction par défaut
await sendFormDataToBackend(formData);
alert('Formulaire avec fichier(s) envoyé avec succès');
}
} else {
// Pas de fichier, on peut utiliser JSON
const formattedData = {
formId: formConfig.id,
responses: { ...data },
};
if (onFormSubmit) {
// Utiliser le callback personnalisé si fourni
await onFormSubmit(formattedData, false);
} else {
// Afficher un message pour démonstration
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
}
}
reset(); // Réinitialiser le formulaire après soumission
} catch (error) {
logger.error('Erreur lors de la soumission du formulaire:', error);
alert(`Erreur lors de l'envoi du formulaire: ${error.message}`);
}
logger.debug('=== FIN onSubmit ===');
};
const onError = (errors) => {
logger.error('=== ERREURS DE VALIDATION ===');
logger.error('Erreurs :', errors);
alert('Erreurs de validation : ' + JSON.stringify(errors, null, 2));
};
return (
<form
onSubmit={handleSubmit(onSubmit, onError)}
className="max-w-md mx-auto"
>
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
<h2 className="text-2xl font-bold text-center mb-4">
{formConfig.title}
</h2>
{formConfig.fields.map((field) => (
<div
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
className="flex flex-col mt-4"
>
{field.type === 'heading1' && (
<h1 className="text-3xl font-bold mb-3">{field.text}</h1>
)}
{field.type === 'heading2' && (
<h2 className="text-2xl font-bold mb-3">{field.text}</h2>
)}
{field.type === 'heading3' && (
<h3 className="text-xl font-bold mb-2">{field.text}</h3>
)}
{field.type === 'heading4' && (
<h4 className="text-lg font-bold mb-2">{field.text}</h4>
)}
{field.type === 'heading5' && (
<h5 className="text-base font-bold mb-1">{field.text}</h5>
)}
{field.type === 'heading6' && (
<h6 className="text-sm font-bold mb-1">{field.text}</h6>
)}
{field.type === 'paragraph' && <p className="mb-4">{field.text}</p>}
{(field.type === 'text' ||
field.type === 'email' ||
field.type === 'date') && (
<Controller
name={field.id}
control={control}
rules={{
required: field.required,
pattern: field.validation?.pattern
? new RegExp(field.validation.pattern)
: undefined,
minLength: field.validation?.minLength,
maxLength: field.validation?.maxLength,
}}
render={({ field: { onChange, value, name } }) => (
<InputTextIcon
label={field.label}
required={field.required}
IconItem={field.icon ? getIcon(field.icon) : null}
type={field.type}
name={name}
value={value || ''}
onChange={onChange}
errorMsg={
errors[field.id]
? field.required
? `${field.label} est requis`
: 'Champ invalide'
: ''
}
/>
)}
/>
)}
{field.type === 'phone' && (
<Controller
name={field.id}
control={control}
rules={{ required: field.required }}
render={({ field: { onChange, value, name } }) => (
<InputPhone
label={field.label}
required={field.required}
name={name}
value={value || ''}
onChange={onChange}
errorMsg={
errors[field.id]
? field.required
? `${field.label} est requis`
: 'Champ invalide'
: ''
}
/>
)}
/>
)}
{field.type === 'select' && (
<Controller
name={field.id}
control={control}
rules={{ required: field.required }}
render={({ field: { onChange, value, name } }) => (
<SelectChoice
label={field.label}
required={field.required}
name={name}
selected={value || ''}
callback={onChange}
choices={field.options.map((e) => ({ label: e, value: e }))}
placeHolder={`Sélectionner ${field.label.toLowerCase()}`}
errorMsg={
errors[field.id]
? field.required
? `${field.label} est requis`
: 'Champ invalide'
: ''
}
/>
)}
/>
)}
{field.type === 'radio' && (
<Controller
name={field.id}
control={control}
rules={{ required: field.required }}
render={({ field: { onChange, value, name } }) => (
<RadioList
items={field.options.map((option, idx) => ({
id: idx,
label: option,
}))}
formData={{
[field.id]: value
? field.options.findIndex((o) => o === value)
: '',
}}
handleChange={(e) =>
onChange(field.options[parseInt(e.target.value)])
}
fieldName={field.id}
sectionLabel={field.label}
required={field.required}
/>
)}
/>
)}
{field.type === 'checkbox' && (
<Controller
name={field.id}
control={control}
defaultValue={field.checked || false}
rules={{ required: field.required }}
render={({ field: { onChange, value, name } }) => (
<div>
<CheckBox
item={{ id: field.id, label: field.label }}
formData={{ [field.id]: value || false }}
handleChange={(e) => onChange(e.target.checked)}
fieldName={field.id}
itemLabelFunc={(item) => item.label}
horizontal={field.horizontal || false}
/>
{errors[field.id] && (
<p className="text-red-500 text-sm mt-1">
{field.required
? `${field.label} est requis`
: 'Champ invalide'}
</p>
)}
</div>
)}
/>
)}
{field.type === 'toggle' && (
<Controller
name={field.id}
control={control}
defaultValue={field.checked || false}
rules={{ required: field.required }}
render={({ field: { onChange, value, name } }) => (
<div>
<ToggleSwitch
name={field.id}
label={field.label + (field.required ? ' *' : '')}
checked={value || false}
onChange={(e) => onChange(e.target.checked)}
/>
{errors[field.id] && (
<p className="text-red-500 text-sm mt-1">
{field.required
? `${field.label} est requis`
: 'Champ invalide'}
</p>
)}
</div>
)}
/>
)}
{field.type === 'file' && (
<Controller
name={field.id}
control={control}
rules={{ required: field.required }}
render={({ field: { onChange, value } }) => (
<FileUpload
selectionMessage={field.label}
required={field.required}
uploadedFileName={value ? value[0]?.name : null}
onFileSelect={(file) => {
// Créer un objet de type FileList similaire pour la compatibilité
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
onChange(dataTransfer.files);
}}
errorMsg={
errors[field.id]
? field.required
? `${field.label} est requis`
: 'Champ invalide'
: ''
}
/>
)}
/>
)}
{field.type === 'textarea' && (
<Controller
name={field.id}
control={control}
rules={{ required: field.required }}
render={({ field: { onChange, value } }) => (
<WisiwigTextArea
label={field.label}
placeholder={field.placeholder}
value={value || ''}
onChange={onChange}
required={field.required}
errorMsg={
errors[field.id]
? field.required
? `${field.label} est requis`
: 'Champ invalide'
: ''
}
/>
)}
/>
)}
</div>
))}
<div className="form-group-submit mt-4">
<Button
type="submit"
primary
text={formConfig.submitLabel ? formConfig.submitLabel : 'Envoyer'}
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
/>
</div>
</form>
);
}

View File

@ -1,616 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import InputTextIcon from './InputTextIcon';
import FormRenderer from './FormRenderer';
import AddFieldModal from './AddFieldModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
Edit2,
Trash2,
PlusCircle,
Download,
Upload,
GripVertical,
TextCursorInput,
AtSign,
Calendar,
ChevronDown,
Type,
AlignLeft,
Save,
ChevronUp,
Heading1,
Heading2,
Heading3,
Heading4,
Heading5,
Heading6,
Code,
Eye,
EyeOff,
Phone,
Radio,
ToggleLeft,
CheckSquare,
FileUp,
} from 'lucide-react';
const FIELD_TYPES_ICON = {
text: { icon: TextCursorInput },
email: { icon: AtSign },
phone: { icon: Phone },
date: { icon: Calendar },
select: { icon: ChevronDown },
radio: { icon: Radio },
checkbox: { icon: CheckSquare },
toggle: { icon: ToggleLeft },
file: { icon: FileUp },
textarea: { icon: Type },
paragraph: { icon: AlignLeft },
heading1: { icon: Heading1 },
heading2: { icon: Heading2 },
heading3: { icon: Heading3 },
heading4: { icon: Heading4 },
heading5: { icon: Heading5 },
heading6: { icon: Heading6 },
};
// Type d'item pour le drag and drop
const ItemTypes = {
FIELD: 'field',
};
// Composant pour un champ draggable
const DraggableFieldItem = ({
field,
index,
moveField,
editField,
deleteField,
}) => {
const ref = React.useRef(null);
// Configuration du drag (ce qu'on peut déplacer)
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.FIELD,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
// Configuration du drop (où on peut déposer)
const [, drop] = useDrop({
accept: ItemTypes.FIELD,
hover: (item, monitor) => {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Ne rien faire si on survole le même élément
if (dragIndex === hoverIndex) {
return;
}
// Déterminer la position de la souris par rapport à l'élément survolé
const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Ne pas remplacer si on n'a pas dépassé la moitié de l'élément
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Effectuer le déplacement
moveField(dragIndex, hoverIndex);
// Mettre à jour l'index de l'élément déplacé
item.index = hoverIndex;
},
});
// Combiner drag et drop sur le même élément de référence
drag(drop(ref));
return (
<div
ref={ref}
className={`flex items-center justify-between bg-gray-50 p-3 rounded border border-gray-200 ${
isDragging ? 'opacity-50' : ''
}`}
>
<div className="flex items-center gap-2">
<div className="cursor-move text-gray-400 hover:text-gray-600">
<GripVertical size={18} />
</div>
{FIELD_TYPES_ICON[field.type] &&
React.createElement(FIELD_TYPES_ICON[field.type].icon, {
size: 18,
className: 'text-gray-600',
})}
<span className="font-medium">
{field.type === 'paragraph'
? 'Paragraphe'
: field.type.startsWith('heading')
? `Titre ${field.type.replace('heading', '')}`
: field.label}
</span>
<span className="text-sm text-gray-500">
({field.type}){field.required && ' *'}
</span>
</div>
<div className="flex gap-1">
<button
onClick={() => editField(index)}
className="p-1 text-blue-500 hover:text-blue-700"
title="Modifier"
>
<Edit2 size={16} />
</button>
<button
onClick={() => deleteField(index)}
className="p-1 text-red-500 hover:text-red-700"
title="Supprimer"
>
<Trash2 size={16} />
</button>
</div>
</div>
);
};
export default function FormTemplateBuilder() {
const [formConfig, setFormConfig] = useState({
id: 0,
title: 'Nouveau formulaire',
submitLabel: 'Envoyer',
fields: [],
});
const [showAddFieldModal, setShowAddFieldModal] = useState(false);
const [editingIndex, setEditingIndex] = useState(-1);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState({ type: '', text: '' });
const [showScrollButton, setShowScrollButton] = useState(false);
const [showJsonSection, setShowJsonSection] = useState(false);
const { reset: resetField } = useForm();
// Gérer l'affichage du bouton de défilement
useEffect(() => {
const handleScroll = () => {
// Afficher le bouton quand on descend d'au moins 300px
setShowScrollButton(window.scrollY > 300);
};
// Ajouter l'écouteur d'événement
window.addEventListener('scroll', handleScroll);
// Nettoyage de l'écouteur lors du démontage du composant
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// Fonction pour remonter en haut de la page
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
// Générer un ID unique pour les champs
const generateFieldId = (label) => {
return label
.toLowerCase()
.replace(/[àáâãäå]/g, 'a')
.replace(/[èéêë]/g, 'e')
.replace(/[ìíîï]/g, 'i')
.replace(/[òóôõö]/g, 'o')
.replace(/[ùúûü]/g, 'u')
.replace(/[ç]/g, 'c')
.replace(/[^a-z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
};
// Ajouter ou modifier un champ
const handleFieldSubmit = (data, currentField, editIndex) => {
const isHeadingType = data.type.startsWith('heading');
const isContentTypeOnly = data.type === 'paragraph' || isHeadingType;
const fieldData = {
...data,
id: isContentTypeOnly
? undefined
: generateFieldId(data.label || 'field'),
options: ['select', 'radio'].includes(data.type)
? currentField.options
: undefined,
icon: data.icon || currentField.icon || undefined,
placeholder: data.placeholder || undefined,
text: isContentTypeOnly ? data.text : undefined,
checked: ['checkbox', 'toggle'].includes(data.type)
? currentField.checked
: undefined,
horizontal:
data.type === 'checkbox' ? currentField.horizontal : undefined,
acceptTypes: data.type === 'file' ? currentField.acceptTypes : undefined,
maxSize: data.type === 'file' ? currentField.maxSize : undefined,
validation: ['phone', 'email', 'text'].includes(data.type)
? currentField.validation
: undefined,
};
// Nettoyer les propriétés undefined
Object.keys(fieldData).forEach((key) => {
if (fieldData[key] === undefined || fieldData[key] === '') {
delete fieldData[key];
}
});
const newFields = [...formConfig.fields];
if (editIndex >= 0) {
newFields[editIndex] = fieldData;
} else {
newFields.push(fieldData);
}
setFormConfig({ ...formConfig, fields: newFields });
setEditingIndex(-1);
}; // Modifier un champ existant
const editField = (index) => {
setEditingIndex(index);
setShowAddFieldModal(true);
};
// Supprimer un champ
const deleteField = (index) => {
const newFields = formConfig.fields.filter((_, i) => i !== index);
setFormConfig({ ...formConfig, fields: newFields });
};
// Déplacer un champ
const moveField = (dragIndex, hoverIndex) => {
const newFields = [...formConfig.fields];
const draggedField = newFields[dragIndex];
// Supprimer l'élément déplacé
newFields.splice(dragIndex, 1);
// Insérer l'élément à sa nouvelle position
newFields.splice(hoverIndex, 0, draggedField);
setFormConfig({ ...formConfig, fields: newFields });
};
// Exporter le JSON
const exportJson = () => {
const jsonString = JSON.stringify(formConfig, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `formulaire_${formConfig.title.replace(/\s+/g, '_').toLowerCase()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Importer un JSON
const importJson = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
setFormConfig(imported);
} catch (error) {
alert('Erreur lors de l&apos;importation du fichier JSON');
}
};
reader.readAsText(file);
}
};
// Sauvegarder le formulaire (pour le backend)
const saveFormTemplate = async () => {
// Validation basique
if (!formConfig.title.trim()) {
setSaveMessage({
type: 'error',
text: 'Le titre du formulaire est requis',
});
return;
}
if (formConfig.fields.length === 0) {
setSaveMessage({
type: 'error',
text: 'Ajoutez au moins un champ au formulaire',
});
return;
}
setSaving(true);
setSaveMessage({ type: '', text: '' });
try {
// Simulation d'envoi au backend (à remplacer par l'appel API réel)
// const response = await fetch('/api/form-templates', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(formConfig),
// });
// if (!response.ok) {
// throw new Error('Erreur lors de l\'enregistrement du formulaire');
// }
// const data = await response.json();
// Simulation d'une réponse du backend
await new Promise((resolve) => setTimeout(resolve, 1000));
setSaveMessage({
type: 'success',
text: 'Formulaire enregistré avec succès',
});
// Si le backend renvoie un ID, on peut mettre à jour l'ID du formulaire
// setFormConfig({ ...formConfig, id: data.id });
} catch (error) {
setSaveMessage({
type: 'error',
text:
error.message || "Une erreur est survenue lors de l'enregistrement",
});
} finally {
setSaving(false);
}
};
return (
<DndProvider backend={HTML5Backend}>
<div className="max-w-6xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* Panel de configuration */}
<div
className={
showJsonSection
? 'lg:col-span-3 space-y-6'
: 'lg:col-span-5 space-y-6'
}
>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-6">
Configuration du formulaire
</h2>
{/* Configuration générale */}
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold mr-4">
Paramètres généraux
</h3>
<div className="flex gap-2">
<button
onClick={() => setShowJsonSection(!showJsonSection)}
className="px-4 py-2 rounded-md inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-600 text-white"
title={
showJsonSection ? 'Masquer le JSON' : 'Afficher le JSON'
}
>
{showJsonSection ? (
<EyeOff size={18} />
) : (
<Eye size={18} />
)}
<span>
{showJsonSection ? 'Masquer JSON' : 'Afficher JSON'}
</span>
</button>
<button
onClick={saveFormTemplate}
disabled={saving}
className={`px-4 py-2 rounded-md inline-flex items-center gap-2 ${
saving
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-600 text-white'
}`}
title="Enregistrer le formulaire"
>
<Save size={18} />
<span>
{saving ? 'Enregistrement...' : 'Enregistrer'}
</span>
</button>
</div>
</div>
{saveMessage.text && (
<div
className={`p-3 rounded ${
saveMessage.type === 'error'
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'
}`}
>
{saveMessage.text}
</div>
)}
<InputTextIcon
label="Titre du formulaire"
name="title"
value={formConfig.title}
onChange={(e) =>
setFormConfig({ ...formConfig, title: e.target.value })
}
required
/>
<InputTextIcon
label="Texte du bouton de soumission du formulaire"
name="submitLabel"
value={formConfig.submitLabel}
onChange={(e) =>
setFormConfig({
...formConfig,
submitLabel: e.target.value,
})
}
/>
</div>
{/* Liste des champs */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold mr-4">
Champs du formulaire ({formConfig.fields.length})
</h3>
<button
onClick={() => {
setEditingIndex(-1);
setShowAddFieldModal(true);
}}
className="p-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
title="Ajouter un champ"
>
<PlusCircle size={18} />
</button>
</div>
{formConfig.fields.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
<p className="text-gray-500 italic mb-4">
Aucun champ ajouté
</p>
<button
onClick={() => {
setEditingIndex(-1);
setShowAddFieldModal(true);
}}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors inline-flex items-center gap-2"
>
<PlusCircle size={18} />
<span>Ajouter mon premier champ</span>
</button>
</div>
) : (
<div className="space-y-2">
{formConfig.fields.map((field, index) => (
<DraggableFieldItem
key={index}
field={field}
index={index}
moveField={moveField}
editField={editField}
deleteField={deleteField}
/>
))}
</div>
)}
</div>
{/* Actions */}
<div className="mt-6">
{/* Les actions ont été déplacées dans la section JSON généré */}
</div>
</div>
</div>
{/* JSON généré */}
{showJsonSection && (
<div className="lg:col-span-2">
<div className="bg-white p-6 rounded-lg shadow h-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold mr-4">JSON généré</h3>
<div className="flex gap-2">
<button
onClick={exportJson}
className="p-2 bg-purple-500 text-white rounded-md hover:bg-purple-600 transition-colors"
title="Exporter JSON"
>
<Download size={18} />
</button>
<label
className="p-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 cursor-pointer transition-colors"
title="Importer JSON"
>
<Upload size={18} />
<input
type="file"
accept=".json"
onChange={importJson}
className="hidden"
/>
</label>
</div>
</div>
<pre className="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">
{JSON.stringify(formConfig, null, 2)}
</pre>
</div>
</div>
)}
</div>
{/* Aperçu */}
<div className="mt-6">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Aperçu du formulaire</h3>
<div className="border-2 border-dashed border-gray-300 p-6 rounded">
{formConfig.fields.length > 0 ? (
<FormRenderer formConfig={formConfig} />
) : (
<p className="text-gray-500 italic text-center">
Ajoutez des champs pour voir l&apos;aperçu
</p>
)}
</div>
</div>
</div>
{/* Modal d'ajout/modification de champ */}
<AddFieldModal
isOpen={showAddFieldModal}
onClose={() => setShowAddFieldModal(false)}
onSubmit={handleFieldSubmit}
editingField={
editingIndex >= 0 ? formConfig.fields[editingIndex] : null
}
editingIndex={editingIndex}
/>
{/* Bouton flottant pour remonter en haut */}
{showScrollButton && (
<div className="fixed bottom-6 right-6 z-10">
<button
onClick={scrollToTop}
className="p-4 rounded-full shadow-lg flex items-center justify-center bg-gray-500 hover:bg-gray-600 text-white transition-all duration-300"
title="Remonter en haut de la page"
>
<ChevronUp size={24} />
</button>
</div>
)}
</div>
</DndProvider>
);
}

View File

@ -1,19 +0,0 @@
export const FIELD_TYPES = [
{ value: 'text', label: 'Texte' },
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Téléphone' },
{ value: 'date', label: 'Date' },
{ value: 'select', label: 'Liste déroulante' },
{ value: 'radio', label: 'Boutons radio' },
{ value: 'checkbox', label: 'Case à cocher' },
{ value: 'toggle', label: 'Interrupteur' },
{ value: 'file', label: 'Upload de fichier' },
{ value: 'textarea', label: 'Zone de texte riche' },
{ value: 'paragraph', label: 'Paragraphe' },
{ value: 'heading1', label: 'Titre 1' },
{ value: 'heading2', label: 'Titre 2' },
{ value: 'heading3', label: 'Titre 3' },
{ value: 'heading4', label: 'Titre 4' },
{ value: 'heading5', label: 'Titre 5' },
{ value: 'heading6', label: 'Titre 6' },
];

View File

@ -1,145 +0,0 @@
import React, { useMemo, useState } from 'react';
import * as LucideIcons from 'lucide-react';
import Button from './Button';
export default function IconSelector({
isOpen,
onClose,
onSelect,
selectedIcon = '',
}) {
const [searchTerm, setSearchTerm] = useState('');
const excludedKeys = new Set([
'Icon',
'DynamicIcon',
'createLucideIcon',
'default',
'icons',
]);
const allIcons = Object.keys(LucideIcons).filter((key) => {
// Exclure les utilitaires
if (excludedKeys.has(key)) return false;
return true;
});
const filteredIcons = useMemo(() => {
if (!searchTerm) return allIcons;
return allIcons.filter((iconName) =>
iconName.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [searchTerm, allIcons]);
if (!isOpen) return null;
const selectIcon = (iconName) => {
onSelect(iconName);
onClose();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[85vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">
Choisir une icône ({filteredIcons.length} / {allIcons.length}{' '}
icônes)
</h3>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-xl"
>
</button>
</div>
{/* Barre de recherche */}
<div className="mb-6">
<div className="relative">
<input
type="text"
placeholder="Rechercher une icône..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-3 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<LucideIcons.Search
className="absolute left-3 top-3.5 text-gray-400"
size={18}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
>
<LucideIcons.X size={18} />
</button>
)}
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{filteredIcons.map((iconName) => {
try {
const IconComponent = LucideIcons[iconName];
return (
<button
key={iconName}
onClick={() => selectIcon(iconName)}
className={`
p-5 rounded-lg border-2 transition-all duration-200
hover:bg-blue-50 hover:border-blue-300 hover:shadow-md hover:scale-105
flex flex-col items-center justify-center gap-4 min-h-[140px] w-full
${
selectedIcon === iconName
? 'bg-blue-100 border-blue-500 shadow-md scale-105'
: 'bg-gray-50 border-gray-200'
}
`}
title={iconName}
>
<IconComponent
size={32}
className="text-gray-700 flex-shrink-0"
/>
<span className="text-xs text-gray-600 text-center leading-tight break-words px-1 overflow-hidden max-w-full">
{iconName}
</span>
</button>
);
} catch (error) {
// En cas d'erreur avec une icône spécifique, ne pas la rendre
return null;
}
})}
</div>
<div className="mt-6 flex justify-between items-center">
<p className="text-sm text-gray-500">
{searchTerm ? (
<>
{filteredIcons.length} icône(s) trouvée(s) sur {allIcons.length}{' '}
disponibles
</>
) : (
<>Total : {allIcons.length} icônes disponibles</>
)}
</p>
<div className="flex gap-2">
<Button
text="Aucune icône"
onClick={() => selectIcon('')}
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
/>
<Button
text="Annuler"
onClick={onClose}
className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import ToggleSwitch from '@/components/Form/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import { useNotification } from '@/context/NotificationContext'; import { useNotification } from '@/context/NotificationContext';

View File

@ -1,6 +1,6 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react'; import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react';
import RadioList from '@/components/Form/RadioList'; import RadioList from '@/components/RadioList';
const LEVELS = [ const LEVELS = [
{ value: 0, label: 'Non évalué' }, { value: 0, label: 'Non évalué' },

View File

@ -1,5 +1,3 @@
import React from 'react';
export default function InputTextIcon({ export default function InputTextIcon({
name, name,
type, type,
@ -33,11 +31,9 @@ export default function InputTextIcon({
!enable ? 'bg-gray-100 cursor-not-allowed' : '' !enable ? 'bg-gray-100 cursor-not-allowed' : ''
}`} }`}
> >
{IconItem ? (
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm"> <span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
<IconItem /> {IconItem && <IconItem />}
</span> </span>
) : null}
<input <input
type={type} type={type}
id={name} id={name}

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/FileUpload';
import { Upload, Eye, Trash2, FileText } from 'lucide-react'; import { Upload, Eye, Trash2, FileText } from 'lucide-react';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';

View File

@ -1,6 +1,6 @@
// Import des dépendances nécessaires // Import des dépendances nécessaires
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { import {
fetchSchoolFileTemplatesFromRegistrationFiles, fetchSchoolFileTemplatesFromRegistrationFiles,
@ -220,7 +220,9 @@ export default function InscriptionFormShared({
.then((data) => { .then((data) => {
setProfiles(data); setProfiles(data);
}) })
.catch((error) => logger.error('Error fetching profiles : ', error)); .catch((error) =>
logger.error('Error fetching profiles : ', error)
);
if (selectedEstablishmentId) { if (selectedEstablishmentId) {
// Fetch data for registration payment modes // Fetch data for registration payment modes

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import RadioList from '@/components/Form/RadioList'; import RadioList from '@/components/RadioList';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
export default function PaymentMethodSelector({ export default function PaymentMethodSelector({

View File

@ -1,5 +1,5 @@
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import InputPhone from '@/components/Form/InputPhone'; import InputPhone from '@/components/InputPhone';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Trash2, Plus, Users } from 'lucide-react'; import { Trash2, Plus, Users } from 'lucide-react';

View File

@ -1,4 +1,4 @@
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Trash2, Plus, Users } from 'lucide-react'; import { Trash2, Plus, Users } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';

View File

@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import { fetchRegisterForm } from '@/app/actions/subscriptionAction'; import { fetchRegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/FileUpload';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import { levels, genders } from '@/utils/constants'; import { levels, genders } from '@/utils/constants';
@ -112,10 +112,13 @@ export default function StudentInfoForm({
(field === 'birth_place' && (field === 'birth_place' &&
(!formData.birth_place || formData.birth_place.trim() === '')) || (!formData.birth_place || formData.birth_place.trim() === '')) ||
(field === 'birth_postal_code' && (field === 'birth_postal_code' &&
(!formData.birth_postal_code || (
!formData.birth_postal_code ||
String(formData.birth_postal_code).trim() === '' || String(formData.birth_postal_code).trim() === '' ||
isNaN(Number(formData.birth_postal_code)) || isNaN(Number(formData.birth_postal_code)) ||
!Number.isInteger(Number(formData.birth_postal_code)))) || !Number.isInteger(Number(formData.birth_postal_code))
)
) ||
(field === 'address' && (field === 'address' &&
(!formData.address || formData.address.trim() === '')) || (!formData.address || formData.address.trim() === '')) ||
(field === 'attending_physician' && (field === 'attending_physician' &&

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/Form/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import { import {
fetchSchoolFileTemplatesFromRegistrationFiles, fetchSchoolFileTemplatesFromRegistrationFiles,
@ -10,7 +10,7 @@ import {
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { School, CheckCircle, Hourglass, FileText } from 'lucide-react'; import { School, CheckCircle, Hourglass, FileText } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
export default function ValidateSubscription({ export default function ValidateSubscription({
studentId, studentId,

View File

@ -4,7 +4,7 @@ import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
const paymentPlansOptions = [ const paymentPlansOptions = [
{ id: 1, name: '1 fois', frequency: 1 }, { id: 1, name: '1 fois', frequency: 1 },

View File

@ -2,9 +2,9 @@ import React, { useState, useRef, useCallback } from 'react';
import TreeView from '@/components/Structure/Competencies/TreeView'; import TreeView from '@/components/Structure/Competencies/TreeView';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { Award, CheckCircle } from 'lucide-react'; import { Award, CheckCircle } from 'lucide-react';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
import Button from '@/components/Form/Button'; import Button from '@/components/Button';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { import {
fetchEstablishmentCompetencies, fetchEstablishmentCompetencies,

View File

@ -2,10 +2,10 @@ import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import SelectChoice from '@/components/Form/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import TeacherItem from '@/components/Structure/Configuration/TeacherItem'; import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
import MultiSelect from '@/components/Form/MultiSelect'; import MultiSelect from '@/components/MultiSelect';
import LevelLabel from '@/components/CustomLabels/LevelLabel'; import LevelLabel from '@/components/CustomLabels/LevelLabel';
import { DndProvider, useDrop } from 'react-dnd'; import { DndProvider, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';

View File

@ -2,7 +2,7 @@ import { Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import InputTextWithColorIcon from '@/components/Form/InputTextWithColorIcon'; import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';

View File

@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react';
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react'; import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/Form/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import TeacherItem from './TeacherItem'; import TeacherItem from './TeacherItem';
import logger from '@/utils/logger'; import logger from '@/utils/logger';

View File

@ -7,7 +7,7 @@ import {
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import { DocusealBuilder } from '@docuseal/react'; import { DocusealBuilder } from '@docuseal/react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import MultiSelect from '@/components/Form/MultiSelect'; // Import du composant MultiSelect import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
@ -121,13 +121,7 @@ export default function FileUploadDocuSeal({
guardianDetails.forEach((guardian, index) => { guardianDetails.forEach((guardian, index) => {
logger.debug('creation du clone avec required : ', is_required); logger.debug('creation du clone avec required : ', is_required);
cloneTemplate( cloneTemplate(templateMaster?.id, guardian.email, is_required, selectedEstablishmentId, apiDocuseal)
templateMaster?.id,
guardian.email,
is_required,
selectedEstablishmentId,
apiDocuseal
)
.then((clonedDocument) => { .then((clonedDocument) => {
// Sauvegarde des schoolFileTemplates clonés dans la base de données // Sauvegarde des schoolFileTemplates clonés dans la base de données
const data = { const data = {

View File

@ -1,14 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react'; import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import MultiSelect from '@/components/Form/MultiSelect'; import MultiSelect from '@/components/MultiSelect';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction'; import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import ToggleSwitch from '@/components/Form/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import { useNotification } from '@/context/NotificationContext'; import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage'; import AlertMessage from '@/components/AlertMessage';

View File

@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react'; import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';

View File

@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react'; import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import CheckBox from '@/components/Form/CheckBox'; import CheckBox from '@/components/CheckBox';
import InputText from '@/components/Form/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';

View File

@ -4,10 +4,10 @@ import 'react-quill/dist/quill.snow.css';
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }); const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
export default function WisiwigTextArea({ export default function WisiwigTextArea({
label = 'Zone de Texte', label = 'Mail',
value, value,
onChange, onChange,
placeholder = 'Ecrivez votre texte ici...', placeholder = 'Ecrivez votre mail ici...',
className = 'h-64', className = 'h-64',
required = false, required = false,
errorMsg, errorMsg,

View File

@ -1,22 +0,0 @@
TZ="Europe/Paris"
TEST_MODE=true
CSRF_COOKIE_SECURE=true
CSRF_COOKIE_DOMAIN=".localhost"
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
BASE_URL=http://localhost:3000
DEBUG=false
EMAIL_HOST="smtp.hostinger.com"
EMAIL_PORT="587"
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=''
EMAIL_USE_TLS=true
EMAIL_USE_SSL=false
DB_NAME="school"
DB_USER="postgres"
DB_PASSWORD="postgres"
DB_HOST="database"
DB_PORT="5432"
URL_DJANGO="http://localhost:8080"
SECRET_KEY="<SIGNINGKEY>"

View File

@ -1,19 +1,15 @@
services: services:
redis: redis:
image: "redis:latest" image: 'redis:latest'
volumes:
- redis-data:/data
expose: expose:
- 6379 - 6379
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
database: database:
image: "postgres:latest" image: 'postgres:latest'
expose: expose:
- 5432 - 5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@ -24,13 +20,17 @@ services:
image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest
ports: ports:
- 8080:8080 - 8080:8080
env_file: "./conf/backend.env" environment:
- TZ=Europe/Paris
- TEST_MODE=True
links: links:
- "database:database" - "database:database"
- "redis:redis" - "redis:redis"
depends_on: depends_on:
- redis - redis
- database - database
volumes:
- ./conf/application.json:/Back-End/Subscriptions/Configuration/application.json
command: python start.py command: python start.py
frontend: frontend:
@ -40,8 +40,6 @@ services:
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
- NODE_ENV=production - NODE_ENV=production
volumes: - NEXT_PUBLIC_API_URL=http://toto:8080
- ./conf/env:/app/.env depends_on:
volumes: - backend
postgres-data:
redis-data:

View File

@ -1,24 +1,55 @@
services: services:
redis: redis:
image: "redis:latest" image: "redis:latest"
volumes: ports:
- redis-data:/data - 6379:6379
expose:
- 6379
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
database: database:
image: "postgres:latest" image: "postgres:latest"
expose: ports:
- 5432 - 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: school POSTGRES_DB: school
TZ: Europe/Paris TZ: Europe/Paris
# docuseal_db:
# image: postgres:latest
# environment:
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# DOCUSEAL_DB_HOST: docuseal_db
# POSTGRES_DB: docuseal
# ports:
# - 5433:5432 # port différent si besoin d'accès direct depuis l'hôte
# docuseal:
# image: docuseal/docuseal:latest
# container_name: docuseal_app
# depends_on:
# - docuseal_db
# ports:
# - "3001:3000"
# environment:
# DATABASE_URL: postgresql://postgres:postgres@docuseal_db:5432/docuseal
# volumes:
# - ./docuseal:/data/docuseal
# caddy:
# image: caddy:2
# container_name: caddy
# restart: unless-stopped
# ports:
# - "4000:4443"
# volumes:
# - ./Caddyfile:/etc/caddy/Caddyfile
# - caddy_data:/data
# - caddy_config:/config
# depends_on:
# - docuseal
backend: backend:
build: build:
@ -27,15 +58,54 @@ services:
- 8080:8080 - 8080:8080
volumes: volumes:
- ./Back-End:/Back-End - ./Back-End:/Back-End
env_file: "./conf/backend.env" environment:
- TZ=Europe/Paris
- TEST_MODE=True
- CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
- CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
- BASE_URL=http://localhost:3000
links: links:
- "database:database" - "database:database"
- "redis:redis" - "redis:redis"
depends_on: depends_on:
- redis - redis
- database - database
#- docuseal
command: python start.py command: python start.py
# init_docuseal_users:
# build:
# context: .
# dockerfile: Dockerfile
# depends_on:
# - docuseal
# environment:
# DOCUSEAL_DB_HOST: docuseal_db
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# USER_FIRST_NAME: n3wt
# USER_LAST_NAME: school
# USER_COMPANY: n3wt.innov
# USER_EMAIL: n3wt.school@gmail.com
# USER_PASSWORD: n3wt1234
# volumes:
# - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh
# frontend:
# build:
# context: ./Front-End
# args:
# - BUILD_MODE=development
# ports:
# - 3000:3000
# volumes:
# - ./Front-End:/app
# env_file:
# - .env
# environment:
# - TZ=Europe/Paris
# depends_on:
# - backend
volumes: volumes:
postgres-data: caddy_data:
redis-data: caddy_config:

View File

@ -1,94 +0,0 @@
# 🧭 Premiers Pas avec N3WT-SCHOOL
Bienvenue dans **N3WT-SCHOOL** !
Ce guide rapide vous accompagnera dans les premières étapes de configuration de votre instance afin de la rendre pleinement opérationnelle pour votre établissement.
## ✅ Étapes à suivre :
1. **Configurer la signature électronique des documents via Docuseal**
2. **Activer l'envoi d'e-mails depuis la plateforme**
---
## ✍️ 1. Configuration de la signature électronique (Docuseal)
Afin de permettre la signature électronique des documents administratifs (inscriptions, conventions, etc.), N3WT-SCHOOL s'appuie sur [**Docuseal**](https://docuseal.com), un service sécurisé de signature électronique.
### Étapes :
1. Connectez-vous ou créez un compte sur Docuseal :
👉 [https://docuseal.com/sign_in](https://docuseal.com/sign_in)
2. Une fois connecté, accédez à la section API :
👉 [https://console.docuseal.com/api](https://console.docuseal.com/api)
3. Copiez votre **X-Auth-Token** personnel.
Ce jeton permettra à N3WT-SCHOOL de se connecter à votre compte Docuseal.
4. **Envoyez votre X-Auth-Token à l'équipe N3WT-SCHOOL** pour qu'un administrateur puisse finaliser la configuration :
✉️ Contact : [contact@n3wtschool.com](mailto:contact@n3wtschool.com)
> ⚠️ Cette opération doit impérativement être réalisée par un administrateur N3WT-SCHOOL.
> Ne partagez pas ce token en dehors de ce cadre.
---
## 📧 2. Configuration de l'envoi de-mails
Lenvoi de mails depuis N3WT-SCHOOL est requis pour :
- Notifications aux étudiants
- Accusés de réception
- Envoi de documents (factures, conventions…)
Vous devrez renseigner les informations de votre fournisseur SMTP dans **Paramètres > E-mail** de lapplication.
### Informations requises :
- Hôte SMTP
- Port SMTP
- Type de sécurité (TLS / SSL)
- Adresse e-mail (utilisateur SMTP)
- Mot de passe ou **mot de passe applicatif**
---
## 🔐 Mot de passe applicatif (Gmail, Outlook, etc.)
Certains fournisseurs (notamment **Gmail**, **Yahoo**, **iCloud**) ne permettent pas dutiliser directement votre mot de passe personnel pour des applications tierces.
Vous devez créer un **mot de passe applicatif**.
### Exemple : Créer un mot de passe applicatif avec Gmail
1. Connectez-vous à [votre compte Google](https://myaccount.google.com)
2. Allez dans **Sécurité > Validation en 2 étapes**
3. Activez la validation en 2 étapes si ce nest pas déjà fait
4. Ensuite, allez dans **Mots de passe des applications**
5. Sélectionnez une application (ex. : "Autre (personnalisée)") et nommez-la "N3WT-SCHOOL"
6. Copiez le mot de passe généré et utilisez-le comme **mot de passe SMTP**
> 📎 Vous pouvez consulter laide officielle de Google ici :
> [Créer un mot de passe dapplication Google](https://support.google.com/accounts/answer/185833)
---
## 🗂️ Configuration SMTP — Fournisseurs courants
| Fournisseur | SMTP Host | Port TLS | Port SSL | Sécurité | Lien aide SMTP |
| ----------------- | ------------------- | -------- | -------- | -------- | ---------------------------------------------------------------------------- |
| Gmail | smtp.gmail.com | 587 | 465 | TLS/SSL | [Aide SMTP Gmail](https://support.google.com/mail/answer/7126229?hl=fr) |
| Outlook / Hotmail | smtp.office365.com | 587 | — | TLS | [Aide SMTP Outlook](https://support.microsoft.com/fr-fr/office) |
| Yahoo Mail | smtp.mail.yahoo.com | 587 | 465 | TLS/SSL | [Aide SMTP Yahoo](https://help.yahoo.com/kb/SLN4724.html) |
| iCloud Mail | smtp.mail.me.com | 587 | 465 | TLS/SSL | [Aide iCloud SMTP](https://support.apple.com/fr-fr/HT202304) |
| OVH | ssl0.ovh.net | 587 | 465 | TLS/SSL | [Aide OVH SMTP](https://help.ovhcloud.com/csm/fr-email-general-settings) |
| Infomaniak | mail.infomaniak.com | 587 | 465 | TLS/SSL | [Aide SMTP Infomaniak](https://www.infomaniak.com/fr/support/faq/1817) |
| Gandi | mail.gandi.net | 587 | 465 | TLS/SSL | [Aide SMTP Gandi](https://docs.gandi.net/fr/mail/faq/envoyer_des_mails.html) |
> 📝 Si votre fournisseur ne figure pas dans cette liste, n'hésitez pas à contacter votre fournisseur de mail pour obtenir ces informations.
---
## 🎉 Vous êtes prêt·e !
Une fois ces deux configurations effectuées, votre instance N3WT-SCHOOL est prête à fonctionner pleinement.
Vous pourrez ensuite ajouter vos formations, étudiants, documents et automatiser toute votre gestion scolaire.

View File

@ -1,6 +1,6 @@
{ {
"name": "n3wt-school", "name": "n3wt-school",
"version": "0.0.3", "version": "0.0.2",
"scripts": { "scripts": {
"prepare": "husky", "prepare": "husky",
"release": "standard-version", "release": "standard-version",