mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Messagerie WIP [#17]
This commit is contained in:
@ -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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -1,10 +1,35 @@
|
||||
'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';
|
||||
|
||||
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) => console.log(`Onglet actif : ${tabId}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -24,7 +25,6 @@ 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();
|
||||
@ -35,7 +35,7 @@ export default function SettingsPage() {
|
||||
// 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 || '');
|
||||
@ -46,7 +46,11 @@ export default function SettingsPage() {
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors du chargement des paramètres SMTP:', error);
|
||||
setStatusMessage('Erreur lors du chargement des paramètres SMTP.');
|
||||
showNotification(
|
||||
'Erreur lors du chargement des paramètres SMTP.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
||||
@ -113,7 +117,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 +129,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,48 +176,54 @@ export default function SettingsPage() {
|
||||
</TabContent>
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<InputText
|
||||
label="Serveur SMTP"
|
||||
value={smtpServer}
|
||||
onChange={handleSmtpServerChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Port SMTP"
|
||||
value={smtpPort}
|
||||
onChange={handleSmtpPortChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Utilisateur SMTP"
|
||||
value={smtpUser}
|
||||
onChange={handleSmtpUserChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe SMTP"
|
||||
type="password"
|
||||
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 className="grid grid-cols-2 gap-4">
|
||||
<InputText
|
||||
label="Serveur SMTP"
|
||||
value={smtpServer}
|
||||
onChange={handleSmtpServerChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Port SMTP"
|
||||
value={smtpPort}
|
||||
onChange={handleSmtpPortChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Utilisateur SMTP"
|
||||
value={smtpUser}
|
||||
onChange={handleSmtpUserChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe SMTP"
|
||||
type="password"
|
||||
value={smtpPassword}
|
||||
onChange={handleSmtpPasswordChange}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
BE_GESTIONMESSAGERIE_MESSAGES_URL,
|
||||
BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL,
|
||||
BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
const requestResponseHandler = async (response) => {
|
||||
@ -8,8 +9,6 @@ const requestResponseHandler = async (response) => {
|
||||
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;
|
||||
@ -33,3 +32,13 @@ export const sendMessage = (data, csrfToken) => {
|
||||
body: JSON.stringify(data),
|
||||
}).then(requestResponseHandler);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@ -15,8 +15,12 @@ const requestResponseHandler = async (response) => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
export const fetchSmtpSettings = (csrfToken) => {
|
||||
return fetch(`${BE_SETTINGS_SMTP_URL}/`, {
|
||||
export const fetchSmtpSettings = (csrfToken, establishment_id = null) => {
|
||||
let url = `${BE_SETTINGS_SMTP_URL}/`;
|
||||
if (establishment_id) {
|
||||
url += `?establishment_id=${establishment_id}`;
|
||||
}
|
||||
return fetch(`${url}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
|
||||
52
Front-End/src/components/Admin/AnnouncementScheduler.js
Normal file
52
Front-End/src/components/Admin/AnnouncementScheduler.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
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
|
||||
console.log('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>
|
||||
);
|
||||
}
|
||||
@ -1,75 +1,163 @@
|
||||
'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';
|
||||
|
||||
// Charger Quill dynamiquement pour éviter les problèmes de SSR
|
||||
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
|
||||
import 'react-quill/dist/quill.snow.css'; // Importer les styles de Quill
|
||||
|
||||
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
|
||||
|
||||
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) => {
|
||||
console.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,
|
||||
establishment_id: selectedEstablishmentId, // Ajouter l'establishment_id à la payload
|
||||
};
|
||||
sendMessage(data);
|
||||
|
||||
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) {
|
||||
console.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 (
|
||||
<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={() => (window.location.href = '/admin/settings')} // Redirige vers la page des paramètres
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<h2 className="text-sm font-medium text-gray-700">
|
||||
Email from {fromEmail}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium">Sujet</label>
|
||||
<input
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-4">
|
||||
{/* To */}
|
||||
<RecipientInput
|
||||
label="Destinataires"
|
||||
recipients={recipients}
|
||||
setRecipients={setRecipients}
|
||||
searchRecipients={searchRecipients} // Passer l'action de recherche
|
||||
establishmentId={selectedEstablishmentId} // Passer l'ID de l'établissement
|
||||
/>
|
||||
|
||||
{/* Cc and Bcc */}
|
||||
<div className="flex space-x-4">
|
||||
<RecipientInput
|
||||
label="Cc"
|
||||
placeholder="Add Cc"
|
||||
recipients={cc}
|
||||
setRecipients={setCc}
|
||||
/>
|
||||
<RecipientInput
|
||||
label="Bcc"
|
||||
placeholder="Add Bcc"
|
||||
recipients={bcc}
|
||||
setRecipients={setBcc}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder="Enter subject"
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email Body */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Your email
|
||||
</label>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={message}
|
||||
onChange={setMessage}
|
||||
placeholder="Write your email here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={handleSendEmail}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
Send email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium">Message</label>
|
||||
<Editor
|
||||
apiKey="8ftyao41dcp1et0p409ipyrdtp14wxs0efqdofvrjq1vo2gi" // Remplacez par votre clé API TinyMCE
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSendEmail}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Envoyer
|
||||
</button>
|
||||
{status && <p className="mt-4 text-sm">{status}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
25
Front-End/src/components/Admin/InstantMessaging.js
Normal file
25
Front-End/src/components/Admin/InstantMessaging.js
Normal file
@ -0,0 +1,25 @@
|
||||
// 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';
|
||||
|
||||
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) => {
|
||||
console.log(`Message envoyé à ${contact.name}: ${message}`);
|
||||
};
|
||||
|
||||
return <Chat contacts={contacts} onSendMessage={handleSendMessage} />;
|
||||
}
|
||||
@ -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>
|
||||
<div className="alert-actions mt-4">
|
||||
<a
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
|
||||
href={buttonLink}
|
||||
>
|
||||
{buttonText} <i className="icon profile-add"></i>
|
||||
</a>
|
||||
</div>
|
||||
{actionLabel && onAction && (
|
||||
<div className="alert-actions mt-4">
|
||||
<button
|
||||
className="btn primary bg-emerald-500 text-white rounded-md px-4 py-2 hover:bg-emerald-600"
|
||||
onClick={onAction}
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
205
Front-End/src/components/Chat.js
Normal file
205
Front-End/src/components/Chat.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,12 @@ const CheckBox = ({
|
||||
horizontal,
|
||||
}) => {
|
||||
console.log(formData);
|
||||
const isChecked = formData[fieldName].includes(parseInt(item.id));
|
||||
|
||||
// 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}
|
||||
|
||||
@ -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>
|
||||
© {new Date().getFullYear()} N3WT-INNOV Tous droits réservés.
|
||||
|
||||
144
Front-End/src/components/RecipientInput.js
Normal file
144
Front-End/src/components/RecipientInput.js
Normal file
@ -0,0 +1,144 @@
|
||||
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
|
||||
|
||||
export default function RecipientInput({
|
||||
label,
|
||||
recipients,
|
||||
setRecipients,
|
||||
searchRecipients, // Fonction pour effectuer la recherche
|
||||
establishmentId, // ID de l'établissement
|
||||
}) {
|
||||
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) {
|
||||
console.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.some((r) => r.email === trimmedValue)) {
|
||||
setRecipients([...recipients, { email: 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.some((r) => r.email === suggestion.email)) {
|
||||
setRecipients([...recipients, suggestion]);
|
||||
}
|
||||
setInputValue('');
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const handleRemoveRecipient = (email) => {
|
||||
setRecipients(recipients.filter((recipient) => recipient.email !== email));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className="flex flex-wrap items-center gap-2 p-2 border rounded">
|
||||
{recipients.map((recipient, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center bg-gray-100 text-gray-700 px-2 py-1 rounded-full"
|
||||
>
|
||||
<img
|
||||
src={getGravatarUrl(recipient.email)}
|
||||
alt={recipient.email}
|
||||
className="w-6 h-6 rounded-full mr-2"
|
||||
/>
|
||||
<span className="mr-2">
|
||||
{recipient.first_name && recipient.last_name
|
||||
? `${recipient.first_name} ${recipient.last_name}`
|
||||
: recipient.email}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveRecipient(recipient.email)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Rechercher des destinataires"
|
||||
className="flex-1 p-1 outline-none"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -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) =>
|
||||
|
||||
@ -54,6 +54,7 @@ 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`;
|
||||
|
||||
Reference in New Issue
Block a user