feat: Mise en place du Backend-messagerie [#17]

This commit is contained in:
Luc SORIGNET
2025-05-17 11:35:57 +02:00
parent fc9a1ed252
commit c6bc0d0b51
9 changed files with 291 additions and 188 deletions

View File

@ -1,46 +1,16 @@
'use client';
import React from 'react';
import React, { useEffect, useState } from 'react';
import Chat from '@/components/Chat';
import { getGravatarUrl } from '@/utils/gravatar';
const contacts = [
{
id: 1,
name: 'Facturation',
profilePic: getGravatarUrl('facturation@n3wtschool.com'),
},
{
id: 2,
name: 'Enseignant 1',
profilePic: getGravatarUrl('enseignant@n3wtschool.com'),
},
{
id: 3,
name: 'Contact',
profilePic: getGravatarUrl('contact@n3wtschool.com'),
},
];
import { useSession } from '@/context/SessionContext';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function MessageriePage() {
const simulateResponse = (contactId, setMessages) => {
setTimeout(() => {
setMessages((prevMessages) => {
const contactMessages = prevMessages[contactId] || [];
return {
...prevMessages,
[contactId]: [
...contactMessages,
{
id: contactMessages.length + 2,
text: 'Réponse automatique',
isResponse: true,
date: new Date(),
},
],
};
});
}, 2000);
};
const { user } = useSession(); // Doit fournir l'id du parent connecté
const { selectedEstablishmentId } = useEstablishment();
return <Chat contacts={contacts} simulateResponse={simulateResponse} />;
if (!user) return <div>Chargement...</div>;
return (
<Chat userProfileId={user.id} establishmentId={selectedEstablishmentId} />
);
}

View File

@ -1,26 +1,88 @@
// filepath: d:\Dev\n3wt-innov\n3wt-school\Front-End\src\components\Admin\InstantMessaging.js
import React from 'react';
import React, { useEffect, useState } 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'),
},
];
import { useEstablishment } from '@/context/EstablishmentContext';
import {
fetchConversations,
sendMessage,
searchRecipients,
} from '@/app/actions/messagerieAction';
import RecipientInput from '@/components/RecipientInput';
import Modal from '@/components/Modal';
export default function InstantMessaging({ csrfToken }) {
const handleSendMessage = (contact, message) => {
logger.debug(`Message envoyé à ${contact.name}: ${message}`);
const { user, selectedEstablishmentId } = useEstablishment();
const [discussions, setDiscussions] = useState([]);
const [recipients, setRecipients] = useState([]); // Liste des correspondants sélectionnés
const [showModal, setShowModal] = useState(false);
const [firstMessage, setFirstMessage] = useState('');
useEffect(() => {
if (user) {
fetchConversations(user.id).then(setDiscussions);
}
}, [user]);
// Fonction pour ajouter une nouvelle discussion avec plusieurs correspondants
const handleCreateDiscussion = async () => {
if (!user || recipients.length === 0 || !firstMessage.trim()) return;
for (const recipient of recipients) {
await sendMessage({
emetteur: user.id,
destinataire: recipient.id,
objet: '',
corpus: firstMessage,
conversation_id: undefined, // L'API générera un nouvel ID
});
}
setRecipients([]);
setFirstMessage('');
setShowModal(false);
fetchConversations(user.id).then(setDiscussions);
};
return <Chat contacts={contacts} onSendMessage={handleSendMessage} />;
if (!user) return <div>Chargement...</div>;
return (
<div className="h-full w-full">
<Chat
userProfileId={user.id}
establishmentId={selectedEstablishmentId}
discussions={discussions}
setDiscussions={setDiscussions}
onShowCreateDiscussion={() => setShowModal(true)}
/>
<Modal
isOpen={showModal}
setIsOpen={setShowModal}
title="Nouvelle discussion"
modalClassName="w-full max-w-xs sm:max-w-md"
>
<div className="p-2 sm:p-4">
<h3 className="text-lg font-bold mb-2">Nouvelle discussion</h3>
<RecipientInput
label="Rechercher un correspondant"
recipients={recipients}
setRecipients={setRecipients}
searchRecipients={searchRecipients}
establishmentId={selectedEstablishmentId}
required
/>
<input
type="text"
value={firstMessage}
onChange={(e) => setFirstMessage(e.target.value)}
placeholder="Premier message"
className="w-full p-2 mb-2 border rounded"
/>
<button
onClick={handleCreateDiscussion}
className="w-full p-2 bg-green-500 text-white rounded-lg"
>
Démarrer la discussion
</button>
</div>
</Modal>
</div>
);
}

View File

@ -1,48 +1,53 @@
import React, { useState, useRef, useEffect } from 'react';
import { SendHorizontal } from 'lucide-react';
import { SendHorizontal, Plus } from 'lucide-react';
import Image from 'next/image';
import {
fetchConversations,
fetchMessages,
sendMessage,
markAsRead,
} from '@/app/actions/messagerieAction';
export default function Chat({
discussions,
setDiscussions,
onSendMessage,
simulateResponse,
userProfileId,
establishmentId,
discussions: discussionsProp,
setDiscussions: setDiscussionsProp,
onCreateDiscussion,
onShowCreateDiscussion,
}) {
const [discussions, setDiscussions] = useState(discussionsProp || []);
const [selectedDiscussion, setSelectedDiscussion] = useState(null);
const [messages, setMessages] = useState({});
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(() => {
if (userProfileId) {
fetchConversations(userProfileId).then(setDiscussions);
}
}, [userProfileId]);
useEffect(() => {
scrollToBottom();
}, [messages]);
if (selectedDiscussion) {
fetchMessages(selectedDiscussion.conversation_id).then(setMessages);
// Marquer comme lu
markAsRead(selectedDiscussion.conversation_id, userProfileId);
}
}, [selectedDiscussion, userProfileId]);
const handleSendMessage = () => {
const handleSendMessage = async () => {
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);
await sendMessage({
conversation_id: selectedDiscussion.conversation_id,
emetteur: userProfileId,
destinataire: selectedDiscussion.interlocuteur.id,
corpus: newMessage,
objet: '',
});
setNewMessage('');
onSendMessage && onSendMessage(selectedDiscussion, newMessage);
simulateResponse && simulateResponse(selectedDiscussion.id, setMessages);
fetchMessages(selectedDiscussion.conversation_id).then(setMessages);
fetchConversations(userProfileId).then(setDiscussions);
}
};
@ -52,89 +57,55 @@ export default function Chat({
}
};
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 className="flex h-full w-full">
{/* Bandeau droit : Liste des discussions */}
<div className="w-1/4 min-w-[280px] bg-gray-100 border-r border-gray-300 p-4 flex flex-col">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">Discussions</h2>
<button
className="p-2 rounded-full bg-blue-500 hover:bg-blue-600 text-white shadow"
title="Nouvelle discussion"
onClick={onShowCreateDiscussion}
>
<Plus size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto">
{discussions && discussions.length > 0 ? (
discussions.map((discussion) => (
<div
key={discussion.id}
className={`flex items-center p-2 mb-2 cursor-pointer rounded transition-colors ${
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">
{discussion.lastMessageDate &&
new Date(discussion.lastMessageDate).toLocaleTimeString()}
</span>
</div>
<span className="text-xs text-gray-400">
{new Date(discussion.lastMessageDate).toLocaleTimeString()}
</span>
</div>
))
) : (
<p className="text-gray-500">Aucune discussion disponible.</p>
)}
))
) : (
<p className="text-gray-500">Aucune discussion disponible.</p>
)}
</div>
</div>
{/* Zone de chat */}
@ -152,11 +123,10 @@ export default function Chat({
<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) => (
messages.map((message) => (
<div
key={message.id}
className={`flex mb-4 ${
@ -170,16 +140,16 @@ export default function Chat({
: 'bg-blue-500 text-white'
}`}
>
<p>{message.text}</p>
<p>{message.corpus}</p>
<span className="text-xs text-gray-500 block mt-1">
{new Date(message.date).toLocaleTimeString()}
{message.date &&
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">
@ -189,7 +159,7 @@ export default function Chat({
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}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
/>
<button
onClick={handleSendMessage}

View File

@ -51,8 +51,10 @@ export const BE_PLANNING_PLANNINGS_URL = `${BASE_URL}/Planning/plannings`;
export const BE_PLANNING_EVENTS_URL = `${BASE_URL}/Planning/events`;
// GESTION MESSAGERIE
export const BE_GESTIONMESSAGERIE_CONVERSATIONS_URL = `${BASE_URL}/GestionMessagerie/conversations`;
export const BE_GESTIONMESSAGERIE_CONVERSATION_MESSAGES_URL = `${BASE_URL}/GestionMessagerie/conversations/messages`;
export const BE_GESTIONMESSAGERIE_MARK_AS_READ_URL = `${BASE_URL}/GestionMessagerie/conversations/mark-as-read`;
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`;