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

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

View File

@ -1,6 +1,7 @@
import { usePlanning, RecurrenceType } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
import { useNotification } from '@/context/NotificationContext';
export default function EventModal({
isOpen,
@ -10,6 +11,7 @@ export default function EventModal({
}) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
usePlanning();
const { showNotification } = useNotification();
// S'assurer que planning est défini lors du premier rendu
React.useEffect(() => {
@ -46,7 +48,11 @@ export default function EventModal({
e.preventDefault();
if (!eventData.planning) {
alert('Veuillez sélectionner un planning');
showNotification(
'Veuillez sélectionner un planning',
'warning',
'Attention'
);
return;
}

View File

@ -0,0 +1,70 @@
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
const typeStyles = {
success: {
icon: <CheckCircle className="h-6 w-6 text-green-500" />,
bg: 'bg-green-300',
},
error: {
icon: <AlertCircle className="h-6 w-6 text-red-500" />,
bg: 'bg-red-300',
},
info: {
icon: <Info className="h-6 w-6 text-blue-500" />,
bg: 'bg-blue-300',
},
warning: {
icon: <AlertTriangle className="h-6 w-6 text-yellow-500" />,
bg: 'bg-yellow-300',
},
};
export default function FlashNotification({
title,
message,
type = 'info',
onClose,
}) {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false); // Déclenche la disparition
setTimeout(onClose, 300); // Appelle onClose après l'animation
}, 3000); // Notification visible pendant 3 secondes
return () => clearTimeout(timer);
}, [onClose]);
if (!message || !isVisible) return null;
const { icon, bg } = typeStyles[type] || typeStyles.info;
return (
<motion.div
initial={{ opacity: 0, x: 50 }} // Animation d'entrée
animate={{ opacity: 1, x: 0 }} // Animation visible
exit={{ opacity: 0, x: 50 }} // Animation de sortie
transition={{ duration: 0.3 }} // Durée des animations
className="fixed top-5 right-5 flex items-stretch w-96 rounded-lg shadow-lg bg-white z-50 border border-gray-200"
>
{/* Rectangle gauche avec l'icône */}
<div className={`flex items-center justify-center w-12 ${bg}`}>
{icon}
</div>
{/* Zone de texte */}
<div className="flex-1 p-4">
<p className="font-bold text-black">{title}</p>
<p className="text-gray-700">{message}</p>
</div>
{/* Bouton de fermeture */}
<button
onClick={() => setIsVisible(false)}
className="text-gray-500 hover:text-gray-700 focus:outline-none p-2"
>
<X className="h-5 w-5" />
</button>
</motion.div>
);
}

View File

@ -31,7 +31,7 @@ export default function InputTextIcon({
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
}`}
>
<span className="inline-flex items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
{IconItem && <IconItem />}
</span>
<input

View File

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

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const SidebarTabs = ({ tabs, onTabChange }) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
@ -30,15 +31,24 @@ const SidebarTabs = ({ tabs, onTabChange }) => {
</div>
{/* Tabs Content */}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner">
{tabs.map((tab) => (
<div
key={tab.id}
className={`${activeTab === tab.id ? 'block' : 'hidden'}`}
>
{tab.content}
</div>
))}
<div className="flex-1 overflow-y-auto p-4 rounded-b-lg shadow-inner relative">
<AnimatePresence mode="wait">
{tabs.map(
(tab) =>
activeTab === tab.id && (
<motion.div
key={tab.id}
initial={{ opacity: 0, x: 50 }} // Animation d'entrée
animate={{ opacity: 1, x: 0 }} // Animation visible
exit={{ opacity: 0, x: -50 }} // Animation de sortie
transition={{ duration: 0.3 }} // Durée des animations
className="absolute w-full h-full"
>
{tab.content}
</motion.div>
)
)}
</AnimatePresence>
</div>
</div>
);

View File

@ -29,6 +29,7 @@ import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection'
import SectionHeader from '@/components/SectionHeader';
import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext';
export default function FilesGroupsManagement({
csrfToken,
@ -52,6 +53,7 @@ export default function FilesGroupsManagement({
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [isLoading, setIsLoading] = useState(false);
const { showNotification } = useNotification();
const handleReloadTemplates = () => {
setReloadTemplates(true);
@ -136,15 +138,20 @@ export default function FilesGroupsManagement({
(fichier) => fichier.id !== templateMaster.id
)
);
setPopupMessage(
`Le document "${templateMaster.name}" a été correctement supprimé.`
showNotification(
`Le document "${templateMaster.name}" a été correctement supprimé.`,
'success',
'Succès'
);
setPopupVisible(true);
setRemovePopupVisible(false);
setIsLoading(false);
} else {
setPopupMessage(
`Erreur lors de la suppression du document "${templateMaster.name}".`
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setPopupVisible(true);
setRemovePopupVisible(false);
@ -153,16 +160,20 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
console.error('Error deleting file from database:', error);
setPopupMessage(
`Erreur lors de la suppression du document "${templateMaster.name}".`
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setPopupVisible(true);
setRemovePopupVisible(false);
setIsLoading(false);
});
} else {
setPopupMessage(
`Erreur lors de la suppression du document "${templateMaster.name}".`
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setPopupVisible(true);
setRemovePopupVisible(false);
@ -171,8 +182,10 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
console.error('Error removing template from DocuSeal:', error);
setPopupMessage(
`Erreur lors de la suppression du document "${templateMaster.name}".`
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setPopupVisible(true);
setRemovePopupVisible(false);
@ -253,7 +266,11 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
showNotification(
'Erreur lors de la modification du fichier',
'error',
'Erreur'
);
});
};
@ -271,7 +288,11 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
console.error('Error handling group:', error);
alert("Erreur lors de l'opération sur le groupe");
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
'Erreur'
);
});
} else {
// Ajouter l'établissement sélectionné lors de la création d'un nouveau groupe
@ -287,7 +308,11 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
console.error('Error handling group:', error);
alert("Erreur lors de l'opération sur le groupe");
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
'Erreur'
);
});
}
};
@ -303,8 +328,10 @@ export default function FilesGroupsManagement({
(file) => file.group && file.group.id === groupId
);
if (filesInGroup.length > 0) {
alert(
"Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe."
showNotification(
"Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe.",
'error',
'Erreur'
);
return;
}
@ -319,13 +346,15 @@ export default function FilesGroupsManagement({
throw new Error('Erreur lors de la suppression du groupe.');
}
setGroups(groups.filter((group) => group.id !== groupId));
alert('Groupe supprimé avec succès.');
showNotification('Groupe supprimé avec succès.', 'success', 'Succès');
})
.catch((error) => {
console.error('Error deleting group:', error);
alert(
showNotification(
error.message ||
"Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe."
"Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe.",
'error',
'Erreur'
);
});
}
@ -342,8 +371,10 @@ export default function FilesGroupsManagement({
})
.catch((error) => {
logger.error('Erreur lors de la création du document parent:', error);
alert(
'Une erreur est survenue lors de la création du document parent.'
showNotification(
'Une erreur est survenue lors de la création du document parent.',
'error',
'Erreur'
);
throw error;
});
@ -365,8 +396,10 @@ export default function FilesGroupsManagement({
'Erreur lors de la modification du document parent:',
error
);
alert(
'Une erreur est survenue lors de la modification du document parent.'
showNotification(
'Une erreur est survenue lors de la modification du document parent.',
'error',
'Erreur'
);
throw error;
});

View File

@ -1,6 +1,7 @@
import { usePlanning } from '@/context/PlanningContext';
import { format } from 'date-fns';
import React from 'react';
import { useNotification } from '@/context/NotificationContext';
export default function ScheduleEventModal({
isOpen,
@ -13,6 +14,7 @@ export default function ScheduleEventModal({
}) {
const { addEvent, handleUpdateEvent, handleDeleteEvent, schedules } =
usePlanning();
const { showNotification } = useNotification();
React.useEffect(() => {
if (!eventData?.planning && schedules.length > 0) {
@ -78,22 +80,34 @@ export default function ScheduleEventModal({
e.preventDefault();
if (!eventData.speciality) {
alert('Veuillez sélectionner une matière');
showNotification(
'Veuillez sélectionner une matière',
'warning',
'Attention'
);
return;
}
if (!eventData.teacher) {
alert('Veuillez sélectionner un professeur');
showNotification(
'Veuillez sélectionner un professeur',
'warning',
'Attention'
);
return;
}
if (!eventData.planning) {
alert('Veuillez sélectionner un planning');
showNotification(
'Veuillez sélectionner un planning',
'warning',
'Attention'
);
return;
}
if (!eventData.location) {
alert('Veuillez saisir un lieu');
showNotification('Veuillez saisir un lieu', 'warning', 'Attention');
return;
}