feat: Preparation des modèles Settings pour l'enregistrement SMTP [#17]

This commit is contained in:
Luc SORIGNET
2025-05-05 09:25:07 +02:00
parent 99a882a64a
commit eda6f587fb
33 changed files with 468 additions and 74 deletions

1
.env
View File

@ -1 +0,0 @@
DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg"

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.venv/
.env
node_modules/
hardcoded-strings-report.md

View File

@ -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

View File

@ -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',

View File

@ -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<format>\.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'),

View File

@ -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,

View File

@ -0,0 +1 @@
default_app_config = 'Settings.apps.SettingsConfig'

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SettingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'Settings'

View File

@ -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})"

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import SMTPSettings
class SMTPSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = SMTPSettings
fields = '__all__'

View File

@ -0,0 +1,6 @@
from django.urls import path
from .views import SMTPSettingsView
urlpatterns = [
path('smtp-settings/', SMTPSettingsView.as_view(), name='smtp_settings'),
]

View File

@ -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)

Binary file not shown.

View File

@ -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"],

View File

@ -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);

View File

@ -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 }) => (
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
@ -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

View File

@ -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}
/>
<div className="flex items-center space-x-4">
<label>
<input
type="checkbox"
checked={useTls}
onChange={handleUseTlsChange}
/>
Utiliser TLS
</label>
<label>
<input
type="checkbox"
checked={useSsl}
onChange={handleUseSslChange}
/>
Utiliser SSL
</label>
</div>
<Button type="submit" primary text="Mettre à jour"></Button>
</form>
{statusMessage && <p className="mt-4 text-sm">{statusMessage}</p>}
</TabContent>
</div>
</div>

View File

@ -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';

View File

@ -160,7 +160,7 @@ export default function CreateSubscriptionPage() {
const requestErrorHandler = (err) => {
logger.error('Error fetching data:', err);
setErrors(err);
//setErrors(err);
};
useEffect(() => {

View File

@ -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

View File

@ -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;

View File

@ -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);
};

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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: <CheckCircle className="h-6 w-6 text-green-500" />,
bg: 'bg-green-300',
},
error: {
icon: <AlertCircle className="h-6 w-6 text-red-500" />,
bg: 'bg-red-300',
},
info: {
icon: <Info className="h-6 w-6 text-blue-500" />,
bg: 'bg-blue-300',
},
warning: {
icon: <AlertTriangle className="h-6 w-6 text-yellow-500" />,
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 (
<motion.div
initial={{ opacity: 0, x: 50 }} // Animation d'entrée
animate={{ opacity: 1, x: 0 }} // Animation visible
exit={{ opacity: 0, x: 50 }} // Animation de sortie
transition={{ duration: 0.3 }} // Durée des animations
className="fixed top-5 right-5 flex items-stretch w-96 rounded-lg shadow-lg bg-white z-50 border border-gray-200"
>
{/* Rectangle gauche avec l'icône */}
<div className={`flex items-center justify-center w-12 ${bg}`}>
{icon}
</div>
{/* Zone de texte */}
<div className="flex-1 p-4">
<p className="font-bold text-black">{title}</p>
<p className="text-gray-700">{message}</p>
</div>
{/* Bouton de fermeture */}
<button
onClick={() => setIsVisible(false)}
className="text-gray-500 hover:text-gray-700 focus:outline-none p-2"
>
<X className="h-5 w-5" />
</button>
</motion.div>
);
}

View File

@ -31,7 +31,7 @@ export default function InputTextIcon({
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
}`}
>
<span className="inline-flex 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 />}
</span>
<input

View File

@ -4,6 +4,7 @@ import { SessionProvider } from 'next-auth/react';
import { CsrfProvider } from '@/context/CsrfContext';
import { NextIntlClientProvider } from 'next-intl';
import { EstablishmentProvider } from '@/context/EstablishmentContext';
import { NotificationProvider } from '@/context/NotificationContext';
import { ClassesProvider } from '@/context/ClassesContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -14,18 +15,20 @@ export default function Providers({ children, messages, locale, session }) {
locale = 'fr'; // Valeur par défaut
}
return (
<SessionProvider session={session}>
<DndProvider backend={HTML5Backend}>
<CsrfProvider>
<EstablishmentProvider>
<ClassesProvider>
<NextIntlClientProvider messages={messages} locale={locale}>
{children}
</NextIntlClientProvider>
</ClassesProvider>
</EstablishmentProvider>
</CsrfProvider>
</DndProvider>
</SessionProvider>
<NotificationProvider>
<SessionProvider session={session}>
<DndProvider backend={HTML5Backend}>
<CsrfProvider>
<EstablishmentProvider>
<ClassesProvider>
<NextIntlClientProvider messages={messages} locale={locale}>
{children}
</NextIntlClientProvider>
</ClassesProvider>
</EstablishmentProvider>
</CsrfProvider>
</DndProvider>
</SessionProvider>
</NotificationProvider>
);
}

View File

@ -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 }) => {
</div>
{/* Tabs Content */}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner">
{tabs.map((tab) => (
<div
key={tab.id}
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
>
{tab.content}
</div>
))}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner relative">
<AnimatePresence mode="wait">
{tabs.map(
(tab) =>
activeTab === tab.id && (
<motion.div
key={tab.id}
initial={{ opacity: 0, x: 50 }} // Animation d'entrée
animate={{ opacity: 1, x: 0 }} // Animation visible
exit={{ opacity: 0, x: -50 }} // Animation de sortie
transition={{ duration: 0.3 }} // Durée des animations
className="absolute w-full h-full"
>
{tab.content}
</motion.div>
)
)}
</AnimatePresence>
</div>
</div>
);

View File

@ -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;
});

View File

@ -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;
}

View File

@ -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 (
<NotificationContext.Provider value={{ showNotification }}>
{notification.message && (
<FlashNotification
title={notification.title}
message={notification.message}
type={notification.type}
onClose={clearNotification}
/>
)}
{children}
</NotificationContext.Provider>
);
};
export const useNotification = () => useContext(NotificationContext);

View File

@ -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 = `/`;