From eda6f587fb21bf041784209228518c8a6f03b1b5 Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Mon, 5 May 2025 09:25:07 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Preparation=20des=20mod=C3=A8les=20Sett?= =?UTF-8?q?ings=20pour=20l'enregistrement=20SMTP=20[#17]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - .gitignore | 1 + Back-End/Dockerfile | 6 -- Back-End/N3wtSchool/settings.py | 1 + Back-End/N3wtSchool/urls.py | 1 + Back-End/Planning/models.py | 2 +- Back-End/Settings/__init__.py | 1 + Back-End/Settings/admin.py | 3 + Back-End/Settings/apps.py | 5 + Back-End/Settings/models.py | 17 +++ Back-End/Settings/serializers.py | 7 ++ Back-End/Settings/urls.py | 6 ++ Back-End/Settings/views.py | 55 ++++++++++ Back-End/requirements.txt | Bin 2584 -> 1239 bytes Back-End/start.py | 1 + Front-End/src/app/[locale]/admin/layout.js | 3 +- Front-End/src/app/[locale]/admin/page.js | 8 +- .../src/app/[locale]/admin/settings/page.js | 98 ++++++++++++++++-- .../src/app/[locale]/admin/structure/page.js | 1 + .../subscriptions/createSubscription/page.js | 2 +- .../src/app/[locale]/parents/settings/page.js | 8 +- Front-End/src/app/actions/messagerieAction.js | 1 + Front-End/src/app/actions/settingsAction.js | 37 +++++++ .../src/app/actions/subscriptionAction.js | 2 + .../src/components/Calendar/EventModal.js | 8 +- Front-End/src/components/FlashNotification.js | 70 +++++++++++++ Front-End/src/components/InputTextIcon.js | 2 +- Front-End/src/components/Providers.js | 29 +++--- Front-End/src/components/SidebarTabs.js | 28 +++-- .../Structure/Files/FilesGroupsManagement.js | 77 ++++++++++---- .../Structure/Planning/ScheduleEventModal.js | 22 +++- Front-End/src/context/NotificationContext.js | 36 +++++++ Front-End/src/utils/Url.js | 3 + 33 files changed, 468 insertions(+), 74 deletions(-) delete mode 100644 .env create mode 100644 Back-End/Settings/__init__.py create mode 100644 Back-End/Settings/admin.py create mode 100644 Back-End/Settings/apps.py create mode 100644 Back-End/Settings/models.py create mode 100644 Back-End/Settings/serializers.py create mode 100644 Back-End/Settings/urls.py create mode 100644 Back-End/Settings/views.py create mode 100644 Front-End/src/app/actions/settingsAction.js create mode 100644 Front-End/src/components/FlashNotification.js create mode 100644 Front-End/src/context/NotificationContext.js diff --git a/.env b/.env deleted file mode 100644 index 1951496..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg" \ No newline at end of file diff --git a/.gitignore b/.gitignore index bf46567..3d1c146 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .venv/ +.env node_modules/ hardcoded-strings-report.md \ No newline at end of file diff --git a/Back-End/Dockerfile b/Back-End/Dockerfile index 0aa05ac..25f4136 100644 --- a/Back-End/Dockerfile +++ b/Back-End/Dockerfile @@ -8,14 +8,8 @@ WORKDIR /Back-End # Allows docker to cache installed dependencies between builds COPY requirements.txt requirements.txt RUN pip install -r requirements.txt -RUN pip install pymupdf # Mounts the application code to the image COPY . . EXPOSE 8080 - -ENV DJANGO_SETTINGS_MODULE N3wtSchool.settings -ENV DJANGO_SUPERUSER_PASSWORD=admin -ENV DJANGO_SUPERUSER_USERNAME=admin -ENV DJANGO_SUPERUSER_EMAIL=admin@n3wtschool.com diff --git a/Back-End/N3wtSchool/settings.py b/Back-End/N3wtSchool/settings.py index 14ce667..777f462 100644 --- a/Back-End/N3wtSchool/settings.py +++ b/Back-End/N3wtSchool/settings.py @@ -45,6 +45,7 @@ INSTALLED_APPS = [ 'School.apps.SchoolConfig', 'Planning.apps.PlanningConfig', 'Establishment.apps.EstablishmentConfig', + 'Settings.apps.SettingsConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/Back-End/N3wtSchool/urls.py b/Back-End/N3wtSchool/urls.py index 048a202..93c7293 100644 --- a/Back-End/N3wtSchool/urls.py +++ b/Back-End/N3wtSchool/urls.py @@ -47,6 +47,7 @@ urlpatterns = [ path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')), path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')), path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')), + path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')), # Documentation Api re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), diff --git a/Back-End/Planning/models.py b/Back-End/Planning/models.py index 78d2ce8..86be22f 100644 --- a/Back-End/Planning/models.py +++ b/Back-End/Planning/models.py @@ -14,7 +14,7 @@ class RecursionType(models.IntegerChoices): RECURSION_CUSTOM = 4, _('Personnalisé') class Planning(models.Model): - establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT) + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE) school_class = models.ForeignKey( SchoolClass, on_delete=models.CASCADE, diff --git a/Back-End/Settings/__init__.py b/Back-End/Settings/__init__.py new file mode 100644 index 0000000..6ddb826 --- /dev/null +++ b/Back-End/Settings/__init__.py @@ -0,0 +1 @@ +default_app_config = 'Settings.apps.SettingsConfig' diff --git a/Back-End/Settings/admin.py b/Back-End/Settings/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Back-End/Settings/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Back-End/Settings/apps.py b/Back-End/Settings/apps.py new file mode 100644 index 0000000..32f1bfd --- /dev/null +++ b/Back-End/Settings/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class SettingsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'Settings' diff --git a/Back-End/Settings/models.py b/Back-End/Settings/models.py new file mode 100644 index 0000000..9f3a999 --- /dev/null +++ b/Back-End/Settings/models.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from Establishment.models import Establishment + +class SMTPSettings(models.Model): + establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE) + smtp_server = models.CharField(max_length=255) + smtp_port = models.PositiveIntegerField() + smtp_user = models.CharField(max_length=255) + smtp_password = models.CharField(max_length=255) + use_tls = models.BooleanField(default=True) + use_ssl = models.BooleanField(default=False) + + def __str__(self): + return f"SMTP Settings ({self.smtp_server}:{self.smtp_port})" \ No newline at end of file diff --git a/Back-End/Settings/serializers.py b/Back-End/Settings/serializers.py new file mode 100644 index 0000000..380e5e9 --- /dev/null +++ b/Back-End/Settings/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import SMTPSettings + +class SMTPSettingsSerializer(serializers.ModelSerializer): + class Meta: + model = SMTPSettings + fields = '__all__' \ No newline at end of file diff --git a/Back-End/Settings/urls.py b/Back-End/Settings/urls.py new file mode 100644 index 0000000..a8842b8 --- /dev/null +++ b/Back-End/Settings/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import SMTPSettingsView + +urlpatterns = [ + path('smtp-settings/', SMTPSettingsView.as_view(), name='smtp_settings'), +] \ No newline at end of file diff --git a/Back-End/Settings/views.py b/Back-End/Settings/views.py new file mode 100644 index 0000000..d3612ce --- /dev/null +++ b/Back-End/Settings/views.py @@ -0,0 +1,55 @@ +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from .models import SMTPSettings +from .serializers import SMTPSettingsSerializer +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +class SMTPSettingsView(APIView): + """ + API pour gérer les paramètres SMTP. + """ + + @swagger_auto_schema( + operation_description="Récupérer les paramètres SMTP", + responses={ + 200: SMTPSettingsSerializer(), + 404: openapi.Response(description="Aucun paramètre SMTP trouvé."), + 500: openapi.Response(description="Erreur interne du serveur."), + }, + ) + def get(self, request): + try: + smtp_settings = SMTPSettings.objects.first() + if not smtp_settings: + return Response({'error': 'Aucun paramètre SMTP trouvé.'}, status=status.HTTP_404_NOT_FOUND) + serializer = SMTPSettingsSerializer(smtp_settings) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @swagger_auto_schema( + operation_description="Créer ou mettre à jour les paramètres SMTP", + request_body=SMTPSettingsSerializer, + responses={ + 200: SMTPSettingsSerializer(), + 400: openapi.Response(description="Données invalides."), + 500: openapi.Response(description="Erreur interne du serveur."), + }, + ) + def post(self, request): + data = request.data + try: + smtp_settings = SMTPSettings.objects.first() + if smtp_settings: + serializer = SMTPSettingsSerializer(smtp_settings, data=data) + else: + serializer = SMTPSettingsSerializer(data=data) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index dd546a655a972b441252148e996b49553b493104..68b0d90df54b0fd3380efb1e604f8fb60e6dfe21 100644 GIT binary patch literal 1239 zcmZux!EW0y487}LEZB0~v-VU*I}PZeBu(=1 zy{E|e9QwZFg4aw&IT%$%ohBJ{?E9A2lukzz^?|}i-XL_1ifhQe?;GABGZ<@)jAP&L z${eMwjw?;wpI7@48cN}GqHR{y& zz`*xRQBaL$T>Ua@>H5CrEln%=V3(tDsqFZdj5^r+XF%Ysk8+k2@Av~#(YtEYNkxOh zaC(Dui!|MNj518PXGeS>nNq@qDhuk~5iZSWS~BlsJgRI4O-$hQNH`C_?W6m z%TY%_+!dC#(kzV~{FSmz4dh!o>e1lSJE2=Hn_1ty)78Of3+^AVyxtlhVLsqKeen^L zffy+KzuJ2;bFlj3Qurs9N3WKoC78lEx+Wf~6&9daH2jgh$WzgT_BCwEv+b-I3f0Vv z4wPbD&5Sz`N5&Uv$R;O04+|a9LO*+*v4Z}unFsA(exf)?b&}8zJK-3?0{rDK*g%L- zj(!gLJ$r9Y2DnL;-JXTCDj$xb8jO=y~YlS^8!1-K7zOSW68i@&<=xGI@`hyf$S*$JZ@qVH`%xqZpT&h9BP)= z09D@gHEWlkY;kV`T3No~0!Ux5qVZDthszWfwhYhuZ-J?S#5vS8|~!Vt^Z5#{L3IgL*Rn literal 2584 zcma);PjAyu5X9dZiI0-1aa$+{4oCKQL1zwrv{F_$2c>^4z4Ue0BR6Lk8%uksh3nl+{Y#ZuAb` zPr(2^(Pzkv(>h&;N9%H=sE(M=MvB_#ew5EU`JBmX8y3gGKI>FREWC}RkDzRK^_Nr=Ow6Ts(5e64UXfgVT|Gls*}$k5SUqq>{zw-&QEF zRgR0WAXB~*jm%0t5A9Hl&b<_~hdp{68oRR(@zjAz+w)-$A0p{x`rs+FpNScrO>Ul! zS7L<*R%mGJneBvkO=oE*ADg}d>CSOK^HkvfEHA_uKJ?H`dhZ4kGSp@z{qKZDb$6RN zmRfDZYou-dGyZ*on6aA-ZnW?>$y7}i zRph)DC-&b;$~vR0TlT;VqJNhzbgy(R<+0Io&XrtFd$wzFdQ{AnEap;|Nk9YkmnV^F zcoR3etpg2D?>B1?rqY!WA9da;s;7{6)j&Obupy1`B(jt5^^RIi^%v&$ZEI@lDgG+! zPw9L57Wrg$IeYfWR29tXm(IuTYkHqPh|5q3cMF_4vGV>x1Evog@k{hW9M#046JvK6 zwS-gQQ@lE_nYxC~a)%WDM`BKgbO*Li!J+&{Sz~hfk=~^*>2sXJ&pJ_oTd|^|>?Shc zx5Rw@-6~kQZ@u|Zy%V>b4>JqLv#2eYTk&VRce?S;=}G@dbmre14(-RGouhAMyHu9B zce1)nNk-hO3lHbDOdYDV%Dh&Lts-JijIvicoiZV8PC?Zj-S)2U)F!qUA!eEZ&s6Tfh6-xpTMEWj3xALPM%vl>iN@#kOqKg(=; Ak^lez diff --git a/Back-End/start.py b/Back-End/start.py index 4e08847..af2b0e5 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -16,6 +16,7 @@ commands = [ ["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "flush", "--noinput"], ["python", "manage.py", "makemigrations", "Establishment", "--noinput"], + ["python", "manage.py", "makemigrations", "Settings", "--noinput"], ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"], ["python", "manage.py", "makemigrations", "Planning", "--noinput"], ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"], diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index 1d6fae5..b6ebcc3 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -28,7 +28,7 @@ import { FE_ADMIN_GRADES_URL, FE_ADMIN_PLANNING_URL, FE_ADMIN_SETTINGS_URL, - FE_ADMIN_MESSAGERIE_URL + FE_ADMIN_MESSAGERIE_URL, } from '@/utils/Url'; import { disconnect } from '@/app/actions/authAction'; @@ -38,7 +38,6 @@ import Footer from '@/components/Footer'; import { getRightStr, RIGHTS } from '@/utils/rights'; import { useEstablishment } from '@/context/EstablishmentContext'; - export default function Layout({ children }) { const t = useTranslations('sidebar'); const [isSidebarOpen, setIsSidebarOpen] = useState(false); diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 781b29e..09697ad 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -10,7 +10,7 @@ import logger from '@/utils/logger'; import { fetchRegisterForms } from '@/app/actions/subscriptionAction'; import { fetchUpcomingEvents } from '@/app/actions/planningAction'; import { useEstablishment } from '@/context/EstablishmentContext'; - +import { useNotification } from '@/context/NotificationContext'; // Composant EventCard pour afficher les événements const EventCard = ({ title, date, description, type }) => (
@@ -39,6 +39,7 @@ export default function DashboardPage() { const [classes, setClasses] = useState([]); const { selectedEstablishmentId } = useEstablishment(); + const { showNotification } = useNotification(); useEffect(() => { if (!selectedEstablishmentId) return; @@ -64,6 +65,11 @@ export default function DashboardPage() { }) .catch((error) => { logger.error('Error fetching classes:', error); + showNotification( + 'Error fetching classes: ' + error.message, + 'error', + 'Erreur' + ); }); // Fetch des formulaires d'inscription diff --git a/Front-End/src/app/[locale]/admin/settings/page.js b/Front-End/src/app/[locale]/admin/settings/page.js index f9bcd12..f2655f2 100644 --- a/Front-End/src/app/[locale]/admin/settings/page.js +++ b/Front-End/src/app/[locale]/admin/settings/page.js @@ -1,10 +1,17 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Tab from '@/components/Tab'; import TabContent from '@/components/TabContent'; import Button from '@/components/Button'; import InputText from '@/components/InputText'; import logger from '@/utils/logger'; +import { + fetchSmtpSettings, + editSmtpSettings, +} from '@/app/actions/settingsAction'; +import { useEstablishment } from '@/context/EstablishmentContext'; +import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken +import { useNotification } from '@/context/NotificationContext'; export default function SettingsPage() { const [activeTab, setActiveTab] = useState('structure'); @@ -15,11 +22,35 @@ export default function SettingsPage() { const [smtpPort, setSmtpPort] = useState(''); const [smtpUser, setSmtpUser] = useState(''); const [smtpPassword, setSmtpPassword] = useState(''); - + const [useTls, setUseTls] = useState(true); + const [useSsl, setUseSsl] = useState(false); + const [statusMessage, setStatusMessage] = useState(''); + const { selectedEstablishmentId } = useEstablishment(); + const csrfToken = useCsrfToken(); // Récupération du csrfToken + const { showNotification } = useNotification(); const handleTabClick = (tab) => { setActiveTab(tab); }; + // Charger les paramètres SMTP existants + useEffect(() => { + if (activeTab === 'smtp') { + fetchSmtpSettings(csrfToken) // Passer le csrfToken ici + .then((data) => { + setSmtpServer(data.smtp_server || ''); + setSmtpPort(data.smtp_port || ''); + setSmtpUser(data.smtp_user || ''); + setSmtpPassword(data.smtp_password || ''); + setUseTls(data.use_tls || false); + setUseSsl(data.use_ssl || false); + }) + .catch((error) => { + logger.error('Erreur lors du chargement des paramètres SMTP:', error); + setStatusMessage('Erreur lors du chargement des paramètres SMTP.'); + }); + } + }, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance + const handleEmailChange = (e) => { setEmail(e.target.value); }; @@ -48,24 +79,50 @@ export default function SettingsPage() { setSmtpPassword(e.target.value); }; + const handleUseTlsChange = (e) => { + setUseTls(e.target.checked); + }; + + const handleUseSslChange = (e) => { + setUseSsl(e.target.checked); + }; + const handleSubmit = (e) => { e.preventDefault(); if (password !== confirmPassword) { - alert('Les mots de passe ne correspondent pas'); + showNotification( + 'Les mots de passe ne correspondent pas', + 'error', + 'Erreur' + ); return; } - // Logique pour mettre à jour l'email et le mot de passe - logger.debug('Email:', email); - logger.debug('Password:', password); }; const handleSmtpSubmit = (e) => { e.preventDefault(); - // Logique pour mettre à jour les paramètres SMTP - logger.debug('SMTP Server:', smtpServer); - logger.debug('SMTP Port:', smtpPort); - logger.debug('SMTP User:', smtpUser); - logger.debug('SMTP Password:', smtpPassword); + const smtpData = { + establishment: selectedEstablishmentId, + smtp_server: smtpServer, + smtp_port: smtpPort, + smtp_user: smtpUser, + smtp_password: smtpPassword, + use_tls: useTls, + use_ssl: useSsl, + }; + + editSmtpSettings(smtpData, csrfToken) // Passer le csrfToken ici + .then(() => { + setStatusMessage('Paramètres SMTP mis à jour avec succès.'); + logger.debug('SMTP Settings Updated:', smtpData); + }) + .catch((error) => { + logger.error( + 'Erreur lors de la mise à jour des paramètres SMTP:', + error + ); + setStatusMessage('Erreur lors de la mise à jour des paramètres SMTP.'); + }); }; return ( @@ -128,8 +185,27 @@ export default function SettingsPage() { value={smtpPassword} onChange={handleSmtpPasswordChange} /> +
+ + +
+ {statusMessage &&

{statusMessage}

}
diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 422acb1..ab9c7e5 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,5 +1,6 @@ 'use client'; import React, { useState, useEffect } from 'react'; + import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js index 4c1a159..073cc78 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -160,7 +160,7 @@ export default function CreateSubscriptionPage() { const requestErrorHandler = (err) => { logger.error('Error fetching data:', err); - setErrors(err); + //setErrors(err); }; useEffect(() => { diff --git a/Front-End/src/app/[locale]/parents/settings/page.js b/Front-End/src/app/[locale]/parents/settings/page.js index a6211e4..dd647c0 100644 --- a/Front-End/src/app/[locale]/parents/settings/page.js +++ b/Front-End/src/app/[locale]/parents/settings/page.js @@ -3,11 +3,13 @@ import React, { useState } from 'react'; import Button from '@/components/Button'; import InputText from '@/components/InputText'; import logger from '@/utils/logger'; +import { useNotification } from '@/context/NotificationContext'; export default function SettingsPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const { showNotification } = useNotification(); const handleEmailChange = (e) => { setEmail(e.target.value); @@ -24,7 +26,11 @@ export default function SettingsPage() { const handleSubmit = (e) => { e.preventDefault(); if (password !== confirmPassword) { - alert('Les mots de passe ne correspondent pas'); + showNotification( + 'Les mots de passe ne correspondent pas', + 'error', + 'Erreur' + ); return; } // Logique pour mettre à jour l'email et le mot de passe diff --git a/Front-End/src/app/actions/messagerieAction.js b/Front-End/src/app/actions/messagerieAction.js index 2ae326a..0a25e21 100644 --- a/Front-End/src/app/actions/messagerieAction.js +++ b/Front-End/src/app/actions/messagerieAction.js @@ -9,6 +9,7 @@ const requestResponseHandler = async (response) => { return body; } // Throw an error with the JSON body containing the form errors + const error = new Error(body?.errorMessage || 'Une erreur est survenue'); error.details = body; throw error; diff --git a/Front-End/src/app/actions/settingsAction.js b/Front-End/src/app/actions/settingsAction.js new file mode 100644 index 0000000..8d1229f --- /dev/null +++ b/Front-End/src/app/actions/settingsAction.js @@ -0,0 +1,37 @@ +import { BE_SETTINGS_SMTP_URL } from '@/utils/Url'; + +export const PENDING = 'pending'; +export const SUBSCRIBED = 'subscribed'; +export const ARCHIVED = 'archived'; + +const requestResponseHandler = async (response) => { + const body = await response.json(); + if (response.ok) { + return body; + } + // Throw an error with the JSON body containing the form errors + const error = new Error(body?.errorMessage || 'Une erreur est survenue'); + error.details = body; + throw error; +}; + +export const fetchSmtpSettings = (csrfToken) => { + return fetch(`${BE_SETTINGS_SMTP_URL}/`, { + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + }).then(requestResponseHandler); +}; + +export const editSmtpSettings = (data, csrfToken) => { + return fetch(`${BE_SETTINGS_SMTP_URL}/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(data), + credentials: 'include', + }).then(requestResponseHandler); +}; diff --git a/Front-End/src/app/actions/subscriptionAction.js b/Front-End/src/app/actions/subscriptionAction.js index 1d53db9..2a59601 100644 --- a/Front-End/src/app/actions/subscriptionAction.js +++ b/Front-End/src/app/actions/subscriptionAction.js @@ -7,6 +7,7 @@ import { } from '@/utils/Url'; import { CURRENT_YEAR_FILTER } from '@/utils/constants'; +import { useNotification } from '@/context/NotificationContext'; const requestResponseHandler = async (response) => { const body = await response.json(); @@ -16,6 +17,7 @@ const requestResponseHandler = async (response) => { // Throw an error with the JSON body containing the form errors const error = new Error(body?.errorMessage || 'Une erreur est survenue'); error.details = body; + showNotification('Une erreur inattendue est survenue.', 'error', 'Erreur'); throw error; }; diff --git a/Front-End/src/components/Calendar/EventModal.js b/Front-End/src/components/Calendar/EventModal.js index 391d625..3375acb 100644 --- a/Front-End/src/components/Calendar/EventModal.js +++ b/Front-End/src/components/Calendar/EventModal.js @@ -1,6 +1,7 @@ import { usePlanning, RecurrenceType } from '@/context/PlanningContext'; import { format } from 'date-fns'; import React from 'react'; +import { useNotification } from '@/context/NotificationContext'; export default function EventModal({ isOpen, @@ -10,6 +11,7 @@ export default function EventModal({ }) { const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning(); + const { showNotification } = useNotification(); // S'assurer que planning est défini lors du premier rendu React.useEffect(() => { @@ -46,7 +48,11 @@ export default function EventModal({ e.preventDefault(); if (!eventData.planning) { - alert('Veuillez sélectionner un planning'); + showNotification( + 'Veuillez sélectionner un planning', + 'warning', + 'Attention' + ); return; } diff --git a/Front-End/src/components/FlashNotification.js b/Front-End/src/components/FlashNotification.js new file mode 100644 index 0000000..722ddd0 --- /dev/null +++ b/Front-End/src/components/FlashNotification.js @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; + +const typeStyles = { + success: { + icon: , + bg: 'bg-green-300', + }, + error: { + icon: , + bg: 'bg-red-300', + }, + info: { + icon: , + bg: 'bg-blue-300', + }, + warning: { + icon: , + bg: 'bg-yellow-300', + }, +}; + +export default function FlashNotification({ + title, + message, + type = 'info', + onClose, +}) { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(false); // Déclenche la disparition + setTimeout(onClose, 300); // Appelle onClose après l'animation + }, 3000); // Notification visible pendant 3 secondes + return () => clearTimeout(timer); + }, [onClose]); + + if (!message || !isVisible) return null; + + const { icon, bg } = typeStyles[type] || typeStyles.info; + + return ( + + {/* Rectangle gauche avec l'icône */} +
+ {icon} +
+ {/* Zone de texte */} +
+

{title}

+

{message}

+
+ {/* Bouton de fermeture */} + +
+ ); +} diff --git a/Front-End/src/components/InputTextIcon.js b/Front-End/src/components/InputTextIcon.js index 42c1f42..1f54ed7 100644 --- a/Front-End/src/components/InputTextIcon.js +++ b/Front-End/src/components/InputTextIcon.js @@ -31,7 +31,7 @@ export default function InputTextIcon({ !enable ? 'bg-gray-100 cursor-not-allowed' : '' }`} > - + {IconItem && } - - - - - - {children} - - - - - - + + + + + + + + {children} + + + + + + + ); } diff --git a/Front-End/src/components/SidebarTabs.js b/Front-End/src/components/SidebarTabs.js index 828dc5b..36ff68a 100644 --- a/Front-End/src/components/SidebarTabs.js +++ b/Front-End/src/components/SidebarTabs.js @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; const SidebarTabs = ({ tabs, onTabChange }) => { const [activeTab, setActiveTab] = useState(tabs[0].id); @@ -30,15 +31,24 @@ const SidebarTabs = ({ tabs, onTabChange }) => { {/* Tabs Content */} -
- {tabs.map((tab) => ( -
- {tab.content} -
- ))} +
+ + {tabs.map( + (tab) => + activeTab === tab.id && ( + + {tab.content} + + ) + )} +
); diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js index 840fec3..25261eb 100644 --- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js +++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js @@ -29,6 +29,7 @@ import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection' import SectionHeader from '@/components/SectionHeader'; import Popup from '@/components/Popup'; import Loader from '@/components/Loader'; +import { useNotification } from '@/context/NotificationContext'; export default function FilesGroupsManagement({ csrfToken, @@ -52,6 +53,7 @@ export default function FilesGroupsManagement({ const [removePopupMessage, setRemovePopupMessage] = useState(''); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [isLoading, setIsLoading] = useState(false); + const { showNotification } = useNotification(); const handleReloadTemplates = () => { setReloadTemplates(true); @@ -136,15 +138,20 @@ export default function FilesGroupsManagement({ (fichier) => fichier.id !== templateMaster.id ) ); - setPopupMessage( - `Le document "${templateMaster.name}" a été correctement supprimé.` + showNotification( + `Le document "${templateMaster.name}" a été correctement supprimé.`, + 'success', + 'Succès' ); + setPopupVisible(true); setRemovePopupVisible(false); setIsLoading(false); } else { - setPopupMessage( - `Erreur lors de la suppression du document "${templateMaster.name}".` + showNotification( + `Erreur lors de la suppression du document "${templateMaster.name}".`, + 'error', + 'Erreur' ); setPopupVisible(true); setRemovePopupVisible(false); @@ -153,16 +160,20 @@ export default function FilesGroupsManagement({ }) .catch((error) => { console.error('Error deleting file from database:', error); - setPopupMessage( - `Erreur lors de la suppression du document "${templateMaster.name}".` + showNotification( + `Erreur lors de la suppression du document "${templateMaster.name}".`, + 'error', + 'Erreur' ); setPopupVisible(true); setRemovePopupVisible(false); setIsLoading(false); }); } else { - setPopupMessage( - `Erreur lors de la suppression du document "${templateMaster.name}".` + showNotification( + `Erreur lors de la suppression du document "${templateMaster.name}".`, + 'error', + 'Erreur' ); setPopupVisible(true); setRemovePopupVisible(false); @@ -171,8 +182,10 @@ export default function FilesGroupsManagement({ }) .catch((error) => { console.error('Error removing template from DocuSeal:', error); - setPopupMessage( - `Erreur lors de la suppression du document "${templateMaster.name}".` + showNotification( + `Erreur lors de la suppression du document "${templateMaster.name}".`, + 'error', + 'Erreur' ); setPopupVisible(true); setRemovePopupVisible(false); @@ -253,7 +266,11 @@ export default function FilesGroupsManagement({ }) .catch((error) => { console.error('Error editing file:', error); - alert('Erreur lors de la modification du fichier'); + showNotification( + 'Erreur lors de la modification du fichier', + 'error', + 'Erreur' + ); }); }; @@ -271,7 +288,11 @@ export default function FilesGroupsManagement({ }) .catch((error) => { console.error('Error handling group:', error); - alert("Erreur lors de l'opération sur le groupe"); + showNotification( + "Erreur lors de l'opération sur le groupe", + 'error', + 'Erreur' + ); }); } else { // Ajouter l'établissement sélectionné lors de la création d'un nouveau groupe @@ -287,7 +308,11 @@ export default function FilesGroupsManagement({ }) .catch((error) => { console.error('Error handling group:', error); - alert("Erreur lors de l'opération sur le groupe"); + showNotification( + "Erreur lors de l'opération sur le groupe", + 'error', + 'Erreur' + ); }); } }; @@ -303,8 +328,10 @@ export default function FilesGroupsManagement({ (file) => file.group && file.group.id === groupId ); if (filesInGroup.length > 0) { - alert( - "Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe." + showNotification( + "Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe.", + 'error', + 'Erreur' ); return; } @@ -319,13 +346,15 @@ export default function FilesGroupsManagement({ throw new Error('Erreur lors de la suppression du groupe.'); } setGroups(groups.filter((group) => group.id !== groupId)); - alert('Groupe supprimé avec succès.'); + showNotification('Groupe supprimé avec succès.', 'success', 'Succès'); }) .catch((error) => { console.error('Error deleting group:', error); - alert( + showNotification( error.message || - "Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe." + "Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe.", + 'error', + 'Erreur' ); }); } @@ -342,8 +371,10 @@ export default function FilesGroupsManagement({ }) .catch((error) => { logger.error('Erreur lors de la création du document parent:', error); - alert( - 'Une erreur est survenue lors de la création du document parent.' + showNotification( + 'Une erreur est survenue lors de la création du document parent.', + 'error', + 'Erreur' ); throw error; }); @@ -365,8 +396,10 @@ export default function FilesGroupsManagement({ 'Erreur lors de la modification du document parent:', error ); - alert( - 'Une erreur est survenue lors de la modification du document parent.' + showNotification( + 'Une erreur est survenue lors de la modification du document parent.', + 'error', + 'Erreur' ); throw error; }); diff --git a/Front-End/src/components/Structure/Planning/ScheduleEventModal.js b/Front-End/src/components/Structure/Planning/ScheduleEventModal.js index 9f2f69a..7d7dd4e 100644 --- a/Front-End/src/components/Structure/Planning/ScheduleEventModal.js +++ b/Front-End/src/components/Structure/Planning/ScheduleEventModal.js @@ -1,6 +1,7 @@ import { usePlanning } from '@/context/PlanningContext'; import { format } from 'date-fns'; import React from 'react'; +import { useNotification } from '@/context/NotificationContext'; export default function ScheduleEventModal({ isOpen, @@ -13,6 +14,7 @@ export default function ScheduleEventModal({ }) { const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } = usePlanning(); + const { showNotification } = useNotification(); React.useEffect(() => { if (!eventData?.planning && schedules.length > 0) { @@ -78,22 +80,34 @@ export default function ScheduleEventModal({ e.preventDefault(); if (!eventData.speciality) { - alert('Veuillez sélectionner une matière'); + showNotification( + 'Veuillez sélectionner une matière', + 'warning', + 'Attention' + ); return; } if (!eventData.teacher) { - alert('Veuillez sélectionner un professeur'); + showNotification( + 'Veuillez sélectionner un professeur', + 'warning', + 'Attention' + ); return; } if (!eventData.planning) { - alert('Veuillez sélectionner un planning'); + showNotification( + 'Veuillez sélectionner un planning', + 'warning', + 'Attention' + ); return; } if (!eventData.location) { - alert('Veuillez saisir un lieu'); + showNotification('Veuillez saisir un lieu', 'warning', 'Attention'); return; } diff --git a/Front-End/src/context/NotificationContext.js b/Front-End/src/context/NotificationContext.js new file mode 100644 index 0000000..1c6ca80 --- /dev/null +++ b/Front-End/src/context/NotificationContext.js @@ -0,0 +1,36 @@ +import React, { createContext, useState, useContext } from 'react'; +import FlashNotification from '@/components/FlashNotification'; + +const NotificationContext = createContext(); + +export const NotificationProvider = ({ children }) => { + const [notification, setNotification] = useState({ + message: '', + type: '', + title: '', + }); + + const showNotification = (message, type = 'info', title = '') => { + setNotification({ message, type, title }); + }; + + const clearNotification = () => { + setNotification({ message: '', type: '', title: '' }); + }; + + return ( + + {notification.message && ( + + )} + {children} + + ); +}; + +export const useNotification = () => useContext(NotificationContext); diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index 70e3a4a..8e04974 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -55,6 +55,9 @@ export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/ export const BE_GESTIONMESSAGERIE_MESSAGERIE_URL = `${BASE_URL}/GestionMessagerie/messagerie`; export const BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL = `${BASE_URL}/GestionMessagerie/send-email/`; +// SETTINGS +export const BE_SETTINGS_SMTP_URL = `${BASE_URL}/Settings/smtp-settings`; + // URL FRONT-END export const FE_HOME_URL = `/`;