Added Chat Page

This commit is contained in:
2026-01-21 18:13:35 +01:00
parent 0ca8ce8b52
commit 3eed79ca93
37 changed files with 3966 additions and 64 deletions

16
app/api/auth/me/route.ts Normal file
View File

@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth';
// GET - Récupérer l'utilisateur actuel
export async function GET() {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
return NextResponse.json(user);
} catch (error) {
console.error('Erreur lors de la récupération de l\'utilisateur:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,149 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// Server-Sent Events pour les notifications en temps réel
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await getCurrentUser();
if (!user) {
return new Response('Non autorisé', { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return new Response('Conversation non trouvée', { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return new Response('Accès non autorisé', { status: 403 });
}
// Créer un stream SSE
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
let isClosed = false;
// Fonction pour envoyer un événement
const sendEvent = (data: any) => {
if (isClosed) return;
try {
const message = `data: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(message));
} catch (error) {
// Le controller est fermé, arrêter le polling
isClosed = true;
clearInterval(pollInterval);
}
};
// Envoyer un événement de connexion
sendEvent({ type: 'connected', conversationId: params.id });
// Polling pour vérifier les nouveaux messages
let lastMessageId: string | null = null;
const pollInterval = setInterval(async () => {
if (isClosed) {
clearInterval(pollInterval);
return;
}
try {
const lastMessage = await prisma.message.findFirst({
where: {
conversationId: params.id,
...(lastMessageId ? { id: { gt: lastMessageId } } : {}),
},
orderBy: {
createdAt: 'desc',
},
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
files: true,
},
});
if (lastMessage) {
lastMessageId = lastMessage.id;
sendEvent({
type: 'new_message',
message: lastMessage,
});
}
// Vérifier les mises à jour de conversation
const updatedConversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},
});
if (updatedConversation) {
sendEvent({
type: 'conversation_updated',
conversation: updatedConversation,
});
}
} catch (error) {
// Si c'est une erreur de controller fermé, arrêter le polling
if (error instanceof Error && error.message.includes('closed')) {
isClosed = true;
clearInterval(pollInterval);
} else {
console.error('Erreur lors du polling:', error);
}
}
}, 1000); // Poll toutes les secondes
// Nettoyer lors de la fermeture
const cleanup = () => {
isClosed = true;
clearInterval(pollInterval);
try {
controller.close();
} catch (e) {
// Ignorer les erreurs de fermeture
}
};
// Gérer la fermeture de la connexion
request.signal?.addEventListener('abort', cleanup);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}

View File

@@ -0,0 +1,220 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
// GET - Récupérer les messages d'une conversation
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '50');
const cursor = searchParams.get('cursor');
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Mettre à jour lastReadAt pour l'utilisateur
await prisma.conversationParticipant.updateMany({
where: {
conversationId: params.id,
userId: user.id,
},
data: {
lastReadAt: new Date(),
},
});
const where: any = {
conversationId: params.id,
};
if (cursor) {
where.id = {
lt: cursor,
};
}
const messages = await prisma.message.findMany({
where,
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
files: true,
},
orderBy: {
createdAt: 'desc',
},
take: limit,
});
return NextResponse.json({
messages: messages.reverse(), // Inverser pour avoir les plus anciens en premier
hasMore: messages.length === limit,
cursor: messages.length > 0 ? messages[0].id : null,
});
} catch (error) {
console.error('Erreur lors de la récupération des messages:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// POST - Envoyer un message dans une conversation
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
const formData = await request.formData();
const content = formData.get('content') as string | null;
const files = formData.getAll('files') as File[];
if (!content && (!files || files.length === 0)) {
return NextResponse.json(
{ error: 'Le message doit contenir du texte ou au moins un fichier' },
{ status: 400 }
);
}
// Créer le message
const message = await prisma.message.create({
data: {
conversationId: params.id,
senderId: user.id,
content: content || null,
},
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
files: true,
},
});
// Traiter les fichiers si présents
if (files && files.length > 0) {
const uploadDir = join(process.cwd(), 'public', 'uploads', 'messages');
// Créer le dossier s'il n'existe pas
if (!existsSync(uploadDir)) {
mkdirSync(uploadDir, { recursive: true });
}
const fileRecords = await Promise.all(
files.map(async (file) => {
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Générer un nom de fichier unique
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 15);
const fileExtension = file.name.split('.').pop();
const filename = `${timestamp}-${randomStr}.${fileExtension}`;
const filepath = join(uploadDir, filename);
// Sauvegarder le fichier
await writeFile(filepath, buffer);
// Créer l'enregistrement en base de données
return prisma.messageFile.create({
data: {
messageId: message.id,
filename: file.name,
filepath: `/uploads/messages/${filename}`,
fileType: file.type,
fileSize: file.size,
},
});
})
);
// Récupérer le message avec les fichiers
const messageWithFiles = await prisma.message.findUnique({
where: { id: message.id },
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
files: true,
},
});
// Mettre à jour la date de mise à jour de la conversation
await prisma.conversation.update({
where: { id: params.id },
data: { updatedAt: new Date() },
});
return NextResponse.json(messageWithFiles, { status: 201 });
}
// Mettre à jour la date de mise à jour de la conversation
await prisma.conversation.update({
where: { id: params.id },
data: { updatedAt: new Date() },
});
return NextResponse.json(message, { status: 201 });
} catch (error) {
console.error('Erreur lors de l\'envoi du message:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,171 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// POST - Ajouter des participants à une conversation
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const body = await request.json();
const { userIds } = body;
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
return NextResponse.json(
{ error: 'Au moins un utilisateur est requis' },
{ status: 400 }
);
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Seuls les groupes peuvent avoir des participants ajoutés
if (conversation.type !== 'group') {
return NextResponse.json(
{ error: 'Seuls les groupes peuvent avoir des participants ajoutés' },
{ status: 400 }
);
}
// Vérifier que les utilisateurs existent
const users = await prisma.user.findMany({
where: {
id: {
in: userIds,
},
},
});
if (users.length !== userIds.length) {
return NextResponse.json(
{ error: 'Un ou plusieurs utilisateurs sont invalides' },
{ status: 400 }
);
}
// Filtrer les utilisateurs déjà participants
const existingParticipantIds = conversation.participants.map((p) => p.userId);
const newUserIds = userIds.filter((id: string) => !existingParticipantIds.includes(id));
if (newUserIds.length === 0) {
return NextResponse.json(
{ error: 'Tous les utilisateurs sont déjà participants' },
{ status: 400 }
);
}
// Ajouter les nouveaux participants
await prisma.conversationParticipant.createMany({
data: newUserIds.map((userId: string) => ({
conversationId: params.id,
userId,
})),
});
const updatedConversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},
});
return NextResponse.json(updatedConversation);
} catch (error) {
console.error('Erreur lors de l\'ajout des participants:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// DELETE - Retirer un participant d'une conversation
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const searchParams = request.nextUrl.searchParams;
const userId = searchParams.get('userId');
if (!userId) {
return NextResponse.json({ error: 'userId est requis' }, { status: 400 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Seuls les groupes peuvent avoir des participants retirés
if (conversation.type !== 'group') {
return NextResponse.json(
{ error: 'Seuls les groupes peuvent avoir des participants retirés' },
{ status: 400 }
);
}
// Vérifier que le participant existe dans la conversation
const participant = conversation.participants.find((p) => p.userId === userId);
if (!participant) {
return NextResponse.json(
{ error: 'Participant non trouvé dans la conversation' },
{ status: 404 }
);
}
await prisma.conversationParticipant.delete({
where: { id: participant.id },
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors du retrait du participant:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// POST - Marquer les messages comme lus
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Mettre à jour lastReadAt pour l'utilisateur
await prisma.conversationParticipant.updateMany({
where: {
conversationId: params.id,
userId: user.id,
},
data: {
lastReadAt: new Date(),
},
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la mise à jour de la lecture:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// GET - Récupérer les informations de lecture pour une conversation
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: {
select: {
userId: true,
lastReadAt: true,
},
},
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
return NextResponse.json({
participants: conversation.participants.map((p) => ({
userId: p.userId,
lastReadAt: p.lastReadAt,
})),
});
} catch (error) {
console.error('Erreur lors de la récupération des lectures:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,163 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Récupérer une conversation spécifique
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
return NextResponse.json(conversation);
} catch (error) {
console.error('Erreur lors de la récupération de la conversation:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// PUT - Mettre à jour une conversation (nom du groupe, etc.)
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const body = await request.json();
const { name } = body;
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Seuls les groupes peuvent être renommés
if (conversation.type !== 'group') {
return NextResponse.json(
{ error: 'Seuls les groupes peuvent être renommés' },
{ status: 400 }
);
}
const updatedConversation = await prisma.conversation.update({
where: { id: params.id },
data: { name },
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},
});
return NextResponse.json(updatedConversation);
} catch (error) {
console.error('Erreur lors de la mise à jour de la conversation:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// DELETE - Supprimer une conversation (ou quitter un groupe)
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Si c'est un groupe, retirer l'utilisateur
// Si c'est une conversation directe, supprimer la conversation
if (conversation.type === 'group') {
await prisma.conversationParticipant.deleteMany({
where: {
conversationId: params.id,
userId: user.id,
},
});
} else {
// Pour les conversations directes, supprimer complètement
await prisma.conversation.delete({
where: { id: params.id },
});
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la suppression de la conversation:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// Store temporaire pour les indicateurs de frappe (en production, utiliser Redis ou une DB)
const typingUsers = new Map<string, { userId: string; timestamp: number }>();
// Nettoyer les anciens indicateurs toutes les 5 secondes
setInterval(() => {
const now = Date.now();
for (const [key, value] of typingUsers.entries()) {
if (now - value.timestamp > 5000) {
typingUsers.delete(key);
}
}
}, 5000);
// POST - Signaler que l'utilisateur est en train d'écrire
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Enregistrer que l'utilisateur est en train d'écrire
const key = `${params.id}:${user.id}`;
typingUsers.set(key, {
userId: user.id,
timestamp: Date.now(),
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la signalisation du typing:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// GET - Récupérer les utilisateurs en train d'écrire
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversation = await prisma.conversation.findUnique({
where: { id: params.id },
include: {
participants: true,
},
});
if (!conversation) {
return NextResponse.json({ error: 'Conversation non trouvée' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant
const isParticipant = conversation.participants.some((p) => p.userId === user.id);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
// Récupérer les utilisateurs en train d'écrire (sauf l'utilisateur actuel)
const typing = Array.from(typingUsers.entries())
.filter(([key]) => key.startsWith(`${params.id}:`))
.filter(([key]) => !key.endsWith(`:${user.id}`))
.map(([, value]) => value.userId);
// Récupérer les informations des utilisateurs
const typingUsersData = await prisma.user.findMany({
where: {
id: {
in: typing,
},
},
select: {
id: true,
email: true,
name: true,
},
});
return NextResponse.json({ typing: typingUsersData });
} catch (error) {
console.error('Erreur lors de la récupération du typing:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,236 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Liste toutes les conversations de l'utilisateur
export async function GET(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const conversations = await prisma.conversation.findMany({
where: {
participants: {
some: {
userId: user.id,
},
},
},
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
messages: {
orderBy: {
createdAt: 'desc',
},
take: 1,
include: {
sender: {
select: {
id: true,
name: true,
email: true,
},
},
files: true,
},
},
},
orderBy: {
updatedAt: 'desc',
},
});
// Formater les conversations avec les informations nécessaires
const formattedConversations = await Promise.all(
conversations.map(async (conv) => {
const lastMessage = conv.messages[0] || null;
const otherParticipants = conv.participants
.filter((p) => p.userId !== user.id)
.map((p) => p.user);
// Pour les conversations directes, utiliser le nom de l'autre participant
const displayName =
conv.type === 'group'
? conv.name || 'Groupe sans nom'
: otherParticipants[0]?.name || otherParticipants[0]?.email || 'Utilisateur';
// Récupérer lastReadAt pour l'utilisateur actuel
const userParticipant = conv.participants.find((p) => p.userId === user.id);
const lastReadAt = userParticipant?.lastReadAt || null;
// Compter les messages non lus
let unreadCount = 0;
if (lastMessage && lastReadAt) {
const unreadMessages = await prisma.message.count({
where: {
conversationId: conv.id,
senderId: { not: user.id }, // Exclure les messages de l'utilisateur
createdAt: { gt: lastReadAt },
},
});
unreadCount = unreadMessages;
} else if (lastMessage && !lastReadAt) {
// Si jamais lu, compter tous les messages qui ne sont pas de l'utilisateur
const totalUnread = await prisma.message.count({
where: {
conversationId: conv.id,
senderId: { not: user.id },
},
});
unreadCount = totalUnread;
}
return {
id: conv.id,
name: conv.name,
type: conv.type,
displayName,
participants: conv.participants.map((p) => ({
id: p.user.id,
email: p.user.email,
name: p.user.name,
})),
lastMessage: lastMessage
? {
id: lastMessage.id,
content: lastMessage.content,
senderId: lastMessage.senderId,
senderName: lastMessage.sender.name || lastMessage.sender.email,
createdAt: lastMessage.createdAt,
hasFiles: lastMessage.files.length > 0,
}
: null,
unreadCount,
updatedAt: conv.updatedAt,
createdAt: conv.createdAt,
};
})
);
return NextResponse.json(formattedConversations);
} catch (error) {
console.error('Erreur lors de la récupération des conversations:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// POST - Créer une nouvelle conversation
export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const body = await request.json();
const { participantIds, name, type } = body;
if (!participantIds || !Array.isArray(participantIds) || participantIds.length === 0) {
return NextResponse.json(
{ error: 'Au moins un participant est requis' },
{ status: 400 }
);
}
// Filtrer l'utilisateur actuel de la liste des participants (on ne peut pas créer une conversation avec soi-même)
const filteredParticipantIds = participantIds.filter((id: string) => id !== user.id);
if (filteredParticipantIds.length === 0) {
return NextResponse.json(
{ error: 'Vous ne pouvez pas créer une conversation avec vous-même' },
{ status: 400 }
);
}
// Vérifier que tous les participants existent
const participants = await prisma.user.findMany({
where: {
id: {
in: filteredParticipantIds,
},
},
});
if (participants.length !== filteredParticipantIds.length) {
return NextResponse.json(
{ error: 'Un ou plusieurs participants sont invalides' },
{ status: 400 }
);
}
// Pour les conversations directes, vérifier si une conversation existe déjà
if (type === 'direct' && filteredParticipantIds.length === 1) {
const existingConversation = await prisma.conversation.findFirst({
where: {
type: 'direct',
participants: {
every: {
userId: {
in: [user.id, filteredParticipantIds[0]],
},
},
},
},
include: {
participants: true,
},
});
if (existingConversation) {
// Vérifier que les deux participants sont bien dans cette conversation
const participantUserIds = existingConversation.participants.map((p) => p.userId);
if (
participantUserIds.includes(user.id) &&
participantUserIds.includes(filteredParticipantIds[0]) &&
participantUserIds.length === 2
) {
return NextResponse.json(existingConversation);
}
}
}
// Créer la conversation avec tous les participants (y compris l'utilisateur actuel)
const conversation = await prisma.conversation.create({
data: {
name: type === 'group' ? name : null,
type: type || 'direct',
participants: {
create: [
{ userId: user.id },
...filteredParticipantIds.map((id: string) => ({ userId: id })),
],
},
},
include: {
participants: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},
});
return NextResponse.json(conversation, { status: 201 });
} catch (error) {
console.error('Erreur lors de la création de la conversation:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,142 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Récupérer un message spécifique
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const message = await prisma.message.findUnique({
where: { id: params.id },
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
conversation: {
include: {
participants: true,
},
},
files: true,
},
});
if (!message) {
return NextResponse.json({ error: 'Message non trouvé' }, { status: 404 });
}
// Vérifier que l'utilisateur est participant à la conversation
const isParticipant = message.conversation.participants.some(
(p) => p.userId === user.id
);
if (!isParticipant) {
return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
}
return NextResponse.json(message);
} catch (error) {
console.error('Erreur lors de la récupération du message:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// PUT - Mettre à jour un message
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const body = await request.json();
const { content } = body;
const message = await prisma.message.findUnique({
where: { id: params.id },
});
if (!message) {
return NextResponse.json({ error: 'Message non trouvé' }, { status: 404 });
}
// Vérifier que l'utilisateur est l'auteur du message
if (message.senderId !== user.id) {
return NextResponse.json(
{ error: 'Vous ne pouvez modifier que vos propres messages' },
{ status: 403 }
);
}
const updatedMessage = await prisma.message.update({
where: { id: params.id },
data: { content },
include: {
sender: {
select: {
id: true,
email: true,
name: true,
},
},
files: true,
},
});
return NextResponse.json(updatedMessage);
} catch (error) {
console.error('Erreur lors de la mise à jour du message:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// DELETE - Supprimer un message
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const message = await prisma.message.findUnique({
where: { id: params.id },
});
if (!message) {
return NextResponse.json({ error: 'Message non trouvé' }, { status: 404 });
}
// Vérifier que l'utilisateur est l'auteur du message
if (message.senderId !== user.id) {
return NextResponse.json(
{ error: 'Vous ne pouvez supprimer que vos propres messages' },
{ status: 403 }
);
}
await prisma.message.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la suppression du message:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// POST - Archiver un trajet
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const trajet = await prisma.trajet.update({
where: { id: params.id },
data: {
archived: true,
},
include: {
adherent: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
email: true,
},
},
chauffeur: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
},
},
},
});
return NextResponse.json(trajet);
} catch (error) {
console.error('Erreur lors de l\'archivage du trajet:', error);
return NextResponse.json(
{ error: 'Erreur serveur' },
{ status: 500 }
);
}
}
// DELETE - Restaurer un trajet archivé
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const trajet = await prisma.trajet.update({
where: { id: params.id },
data: {
archived: false,
},
include: {
adherent: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
email: true,
},
},
chauffeur: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
},
},
},
});
return NextResponse.json(trajet);
} catch (error) {
console.error('Erreur lors de la restauration du trajet:', error);
return NextResponse.json(
{ error: 'Erreur serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Liste tous les trajets archivés
export async function GET(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const searchParams = request.nextUrl.searchParams;
const limit = searchParams.get('limit');
const trajets = await prisma.trajet.findMany({
where: {
archived: true,
},
include: {
adherent: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
email: true,
},
},
chauffeur: {
select: {
id: true,
nom: true,
prenom: true,
telephone: true,
},
},
},
orderBy: {
updatedAt: 'desc' as const,
},
take: limit ? parseInt(limit) : undefined,
});
return NextResponse.json(trajets);
} catch (error) {
console.error('Erreur lors de la récupération des trajets archivés:', error);
return NextResponse.json(
{ error: 'Erreur serveur' },
{ status: 500 }
);
}
}

View File

@@ -15,7 +15,9 @@ export async function GET(request: NextRequest) {
const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');
const where: any = {};
const where: any = {
archived: false, // Exclure les trajets archivés par défaut
};
// Filtrer par date si fourni
if (startDate || endDate) {

42
app/api/users/route.ts Normal file
View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Liste tous les utilisateurs (pour sélectionner des participants)
export async function GET(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const searchParams = request.nextUrl.searchParams;
const search = searchParams.get('search');
const where: any = {};
if (search) {
where.OR = [
{ name: { contains: search } },
{ email: { contains: search } },
];
}
const users = await prisma.user.findMany({
where,
select: {
id: true,
email: true,
name: true,
createdAt: true,
},
orderBy: {
name: 'asc',
},
});
return NextResponse.json(users);
} catch (error) {
console.error('Erreur lors de la récupération des utilisateurs:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -0,0 +1,21 @@
import { getCurrentUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
import DashboardLayout from '@/components/DashboardLayout';
import ArchivesTrajets from '@/components/ArchivesTrajets';
export default async function ArchivesPage() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
return (
<DashboardLayout user={user}>
<div className="p-8">
<h1 className="text-3xl font-semibold text-cblack mb-2">Archives</h1>
<p className="text-sm text-cgray mb-8">Trajets archivés</p>
<ArchivesTrajets />
</div>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,20 @@
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/lib/auth';
import DashboardLayout from '@/components/DashboardLayout';
import Messagerie from '@/components/Messagerie';
export default async function MessageriePage() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
return (
<DashboardLayout user={user}>
<div className="h-full">
<Messagerie />
</div>
</DashboardLayout>
);
}

View File

@@ -99,6 +99,17 @@ body {
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.2s ease-out;
}
@@ -106,3 +117,7 @@ body {
.animate-slideUp {
animation: slideUp 0.3s ease-out;
}
.animate-slideInRight {
animation: slideInRight 0.3s ease-out;
}

View File

@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import "./globals.css";
import { NotificationProvider } from "@/components/NotificationProvider";
import MessageNotifications from "@/components/MessageNotifications";
const poppins = Poppins({
weight: ["300", "400", "500", "600", "700"],
@@ -21,7 +23,12 @@ export default function RootLayout({
}>) {
return (
<html lang="fr">
<body className={poppins.variable}>{children}</body>
<body className={poppins.variable}>
<NotificationProvider>
{children}
<MessageNotifications />
</NotificationProvider>
</body>
</html>
);
}