Merge pull request 'feat-17-Messagerie_WIP' (#58) from feat-17-Messagerie_WIP into develop

Reviewed-on: https://git.v0id.ovh/n3wt-innov/n3wt-school/pulls/58
This commit is contained in:
Luc SORIGNET
2025-05-12 12:00:36 +00:00
70 changed files with 2206 additions and 1749 deletions

View File

@ -0,0 +1 @@
cd $(dirname "$0")/../Front-End/ && npm run lint-light

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"eslint.workingDirectories": ["./Front-End"]
}

View File

@ -25,7 +25,7 @@ from django.db.models import Q
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
from Subscriptions.models import RegistrationForm, Guardian
import Subscriptions.mailManager as mailer
import N3wtSchool.mailManager as mailer
import Subscriptions.util as util
import logging
from N3wtSchool import bdd, error, settings

View File

@ -1,5 +1,5 @@
from django.urls import path, re_path
from .views import SendEmailView
from .views import SendEmailView, search_recipients
from GestionMessagerie.views import MessagerieView, MessageView, MessageSimpleView
urlpatterns = [
@ -7,4 +7,5 @@ urlpatterns = [
re_path(r'^messages$', MessageView.as_view(), name="messages"),
re_path(r'^messages/(?P<id>[0-9]+)$', MessageSimpleView.as_view(), name="messages"),
path('send-email/', SendEmailView.as_view(), name='send_email'),
path('search-recipients/', search_recipients, name='search_recipients'),
]

View File

@ -6,12 +6,18 @@ from django.utils.html import strip_tags
from django.conf import settings
from rest_framework.response import Response
from rest_framework import status
from django.db.models import Q
from Auth.models import Profile # Assurez-vous que le modèle Profile contient les informations nécessaires
from .models import *
from School.models import Teacher, ProfileRole
from Settings.models import SMTPSettings # Assurez-vous que le chemin est correct
from GestionMessagerie.serializers import MessageSerializer
from School.serializers import TeacherSerializer
from N3wtSchool import bdd
import N3wtSchool.mailManager as mailer
class MessagerieView(APIView):
def get(self, request, profile_id):
@ -44,23 +50,96 @@ class SendEmailView(APIView):
def post(self, request):
data = request.data
recipients = data.get('recipients', [])
cc = data.get('cc', [])
bcc = data.get('bcc', [])
subject = data.get('subject', 'Notification')
message = data.get('message', '')
establishment_id = data.get('establishment_id', '')
if not recipients or not message:
return Response({'error': 'Les destinataires et le message sont requis.'}, status=status.HTTP_400_BAD_REQUEST)
try:
plain_message = strip_tags(message)
send_mail(
subject,
plain_message,
settings.EMAIL_HOST_USER,
recipients,
html_message=message,
fail_silently=False,
# Récupérer la connexion SMTP
connection = mailer.getConnection(establishment_id)
# Envoyer l'email
return mailer.sendMail(
subject=subject,
message=message,
recipients=recipients,
cc=cc,
bcc=bcc,
attachments=[],
connection=connection
)
return Response({'message': 'Email envoyé avec succès.'}, status=status.HTTP_200_OK)
except NotFound as e:
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ContactsView(APIView):
"""
API pour récupérer les contacts associés à un établissement.
"""
def get(self, request, establishment_id):
try:
# Récupérer les enseignants associés à l'établissement
teachers = Teacher.objects.filter(profile_role__establishment_id=establishment_id)
teachers_serializer = TeacherSerializer(teachers, many=True)
# Ajouter un contact pour l'administration
admin_contact = {
"id": "admin",
"name": "Administration",
"email": "admin@etablissement.com",
"profilePic": "https://www.gravatar.com/avatar/admin"
}
contacts = [admin_contact] + teachers_serializer.data
return Response(contacts, status=status.HTTP_200_OK)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def search_recipients(request):
"""
API pour rechercher des destinataires en fonction d'un terme de recherche et d'un établissement.
"""
query = request.GET.get('q', '').strip() # Récupérer le terme de recherche depuis les paramètres GET
establishment_id = request.GET.get('establishment_id', None) # Récupérer l'ID de l'établissement
if not query:
return JsonResponse([], safe=False) # Retourner une liste vide si aucun terme n'est fourni
if not establishment_id:
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
# Rechercher dans les champs pertinents (nom, prénom, email) et filtrer par establishment_id
profiles = Profile.objects.filter(
Q(first_name__icontains=query) |
Q(last_name__icontains=query) |
Q(email__icontains=query),
roles__establishment_id=establishment_id, # Utiliser 'roles' au lieu de 'profilerole'
roles__is_active=True # Filtrer uniquement les ProfileRole actifs
).distinct()
# Construire la réponse avec les rôles associés
results = []
for profile in profiles:
profile_roles = ProfileRole.objects.filter(
profile=profile,
establishment_id=establishment_id,
is_active=True # Inclure uniquement les ProfileRole actifs
).values(
'id', 'role_type', 'establishment__name', 'is_active'
)
results.append({
'id': profile.id,
'first_name': profile.first_name,
'last_name': profile.last_name,
'email': profile.email,
'roles': list(profile_roles) # Inclure tous les rôles actifs associés pour cet établissement
})
return JsonResponse(results, safe=False)

View File

@ -0,0 +1,8 @@
{
"hostSMTP": "",
"portSMTP": 25,
"username": "",
"password": "",
"useSSL": false,
"useTLS": false
}

View File

@ -1,8 +1,67 @@
from django.core.mail import send_mail, EmailMultiAlternatives, EmailMessage
from django.core.mail import send_mail, get_connection, EmailMultiAlternatives, EmailMessage
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
import re
from N3wtSchool import settings
from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import NotFound
from Settings.models import SMTPSettings
from Establishment.models import Establishment # Importer le modèle Establishment
def getConnection(id_establishement):
try:
# Récupérer l'instance de l'établissement
establishment = Establishment.objects.get(id=id_establishement)
# Récupérer les paramètres SMTP associés à l'établissement
smtp_settings = SMTPSettings.objects.get(establishment=establishment)
# Créer une connexion SMTP avec les paramètres récupérés
connection = get_connection(
host=smtp_settings.smtp_server,
port=smtp_settings.smtp_port,
username=smtp_settings.smtp_user,
password=smtp_settings.smtp_password,
use_tls=smtp_settings.use_tls,
use_ssl=smtp_settings.use_ssl
)
return connection
except Establishment.DoesNotExist:
raise NotFound(f"Aucun établissement trouvé avec l'ID {id_establishement}")
except SMTPSettings.DoesNotExist:
raise NotFound(f"Aucun paramètre SMTP trouvé pour l'établissement {id_establishement}")
def sendMail(subject, message, recipients, cc=[], bcc=[], attachments=[], connection=None):
try:
plain_message = strip_tags(message)
from_email = settings.EMAIL_HOST_USER
if connection is not None:
from_email = connection.username
email = EmailMultiAlternatives(
subject=subject,
body=plain_message,
from_email=from_email,
to=recipients,
cc=cc,
bcc=bcc,
connection=connection
)
email.attach_alternative(message, "text/html")
# Ajout des pièces jointes
for attachment in attachments:
# attachment doit être un tuple (filename, content, mimetype)
# ex: ("document.pdf", fichier.read(), "application/pdf")
email.attach(*attachment)
email.send(fail_silently=False)
return Response({'message': 'Email envoyé avec succès.'}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def envoieReinitMotDePasse(recipients, code):
errorMessage = ''
@ -14,9 +73,8 @@ def envoieReinitMotDePasse(recipients, code):
}
subject = EMAIL_REINIT_SUBJECT
html_message = render_to_string('emails/resetPassword.html', context)
plain_message = strip_tags(html_message)
from_email = settings.EMAIL_HOST_USER
send_mail(subject, plain_message, from_email, [recipients], html_message=html_message)
sendMail(subject, html_message, recipients)
except Exception as e:
errorMessage = str(e)
@ -36,10 +94,8 @@ def sendRegisterForm(recipients, establishment_id):
subject = EMAIL_INSCRIPTION_SUBJECT
html_message = render_to_string('emails/inscription.html', context)
plain_message = strip_tags(html_message)
from_email = settings.EMAIL_HOST_USER
sendMail(subject, html_message, recipients)
send_mail(subject, plain_message, from_email, [recipients], html_message=html_message)
except Exception as e:
errorMessage = str(e)
@ -59,10 +115,7 @@ def sendMandatSEPA(recipients, establishment_id):
subject = EMAIL_INSCRIPTION_SUBJECT
html_message = render_to_string('emails/sepa.html', context)
plain_message = strip_tags(html_message)
from_email = settings.EMAIL_HOST_USER
send_mail(subject, plain_message, from_email, [recipients], html_message=html_message)
sendMail(subject, html_message, recipients)
except Exception as e:
errorMessage = str(e)
@ -74,13 +127,8 @@ def envoieRelanceDossierInscription(recipients, code):
EMAIL_RELANCE_CORPUS = 'Bonjour,\nN\'ayant pas eu de retour de votre part, nous vous renvoyons le lien vers le formulaire d\'inscription : ' + BASE_URL + '/users/login\nCordialement'
errorMessage = ''
try:
send_mail(
EMAIL_RELANCE_SUBJECT,
EMAIL_RELANCE_CORPUS%str(code),
settings.EMAIL_HOST_USER,
[recipients],
fail_silently=False,
)
sendMail(EMAIL_RELANCE_SUBJECT, EMAIL_RELANCE_CORPUS%str(code), recipients)
except Exception as e:
errorMessage = str(e)

View File

@ -14,6 +14,10 @@ from pathlib import Path
import json
import os
from datetime import timedelta
import logging
# Configuration du logger
logger = logging.getLogger(__name__)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -219,23 +223,29 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#################### Application Settings ##############################
########################################################################
with open('Subscriptions/Configuration/application.json', 'r') as f:
jsonObject = json.load(f)
DJANGO_SUPERUSER_PASSWORD='admin'
DJANGO_SUPERUSER_USERNAME='admin'
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
# Configuration de l'email de l'application
smtp_config_file = 'N3wtSchool/Configuration/application.json'
EMAIL_HOST='smtp.gmail.com'
EMAIL_PORT=587
EMAIL_HOST_USER=jsonObject['mailFrom']
EMAIL_HOST_PASSWORD=jsonObject['password']
if os.path.exists(smtp_config_file):
try:
with open(smtp_config_file, 'r') as f:
smtpSettings = json.load(f)
EMAIL_HOST = smtpSettings.get('hostSMTP', '')
EMAIL_PORT = smtpSettings.get('portSMTP', 587)
EMAIL_HOST_USER = smtpSettings.get('username', '')
EMAIL_HOST_PASSWORD = smtpSettings.get('password', '')
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_USE_TLS = smtpSettings.get('useTLS', True)
EMAIL_USE_SSL = smtpSettings.get('useSSL', False)
except Exception as e:
logger.error(f"Erreur lors de la lecture du fichier de configuration SMTP : {e}")
else:
logger.error(f"Fichier de configuration SMTP introuvable : {smtp_config_file}")
DOCUMENT_DIR = 'documents'

View File

@ -12,25 +12,51 @@ class SMTPSettingsView(APIView):
"""
@swagger_auto_schema(
operation_description="Récupérer les paramètres SMTP",
operation_description="Récupérer les paramètres SMTP pour un établissement spécifique ou tous les paramètres si aucun ID n'est fourni",
manual_parameters=[
openapi.Parameter(
'establishment_id',
openapi.IN_QUERY,
description="ID de l'établissement (facultatif)",
type=openapi.TYPE_INTEGER,
required=False
)
],
responses={
200: SMTPSettingsSerializer(),
200: SMTPSettingsSerializer(many=True),
404: openapi.Response(description="Aucun paramètre SMTP trouvé."),
500: openapi.Response(description="Erreur interne du serveur."),
},
)
def get(self, request):
establishment_id = request.query_params.get('establishment_id')
try:
smtp_settings = SMTPSettings.objects.first()
if establishment_id:
# Récupérer les paramètres SMTP pour un établissement spécifique
smtp_settings = SMTPSettings.objects.filter(establishment_id=establishment_id).first()
if not smtp_settings:
return Response({'error': 'Aucun paramètre SMTP trouvé.'}, status=status.HTTP_404_NOT_FOUND)
return Response(
{'error': f"Aucun paramètre SMTP trouvé pour l'établissement {establishment_id}."},
status=status.HTTP_404_NOT_FOUND
)
serializer = SMTPSettingsSerializer(smtp_settings)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
# Récupérer tous les paramètres SMTP
smtp_settings = SMTPSettings.objects.all()
if not smtp_settings.exists():
return Response(
{'error': "Aucun paramètre SMTP trouvé."},
status=status.HTTP_404_NOT_FOUND
)
serializer = SMTPSettingsSerializer(smtp_settings, many=True)
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",
operation_description="Créer ou mettre à jour les paramètres SMTP pour un établissement spécifique",
request_body=SMTPSettingsSerializer,
responses={
200: SMTPSettingsSerializer(),

View File

@ -1,4 +0,0 @@
{
"mailFrom":"",
"password":""
}

View File

@ -11,7 +11,7 @@ import json
import os
from django.core.files import File
import Subscriptions.mailManager as mailer
import N3wtSchool.mailManager as mailer
import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
@ -519,4 +519,3 @@ def get_parent_file_templates_by_rf(request, id):
return JsonResponse(serializer.data, safe=False)
except RegistrationParentFileTemplate.DoesNotExist:
return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND)

4
Front-End/.babelrc Normal file
View File

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": []
}

3
Front-End/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
build/
public/

View File

@ -1,3 +1,10 @@
{
"extends": "next/core-web-vitals"
"extends": ["next", "next/core-web-vitals"],
"rules": {
// Ajoutez vos règles personnalisées ici
"react/react-in-jsx-scope": "off", // Désactive l'obligation d'importer React
"no-console": "error", // Avertissement pour les console.log
"semi": ["error", "always"], // Exige un point-virgule à la fin des lignes
"quotes": ["error", "single", { "avoidEscape": true }] // Exige des guillemets simples, sauf si l'on utilise des guillemets doubles à l'intérieur
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,13 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint-light": "next lint --quiet",
"check-strings": "node scripts/check-hardcoded-strings.js"
},
"dependencies": {
"@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9",
"@tinymce/tinymce-react": "^6.1.0",
"date-fns": "^4.1.0",
"framer-motion": "^11.11.11",
"ics": "^3.8.1",
@ -31,11 +31,10 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18",
"react-international-phone": "^4.5.0",
"react-quill": "^2.0.0",
"react-tooltip": "^5.28.0"
},
"devDependencies": {
"@babel/parser": "^7.26.2",
"@babel/traverse": "^7.25.9",
"autoprefixer": "^10.4.20",
"eslint": "^8",
"eslint-config-next": "14.2.11",

View File

@ -3,6 +3,7 @@ import React, { useState } from 'react';
import SelectChoice from '@/components/SelectChoice';
import Button from '@/components/Button';
import Table from '@/components/Table';
import logger from '@/utils/logger';
export default function Page() {
const [formData, setFormData] = useState({
@ -132,7 +133,7 @@ export default function Page() {
<div className="mt-4">
<Button
text="Enregistrer"
onClick={() => console.log('FormData:', formData)}
onClick={() => logger.debug('FormData:', formData)}
primary
className="bg-emerald-500 text-white hover:bg-emerald-600"
/>

View File

@ -151,7 +151,7 @@ export default function Layout({ children }) {
return (
<ProtectedRoute requiredRight={[RIGHTS.ADMIN, RIGHTS.TEACHER]}>
{/* Topbar */}
<header className="absolute top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 px-4 md:px-8 flex items-center justify-between z-10 box-border">
<header className="absolute top-0 left-64 right-0 h-16 bg-white border-b border-gray-200 px-4 md:px-8 flex items-center justify-between z-10 box-border">
<div className="flex items-center">
<button
className="mr-4 md:hidden text-gray-600 hover:text-gray-900"
@ -180,7 +180,7 @@ export default function Layout({ children }) {
{/* Sidebar */}
<div
className={`absolute top-16 bottom-16 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
className={`absolute top-0 bottom-0 left-0 z-30 w-64 bg-white border-r border-gray-200 box-border ${
isSidebarOpen ? 'block' : 'hidden md:block'
}`}
>

View File

@ -1,10 +1,36 @@
'use client';
import React from 'react';
import SidebarTabs from '@/components/SidebarTabs';
import EmailSender from '@/components/Admin/EmailSender';
import InstantMessaging from '@/components/Admin/InstantMessaging';
import AnnouncementScheduler from '@/components/Admin/AnnouncementScheduler';
import logger from '@/utils/logger';
export default function MessageriePage({ csrfToken }) {
const tabs = [
{
id: 'email',
label: 'Envoyer un Mail',
content: <EmailSender csrfToken={csrfToken} />,
},
{
id: 'instant',
label: 'Messagerie Instantanée',
content: <InstantMessaging csrfToken={csrfToken} />,
},
{
id: 'announcement',
label: 'Planifier une Annonce',
content: <AnnouncementScheduler csrfToken={csrfToken} />,
},
];
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Messagerie Admin</h1>
<EmailSender csrfToken={csrfToken} />
<div className="flex h-full w-full">
<SidebarTabs
tabs={tabs}
onTabChange={(tabId) => logger.debug(`Onglet actif : ${tabId}`)}
/>
</div>
);
}

View File

@ -4,6 +4,7 @@ import Tab from '@/components/Tab';
import TabContent from '@/components/TabContent';
import Button from '@/components/Button';
import InputText from '@/components/InputText';
import CheckBox from '@/components/CheckBox'; // Import du composant CheckBox
import logger from '@/utils/logger';
import {
fetchSmtpSettings,
@ -12,6 +13,7 @@ import {
import { useEstablishment } from '@/context/EstablishmentContext';
import { useCsrfToken } from '@/context/CsrfContext'; // Import du hook pour récupérer le csrfToken
import { useNotification } from '@/context/NotificationContext';
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
export default function SettingsPage() {
const [activeTab, setActiveTab] = useState('structure');
@ -24,18 +26,29 @@ export default function SettingsPage() {
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 searchParams = useSearchParams();
const handleTabClick = (tab) => {
setActiveTab(tab);
};
// Ajout : sélection automatique de l'onglet via l'ancre ou le paramètre de recherche
useEffect(() => {
const tabParam = searchParams.get('tab');
if (tabParam === 'smtp') {
setActiveTab('smtp');
} else if (tabParam === 'structure') {
setActiveTab('structure');
}
}, [searchParams]);
// Charger les paramètres SMTP existants
useEffect(() => {
if (activeTab === 'smtp') {
fetchSmtpSettings(csrfToken) // Passer le csrfToken ici
fetchSmtpSettings(csrfToken, selectedEstablishmentId) // Passer le csrfToken ici
.then((data) => {
setSmtpServer(data.smtp_server || '');
setSmtpPort(data.smtp_port || '');
@ -45,8 +58,23 @@ export default function SettingsPage() {
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.');
if (error.response && error.response.status === 404) {
showNotification(
"Les données SMTP n'ont pas été trouvées.",
'warning',
'Attention'
);
} else {
logger.error(
'Erreur lors du chargement des paramètres SMTP:',
error
);
showNotification(
'Erreur lors du chargement des paramètres SMTP.',
'error',
'Erreur'
);
}
});
}
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
@ -113,7 +141,11 @@ export default function SettingsPage() {
editSmtpSettings(smtpData, csrfToken) // Passer le csrfToken ici
.then(() => {
setStatusMessage('Paramètres SMTP mis à jour avec succès.');
showNotification(
'Paramètres SMTP mis à jour avec succès.',
'success',
'Succès'
);
logger.debug('SMTP Settings Updated:', smtpData);
})
.catch((error) => {
@ -121,7 +153,11 @@ export default function SettingsPage() {
'Erreur lors de la mise à jour des paramètres SMTP:',
error
);
setStatusMessage('Erreur lors de la mise à jour des paramètres SMTP.');
showNotification(
'Erreur lors de la mise à jour des paramètres SMTP.',
'error',
'Erreur'
);
});
};
@ -164,6 +200,7 @@ export default function SettingsPage() {
</TabContent>
<TabContent isActive={activeTab === 'smtp'}>
<form onSubmit={handleSmtpSubmit}>
<div className="grid grid-cols-2 gap-4">
<InputText
label="Serveur SMTP"
value={smtpServer}
@ -185,27 +222,32 @@ 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>
<div className="mt-6 border-t pt-4">
<div className="flex items-center space-x-4">
<CheckBox
item={{ id: 'useTls' }}
formData={{ useTls }}
handleChange={() => setUseTls((prev) => !prev)} // Inverser la valeur booléenne
fieldName="useTls"
itemLabelFunc={() => 'Utiliser TLS'}
/>
<CheckBox
item={{ id: 'useSsl' }}
formData={{ useSsl }}
handleChange={() => setUseSsl((prev) => !prev)} // Inverser la valeur booléenne
fieldName="useSsl"
itemLabelFunc={() => 'Utiliser SSL'}
/>
</div>
</div>
<Button
type="submit"
primary
text="Mettre à jour"
className="mt-6"
></Button>
</form>
{statusMessage && <p className="mt-4 text-sm">{statusMessage}</p>}
</TabContent>
</div>
</div>

View File

@ -151,7 +151,7 @@ export default function Page() {
// Envoyer les absences modifiées à une API
absencesToUpdate.forEach(([studentId, absenceData]) => {
console.log('Modification absence élève : ', studentId);
logger.debug('Modification absence élève : ', studentId);
saveAbsence(studentId, absenceData);
});
@ -203,7 +203,7 @@ export default function Page() {
// Appeler la fonction pour supprimer l'absence
deleteAbsences(existingAbsence.id, csrfToken)
.then(() => {
console.log(
logger.debug(
`Absence pour l'élève ${studentId} supprimée avec succès.`
);
// Mettre à jour les absences récupérées
@ -214,7 +214,7 @@ export default function Page() {
});
})
.catch((error) => {
console.error(
logger.error(
`Erreur lors de la suppression de l'absence pour l'élève ${studentId}:`,
error
);
@ -235,7 +235,7 @@ export default function Page() {
const saveAbsence = (studentId, absenceData) => {
if (!absenceData.reason || !studentId || !absenceData.moment) {
console.error('Tous les champs requis doivent être fournis.');
logger.error('Tous les champs requis doivent être fournis.');
return;
}
@ -251,7 +251,7 @@ export default function Page() {
// Modifier une absence existante
editAbsences(absenceData.id, payload, csrfToken)
.then(() => {
console.log(
logger.debug(
`Absence pour l'élève ${studentId} modifiée avec succès.`
);
// Mettre à jour fetchedAbsences et formAbsences localement
@ -265,7 +265,7 @@ export default function Page() {
}));
})
.catch((error) => {
console.error(
logger.error(
`Erreur lors de la modification de l'absence pour l'élève ${studentId}:`,
error
);
@ -274,7 +274,7 @@ export default function Page() {
// Créer une nouvelle absence
createAbsences(payload, csrfToken)
.then((response) => {
console.log(`Absence pour l'élève ${studentId} créée avec succès.`);
logger.debug(`Absence pour l'élève ${studentId} créée avec succès.`);
// Mettre à jour fetchedAbsences et formAbsences localement
setFetchedAbsences((prev) => ({
...prev,
@ -286,7 +286,7 @@ export default function Page() {
}));
})
.catch((error) => {
console.error(
logger.error(
`Erreur lors de la création de l'absence pour l'élève ${studentId}:`,
error
);

View File

@ -385,14 +385,14 @@ export default function CreateSubscriptionPage() {
const guardians = (() => {
if (formDataRef.current.selectedGuardians.length > 0) {
// Cas 3 : Des guardians sont sélectionnés
console.log('Cas 3 : Des guardians sont sélectionnés');
logger.debug('Cas 3 : Des guardians sont sélectionnés');
return formDataRef.current.selectedGuardians.map((guardianId) => ({
id: guardianId,
}));
} else if (formDataRef.current.isExistingParentProfile) {
if (initialGuardianEmail !== existingProfile?.email) {
// Cas 2 : Profil existant différent de l'ancien
console.log(
logger.debug(
"Cas 2 : Profil existant différent de l'ancien, mise à jour du profil",
{
existingProfile,
@ -415,14 +415,14 @@ export default function CreateSubscriptionPage() {
];
} else {
// Cas 4 : Profil existant avec le même email
console.log('Cas 4 : Profil existant avec le même email', {
logger.debug('Cas 4 : Profil existant avec le même email', {
existingProfile,
});
return [];
}
} else {
// Cas 1 : Profil inexistant
console.log("Cas 1 : Profil inexistant, création d'un nouveau profil");
logger.debug("Cas 1 : Profil inexistant, création d'un nouveau profil");
return [
{
profile_role_data: {
@ -444,7 +444,7 @@ export default function CreateSubscriptionPage() {
}
})();
console.log('test : ', guardians);
logger.debug('test : ', guardians);
const data = {
student: {
@ -763,10 +763,12 @@ export default function CreateSubscriptionPage() {
<div className="mx-auto p-12 space-y-12">
{registerFormID ? (
<h1 className="text-2xl font-bold">
Modifier un dossier d'inscription
Modifier un dossier d&apos;inscription
</h1>
) : (
<h1 className="text-2xl font-bold">Créer un dossier d'inscription</h1>
<h1 className="text-2xl font-bold">
Créer un dossier d&apos;inscription
</h1>
)}
{/* Sélection de l'année scolaire */}
@ -1047,7 +1049,7 @@ export default function CreateSubscriptionPage() {
{/* Montant total */}
<div className="flex items-center justify-between bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-300 mt-4">
<span className="text-sm font-medium text-gray-600">
Montant total des frais d'inscription :
Montant total des frais d&apos;inscription :
</span>
<span className="text-lg font-semibold text-gray-800">
{totalRegistrationAmount}

View File

@ -321,7 +321,7 @@ export default function Page({ params: { locale } }) {
.then((data) => {
logger.debug('Success:', data);
setPopupMessage(
`Le dossier d'inscription a été correctement archivé`
"Le dossier d'inscription a été correctement archivé"
);
setPopupVisible(true);
setRegistrationForms(
@ -332,7 +332,7 @@ export default function Page({ params: { locale } }) {
.catch((error) => {
logger.error('Error archiving data:', error);
setPopupMessage(
`Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur.`
"Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur."
);
setPopupVisible(true);
});
@ -349,14 +349,14 @@ export default function Page({ params: { locale } }) {
sendRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
setPopupMessage(`Le dossier d'inscription a été envoyé avec succès`);
setPopupMessage("Le dossier d'inscription a été envoyé avec succès");
setPopupVisible(true);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error archiving data:', error);
setPopupMessage(
`Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur.`
"Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur."
);
setPopupVisible(true);
});

View File

@ -1,7 +1,6 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
import Image from 'next/image';
import React from 'react';
import Chat from '@/components/Chat';
import { getGravatarUrl } from '@/utils/gravatar';
const contacts = [
@ -23,45 +22,7 @@ const contacts = [
];
export default function MessageriePage() {
const [selectedContact, setSelectedContact] = useState(null);
const [messages, setMessages] = useState({});
const [newMessage, setNewMessage] = useState('');
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = () => {
if (newMessage.trim() && selectedContact) {
const contactMessages = messages[selectedContact.id] || [];
setMessages({
...messages,
[selectedContact.id]: [
...contactMessages,
{
id: contactMessages.length + 1,
text: newMessage,
date: new Date(),
},
],
});
setNewMessage('');
simulateContactResponse(selectedContact.id);
}
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
const simulateContactResponse = (contactId) => {
const simulateResponse = (contactId, setMessages) => {
setTimeout(() => {
setMessages((prevMessages) => {
const contactMessages = prevMessages[contactId] || [];
@ -81,79 +42,5 @@ export default function MessageriePage() {
}, 2000);
};
return (
<div className="flex" style={{ height: 'calc(100vh - 128px )' }}>
{' '}
{/* Utilisation de calc pour soustraire la hauteur de l'entête */}
<div className="w-1/4 border-r border-gray-200 p-4 overflow-y-auto h-full ">
{contacts.map((contact) => (
<div
key={contact.id}
className={`p-2 cursor-pointer ${selectedContact?.id === contact.id ? 'bg-gray-200' : ''}`}
onClick={() => setSelectedContact(contact)}
>
<Image
src={contact.profilePic}
alt={`${contact.name}'s profile`}
className="w-8 h-8 rounded-full inline-block mr-2"
width={150}
height={150}
/>
{contact.name}
</div>
))}
</div>
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 h-full">
{selectedContact &&
(messages[selectedContact.id] || []).map((message) => (
<div
key={message.id}
className={`mb-2 p-2 rounded max-w-xs ${message.isResponse ? 'bg-gray-200 justify-self-end' : 'bg-emerald-200 justify-self-start'}`}
style={{
borderRadius: message.isResponse
? '20px 20px 0 20px'
: '20px 20px 20px 0',
minWidth: '25%',
}}
>
<div className="flex items-center mb-1">
<img
src={selectedContact.profilePic}
alt={`${selectedContact.name}'s profile`}
className="w-8 h-8 rounded-full inline-block mr-2"
width={150}
height={150}
/>
<span className="text-xs text-gray-600">
{selectedContact.name}
</span>
<span className="text-xs text-gray-400 ml-2">
{new Date(message.date).toLocaleTimeString()}
</span>
</div>
{message.text}
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="p-4 border-t border-gray-200 flex">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="w-full p-2 border border-gray-300 rounded"
placeholder="Écrire un message..."
onKeyDown={handleKeyPress}
/>
<button
onClick={handleSendMessage}
className="p-2 bg-emerald-500 text-white rounded mr-2"
>
<SendHorizontal />
</button>
</div>
</div>
</div>
);
return <Chat contacts={contacts} simulateResponse={simulateResponse} />;
}

View File

@ -0,0 +1,31 @@
import logger from '@/utils/logger';
/**
*
* @param {*} response
* @returns
*/
export const requestResponseHandler = async (response) => {
try {
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;
error.response = response;
throw error;
} catch (error) {
logger.error('Une erreur est survenue lors du traitement de la réponse', {
error,
response,
});
throw error;
}
};
export const errorHandler = (error) => {
logger.error('Error:', { error });
// Handle the error here, e.g., show a notification
throw error;
};

View File

@ -1,4 +1,5 @@
import { signOut, signIn, getSession } from 'next-auth/react';
import { signOut, signIn } from 'next-auth/react';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import {
BE_AUTH_LOGIN_URL,
BE_AUTH_REFRESH_JWT_URL,
@ -11,17 +12,6 @@ import {
} from '@/utils/Url';
import { PARENT_FILTER } from '@/utils/constants';
const requestResponseHandler = async (response) => {
const body = await response.json();
if (response.ok) {
return body;
}
const error = new Error(body?.errorMessage || 'Une erreur est survenue');
error.details = body;
throw error;
};
/**
* Login action
*/
@ -46,7 +36,7 @@ export const getJWT = (data) => {
body: JSON.stringify(data),
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const refreshJWT = (data) => {
const request = new Request(`${BE_AUTH_REFRESH_JWT_URL}`, {
@ -57,7 +47,7 @@ export const refreshJWT = (data) => {
body: JSON.stringify(data),
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
/**
@ -87,7 +77,9 @@ export const fetchProfileRoles = (
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const updateProfileRoles = (id, data, csrfToken) => {
@ -100,7 +92,7 @@ export const updateProfileRoles = (id, data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const deleteProfileRoles = async (id, csrfToken) => {
@ -127,7 +119,9 @@ export const deleteProfileRoles = async (id, csrfToken) => {
};
export const fetchProfiles = () => {
return fetch(`${BE_AUTH_PROFILES_URL}`).then(requestResponseHandler);
return fetch(`${BE_AUTH_PROFILES_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createProfile = (data, csrfToken) => {
@ -140,7 +134,7 @@ export const createProfile = (data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const deleteProfile = (id, csrfToken) => {
@ -151,7 +145,7 @@ export const deleteProfile = (id, csrfToken) => {
},
credentials: 'include',
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const updateProfile = (id, data, csrfToken) => {
@ -164,7 +158,7 @@ export const updateProfile = (id, data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const sendNewPassword = (data, csrfToken) => {
@ -177,7 +171,7 @@ export const sendNewPassword = (data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const subscribe = (data, csrfToken) => {
@ -190,7 +184,7 @@ export const subscribe = (data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const resetPassword = (uuid, data, csrfToken) => {
@ -203,7 +197,7 @@ export const resetPassword = (uuid, data, csrfToken) => {
credentials: 'include',
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const getResetPassword = (uuid) => {
@ -212,5 +206,7 @@ export const getResetPassword = (uuid) => {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -1,26 +1,18 @@
import {
BE_GESTIONMESSAGERIE_MESSAGES_URL,
BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL,
BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL,
} from '@/utils/Url';
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;
};
import { errorHandler, requestResponseHandler } from './actionsHandlers';
export const fetchMessages = (id) => {
return fetch(`${BE_GESTIONMESSAGERIE_MESSAGES_URL}/${id}`, {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const sendMessage = (data, csrfToken) => {
@ -31,5 +23,19 @@ export const sendMessage = (data, csrfToken) => {
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(data),
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const searchRecipients = (establishmentId, query) => {
const url = `${BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -1,21 +1,8 @@
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
const requestResponseHandler = async (response) => {
const body = response.status !== 204 ? 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 code de retour : ${response.status}`
);
error.details = body;
throw error;
};
import { errorHandler, requestResponseHandler } from './actionsHandlers';
const getData = (url) => {
return fetch(`${url}`).then(requestResponseHandler);
return fetch(`${url}`).then(requestResponseHandler).catch(errorHandler);
};
const createDatas = (url, newData, csrfToken) => {
@ -27,7 +14,9 @@ const createDatas = (url, newData, csrfToken) => {
},
body: JSON.stringify(newData),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
const updateDatas = (url, updatedData, csrfToken) => {
@ -39,7 +28,9 @@ const updateDatas = (url, updatedData, csrfToken) => {
},
body: JSON.stringify(updatedData),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
const removeDatas = (url, csrfToken) => {
@ -50,7 +41,9 @@ const removeDatas = (url, csrfToken) => {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchPlannings = (

View File

@ -8,17 +8,7 @@ import {
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
} from '@/utils/Url';
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;
};
import { errorHandler, requestResponseHandler } from './actionsHandlers';
// FETCH requests
@ -67,7 +57,7 @@ export const fetchRegistrationSchoolFileMasters = (id = null) => {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchRegistrationParentFileMasters = (id = null) => {
@ -81,7 +71,7 @@ export const fetchRegistrationParentFileMasters = (id = null) => {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchRegistrationSchoolFileTemplates = (id = null) => {
@ -95,7 +85,7 @@ export const fetchRegistrationSchoolFileTemplates = (id = null) => {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
// CREATE requests
@ -130,7 +120,9 @@ export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createRegistrationParentFileMaster = (data, csrfToken) => {
@ -142,7 +134,9 @@ export const createRegistrationParentFileMaster = (data, csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
@ -154,7 +148,9 @@ export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
@ -166,7 +162,9 @@ export const createRegistrationParentFileTemplate = (data, csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
// EDIT requests
@ -207,7 +205,9 @@ export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
},
credentials: 'include',
}
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
@ -222,7 +222,9 @@ export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
},
credentials: 'include',
}
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editRegistrationSchoolFileTemplates = (
@ -240,7 +242,9 @@ export const editRegistrationSchoolFileTemplates = (
},
credentials: 'include',
}
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editRegistrationParentFileTemplates = (
@ -258,7 +262,9 @@ export const editRegistrationParentFileTemplates = (
},
credentials: 'include',
}
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
// DELETE requests
@ -343,7 +349,9 @@ export const cloneTemplate = (templateId, email, is_required) => {
email,
is_required,
}),
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const downloadTemplate = (slug) => {
@ -352,7 +360,9 @@ export const downloadTemplate = (slug) => {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const generateToken = (email, id = null) => {
@ -362,5 +372,7 @@ export const generateToken = (email, id = null) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_email: email, id }),
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -9,34 +9,28 @@ import {
BE_SCHOOL_PAYMENT_MODES_URL,
BE_SCHOOL_ESTABLISHMENT_URL,
} from '@/utils/Url';
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;
};
import { errorHandler, requestResponseHandler } from './actionsHandlers';
export const fetchSpecialities = (establishment) => {
return fetch(
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTeachers = (establishment) => {
return fetch(
`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchClasses = (establishment) => {
return fetch(
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchClasse = (id) => {
@ -46,61 +40,79 @@ export const fetchClasse = (id) => {
};
export const fetchSchedules = () => {
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`).then(requestResponseHandler);
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchRegistrationDiscounts = (establishment) => {
return fetch(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTuitionDiscounts = (establishment) => {
return fetch(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchRegistrationFees = (establishment) => {
return fetch(
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTuitionFees = (establishment) => {
return fetch(
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchRegistrationPaymentPlans = (establishment) => {
return fetch(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTuitionPaymentPlans = (establishment) => {
return fetch(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchRegistrationPaymentModes = (establishment) => {
return fetch(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTuitionPaymentModes = (establishment) => {
return fetch(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchEstablishment = (establishment) => {
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`).then(
requestResponseHandler
);
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createDatas = (url, newData, csrfToken) => {
@ -112,7 +124,9 @@ export const createDatas = (url, newData, csrfToken) => {
},
body: JSON.stringify(newData),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const updateDatas = (url, id, updatedData, csrfToken) => {
@ -124,7 +138,9 @@ export const updateDatas = (url, id, updatedData, csrfToken) => {
},
body: JSON.stringify(updatedData),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const removeDatas = (url, id, csrfToken) => {
@ -135,5 +151,7 @@ export const removeDatas = (url, id, csrfToken) => {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -1,27 +1,23 @@
import { BE_SETTINGS_SMTP_URL } from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
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;
export const fetchSmtpSettings = (csrfToken, establishment_id = null) => {
let url = `${BE_SETTINGS_SMTP_URL}/`;
if (establishment_id) {
url += `?establishment_id=${establishment_id}`;
}
// 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}/`, {
return fetch(`${url}`, {
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editSmtpSettings = (data, csrfToken) => {
@ -33,5 +29,7 @@ export const editSmtpSettings = (data, csrfToken) => {
},
body: JSON.stringify(data),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -7,19 +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();
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;
showNotification('Une erreur inattendue est survenue.', 'error', 'Erreur');
throw error;
};
import { errorHandler, requestResponseHandler } from './actionsHandlers';
export const fetchRegisterForms = (
establishment,
@ -36,17 +24,20 @@ export const fetchRegisterForms = (
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchRegisterForm = (id) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
.then(requestResponseHandler);
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchLastGuardian = () => {
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`).then(
requestResponseHandler
);
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editRegisterForm = (id, data, csrfToken) => {
@ -57,7 +48,9 @@ export const editRegisterForm = (id, data, csrfToken) => {
},
body: data,
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createRegisterForm = (data, csrfToken) => {
@ -70,7 +63,9 @@ export const createRegisterForm = (data, csrfToken) => {
},
body: JSON.stringify(data),
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const sendRegisterForm = (id) => {
@ -79,7 +74,9 @@ export const sendRegisterForm = (id) => {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const resendRegisterForm = (id) => {
@ -88,7 +85,9 @@ export const resendRegisterForm = (id) => {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const archiveRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
@ -97,7 +96,9 @@ export const archiveRegisterForm = (id) => {
headers: {
'Content-Type': 'application/json',
},
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchStudents = (establishment, id = null) => {
@ -110,7 +111,7 @@ export const fetchStudents = (establishment, id = null) => {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchChildren = (id, establishment) => {
@ -123,7 +124,7 @@ export const fetchChildren = (id, establishment) => {
},
}
);
return fetch(request).then(requestResponseHandler);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export async function getRegisterFormFileTemplate(fileId) {
@ -206,7 +207,9 @@ export const dissociateGuardian = async (studentId, guardianId) => {
export const fetchAbsences = (establishment) => {
return fetch(
`${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}`
).then(requestResponseHandler);
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const createAbsences = (data, csrfToken) => {
@ -218,7 +221,9 @@ export const createAbsences = (data, csrfToken) => {
'Content-Type': 'application/json',
},
credentials: 'include',
}).then(requestResponseHandler);
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const editAbsences = (absenceId, payload, csrfToken) => {

View File

@ -0,0 +1,53 @@
'use client';
import logger from '@/utils/logger';
import React, { useState } from 'react';
export default function AnnouncementScheduler({ csrfToken }) {
const [title, setTitle] = useState('');
const [date, setDate] = useState('');
const [message, setMessage] = useState('');
const handleSchedule = () => {
// Logique pour planifier une annonce
logger.debug('Annonce planifiée:', { title, date, message });
};
return (
<div className="p-4 bg-white rounded shadow">
<h2 className="text-xl font-bold mb-4">Planifier une Annonce</h2>
<div className="mb-4">
<label className="block font-medium">Titre</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div className="mb-4">
<label className="block font-medium">Date</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div className="mb-4">
<label className="block font-medium">Message</label>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full p-2 border rounded"
rows="5"
/>
</div>
<button
onClick={handleSchedule}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Planifier
</button>
</div>
);
}

View File

@ -1,75 +1,158 @@
'use client';
import React, { useState } from 'react';
import { Editor } from '@tinymce/tinymce-react';
import { sendMessage } from '@/app/actions/messagerieAction';
import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { sendMessage, searchRecipients } from '@/app/actions/messagerieAction';
import { fetchSmtpSettings } from '@/app/actions/settingsAction';
import { useNotification } from '@/context/NotificationContext';
import { useEstablishment } from '@/context/EstablishmentContext';
import AlertMessage from '@/components/AlertMessage';
import RecipientInput from '@/components/RecipientInput';
import { useRouter } from 'next/navigation'; // Ajoute cette ligne
import WisiwigTextArea from '@/components/WisiwigTextArea';
import logger from '@/utils/logger';
import InputText from '@/components/InputText';
import Button from '@/components/Button';
export default function EmailSender({ csrfToken }) {
const [recipients, setRecipients] = useState('');
const [recipients, setRecipients] = useState([]);
const [fromEmail, setFromEmail] = useState('');
const [cc, setCc] = useState([]);
const [bcc, setBcc] = useState([]);
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [status, setStatus] = useState('');
const [smtpConfigured, setSmtpConfigured] = useState(false); // État pour vérifier si SMTP est configuré
const { showNotification } = useNotification();
const { selectedEstablishmentId } = useEstablishment(); // Récupérer l'establishment_id depuis le contexte
const router = useRouter(); // Ajoute cette ligne
useEffect(() => {
// Vérifier si les paramètres SMTP sont configurés
fetchSmtpSettings(csrfToken, selectedEstablishmentId)
.then((data) => {
if (data.smtp_server && data.smtp_port && data.smtp_user) {
setFromEmail(data.smtp_user);
setSmtpConfigured(true);
} else {
setSmtpConfigured(false);
}
})
.catch((error) => {
logger.error('Erreur lors de la vérification des paramètres SMTP:', {
error,
});
setSmtpConfigured(false);
});
}, [csrfToken, selectedEstablishmentId]);
const handleSendEmail = async () => {
const data = {
recipients: recipients.split(',').map((email) => email.trim()),
recipients,
cc,
bcc,
subject,
message,
};
sendMessage(data);
establishment_id: selectedEstablishmentId, // Ajouter l'establishment_id à la payload
};
try {
await sendMessage(data);
showNotification('Email envoyé avec succès.', 'success', 'Succès');
// Réinitialiser les champs après succès
setRecipients([]);
setCc([]);
setBcc([]);
setSubject('');
setMessage('');
} catch (error) {
logger.error("Erreur lors de l'envoi de l'email:", { error });
showNotification(
"Une erreur est survenue lors de l'envoi de l'email.",
'error',
'Erreur'
);
}
};
if (!smtpConfigured) {
return (
<div className="p-4 bg-white rounded shadow">
<h2 className="text-xl font-bold mb-4">Envoyer un Email</h2>
<div className="mb-4">
<label className="block font-medium">
Destinataires (séparés par des virgules)
</label>
<input
type="text"
value={recipients}
onChange={(e) => setRecipients(e.target.value)}
className="w-full p-2 border rounded"
<AlertMessage
type="warning"
title="Configuration SMTP requise"
message="Les paramètres SMTP de cet établissement ne sont pas configurés. Veuillez les configurer dans la page des paramètres."
actionLabel="Aller aux paramètres"
onAction={() => router.push('/admin/settings?tab=smtp')} // Utilise next/navigation ici
/>
);
}
return (
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md">
{/* Form */}
<div className="p-4 flex flex-col min-h-[600px]">
{' '}
{/* Ajout flex-col et min-h */}
{/* Destinataires */}
<RecipientInput
label="Destinataires"
recipients={recipients}
setRecipients={setRecipients}
searchRecipients={searchRecipients}
establishmentId={selectedEstablishmentId}
required
/>
{/* Cc */}
<div className="mt-2">
<RecipientInput
label="Cc"
placeholder="Ajouter Cc"
recipients={cc}
searchRecipients={searchRecipients}
establishmentId={selectedEstablishmentId}
setRecipients={setCc}
/>
</div>
<div className="mb-4">
<label className="block font-medium">Sujet</label>
<input
type="text"
{/* Bcc */}
<div className="mt-2">
<RecipientInput
label="Cci"
placeholder="Ajouter Bcc"
recipients={bcc}
searchRecipients={searchRecipients}
establishmentId={selectedEstablishmentId}
setRecipients={setBcc}
/>
</div>
{/* Subject */}
<InputText
name="subject"
label="Sujet"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Saisir le sujet"
className="mb-4 mt-2"
required
/>
</div>
<div className="mb-4">
<label className="block font-medium">Message</label>
<Editor
apiKey="8ftyao41dcp1et0p409ipyrdtp14wxs0efqdofvrjq1vo2gi" // Remplacez par votre clé API TinyMCE
{/* Email Body */}
<div className="mb-4 flex flex-col">
<WisiwigTextArea
label="Mail"
value={message}
init={{
height: 300,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
],
toolbar:
'undo redo | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help',
}}
onEditorChange={(content) => setMessage(content)}
onChange={setMessage}
placeholder="Ecrivez votre mail ici..."
required
/>
</div>
<button
{/* Footer */}
<div className="flex justify-between items-center mt-10">
<Button
text="Envoyer"
onClick={handleSendEmail}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Envoyer
</button>
{status && <p className="mt-4 text-sm">{status}</p>}
primary
className="px-4 py-2"
/>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,26 @@
// filepath: d:\Dev\n3wt-innov\n3wt-school\Front-End\src\components\Admin\InstantMessaging.js
import React from 'react';
import Chat from '@/components/Chat';
import { getGravatarUrl } from '@/utils/gravatar';
import logger from '@/utils/logger';
const contacts = [
{
id: 1,
name: 'Parent 1',
profilePic: getGravatarUrl('parent1@n3wtschool.com'),
},
{
id: 2,
name: 'Parent 2',
profilePic: getGravatarUrl('parent2@n3wtschool.com'),
},
];
export default function InstantMessaging({ csrfToken }) {
const handleSendMessage = (contact, message) => {
logger.debug(`Message envoyé à ${contact.name}: ${message}`);
};
return <Chat contacts={contacts} onSendMessage={handleSendMessage} />;
}

View File

@ -14,7 +14,7 @@ const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
};
const handleSubmit = () => {
console.log(formData);
logger.debug(formData);
/*onSubmit({
eleve: {
...formData,

View File

@ -1,21 +1,36 @@
import React from 'react';
const AlertMessage = ({ title, message, buttonText, buttonLink }) => {
const AlertMessage = ({
type = 'info',
title,
message,
actionLabel,
onAction,
}) => {
// Définir les styles en fonction du type d'alerte
const typeStyles = {
info: 'bg-blue-100 border-blue-500 text-blue-700',
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
error: 'bg-red-100 border-red-500 text-red-700',
success: 'bg-green-100 border-green-500 text-green-700',
};
const alertStyle = typeStyles[type] || typeStyles.info;
return (
<div
className="alert centered bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"
role="alert"
>
<div className={`alert centered border-l-4 p-4 ${alertStyle}`} role="alert">
<h3 className="font-bold">{title}</h3>
<p className="mt-2">{message}</p>
{actionLabel && onAction && (
<div className="alert-actions mt-4">
<a
<button
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
href={buttonLink}
onClick={onAction}
>
{buttonText} <i className="icon profile-add"></i>
</a>
{actionLabel}
</button>
</div>
)}
</div>
);
};

View File

@ -1,6 +1,7 @@
import { useState } from 'react';
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
import { Plus, Edit2, Eye, EyeOff, Check, X } from 'lucide-react';
import logger from '@/utils/logger';
export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
const {
@ -118,7 +119,7 @@ export default function ScheduleNavigation({ classes, modeSet = 'event' }) {
>
<option value="">Aucune</option>
{classes.map((classe) => {
console.log({ classe });
logger.debug({ classe });
return (
<option key={classe.id} value={classe.id}>
{classe.atmosphere_name}

View File

@ -0,0 +1,205 @@
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
import Image from 'next/image';
export default function Chat({
discussions,
setDiscussions,
onSendMessage,
simulateResponse,
}) {
const [selectedDiscussion, setSelectedDiscussion] = useState(null);
const [messages, setMessages] = useState({});
const [newMessage, setNewMessage] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
const [newDiscussionName, setNewDiscussionName] = useState('');
const [newDiscussionProfilePic, setNewDiscussionProfilePic] = useState('');
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = () => {
if (newMessage.trim() && selectedDiscussion) {
const discussionMessages = messages[selectedDiscussion.id] || [];
const newMessages = {
...messages,
[selectedDiscussion.id]: [
...discussionMessages,
{
id: discussionMessages.length + 1,
text: newMessage,
date: new Date(),
isResponse: false,
},
],
};
setMessages(newMessages);
setNewMessage('');
onSendMessage && onSendMessage(selectedDiscussion, newMessage);
simulateResponse && simulateResponse(selectedDiscussion.id, setMessages);
}
};
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
const handleCreateDiscussion = () => {
if (newDiscussionName.trim()) {
const newDiscussion = {
id: discussions.length + 1,
name: newDiscussionName,
profilePic: newDiscussionProfilePic || '/default-profile.png', // Image par défaut si aucune n'est fournie
lastMessage: '',
lastMessageDate: new Date(),
};
setDiscussions([...discussions, newDiscussion]);
setNewDiscussionName('');
setNewDiscussionProfilePic('');
setShowCreateForm(false);
}
};
return (
<div className="flex h-full">
{/* Liste des discussions */}
<div className="w-1/4 bg-gray-100 border-r border-gray-300 p-4 overflow-y-auto">
<h2 className="text-lg font-bold mb-4">Discussions</h2>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="w-full p-2 mb-4 bg-blue-500 text-white rounded-lg"
>
{showCreateForm ? 'Annuler' : 'Créer une discussion'}
</button>
{showCreateForm && (
<div className="mb-4 p-2 border rounded-lg bg-white">
<input
type="text"
value={newDiscussionName}
onChange={(e) => setNewDiscussionName(e.target.value)}
placeholder="Nom de la discussion"
className="w-full p-2 mb-2 border rounded"
/>
<input
type="text"
value={newDiscussionProfilePic}
onChange={(e) => setNewDiscussionProfilePic(e.target.value)}
placeholder="URL de la photo de profil (optionnel)"
className="w-full p-2 mb-2 border rounded"
/>
<button
onClick={handleCreateDiscussion}
className="w-full p-2 bg-green-500 text-white rounded-lg"
>
Ajouter
</button>
</div>
)}
{discussions && discussions.length > 0 ? (
discussions.map((discussion) => (
<div
key={discussion.id}
className={`flex items-center p-2 mb-2 cursor-pointer rounded ${
selectedDiscussion?.id === discussion.id
? 'bg-blue-100'
: 'hover:bg-gray-200'
}`}
onClick={() => setSelectedDiscussion(discussion)}
>
<Image
src={discussion.profilePic}
alt={`${discussion.name}'s profile`}
className="w-10 h-10 rounded-full mr-3"
width={40}
height={40}
/>
<div className="flex-1">
<p className="font-medium">{discussion.name}</p>
<p className="text-sm text-gray-500 truncate">
{discussion.lastMessage}
</p>
</div>
<span className="text-xs text-gray-400">
{new Date(discussion.lastMessageDate).toLocaleTimeString()}
</span>
</div>
))
) : (
<p className="text-gray-500">Aucune discussion disponible.</p>
)}
</div>
{/* Zone de chat */}
<div className="flex-1 flex flex-col bg-white">
{/* En-tête du chat */}
{selectedDiscussion && (
<div className="flex items-center p-4 border-b border-gray-300">
<Image
src={selectedDiscussion.profilePic}
alt={`${selectedDiscussion.name}'s profile`}
className="w-10 h-10 rounded-full mr-3"
width={40}
height={40}
/>
<h2 className="text-lg font-bold">{selectedDiscussion.name}</h2>
</div>
)}
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{selectedDiscussion &&
(messages[selectedDiscussion.id] || []).map((message) => (
<div
key={message.id}
className={`flex mb-4 ${
message.isResponse ? 'justify-start' : 'justify-end'
}`}
>
<div
className={`p-3 rounded-lg max-w-xs ${
message.isResponse
? 'bg-gray-200 text-gray-800'
: 'bg-blue-500 text-white'
}`}
>
<p>{message.text}</p>
<span className="text-xs text-gray-500 block mt-1">
{new Date(message.date).toLocaleTimeString()}
</span>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Champ de saisie */}
{selectedDiscussion && (
<div className="p-4 border-t border-gray-300 flex items-center">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="flex-1 p-2 border border-gray-300 rounded-lg mr-2"
placeholder="Écrire un message..."
onKeyDown={handleKeyPress}
/>
<button
onClick={handleSendMessage}
className="p-2 bg-blue-500 text-white rounded-lg"
>
<SendHorizontal />
</button>
</div>
)}
</div>
</div>
);
}

View File

@ -1,3 +1,4 @@
import logger from '@/utils/logger';
import React from 'react';
const CheckBox = ({
@ -8,8 +9,13 @@ const CheckBox = ({
itemLabelFunc = () => null,
horizontal,
}) => {
console.log(formData);
const isChecked = formData[fieldName].includes(parseInt(item.id));
logger.debug(formData);
// Vérifier si formData[fieldName] est un tableau ou une valeur booléenne
const isChecked = Array.isArray(formData[fieldName])
? formData[fieldName].includes(parseInt(item.id)) // Si c'est un tableau, vérifier si l'élément est inclus
: formData[fieldName]; // Si c'est une valeur booléenne, l'utiliser directement
return (
<div
key={item.id}

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Check } from 'lucide-react';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
const DateTab = ({
dates,
@ -29,13 +30,13 @@ const DateTab = ({
handleEdit(paymentPlanId, dataWithType)
.then(() => {
setPopupMessage(
`Mise à jour de la date d'échéance effectuée avec succès`
"Mise à jour de la date d'échéance effectuée avec succès"
);
setPopupVisible(true);
setModifiedDates({});
})
.catch((error) => {
console.error(error);
logger.error(error);
});
};

View File

@ -93,7 +93,6 @@ export default function FileUpload({
{typeof existingFile === 'string'
? existingFile.split('/').pop() // Si c'est une chaîne, utilisez split
: existingFile?.name || 'Nom de fichier inconnu'}{' '}
// Sinon, utilisez une propriété ou un fallback
</span>
</p>
</div>

View File

@ -22,6 +22,7 @@ const typeStyles = {
};
export default function FlashNotification({
displayPeriod = 3000,
title,
message,
type = 'info',
@ -33,9 +34,9 @@ export default function FlashNotification({
const timer = setTimeout(() => {
setIsVisible(false); // Déclenche la disparition
setTimeout(onClose, 300); // Appelle onClose après l'animation
}, 3000); // Notification visible pendant 3 secondes
}, displayPeriod); // Notification visible pendant 3 secondes par défaut
return () => clearTimeout(timer);
}, [onClose]);
}, [onClose, displayPeriod]);
if (!message || !isVisible) return null;
@ -47,14 +48,14 @@ export default function FlashNotification({
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"
className="fixed top-5 right-5 flex items-stretch 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}`}>
<div className={`flex items-center justify-center w-14 ${bg}`}>
{icon}
</div>
{/* Zone de texte */}
<div className="flex-1 p-4">
<div className="flex-1 w-96 p-4">
<p className="font-bold text-black">{title}</p>
<p className="text-gray-700">{message}</p>
</div>

View File

@ -2,7 +2,7 @@ import Logo from '@/components/Logo';
export default function Footer({ softwareName, softwareVersion }) {
return (
<footer className="absolute bottom-0 left-0 right-0 h-16 bg-white border-t border-gray-200 flex items-center justify-center box-border">
<footer className="absolute bottom-0 left-64 right-0 h-16 bg-white border-t border-gray-200 flex items-center justify-center box-border">
<div className="text-sm font-light">
<span>
&copy; {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.

View File

@ -6,6 +6,7 @@ import {
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import { BASE_URL } from '@/utils/Url';
import logger from '@/utils/logger';
const FilesModal = ({
isOpen,
@ -23,9 +24,7 @@ const FilesModal = ({
useEffect(() => {
if (!selectedRegisterForm?.student?.id) {
console.error(
'selectedRegisterForm.student.id est invalide ou manquant.'
);
logger.error('selectedRegisterForm.student.id est invalide ou manquant.');
return;
}
@ -37,7 +36,7 @@ const FilesModal = ({
)
.then((schoolFiles) => {
if (!Array.isArray(schoolFiles)) {
console.error(
logger.error(
'Les fichiers scolaires ne sont pas un tableau :',
schoolFiles
);
@ -84,7 +83,7 @@ const FilesModal = ({
setFiles(categorizedFiles);
})
.catch((error) => {
console.error('Erreur lors de la récupération des fichiers :', error);
logger.error('Erreur lors de la récupération des fichiers :', error);
});
}, [selectedRegisterForm]);
@ -144,7 +143,7 @@ const FilesModal = ({
{/* Section Fichiers École */}
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Formulaires de l'établissement
Formulaires de l&apos;établissement
</h3>
<ul className="space-y-2">
{files.schoolFiles.length > 0 ? (

View File

@ -138,7 +138,7 @@ export default function InscriptionFormShared({
// Mettre à jour isPage6Valid en fonction de cette condition
setIsPage6Valid(allRequiredUploaded);
console.log(allRequiredUploaded);
logger.debug(allRequiredUploaded);
}, [parentFileTemplates]);
const handleTemplateSigned = (index) => {
@ -420,7 +420,7 @@ export default function InscriptionFormShared({
formDataToSend.append('photo', formData.photo);
}
console.log('submit : ', jsonData);
logger.debug('submit : ', jsonData);
// Appeler la fonction onSubmit avec les données FormData
onSubmit(formDataToSend);

View File

@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice';
import RadioList from '@/components/RadioList';
import logger from '@/utils/logger';
export default function PaymentMethodSelector({
formData,
@ -18,7 +19,7 @@ export default function PaymentMethodSelector({
(field) => getLocalError(field) !== ''
);
setIsPageValid(isValid);
console.log('formdata : ', formData);
logger.debug('formdata : ', formData);
}, [formData, setIsPageValid]);
const paymentModesOptions = [
@ -68,7 +69,7 @@ export default function PaymentMethodSelector({
{/* Frais d'inscription */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-2xl font-semibold mb-6 text-gray-800 border-b pb-2">
Frais d'inscription
Frais d&apos;inscription
</h2>
<div className="mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">

View File

@ -115,8 +115,8 @@ export default function ResponsableInputFields({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<SectionHeader
icon={Users}
title={`Responsables légaux`}
description={`Remplissez les champs requis`}
title={'Responsables légaux'}
description={'Remplissez les champs requis'}
/>
{guardians.map((item, index) => (
<div className="p-6 " key={index}>

View File

@ -97,8 +97,8 @@ export default function SiblingInputFields({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<SectionHeader
icon={Users}
title={`Frères et Sœurs`}
description={`Ajoutez les informations des frères et sœurs`}
title={'Frères et Sœurs'}
description={'Ajoutez les informations des frères et sœurs'}
/>
{siblings.map((item, index) => (
<div className="p-6" key={index}>

View File

@ -154,8 +154,8 @@ export default function StudentInfoForm({
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 space-y-8">
<SectionHeader
icon={User}
title={`Informations de l'élève`}
description={`Remplissez les champs requis`}
title={"Informations de l'élève"}
description={'Remplissez les champs requis'}
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<InputText

View File

@ -126,7 +126,7 @@ export default function ValidateSubscription({
]
: []),
];
console.log(allTemplates);
logger.debug(allTemplates);
return (
<div className="mb-4 w-full mx-auto">

View File

@ -4,6 +4,7 @@ import Table from '@/components/Table';
import DateTab from '@/components/DateTab';
import InputTextIcon from '@/components/InputTextIcon';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
const paymentPlansOptions = [
{ id: 0, name: '1 fois', frequency: 1 },
@ -118,7 +119,7 @@ const PaymentPlanSelector = ({
});
})
.catch((error) => {
console.error(error);
logger.error(error);
});
};
@ -222,13 +223,13 @@ const PaymentPlanSelector = ({
handleEdit(selectedPlan.id, updatedData)
.then(() => {
setPopupMessage(
`Mise à jour des dates d'échéances effectuée avec succès`
"Mise à jour des dates d'échéances effectuée avec succès"
);
setPopupVisible(true);
setIsDefaultDayModified(false);
})
.catch((error) => {
console.error(error);
logger.error(error);
});
};

View File

@ -8,10 +8,11 @@ import { NotificationProvider } from '@/context/NotificationContext';
import { ClassesProvider } from '@/context/ClassesContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import logger from '@/utils/logger';
export default function Providers({ children, messages, locale, session }) {
if (!locale) {
console.error('Locale non définie dans Providers');
logger.error('Locale non définie dans Providers');
locale = 'fr'; // Valeur par défaut
}
return (

View File

@ -0,0 +1,152 @@
import React, { useState } from 'react';
import { getGravatarUrl } from '@/utils/gravatar'; // Assurez-vous que cette fonction est définie pour générer les URLs Gravatar
import { getRightStr } from '@/utils/rights'; // Fonction existante pour récupérer le nom des rôles
import logger from '@/utils/logger';
export default function RecipientInput({
label,
recipients,
setRecipients,
searchRecipients, // Fonction pour effectuer la recherche
establishmentId, // ID de l'établissement
required = false, // Ajout de la prop required avec valeur par défaut
}) {
const [inputValue, setInputValue] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(-1);
const handleInputChange = async (e) => {
const value = e.target.value;
setInputValue(value);
if (value.trim() !== '') {
try {
const results = await searchRecipients(establishmentId, value);
setSuggestions(results);
} catch (error) {
logger.error('Erreur lors de la recherche des destinataires:', error);
setSuggestions([]);
}
} else {
setSuggestions([]);
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
handleSuggestionClick(suggestions[selectedIndex]);
} else {
const trimmedValue = inputValue.trim();
if (trimmedValue && !recipients.includes(trimmedValue)) {
setRecipients([...recipients, trimmedValue]);
setInputValue('');
setSuggestions([]);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prevIndex) =>
prevIndex < suggestions.length - 1 ? prevIndex + 1 : 0
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prevIndex) =>
prevIndex > 0 ? prevIndex - 1 : suggestions.length - 1
);
}
};
const handleSuggestionClick = (suggestion) => {
if (!recipients.includes(suggestion.email)) {
setRecipients([...recipients, suggestion.email]);
}
setInputValue('');
setSuggestions([]);
};
const handleRemoveRecipient = (email) => {
setRecipients(recipients.filter((recipient) => recipient !== email));
};
return (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div
className={`
mt-1 flex flex-wrap items-center gap-2 border rounded-md
border-gray-200 hover:border-gray-400 focus-within:border-gray-500
transition-colors
`}
>
{recipients.map((email, index) => (
<div
key={index}
className="flex items-center bg-gray-100 text-gray-700 px-2 py-1 rounded-full"
>
<img
src={getGravatarUrl(email)}
alt={email}
className="w-6 h-6 rounded-full mr-2"
/>
<span className="mr-2">{email}</span>
<button
type="button"
onClick={() => handleRemoveRecipient(email)}
className="text-gray-500 hover:text-gray-700"
>
&times;
</button>
</div>
))}
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Rechercher des destinataires"
className="flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md"
required={required}
/>
</div>
{suggestions.length > 0 && (
<ul className="border rounded mt-2 bg-white shadow">
{suggestions.map((suggestion, index) => (
<li
key={suggestion.id}
className={`p-2 cursor-pointer ${
index === selectedIndex ? 'bg-gray-200' : ''
}`}
onClick={() => handleSuggestionClick(suggestion)}
>
<div className="flex items-center gap-2">
<img
src={getGravatarUrl(suggestion.email)}
alt={suggestion.email}
className="w-8 h-8 rounded-full"
/>
<div>
<p className="font-medium">
{suggestion.first_name && suggestion.last_name
? `${suggestion.first_name} ${suggestion.last_name}`
: suggestion.email}
</p>
<p className="text-sm text-gray-500">{suggestion.email}</p>
<p className="text-xs text-gray-400">
{suggestion.roles
.map((role) => getRightStr(role.role_type) || 'Inconnu')
.join(', ')}
</p>
</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}

View File

@ -12,7 +12,7 @@ const SidebarTabs = ({ tabs, onTabChange }) => {
};
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full w-full">
{/* Tabs Header */}
<div className="flex h-14 bg-gray-50 border-b border-gray-200 shadow-sm">
{tabs.map((tab) => (
@ -31,7 +31,7 @@ const SidebarTabs = ({ tabs, onTabChange }) => {
</div>
{/* Tabs Content */}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner relative">
<div className="flex-1 overflow-y-auto rounded-b-lg shadow-inner relative">
<AnimatePresence mode="wait">
{tabs.map(
(tab) =>

View File

@ -52,7 +52,7 @@ export default function FileUploadDocuSeal({
setToken(data.token);
})
.catch((error) =>
console.error('Erreur lors de la génération du token:', error)
logger.error('Erreur lors de la génération du token:', error)
);
}, [fileToEdit]);
@ -129,7 +129,7 @@ export default function FileUploadDocuSeal({
master: templateMaster?.id,
registration_form: guardian.registration_form,
};
console.log('creation : ', data);
logger.debug('creation : ', data);
createRegistrationSchoolFileTemplate(data, csrfToken)
.then((response) => {
logger.debug('Template enregistré avec succès:', response);
@ -166,13 +166,13 @@ export default function FileUploadDocuSeal({
<div className="flex flex-col items-center justify-center h-full text-center space-y-6">
{/* Description de l'étape */}
<p className="text-gray-700 text-base font-medium mb-4">
Étape 1 - Sélectionner au moins un dossier d'inscription
Étape 1 - Sélectionner au moins un dossier d&apos;inscription
</p>
{/* Section centrée pour la sélection des groupes */}
<div className="bg-gray-50 p-8 rounded-lg shadow-md border border-gray-300 transform transition-transform hover:scale-105">
<h3 className="text-xl font-semibold text-gray-800 mb-4 text-center">
Dossiers d'inscription
Dossiers d&apos;inscription
</h3>
<MultiSelect
name="groups"
@ -190,7 +190,7 @@ export default function FileUploadDocuSeal({
{/* Section Dossiers d'inscription repositionnée sur le côté */}
<div className="col-span-2 bg-white p-4 rounded-lg shadow-md border border-gray-200">
<h3 className="text-lg font-medium text-gray-800 mb-4">
Dossiers d'inscription
Dossiers d&apos;inscription
</h3>
<MultiSelect
name="groups"

View File

@ -97,7 +97,7 @@ export default function FilesGroupsManagement({
}
)
.catch((err) => {
console.log(err.message);
logger.debug(err.message);
})
.finally(() => {
setReloadTemplates(false);
@ -155,7 +155,7 @@ export default function FilesGroupsManagement({
}
})
.catch((error) => {
console.error('Error deleting file from database:', error);
logger.error('Error deleting file from database:', error);
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
@ -175,7 +175,7 @@ export default function FilesGroupsManagement({
}
})
.catch((error) => {
console.error('Error removing template from DocuSeal:', error);
logger.error('Error removing template from DocuSeal:', error);
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
@ -207,7 +207,7 @@ export default function FilesGroupsManagement({
return response;
})
.catch((error) => {
console.error('Error removing template:', error);
logger.error('Error removing template:', error);
throw error;
});
};
@ -235,7 +235,7 @@ export default function FilesGroupsManagement({
setIsModalOpen(false);
})
.catch((error) => {
console.error('Error uploading file:', error);
logger.error('Error uploading file:', error);
});
};
@ -258,7 +258,7 @@ export default function FilesGroupsManagement({
setIsModalOpen(false);
})
.catch((error) => {
console.error('Error editing file:', error);
logger.error('Error editing file:', error);
showNotification(
'Erreur lors de la modification du fichier',
'error',
@ -280,7 +280,7 @@ export default function FilesGroupsManagement({
setIsGroupModalOpen(false);
})
.catch((error) => {
console.error('Error handling group:', error);
logger.error('Error handling group:', error);
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
@ -300,7 +300,7 @@ export default function FilesGroupsManagement({
setIsGroupModalOpen(false);
})
.catch((error) => {
console.error('Error handling group:', error);
logger.error('Error handling group:', error);
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
@ -349,7 +349,7 @@ export default function FilesGroupsManagement({
showNotification('Groupe supprimé avec succès.', 'success', 'Succès');
})
.catch((error) => {
console.error('Error deleting group:', error);
logger.error('Error deleting group:', error);
setRemovePopupVisible(false);
setIsLoading(false);
showNotification(

View File

@ -54,7 +54,7 @@ const FeesManagement = ({
<div className="w-4/5 mx-auto flex items-center mt-8">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">
Frais d'inscription
Frais d&apos;inscription
</span>
<hr className="flex-grow border-t-2 border-gray-300" />
</div>

View File

@ -257,9 +257,7 @@ const FeesSection = ({
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
'Attentions ! \nVous êtes sur le point de supprimer un ' +
labelTypeFrais +
".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
`Attentions ! \nVous êtes sur le point de supprimer un ${labelTypeFrais} .\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
handleRemoveFee(fee.id)

View File

@ -0,0 +1,61 @@
import dynamic from 'next/dynamic';
import 'react-quill/dist/quill.snow.css';
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
export default function WisiwigTextArea({
label = 'Mail',
value,
onChange,
placeholder = 'Ecrivez votre mail ici...',
className = 'h-64',
required = false,
errorMsg,
errorLocalMsg,
enable = true,
}) {
return (
<div className={`mb-4 ${className}`}>
<label className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div
className={`
mt-1 border rounded-md
${
errorMsg || errorLocalMsg
? 'border-red-500 hover:border-red-700'
: 'border-gray-200 hover:border-gray-400'
}
${!errorMsg && !errorLocalMsg ? 'focus-within:border-gray-500' : ''}
${!enable ? 'bg-gray-100 cursor-not-allowed' : ''}
`}
>
<ReactQuill
theme="snow"
value={value}
onChange={enable ? onChange : undefined}
placeholder={placeholder}
readOnly={!enable}
className={`bg-white rounded-md border-0 shadow-none !border-0 !outline-none ${!enable ? 'bg-gray-100 cursor-not-allowed' : ''}`}
style={{ minHeight: 250, border: 'none', boxShadow: 'none' }}
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
{errorLocalMsg && (
<p className="mt-2 text-sm text-red-600">{errorLocalMsg}</p>
)}
<style jsx global>{`
.ql-toolbar.ql-snow {
border: none !important;
border-bottom: 1px solid #e5e7eb !important; /* gray-200 */
border-radius: 0.375rem 0.375rem 0 0 !important; /* rounded-t-md */
}
.ql-container.ql-snow {
border: none !important;
}
`}</style>
</div>
);
}

View File

@ -1,6 +1,7 @@
import { getRequestConfig } from 'next-intl/server';
import { routing } from '@/i18n/routing';
import { getAvailableNamespaces } from '@/utils/i18n';
import logger from '@/utils/logger';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
@ -22,7 +23,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
).default;
messages[namespace] = translations;
} catch (error) {
console.warn(
logger.warn(
`Erreur de chargement pour ${namespace} en ${locale}:`,
error
);

View File

@ -1,3 +1,4 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
@ -25,11 +26,11 @@ export default function handler(req, res) {
return response.json();
})
.then((data) => {
console.log('Template cloned successfully:', data);
logger.debug('Template cloned successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
console.error('Error cloning template:', error);
logger.error('Error cloning template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {

View File

@ -1,9 +1,10 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_DOWNLOAD_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'GET') {
const { slug } = req.query;
console.log('slug : ', slug);
logger.debug('slug : ', slug);
fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}`, {
method: 'GET',
@ -20,11 +21,11 @@ export default function handler(req, res) {
return response.json();
})
.then((data) => {
console.log('Template downloaded successfully:', data);
logger.debug('Template downloaded successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
console.error('Error downloading template:', error);
logger.error('Error downloading template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {

View File

@ -1,3 +1,4 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url';
export default function handler(req, res) {
@ -11,17 +12,17 @@ export default function handler(req, res) {
body: JSON.stringify(req.body),
})
.then((response) => {
console.log('Response status:', response.status);
logger.debug('Response status:', response.status);
return response
.json()
.then((data) => ({ status: response.status, data }));
})
.then(({ status, data }) => {
console.log('Response data:', data);
logger.debug('Response data:', data);
res.status(status).json(data);
})
.catch((error) => {
console.error('Error:', error);
logger.error('Error:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {

View File

@ -1,3 +1,4 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_REMOVE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
@ -19,11 +20,11 @@ export default function handler(req, res) {
return response.json();
})
.then((data) => {
console.log('Template removed successfully:', data);
logger.debug('Template removed successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
console.error('Error removing template:', error);
logger.error('Error removing template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {

View File

@ -54,63 +54,68 @@ export const BE_PLANNING_EVENTS_URL = `${BASE_URL}/Planning/events`;
export const BE_GESTIONMESSAGERIE_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/messages`;
export const BE_GESTIONMESSAGERIE_MESSAGERIE_URL = `${BASE_URL}/GestionMessagerie/messagerie`;
export const BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL = `${BASE_URL}/GestionMessagerie/send-email/`;
export const BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL = `${BASE_URL}/GestionMessagerie/search-recipients`;
// SETTINGS
export const BE_SETTINGS_SMTP_URL = `${BASE_URL}/Settings/smtp-settings`;
// URL FRONT-END
export const FE_HOME_URL = `/`;
export const FE_HOME_URL = '/';
// USERS
export const FE_USERS_LOGIN_URL = `/users/login`;
export const FE_USERS_SUBSCRIBE_URL = `/users/subscribe`;
export const FE_USERS_RESET_PASSWORD_URL = `/users/password/reset`;
export const FE_USERS_NEW_PASSWORD_URL = `/users/password/new`;
export const FE_USERS_LOGIN_URL = '/users/login';
export const FE_USERS_SUBSCRIBE_URL = '/users/subscribe';
export const FE_USERS_RESET_PASSWORD_URL = '/users/password/reset';
export const FE_USERS_NEW_PASSWORD_URL = '/users/password/new';
// ADMIN
export const FE_ADMIN_HOME_URL = `/admin`;
export const FE_ADMIN_HOME_URL = '/admin';
// ADMIN/SUBSCRIPTIONS URL
export const FE_ADMIN_SUBSCRIPTIONS_URL = `/admin/subscriptions`;
export const FE_ADMIN_SUBSCRIPTIONS_CREATE_URL = `/admin/subscriptions/createSubscription`;
export const FE_ADMIN_SUBSCRIPTIONS_EDIT_URL = `/admin/subscriptions/editSubscription`;
export const FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL = `/admin/subscriptions/validateSubscription`;
export const FE_ADMIN_SUBSCRIPTIONS_URL = '/admin/subscriptions';
export const FE_ADMIN_SUBSCRIPTIONS_CREATE_URL =
'/admin/subscriptions/createSubscription';
export const FE_ADMIN_SUBSCRIPTIONS_EDIT_URL =
'/admin/subscriptions/editSubscription';
export const FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL =
'/admin/subscriptions/validateSubscription';
//ADMIN/CLASSES URL
export const FE_ADMIN_CLASSES_URL = `/admin/classes`;
export const FE_ADMIN_CLASSES_URL = '/admin/classes';
//ADMIN/STRUCTURE URL
export const FE_ADMIN_STRUCTURE_URL = `/admin/structure`;
export const FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL = `/admin/structure/SchoolClassManagement`;
export const FE_ADMIN_STRUCTURE_URL = '/admin/structure';
export const FE_ADMIN_STRUCTURE_SCHOOLCLASS_MANAGEMENT_URL =
'/admin/structure/SchoolClassManagement';
//ADMIN/DIRECTORY URL
export const FE_ADMIN_DIRECTORY_URL = `/admin/directory`;
export const FE_ADMIN_DIRECTORY_URL = '/admin/directory';
//ADMIN/GRADES URL
export const FE_ADMIN_GRADES_URL = `/admin/grades`;
export const FE_ADMIN_GRADES_URL = '/admin/grades';
//ADMIN/TEACHERS URL
export const FE_ADMIN_TEACHERS_URL = `/admin/teachers`;
export const FE_ADMIN_TEACHERS_URL = '/admin/teachers';
//ADMIN/PLANNING URL
export const FE_ADMIN_PLANNING_URL = `/admin/planning`;
export const FE_ADMIN_PLANNING_URL = '/admin/planning';
//ADMIN/SETTINGS URL
export const FE_ADMIN_SETTINGS_URL = `/admin/settings`;
export const FE_ADMIN_SETTINGS_URL = '/admin/settings';
//ADMIN/MESSAGERIE URL
export const FE_ADMIN_MESSAGERIE_URL = `/admin/messagerie`;
export const FE_ADMIN_MESSAGERIE_URL = '/admin/messagerie';
// PARENT HOME
export const FE_PARENTS_HOME_URL = `/parents`;
export const FE_PARENTS_MESSAGERIE_URL = `/parents/messagerie`;
export const FE_PARENTS_SETTINGS_URL = `/parents/settings`;
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = `/parents/editSubscription`;
export const FE_PARENTS_HOME_URL = '/parents';
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
export const FE_PARENTS_SETTINGS_URL = '/parents/settings';
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
// API DOCUSEAL
export const FE_API_DOCUSEAL_GENERATE_TOKEN = `/api/docuseal/generateToken`;
export const FE_API_DOCUSEAL_CLONE_URL = `/api/docuseal/cloneTemplate`;
export const FE_API_DOCUSEAL_DOWNLOAD_URL = `/api/docuseal/downloadTemplate`;
export const FE_API_DOCUSEAL_GENERATE_TOKEN = '/api/docuseal/generateToken';
export const FE_API_DOCUSEAL_CLONE_URL = '/api/docuseal/cloneTemplate';
export const FE_API_DOCUSEAL_DOWNLOAD_URL = '/api/docuseal/downloadTemplate';
/**
* Fonction pour obtenir l'URL de redirection en fonction du rôle

View File

@ -15,6 +15,7 @@ export async function getAvailableNamespaces(locale) {
.filter((file) => file.endsWith('.json'))
.map((file) => file.replace('.json', ''));
} catch (error) {
// eslint-disable-next-line no-console
console.warn(`Impossible de lire les namespaces pour ${locale}:`, error);
return ['common']; // Namespace par défaut
}

View File

@ -15,21 +15,24 @@ const getCallerInfo = () => {
const logger = {
debug: (...args) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log.apply(console, ['[DEBUG]', `${getCallerInfo()}`, ...args]);
}
},
error: (...args) => {
// Les erreurs sont toujours loguées
// eslint-disable-next-line no-console
console.error.apply(console, ['[ERROR]', `${getCallerInfo()}`, ...args]);
},
warn: (...args) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn.apply(console, ['[WARN]', `${getCallerInfo()}`, ...args]);
}
},
info: (...args) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.info.apply(console, ['[INFO]', `${getCallerInfo()}`, ...args]);
}
},

View File

@ -16,7 +16,7 @@
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"husky": "^9.1.6",
"standard-version": "^9.5.0",
"prettier": "^3.5.3"
"prettier": "^3.5.3",
"standard-version": "^9.5.0"
}
}

View File

@ -1,26 +1,39 @@
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
// Chemins vers les fichiers
const rootPackageJsonPath = path.resolve(__dirname, '../package.json');
const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf8'));
const rootPackageJsonPath = path.resolve(__dirname, "../package.json");
const rootPackageJson = JSON.parse(
fs.readFileSync(rootPackageJsonPath, "utf8")
);
// Lire la version actuelle du package.json principal
const newVersion = rootPackageJson.version;
const frontendPackageJsonPath = path.resolve(__dirname, '../Front-End/package.json');
const frontendPackageJsonPath = path.resolve(
__dirname,
"../Front-End/package.json"
);
// Lire et mettre à jour le package.json du sous-module
const frontendPackageJson = JSON.parse(fs.readFileSync(frontendPackageJsonPath, 'utf8'));
const frontendPackageJson = JSON.parse(
fs.readFileSync(frontendPackageJsonPath, "utf8")
);
frontendPackageJson.version = newVersion;
// Écrire les changements dans le fichier frontend/package.json
fs.writeFileSync(frontendPackageJsonPath, JSON.stringify(frontendPackageJson, null, 2), 'utf8');
fs.writeFileSync(
frontendPackageJsonPath,
JSON.stringify(frontendPackageJson, null, 2),
"utf8"
);
// eslint-disable-next-line no-console
console.log(`Version mise à jour dans frontend/package.json : ${newVersion}`);
// Chemin vers le fichier Python de version
const versionFilePath = path.resolve(__dirname, '../Back-End/__version__.py');
const versionFilePath = path.resolve(__dirname, "../Back-End/__version__.py");
// Mettre à jour le fichier Python
const versionContent = `__version__ = "${newVersion}"\n`;
fs.writeFileSync(versionFilePath, versionContent, 'utf8');
console.log(`Version du backend mise à jour dans ${versionFilePath} : ${newVersion}`);
fs.writeFileSync(versionFilePath, versionContent, "utf8");
// eslint-disable-next-line no-console
console.log(
`Version du backend mise à jour dans ${versionFilePath} : ${newVersion}`
);