mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
Compare commits
6 Commits
A63_TestsU
...
5e62ee5100
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e62ee5100 | |||
| e89d2fc4c3 | |||
| 9481a0132d | |||
| 482e8c1357 | |||
| 0e0141d155 | |||
| 7f002e2e6a |
@ -1,12 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Profile
|
|
||||||
|
|
||||||
class ProfileModelTest(TestCase):
|
|
||||||
def test_create_profile(self):
|
|
||||||
user = Profile.objects.create_user(
|
|
||||||
username="testuser",
|
|
||||||
email="test@example.com",
|
|
||||||
password="testpass123"
|
|
||||||
)
|
|
||||||
self.assertEqual(user.email, "test@example.com")
|
|
||||||
self.assertTrue(user.check_password("testpass123"))
|
|
||||||
@ -1,11 +1,3 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from .models import Domain
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
||||||
class DomainModelTest(TestCase):
|
|
||||||
def test_create_domain(self):
|
|
||||||
domain = Domain.objects.create(name="Mathématiques", cycle=1)
|
|
||||||
self.assertEqual(domain.name, "Mathématiques")
|
|
||||||
self.assertEqual(domain.cycle, 1)
|
|
||||||
self.assertIsNotNone(domain.id)
|
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Establishment, StructureType, EvaluationFrequency
|
|
||||||
|
|
||||||
class EstablishmentModelTest(TestCase):
|
|
||||||
def test_create_establishment(self):
|
|
||||||
est = Establishment.objects.create(
|
|
||||||
name="École Test",
|
|
||||||
address="1 rue de l'École",
|
|
||||||
total_capacity=100,
|
|
||||||
establishment_type=[StructureType.PRIMAIRE],
|
|
||||||
evaluation_frequency=EvaluationFrequency.TRIMESTER,
|
|
||||||
licence_code="ABC123"
|
|
||||||
)
|
|
||||||
self.assertEqual(est.name, "École Test")
|
|
||||||
self.assertEqual(est.establishment_type, [StructureType.PRIMAIRE])
|
|
||||||
self.assertTrue(est.is_active)
|
|
||||||
self.assertIsNotNone(est.created_at)
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Conversation
|
|
||||||
|
|
||||||
class ConversationModelTest(TestCase):
|
|
||||||
def test_create_conversation(self):
|
|
||||||
conv = Conversation.objects.create(
|
|
||||||
name="Groupe Test",
|
|
||||||
conversation_type="group"
|
|
||||||
)
|
|
||||||
self.assertEqual(conv.name, "Groupe Test")
|
|
||||||
self.assertEqual(conv.conversation_type, "group")
|
|
||||||
self.assertTrue(conv.is_active)
|
|
||||||
self.assertIsNotNone(conv.id)
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Notification, TypeNotif
|
|
||||||
from Auth.models import Profile
|
|
||||||
|
|
||||||
class NotificationModelTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = Profile.objects.create_user(
|
|
||||||
username="notifuser",
|
|
||||||
email="notif@example.com",
|
|
||||||
password="testpass123"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_notification(self):
|
|
||||||
notif = Notification.objects.create(
|
|
||||||
user=self.user,
|
|
||||||
message="Un message a été reçu",
|
|
||||||
typeNotification=TypeNotif.NOTIF_MESSAGE
|
|
||||||
)
|
|
||||||
self.assertEqual(notif.user, self.user)
|
|
||||||
self.assertEqual(notif.message, "Un message a été reçu")
|
|
||||||
self.assertFalse(notif.is_read)
|
|
||||||
self.assertEqual(notif.typeNotification, TypeNotif.NOTIF_MESSAGE)
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Planning
|
|
||||||
from Establishment.models import Establishment
|
|
||||||
from School.models import SchoolClass
|
|
||||||
|
|
||||||
class PlanningModelTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.establishment = Establishment.objects.create(
|
|
||||||
name="École Test",
|
|
||||||
address="1 rue de l'École",
|
|
||||||
total_capacity=100,
|
|
||||||
establishment_type=[1],
|
|
||||||
evaluation_frequency=1,
|
|
||||||
licence_code="ABC123"
|
|
||||||
)
|
|
||||||
self.school_class = SchoolClass.objects.create(
|
|
||||||
atmosphere_name="Classe Test",
|
|
||||||
establishment=self.establishment
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_planning(self):
|
|
||||||
planning = Planning.objects.create(
|
|
||||||
school_class=self.school_class,
|
|
||||||
establishment=self.establishment
|
|
||||||
)
|
|
||||||
self.assertEqual(planning.school_class, self.school_class)
|
|
||||||
self.assertEqual(planning.establishment, self.establishment)
|
|
||||||
@ -1,77 +1,3 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from .models import Speciality, Teacher, SchoolClass, Planning, Discount, Fee, PaymentPlan, PaymentMode, Competency, EstablishmentCompetency
|
|
||||||
from Establishment.models import Establishment
|
|
||||||
from Common.models import Category, PaymentPlanType, PaymentModeType
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
class SpecialityModelTest(TestCase):
|
# Create your tests here.
|
||||||
def test_create_speciality(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
speciality = Speciality.objects.create(name="Maths", establishment=est)
|
|
||||||
self.assertEqual(speciality.name, "Maths")
|
|
||||||
self.assertEqual(speciality.establishment, est)
|
|
||||||
|
|
||||||
class TeacherModelTest(TestCase):
|
|
||||||
def test_create_teacher(self):
|
|
||||||
teacher = Teacher.objects.create(last_name="Martin", first_name="Paul")
|
|
||||||
self.assertEqual(teacher.last_name, "Martin")
|
|
||||||
self.assertEqual(teacher.first_name, "Paul")
|
|
||||||
|
|
||||||
class SchoolClassModelTest(TestCase):
|
|
||||||
def test_create_school_class(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
school_class = SchoolClass.objects.create(atmosphere_name="Classe A", establishment=est)
|
|
||||||
self.assertEqual(school_class.atmosphere_name, "Classe A")
|
|
||||||
self.assertEqual(school_class.establishment, est)
|
|
||||||
|
|
||||||
class PlanningModelTest(TestCase):
|
|
||||||
def test_create_planning(self):
|
|
||||||
school_class = SchoolClass.objects.create(atmosphere_name="Classe B", establishment=Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A"))
|
|
||||||
planning = Planning.objects.create(school_class=school_class)
|
|
||||||
self.assertEqual(planning.school_class, school_class)
|
|
||||||
|
|
||||||
class DiscountModelTest(TestCase):
|
|
||||||
def test_create_discount(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
discount = Discount.objects.create(name="Réduction", establishment=est)
|
|
||||||
self.assertEqual(discount.name, "Réduction")
|
|
||||||
self.assertEqual(discount.establishment, est)
|
|
||||||
|
|
||||||
class FeeModelTest(TestCase):
|
|
||||||
def test_create_fee(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
fee = Fee.objects.create(name="Frais", establishment=est)
|
|
||||||
self.assertEqual(fee.name, "Frais")
|
|
||||||
self.assertEqual(fee.establishment, est)
|
|
||||||
|
|
||||||
class PaymentPlanModelTest(TestCase):
|
|
||||||
def test_create_payment_plan(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
plan_type = PaymentPlanType.objects.create(code="A", label="Plan A")
|
|
||||||
payment_plan = PaymentPlan.objects.create(plan_type=plan_type, establishment=est)
|
|
||||||
self.assertEqual(payment_plan.plan_type, plan_type)
|
|
||||||
self.assertEqual(payment_plan.establishment, est)
|
|
||||||
|
|
||||||
class PaymentModeModelTest(TestCase):
|
|
||||||
def test_create_payment_mode(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
mode_type = PaymentModeType.objects.create(label="Espèces")
|
|
||||||
payment_mode = PaymentMode.objects.create(mode=mode_type, establishment=est)
|
|
||||||
self.assertEqual(payment_mode.mode, mode_type)
|
|
||||||
self.assertEqual(payment_mode.establishment, est)
|
|
||||||
|
|
||||||
class CompetencyModelTest(TestCase):
|
|
||||||
def test_create_competency(self):
|
|
||||||
cat = Category.objects.create(name="Maths", domain_id=1)
|
|
||||||
comp = Competency.objects.create(name="Compétence 1", category=cat)
|
|
||||||
self.assertEqual(comp.name, "Compétence 1")
|
|
||||||
self.assertEqual(comp.category, cat)
|
|
||||||
|
|
||||||
class EstablishmentCompetencyModelTest(TestCase):
|
|
||||||
def test_create_establishment_competency(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
cat = Category.objects.create(name="Maths", domain_id=1)
|
|
||||||
comp = Competency.objects.create(name="Compétence 2", category=cat)
|
|
||||||
est_comp = EstablishmentCompetency.objects.create(establishment=est, competency=comp)
|
|
||||||
self.assertEqual(est_comp.establishment, est)
|
|
||||||
self.assertEqual(est_comp.competency, comp)
|
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from .models import Language, Guardian, Sibling, BilanCompetence, Student, RegistrationFileGroup, RegistrationForm, RegistrationSchoolFileMaster, RegistrationParentFileMaster, RegistrationSchoolFileTemplate, StudentCompetency, RegistrationParentFileTemplate, AbsenceManagement
|
|
||||||
from django.utils import timezone
|
|
||||||
from datetime import date
|
|
||||||
from Establishment.models import Establishment
|
|
||||||
|
|
||||||
class LanguageModelTest(TestCase):
|
|
||||||
def test_create_language(self):
|
|
||||||
lang = Language.objects.create(label="Français")
|
|
||||||
self.assertEqual(lang.label, "Français")
|
|
||||||
self.assertIsNotNone(lang.id)
|
|
||||||
|
|
||||||
class GuardianModelTest(TestCase):
|
|
||||||
def test_create_guardian(self):
|
|
||||||
guardian = Guardian.objects.create(last_name="Dupont", first_name="Jean")
|
|
||||||
self.assertEqual(guardian.last_name, "Dupont")
|
|
||||||
self.assertEqual(guardian.first_name, "Jean")
|
|
||||||
|
|
||||||
class SiblingModelTest(TestCase):
|
|
||||||
def test_create_sibling(self):
|
|
||||||
sibling = Sibling.objects.create(last_name="Martin", first_name="Julie")
|
|
||||||
self.assertEqual(sibling.last_name, "Martin")
|
|
||||||
self.assertEqual(sibling.first_name, "Julie")
|
|
||||||
|
|
||||||
class BilanCompetenceModelTest(TestCase):
|
|
||||||
def test_create_bilan(self):
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
bilan = BilanCompetence.objects.create(student=student, period="T1-2024_2025")
|
|
||||||
self.assertEqual(bilan.student, student)
|
|
||||||
self.assertEqual(bilan.period, "T1-2024_2025")
|
|
||||||
|
|
||||||
class StudentModelTest(TestCase):
|
|
||||||
def test_create_student(self):
|
|
||||||
student = Student.objects.create(last_name="Durand", first_name="Paul")
|
|
||||||
self.assertEqual(student.last_name, "Durand")
|
|
||||||
self.assertEqual(student.first_name, "Paul")
|
|
||||||
|
|
||||||
class RegistrationFileGroupModelTest(TestCase):
|
|
||||||
def test_create_group(self):
|
|
||||||
group = RegistrationFileGroup.objects.create(name="Groupe A")
|
|
||||||
self.assertEqual(group.name, "Groupe A")
|
|
||||||
|
|
||||||
class RegistrationFormModelTest(TestCase):
|
|
||||||
def test_create_registration_form(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
group = RegistrationFileGroup.objects.create(name="Groupe B", establishment=est)
|
|
||||||
form = RegistrationForm.objects.create(student=student, fileGroup=group, establishment=est)
|
|
||||||
self.assertEqual(form.student, student)
|
|
||||||
self.assertEqual(form.fileGroup, group)
|
|
||||||
self.assertEqual(form.establishment, est)
|
|
||||||
|
|
||||||
class RegistrationSchoolFileMasterModelTest(TestCase):
|
|
||||||
def test_create_school_file_master(self):
|
|
||||||
master = RegistrationSchoolFileMaster.objects.create(id=1, name="Doc école")
|
|
||||||
self.assertEqual(master.name, "Doc école")
|
|
||||||
|
|
||||||
class RegistrationParentFileMasterModelTest(TestCase):
|
|
||||||
def test_create_parent_file_master(self):
|
|
||||||
master = RegistrationParentFileMaster.objects.create(name="Doc parent")
|
|
||||||
self.assertEqual(master.name, "Doc parent")
|
|
||||||
|
|
||||||
class RegistrationSchoolFileTemplateModelTest(TestCase):
|
|
||||||
def test_create_school_file_template(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
master = RegistrationSchoolFileMaster.objects.create(id=2, name="Doc école 2")
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
group = RegistrationFileGroup.objects.create(name="Groupe C", establishment=est)
|
|
||||||
form = RegistrationForm.objects.create(student=student, fileGroup=group, establishment=est)
|
|
||||||
template = RegistrationSchoolFileTemplate.objects.create(id=2, master=master, name="Fichier école", registration_form=form)
|
|
||||||
self.assertEqual(template.name, "Fichier école")
|
|
||||||
self.assertEqual(template.master, master)
|
|
||||||
|
|
||||||
class StudentCompetencyModelTest(TestCase):
|
|
||||||
def test_create_student_competency(self):
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
# Pour le test, on suppose qu'un modèle School.EstablishmentCompetency existe et est importable
|
|
||||||
# Ici, on ne peut pas créer l'objet sans ce modèle, donc ce test est à adapter selon la base réelle
|
|
||||||
pass
|
|
||||||
|
|
||||||
class RegistrationParentFileTemplateModelTest(TestCase):
|
|
||||||
def test_create_parent_file_template(self):
|
|
||||||
est = Establishment.objects.create(name="École Test", address="1 rue", total_capacity=10, establishment_type=[1], evaluation_frequency=1, licence_code="A")
|
|
||||||
master = RegistrationParentFileMaster.objects.create(name="Doc parent 2")
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
group = RegistrationFileGroup.objects.create(name="Groupe D", establishment=est)
|
|
||||||
form = RegistrationForm.objects.create(student=student, fileGroup=group, establishment=est)
|
|
||||||
template = RegistrationParentFileTemplate.objects.create(master=master, registration_form=form)
|
|
||||||
self.assertEqual(template.master, master)
|
|
||||||
self.assertEqual(template.registration_form, form)
|
|
||||||
|
|
||||||
class AbsenceManagementModelTest(TestCase):
|
|
||||||
def test_create_absence(self):
|
|
||||||
student = Student.objects.create(last_name="Test", first_name="Eleve")
|
|
||||||
absence = AbsenceManagement.objects.create(student=student, day=date.today())
|
|
||||||
self.assertEqual(absence.student, student)
|
|
||||||
self.assertEqual(absence.day, date.today())
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
[pytest]
|
|
||||||
DJANGO_SETTINGS_MODULE = N3wtSchool.settings
|
|
||||||
python_files = tests.py test_*.py *_tests.py
|
|
||||||
@ -66,10 +66,8 @@ 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
|
||||||
daphne==4.1.0
|
daphne==4.1.0
|
||||||
pytest
|
|
||||||
pytest-django
|
|
||||||
pytest-json-report
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
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)
|
||||||
@ -11,6 +12,7 @@ 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"],
|
||||||
@ -32,6 +34,17 @@ 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)
|
||||||
@ -41,14 +54,35 @@ for command in commands:
|
|||||||
# if run_command(test_command) != 0:
|
# if run_command(test_command) != 0:
|
||||||
# exit(1)
|
# exit(1)
|
||||||
|
|
||||||
# Lancer les processus en parallèle
|
if watch_mode:
|
||||||
|
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(["daphne", "-b", "0.0.0.0", "-p", "8080", "N3wtSchool.asgi:application"]),
|
subprocess.Popen([
|
||||||
|
"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:
|
||||||
# Attendre la fin des processus
|
for process in processes:
|
||||||
|
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()
|
||||||
26
Front-End/package-lock.json
generated
26
Front-End/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n3wt-school-front-end",
|
"name": "n3wt-school-front-end",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"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,6 +29,7 @@
|
|||||||
"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"
|
||||||
@ -8834,6 +8835,21 @@
|
|||||||
"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",
|
||||||
@ -17160,6 +17176,12 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -35,19 +35,20 @@
|
|||||||
"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/react": "^13.4.0",
|
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
"@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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/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/Button';
|
import Button from '@/components/Form/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/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCsrfToken } from '@/context/CsrfContext';
|
import { useCsrfToken } from '@/context/CsrfContext';
|
||||||
|
|
||||||
|
|||||||
@ -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/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import GradeView from '@/components/Grades/GradeView';
|
import GradeView from '@/components/Grades/GradeView';
|
||||||
import {
|
import {
|
||||||
fetchStudentCompetencies,
|
fetchStudentCompetencies,
|
||||||
|
|||||||
@ -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/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
|
import CheckBox from '@/components/Form/CheckBox'; // Import du composant CheckBox
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
fetchSmtpSettings,
|
fetchSmtpSettings,
|
||||||
|
|||||||
@ -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/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import {
|
import {
|
||||||
fetchAbsences,
|
fetchAbsences,
|
||||||
createAbsences,
|
createAbsences,
|
||||||
|
|||||||
@ -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/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/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/InputPhone';
|
import InputPhone from '@/components/Form/InputPhone';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import RadioList from '@/components/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/PhoneLabel';
|
import { PhoneLabel } from '@/components/Form/PhoneLabel';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/Form/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,7 +250,12 @@ 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
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
'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/Button';
|
import Button from '@/components/Form/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');
|
||||||
@ -13,6 +15,7 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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/FileUpload';
|
import FileUpload from '@/components/Form/FileUpload';
|
||||||
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||||
import {
|
import {
|
||||||
fetchChildren,
|
fetchChildren,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
|
||||||
|
|||||||
@ -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/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||||
import Button from '@/components/Button'; // Importez le composant Button
|
import Button from '@/components/Form/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,11 +35,7 @@ export default function Page() {
|
|||||||
logger.debug('Sign In Result', result);
|
logger.debug('Sign In Result', result);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
showNotification(
|
showNotification(result.error, 'error', 'Erreur');
|
||||||
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
|
||||||
@ -50,11 +46,7 @@ export default function Page() {
|
|||||||
if (url) {
|
if (url) {
|
||||||
router.push(url);
|
router.push(url);
|
||||||
} else {
|
} else {
|
||||||
showNotification(
|
showNotification('Type de rôle non géré', 'error', 'Erreur');
|
||||||
'Type de rôle non géré',
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@ -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/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/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,25 +25,13 @@ export default function Page() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
logger.debug('Success:', data);
|
logger.debug('Success:', data);
|
||||||
if (data.message !== '') {
|
if (data.message !== '') {
|
||||||
showNotification(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
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(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
data.errorMessage,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
} else if (data.errorFields) {
|
} else if (data.errorFields) {
|
||||||
showNotification(
|
showNotification(data.errorFields.email, 'error', 'Erreur');
|
||||||
data.errorFields.email,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@ -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/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/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,21 +33,12 @@ 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(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
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(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
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,
|
||||||
|
|||||||
@ -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/InputTextIcon';
|
import InputTextIcon from '@/components/Form/InputTextIcon';
|
||||||
import Loader from '@/components/Loader';
|
import Loader from '@/components/Loader';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/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,22 +36,16 @@ export default function Page() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
logger.debug('Success:', data);
|
logger.debug('Success:', data);
|
||||||
if (data.message !== '') {
|
if (data.message !== '') {
|
||||||
showNotification(
|
showNotification(data.message, 'success', 'Succès');
|
||||||
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(
|
showNotification(data.errorMessage, 'error', 'Erreur');
|
||||||
data.errorMessage,
|
|
||||||
'error',
|
|
||||||
'Erreur'
|
|
||||||
);
|
|
||||||
} else if (data.errorFields) {
|
} else if (data.errorFields) {
|
||||||
showNotification(
|
showNotification(
|
||||||
data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
|
data.errorFields.email ||
|
||||||
|
data.errorFields.password1 ||
|
||||||
|
data.errorFields.password2,
|
||||||
'error',
|
'error',
|
||||||
'Erreur'
|
'Erreur'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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/WisiwigTextArea';
|
import WisiwigTextArea from '@/components/Form/WisiwigTextArea';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
export default function EmailSender({ csrfToken }) {
|
export default function EmailSender({ csrfToken }) {
|
||||||
const [recipients, setRecipients] = useState([]);
|
const [recipients, setRecipients] = useState([]);
|
||||||
|
|||||||
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
589
Front-End/src/components/Form/AddFieldModal.js
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ const CheckBox = ({
|
|||||||
handleChange,
|
handleChange,
|
||||||
fieldName,
|
fieldName,
|
||||||
itemLabelFunc = () => null,
|
itemLabelFunc = () => null,
|
||||||
horizontal,
|
horizontal = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 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"
|
className="block text-sm text-center mb-1 font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{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 text-center mb-1 font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
{itemLabelFunc(item)}
|
{itemLabelFunc(item)}
|
||||||
</label>
|
</label>
|
||||||
443
Front-End/src/components/Form/FormRenderer.js
Normal file
443
Front-End/src/components/Form/FormRenderer.js
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
616
Front-End/src/components/Form/FormTemplateBuilder.js
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
Front-End/src/components/Form/FormTypes.js
Normal file
19
Front-End/src/components/Form/FormTypes.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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' },
|
||||||
|
];
|
||||||
145
Front-End/src/components/Form/IconSelector.js
Normal file
145
Front-End/src/components/Form/IconSelector.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default function InputTextIcon({
|
export default function InputTextIcon({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@ -31,9 +33,11 @@ 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}
|
||||||
@ -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 = 'Mail',
|
label = 'Zone de Texte',
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = 'Ecrivez votre mail ici...',
|
placeholder = 'Ecrivez votre texte ici...',
|
||||||
className = 'h-64',
|
className = 'h-64',
|
||||||
required = false,
|
required = false,
|
||||||
errorMsg,
|
errorMsg,
|
||||||
@ -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/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import Popup from '@/components/Popup';
|
import Popup from '@/components/Popup';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
|
||||||
|
|||||||
@ -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/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
|
|
||||||
const LEVELS = [
|
const LEVELS = [
|
||||||
{ value: 0, label: 'Non évalué' },
|
{ value: 0, label: 'Non évalué' },
|
||||||
|
|||||||
@ -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/FileUpload';
|
import FileUpload from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||||
import {
|
import {
|
||||||
fetchSchoolFileTemplatesFromRegistrationFiles,
|
fetchSchoolFileTemplatesFromRegistrationFiles,
|
||||||
@ -220,9 +220,7 @@ export default function InscriptionFormShared({
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setProfiles(data);
|
setProfiles(data);
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) => logger.error('Error fetching profiles : ', error));
|
||||||
logger.error('Error fetching profiles : ', error)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedEstablishmentId) {
|
if (selectedEstablishmentId) {
|
||||||
// Fetch data for registration payment modes
|
// Fetch data for registration payment modes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import RadioList from '@/components/RadioList';
|
import RadioList from '@/components/Form/RadioList';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
export default function PaymentMethodSelector({
|
export default function PaymentMethodSelector({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import InputPhone from '@/components/InputPhone';
|
import InputPhone from '@/components/Form/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';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/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';
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/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/FileUpload';
|
import FileUpload from '@/components/Form/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,13 +112,10 @@ 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' &&
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/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/Button';
|
import Button from '@/components/Form/Button';
|
||||||
|
|
||||||
export default function ValidateSubscription({
|
export default function ValidateSubscription({
|
||||||
studentId,
|
studentId,
|
||||||
|
|||||||
@ -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/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
|
|
||||||
const paymentPlansOptions = [
|
const paymentPlansOptions = [
|
||||||
{ id: 1, name: '1 fois', frequency: 1 },
|
{ id: 1, name: '1 fois', frequency: 1 },
|
||||||
|
|||||||
@ -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/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import CheckBox from '@/components/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import {
|
import {
|
||||||
fetchEstablishmentCompetencies,
|
fetchEstablishmentCompetencies,
|
||||||
|
|||||||
@ -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/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import SelectChoice from '@/components/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
|
import TeacherItem from '@/components/Structure/Configuration/TeacherItem';
|
||||||
import MultiSelect from '@/components/MultiSelect';
|
import MultiSelect from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/InputTextWithColorIcon';
|
import InputTextWithColorIcon from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/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/InputText';
|
import InputText from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/MultiSelect'; // Import du composant MultiSelect
|
import MultiSelect from '@/components/Form/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,7 +121,13 @@ 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(templateMaster?.id, guardian.email, is_required, selectedEstablishmentId, apiDocuseal)
|
cloneTemplate(
|
||||||
|
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 = {
|
||||||
|
|||||||
@ -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/InputText';
|
import InputText from '@/components/Form/InputText';
|
||||||
import MultiSelect from '@/components/MultiSelect';
|
import MultiSelect from '@/components/Form/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/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import AlertMessage from '@/components/AlertMessage';
|
import AlertMessage from '@/components/AlertMessage';
|
||||||
|
|
||||||
|
|||||||
@ -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/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/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';
|
||||||
|
|||||||
@ -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/CheckBox';
|
import CheckBox from '@/components/Form/CheckBox';
|
||||||
import InputText from '@/components/InputText';
|
import InputText from '@/components/Form/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';
|
||||||
|
|||||||
@ -36,20 +36,6 @@ services:
|
|||||||
- database
|
- database
|
||||||
command: python start.py
|
command: python start.py
|
||||||
|
|
||||||
backend-test:
|
|
||||||
build:
|
|
||||||
context: ./Back-End
|
|
||||||
volumes:
|
|
||||||
- ./Back-End:/Back-End
|
|
||||||
env_file: "./conf/backend.env"
|
|
||||||
links:
|
|
||||||
- "database:database"
|
|
||||||
- "redis:redis"
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
command: python manage.py test
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
redis-data:
|
redis-data:
|
||||||
|
|||||||
94
docs/manuels/installation/premier-pas.md
Normal file
94
docs/manuels/installation/premier-pas.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 🧭 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 d’e-mails
|
||||||
|
|
||||||
|
L’envoi 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 l’application.
|
||||||
|
|
||||||
|
### 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 d’utiliser 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 n’est 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 l’aide officielle de Google ici :
|
||||||
|
> [Créer un mot de passe d’application – 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.
|
||||||
Reference in New Issue
Block a user