mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Mise en place du Backend-messagerie [#17]
This commit is contained in:
@ -1,15 +1,15 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.conf import settings
|
|
||||||
from Auth.models import Profile
|
from Auth.models import Profile
|
||||||
|
|
||||||
class Messagerie(models.Model):
|
class Messagerie(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
objet = models.CharField(max_length=200, default="", blank=True)
|
objet = models.CharField(max_length=200, default="", blank=True)
|
||||||
emetteur = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_envoyes')
|
emetteur = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_envoyes')
|
||||||
destinataire = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_recus')
|
destinataire = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_recus')
|
||||||
corpus = models.CharField(max_length=200, default="", blank=True)
|
corpus = models.CharField(max_length=200, default="", blank=True)
|
||||||
|
date_envoi = models.DateTimeField(auto_now_add=True) # Date d'envoi du message
|
||||||
|
is_read = models.BooleanField(default=False) # Statut lu/non lu
|
||||||
|
conversation_id = models.CharField(max_length=100, blank=True, default="") # Pour regrouper les messages par conversation
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Messagerie_'+self.id
|
return f'Messagerie_{self.id}'
|
||||||
@ -8,6 +8,7 @@ class MessageSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Messagerie
|
model = Messagerie
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
read_only_fields = ['date_envoi']
|
||||||
|
|
||||||
def get_destinataire_profil(self, obj):
|
def get_destinataire_profil(self, obj):
|
||||||
return obj.destinataire.email
|
return obj.destinataire.email
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
from .views import SendEmailView, search_recipients
|
from .views import SendEmailView, search_recipients, ConversationListView, ConversationMessagesView, MarkAsReadView
|
||||||
from GestionMessagerie.views import MessagerieView, MessageView, MessageSimpleView
|
from GestionMessagerie.views import MessagerieView, MessageView, MessageSimpleView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -8,4 +8,8 @@ urlpatterns = [
|
|||||||
re_path(r'^messages/(?P<id>[0-9]+)$', MessageSimpleView.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('send-email/', SendEmailView.as_view(), name='send_email'),
|
||||||
path('search-recipients/', search_recipients, name='search_recipients'),
|
path('search-recipients/', search_recipients, name='search_recipients'),
|
||||||
|
# Endpoints pour le chat instantané
|
||||||
|
path('conversations/<int:profile_id>/', ConversationListView.as_view(), name='conversations'),
|
||||||
|
path('conversations/messages/<str:conversation_id>/', ConversationMessagesView.as_view(), name='conversation_messages'),
|
||||||
|
path('conversations/mark-as-read/<str:conversation_id>/', MarkAsReadView.as_view(), name='mark_as_read'),
|
||||||
]
|
]
|
||||||
@ -5,16 +5,18 @@ from django.conf import settings
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from .models import Messagerie
|
||||||
from Auth.models import Profile, ProfileRole
|
from Auth.models import Profile, ProfileRole
|
||||||
|
from GestionMessagerie.serializers import MessageSerializer
|
||||||
from .models import *
|
|
||||||
from School.models import Teacher
|
from School.models import Teacher
|
||||||
|
|
||||||
from GestionMessagerie.serializers import MessageSerializer
|
|
||||||
from School.serializers import TeacherSerializer
|
from School.serializers import TeacherSerializer
|
||||||
|
|
||||||
from N3wtSchool import bdd
|
|
||||||
import N3wtSchool.mailManager as mailer
|
import N3wtSchool.mailManager as mailer
|
||||||
|
from N3wtSchool import bdd
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from rest_framework.exceptions import NotFound
|
||||||
|
|
||||||
class MessagerieView(APIView):
|
class MessagerieView(APIView):
|
||||||
def get(self, request, profile_id):
|
def get(self, request, profile_id):
|
||||||
@ -140,3 +142,70 @@ def search_recipients(request):
|
|||||||
|
|
||||||
return JsonResponse(results, safe=False)
|
return JsonResponse(results, safe=False)
|
||||||
|
|
||||||
|
class ConversationListView(APIView):
|
||||||
|
"""
|
||||||
|
Liste les conversations d'un utilisateur (parent ou enseignant).
|
||||||
|
Retourne la liste des interlocuteurs et le dernier message échangé.
|
||||||
|
"""
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Liste les conversations d'un utilisateur (parent ou enseignant).",
|
||||||
|
responses={200: openapi.Response('Liste des conversations')}
|
||||||
|
)
|
||||||
|
def get(self, request, profile_id):
|
||||||
|
# Récupérer toutes les conversations où l'utilisateur est émetteur ou destinataire
|
||||||
|
messages = Messagerie.objects.filter(Q(emetteur_id=profile_id) | Q(destinataire_id=profile_id))
|
||||||
|
# Grouper par conversation_id
|
||||||
|
conversations = {}
|
||||||
|
for msg in messages.order_by('-date_envoi'):
|
||||||
|
conv_id = msg.conversation_id or f"{min(msg.emetteur_id, msg.destinataire_id)}_{max(msg.emetteur_id, msg.destinataire_id)}"
|
||||||
|
if conv_id not in conversations:
|
||||||
|
conversations[conv_id] = msg
|
||||||
|
# Préparer la réponse
|
||||||
|
data = []
|
||||||
|
for conv_id, last_msg in conversations.items():
|
||||||
|
interlocuteur = last_msg.emetteur if last_msg.destinataire_id == int(profile_id) else last_msg.destinataire
|
||||||
|
data.append({
|
||||||
|
'conversation_id': conv_id,
|
||||||
|
'last_message': MessageSerializer(last_msg).data,
|
||||||
|
'interlocuteur': {
|
||||||
|
'id': interlocuteur.id,
|
||||||
|
'first_name': interlocuteur.first_name,
|
||||||
|
'last_name': interlocuteur.last_name,
|
||||||
|
'email': interlocuteur.email,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class ConversationMessagesView(APIView):
|
||||||
|
"""
|
||||||
|
Récupère tous les messages d'une conversation donnée.
|
||||||
|
"""
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Récupère tous les messages d'une conversation donnée.",
|
||||||
|
responses={200: openapi.Response('Liste des messages')}
|
||||||
|
)
|
||||||
|
def get(self, request, conversation_id):
|
||||||
|
messages = Messagerie.objects.filter(conversation_id=conversation_id).order_by('date_envoi')
|
||||||
|
serializer = MessageSerializer(messages, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class MarkAsReadView(APIView):
|
||||||
|
"""
|
||||||
|
Marque tous les messages reçus dans une conversation comme lus pour l'utilisateur connecté.
|
||||||
|
"""
|
||||||
|
@swagger_auto_schema(
|
||||||
|
operation_description="Marque tous les messages reçus dans une conversation comme lus pour l'utilisateur connecté.",
|
||||||
|
request_body=openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
'profile_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID du profil utilisateur')
|
||||||
|
},
|
||||||
|
required=['profile_id']
|
||||||
|
),
|
||||||
|
responses={200: openapi.Response('Statut OK')}
|
||||||
|
)
|
||||||
|
def post(self, request, conversation_id):
|
||||||
|
profile_id = request.data.get('profile_id')
|
||||||
|
Messagerie.objects.filter(conversation_id=conversation_id, destinataire_id=profile_id, is_read=False).update(is_read=True)
|
||||||
|
return Response({'status': 'ok'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|||||||
25
Back-End/clean_migration_dir.py
Normal file
25
Back-End/clean_migration_dir.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
APPS = [
|
||||||
|
"Establishment",
|
||||||
|
"Settings",
|
||||||
|
"Subscriptions",
|
||||||
|
"Planning",
|
||||||
|
"GestionNotification",
|
||||||
|
"GestionMessagerie",
|
||||||
|
"Auth",
|
||||||
|
"School",
|
||||||
|
]
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
for app in APPS:
|
||||||
|
migrations_path = os.path.join(BASE_DIR, app, "migrations")
|
||||||
|
if os.path.isdir(migrations_path):
|
||||||
|
print(f"Suppression du dossier : {migrations_path}")
|
||||||
|
shutil.rmtree(migrations_path)
|
||||||
|
else:
|
||||||
|
print(f"Aucun dossier de migration trouvé pour {app}")
|
||||||
|
|
||||||
|
print("Nettoyage terminé.")
|
||||||
@ -1,46 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Chat from '@/components/Chat';
|
import Chat from '@/components/Chat';
|
||||||
import { getGravatarUrl } from '@/utils/gravatar';
|
import { useSession } from '@/context/SessionContext';
|
||||||
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
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'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function MessageriePage() {
|
export default function MessageriePage() {
|
||||||
const simulateResponse = (contactId, setMessages) => {
|
const { user } = useSession(); // Doit fournir l'id du parent connecté
|
||||||
setTimeout(() => {
|
const { selectedEstablishmentId } = useEstablishment();
|
||||||
setMessages((prevMessages) => {
|
|
||||||
const contactMessages = prevMessages[contactId] || [];
|
|
||||||
return {
|
|
||||||
...prevMessages,
|
|
||||||
[contactId]: [
|
|
||||||
...contactMessages,
|
|
||||||
{
|
|
||||||
id: contactMessages.length + 2,
|
|
||||||
text: 'Réponse automatique',
|
|
||||||
isResponse: true,
|
|
||||||
date: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Chat contacts={contacts} simulateResponse={simulateResponse} />;
|
if (!user) return <div>Chargement...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chat userProfileId={user.id} establishmentId={selectedEstablishmentId} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,88 @@
|
|||||||
// filepath: d:\Dev\n3wt-innov\n3wt-school\Front-End\src\components\Admin\InstantMessaging.js
|
// 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 Chat from '@/components/Chat';
|
||||||
import { getGravatarUrl } from '@/utils/gravatar';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import logger from '@/utils/logger';
|
import {
|
||||||
|
fetchConversations,
|
||||||
const contacts = [
|
sendMessage,
|
||||||
{
|
searchRecipients,
|
||||||
id: 1,
|
} from '@/app/actions/messagerieAction';
|
||||||
name: 'Parent 1',
|
import RecipientInput from '@/components/RecipientInput';
|
||||||
profilePic: getGravatarUrl('parent1@n3wtschool.com'),
|
import Modal from '@/components/Modal';
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Parent 2',
|
|
||||||
profilePic: getGravatarUrl('parent2@n3wtschool.com'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function InstantMessaging({ csrfToken }) {
|
export default function InstantMessaging({ csrfToken }) {
|
||||||
const handleSendMessage = (contact, message) => {
|
const { user, selectedEstablishmentId } = useEstablishment();
|
||||||
logger.debug(`Message envoyé à ${contact.name}: ${message}`);
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,53 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { SendHorizontal } from 'lucide-react';
|
import { SendHorizontal, Plus } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import {
|
||||||
|
fetchConversations,
|
||||||
|
fetchMessages,
|
||||||
|
sendMessage,
|
||||||
|
markAsRead,
|
||||||
|
} from '@/app/actions/messagerieAction';
|
||||||
|
|
||||||
export default function Chat({
|
export default function Chat({
|
||||||
discussions,
|
userProfileId,
|
||||||
setDiscussions,
|
establishmentId,
|
||||||
onSendMessage,
|
discussions: discussionsProp,
|
||||||
simulateResponse,
|
setDiscussions: setDiscussionsProp,
|
||||||
|
onCreateDiscussion,
|
||||||
|
onShowCreateDiscussion,
|
||||||
}) {
|
}) {
|
||||||
|
const [discussions, setDiscussions] = useState(discussionsProp || []);
|
||||||
const [selectedDiscussion, setSelectedDiscussion] = useState(null);
|
const [selectedDiscussion, setSelectedDiscussion] = useState(null);
|
||||||
const [messages, setMessages] = useState({});
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
||||||
const [newDiscussionName, setNewDiscussionName] = useState('');
|
|
||||||
const [newDiscussionProfilePic, setNewDiscussionProfilePic] = useState('');
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
if (userProfileId) {
|
||||||
};
|
fetchConversations(userProfileId).then(setDiscussions);
|
||||||
|
}
|
||||||
|
}, [userProfileId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
if (selectedDiscussion) {
|
||||||
}, [messages]);
|
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) {
|
if (newMessage.trim() && selectedDiscussion) {
|
||||||
const discussionMessages = messages[selectedDiscussion.id] || [];
|
await sendMessage({
|
||||||
const newMessages = {
|
conversation_id: selectedDiscussion.conversation_id,
|
||||||
...messages,
|
emetteur: userProfileId,
|
||||||
[selectedDiscussion.id]: [
|
destinataire: selectedDiscussion.interlocuteur.id,
|
||||||
...discussionMessages,
|
corpus: newMessage,
|
||||||
{
|
objet: '',
|
||||||
id: discussionMessages.length + 1,
|
});
|
||||||
text: newMessage,
|
|
||||||
date: new Date(),
|
|
||||||
isResponse: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
setMessages(newMessages);
|
|
||||||
setNewMessage('');
|
setNewMessage('');
|
||||||
onSendMessage && onSendMessage(selectedDiscussion, newMessage);
|
fetchMessages(selectedDiscussion.conversation_id).then(setMessages);
|
||||||
simulateResponse && simulateResponse(selectedDiscussion.id, 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 (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full w-full">
|
||||||
{/* Liste des discussions */}
|
{/* Bandeau droit : Liste des discussions */}
|
||||||
<div className="w-1/4 bg-gray-100 border-r border-gray-300 p-4 overflow-y-auto">
|
<div className="w-1/4 min-w-[280px] bg-gray-100 border-r border-gray-300 p-4 flex flex-col">
|
||||||
<h2 className="text-lg font-bold mb-4">Discussions</h2>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<h2 className="text-lg font-bold">Discussions</h2>
|
||||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
<button
|
||||||
className="w-full p-2 mb-4 bg-blue-500 text-white rounded-lg"
|
className="p-2 rounded-full bg-blue-500 hover:bg-blue-600 text-white shadow"
|
||||||
>
|
title="Nouvelle discussion"
|
||||||
{showCreateForm ? 'Annuler' : 'Créer une discussion'}
|
onClick={onShowCreateDiscussion}
|
||||||
</button>
|
>
|
||||||
{showCreateForm && (
|
<Plus size={20} />
|
||||||
<div className="mb-4 p-2 border rounded-lg bg-white">
|
</button>
|
||||||
<input
|
</div>
|
||||||
type="text"
|
<div className="flex-1 overflow-y-auto">
|
||||||
value={newDiscussionName}
|
{discussions && discussions.length > 0 ? (
|
||||||
onChange={(e) => setNewDiscussionName(e.target.value)}
|
discussions.map((discussion) => (
|
||||||
placeholder="Nom de la discussion"
|
<div
|
||||||
className="w-full p-2 mb-2 border rounded"
|
key={discussion.id}
|
||||||
/>
|
className={`flex items-center p-2 mb-2 cursor-pointer rounded transition-colors ${
|
||||||
<input
|
selectedDiscussion?.id === discussion.id
|
||||||
type="text"
|
? 'bg-blue-100'
|
||||||
value={newDiscussionProfilePic}
|
: 'hover:bg-gray-200'
|
||||||
onChange={(e) => setNewDiscussionProfilePic(e.target.value)}
|
}`}
|
||||||
placeholder="URL de la photo de profil (optionnel)"
|
onClick={() => setSelectedDiscussion(discussion)}
|
||||||
className="w-full p-2 mb-2 border rounded"
|
>
|
||||||
/>
|
<Image
|
||||||
<button
|
src={discussion.profilePic}
|
||||||
onClick={handleCreateDiscussion}
|
alt={`${discussion.name}'s profile`}
|
||||||
className="w-full p-2 bg-green-500 text-white rounded-lg"
|
className="w-10 h-10 rounded-full mr-3"
|
||||||
>
|
width={40}
|
||||||
Ajouter
|
height={40}
|
||||||
</button>
|
/>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
)}
|
<p className="font-medium">{discussion.name}</p>
|
||||||
{discussions && discussions.length > 0 ? (
|
<p className="text-sm text-gray-500 truncate">
|
||||||
discussions.map((discussion) => (
|
{discussion.lastMessage}
|
||||||
<div
|
</p>
|
||||||
key={discussion.id}
|
</div>
|
||||||
className={`flex items-center p-2 mb-2 cursor-pointer rounded ${
|
<span className="text-xs text-gray-400">
|
||||||
selectedDiscussion?.id === discussion.id
|
{discussion.lastMessageDate &&
|
||||||
? 'bg-blue-100'
|
new Date(discussion.lastMessageDate).toLocaleTimeString()}
|
||||||
: 'hover:bg-gray-200'
|
</span>
|
||||||
}`}
|
|
||||||
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>
|
</div>
|
||||||
<span className="text-xs text-gray-400">
|
))
|
||||||
{new Date(discussion.lastMessageDate).toLocaleTimeString()}
|
) : (
|
||||||
</span>
|
<p className="text-gray-500">Aucune discussion disponible.</p>
|
||||||
</div>
|
)}
|
||||||
))
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">Aucune discussion disponible.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone de chat */}
|
{/* Zone de chat */}
|
||||||
@ -152,11 +123,10 @@ export default function Chat({
|
|||||||
<h2 className="text-lg font-bold">{selectedDiscussion.name}</h2>
|
<h2 className="text-lg font-bold">{selectedDiscussion.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
{selectedDiscussion &&
|
{selectedDiscussion &&
|
||||||
(messages[selectedDiscussion.id] || []).map((message) => (
|
messages.map((message) => (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`flex mb-4 ${
|
className={`flex mb-4 ${
|
||||||
@ -170,16 +140,16 @@ export default function Chat({
|
|||||||
: 'bg-blue-500 text-white'
|
: 'bg-blue-500 text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p>{message.text}</p>
|
<p>{message.corpus}</p>
|
||||||
<span className="text-xs text-gray-500 block mt-1">
|
<span className="text-xs text-gray-500 block mt-1">
|
||||||
{new Date(message.date).toLocaleTimeString()}
|
{message.date &&
|
||||||
|
new Date(message.date).toLocaleTimeString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Champ de saisie */}
|
{/* Champ de saisie */}
|
||||||
{selectedDiscussion && (
|
{selectedDiscussion && (
|
||||||
<div className="p-4 border-t border-gray-300 flex items-center">
|
<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)}
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
className="flex-1 p-2 border border-gray-300 rounded-lg mr-2"
|
className="flex-1 p-2 border border-gray-300 rounded-lg mr-2"
|
||||||
placeholder="Écrire un message..."
|
placeholder="Écrire un message..."
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
|
|||||||
@ -51,8 +51,10 @@ export const BE_PLANNING_PLANNINGS_URL = `${BASE_URL}/Planning/plannings`;
|
|||||||
export const BE_PLANNING_EVENTS_URL = `${BASE_URL}/Planning/events`;
|
export const BE_PLANNING_EVENTS_URL = `${BASE_URL}/Planning/events`;
|
||||||
|
|
||||||
// GESTION MESSAGERIE
|
// 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_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_SEND_MESSAGE_URL = `${BASE_URL}/GestionMessagerie/send-email/`;
|
||||||
export const BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL = `${BASE_URL}/GestionMessagerie/search-recipients`;
|
export const BE_GESTIONMESSAGERIE_SEARCH_RECIPIENTS_URL = `${BASE_URL}/GestionMessagerie/search-recipients`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user