mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
feat: Preparation des modèles Settings pour l'enregistrement SMTP [#17]
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.venv/
|
||||
.env
|
||||
node_modules/
|
||||
hardcoded-strings-report.md
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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,
|
||||
|
||||
1
Back-End/Settings/__init__.py
Normal file
1
Back-End/Settings/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
default_app_config = 'Settings.apps.SettingsConfig'
|
||||
3
Back-End/Settings/admin.py
Normal file
3
Back-End/Settings/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
Back-End/Settings/apps.py
Normal file
5
Back-End/Settings/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class SettingsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'Settings'
|
||||
17
Back-End/Settings/models.py
Normal file
17
Back-End/Settings/models.py
Normal 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})"
|
||||
7
Back-End/Settings/serializers.py
Normal file
7
Back-End/Settings/serializers.py
Normal file
@ -0,0 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from .models import SMTPSettings
|
||||
|
||||
class SMTPSettingsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SMTPSettings
|
||||
fields = '__all__'
|
||||
6
Back-End/Settings/urls.py
Normal file
6
Back-End/Settings/urls.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import SMTPSettingsView
|
||||
|
||||
urlpatterns = [
|
||||
path('smtp-settings/', SMTPSettingsView.as_view(), name='smtp_settings'),
|
||||
]
|
||||
55
Back-End/Settings/views.py
Normal file
55
Back-End/Settings/views.py
Normal 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.
@ -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"],
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -160,7 +160,7 @@ export default function CreateSubscriptionPage() {
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
setErrors(err);
|
||||
//setErrors(err);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
37
Front-End/src/app/actions/settingsAction.js
Normal file
37
Front-End/src/app/actions/settingsAction.js
Normal 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);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
70
Front-End/src/components/FlashNotification.js
Normal file
70
Front-End/src/components/FlashNotification.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
36
Front-End/src/context/NotificationContext.js
Normal file
36
Front-End/src/context/NotificationContext.js
Normal 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);
|
||||
@ -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 = `/`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user