diff --git a/app/api/conversations/[id]/messages/route.ts b/app/api/conversations/[id]/messages/route.ts index 83c39e0..7b37578 100644 --- a/app/api/conversations/[id]/messages/route.ts +++ b/app/api/conversations/[id]/messages/route.ts @@ -4,6 +4,7 @@ import { getCurrentUser } from '@/lib/auth'; import { writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; +import { createNotification } from '@/lib/notifications'; // GET - Récupérer les messages d'une conversation export async function GET( @@ -23,7 +24,17 @@ export async function GET( const conversation = await prisma.conversation.findUnique({ where: { id: params.id }, include: { - participants: true, + participants: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }, }, }); @@ -101,7 +112,17 @@ export async function POST( const conversation = await prisma.conversation.findUnique({ where: { id: params.id }, include: { - participants: true, + participants: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }, }, }); @@ -197,13 +218,44 @@ export async function POST( }, }); - // Mettre à jour la date de mise à jour de la conversation - await prisma.conversation.update({ - where: { id: params.id }, - data: { updatedAt: new Date() }, - }); + // 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 }); + // Créer des notifications pour tous les participants sauf l'expéditeur + const participants = conversation.participants.filter((p) => p.userId !== user.id); + + // Récupérer le nom de la conversation + let conversationName = ''; + if (conversation.type === 'group') { + conversationName = conversation.name || 'Groupe'; + } else { + // Pour une conversation directe, utiliser le nom de l'autre participant + const otherParticipant = participants.find((p) => p.user); + conversationName = otherParticipant?.user?.name || otherParticipant?.user?.email || 'Utilisateur'; + } + + const messagePreview = content + ? (content.length > 100 ? content.substring(0, 100) + '...' : content) + : files.length > 0 + ? `${files.length} fichier${files.length > 1 ? 's' : ''}` + : 'Nouveau message'; + + await Promise.all( + participants.map((participant) => + createNotification({ + userId: participant.userId, + type: 'message', + title: conversationName, + message: messagePreview, + link: `/dashboard/messagerie?conversation=${params.id}`, + }) + ) + ); + + return NextResponse.json(messageWithFiles, { status: 201 }); } // Mettre à jour la date de mise à jour de la conversation @@ -212,6 +264,35 @@ export async function POST( data: { updatedAt: new Date() }, }); + // Créer des notifications pour tous les participants sauf l'expéditeur + const participants = conversation.participants.filter((p) => p.userId !== user.id); + + // Récupérer le nom de la conversation + let conversationName = ''; + if (conversation.type === 'group') { + conversationName = conversation.name || 'Groupe'; + } else { + // Pour une conversation directe, utiliser le nom de l'autre participant + const otherParticipant = participants.find((p) => p.user); + conversationName = otherParticipant?.user?.name || otherParticipant?.user?.email || 'Utilisateur'; + } + + const messagePreview = content + ? (content.length > 100 ? content.substring(0, 100) + '...' : content) + : 'Nouveau message'; + + await Promise.all( + participants.map((participant) => + createNotification({ + userId: participant.userId, + type: 'message', + title: conversationName, + message: messagePreview, + link: `/dashboard/messagerie?conversation=${params.id}`, + }) + ) + ); + return NextResponse.json(message, { status: 201 }); } catch (error) { console.error('Erreur lors de l\'envoi du message:', error); diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts new file mode 100644 index 0000000..b70b87b --- /dev/null +++ b/app/api/notifications/route.ts @@ -0,0 +1,115 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Récupérer les notifications 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 searchParams = request.nextUrl.searchParams; + const limit = parseInt(searchParams.get('limit') || '50'); + const unreadOnly = searchParams.get('unreadOnly') === 'true'; + + const where: any = { + userId: user.id, + }; + + if (unreadOnly) { + where.read = false; + } + + const notifications = await prisma.notification.findMany({ + where, + orderBy: { + createdAt: 'desc', + }, + take: limit, + }); + + const unreadCount = await prisma.notification.count({ + where: { + userId: user.id, + read: false, + }, + }); + + return NextResponse.json({ + notifications, + unreadCount, + }); + } catch (error) { + console.error('Erreur lors de la récupération des notifications:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// POST - Marquer une notification comme lue +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 { notificationId, markAllAsRead } = body; + + if (markAllAsRead) { + // Marquer toutes les notifications comme lues + await prisma.notification.updateMany({ + where: { + userId: user.id, + read: false, + }, + data: { + read: true, + }, + }); + + return NextResponse.json({ success: true }); + } + + if (!notificationId) { + return NextResponse.json( + { error: 'ID de notification requis' }, + { status: 400 } + ); + } + + // Vérifier que la notification appartient à l'utilisateur + const notification = await prisma.notification.findFirst({ + where: { + id: notificationId, + userId: user.id, + }, + }); + + if (!notification) { + return NextResponse.json( + { error: 'Notification non trouvée' }, + { status: 404 } + ); + } + + // Marquer comme lue + await prisma.notification.update({ + where: { id: notificationId }, + data: { read: true }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erreur lors de la mise à jour de la notification:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/trajets/[id]/route.ts b/app/api/trajets/[id]/route.ts index 3a0893e..5192245 100644 --- a/app/api/trajets/[id]/route.ts +++ b/app/api/trajets/[id]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { getCurrentUser } from '@/lib/auth'; +import { createNotificationForAllUsers } from '@/lib/notifications'; // GET - Récupérer un trajet spécifique export async function GET( @@ -103,6 +104,57 @@ export async function PUT( }, }); + // Créer une notification si le statut a changé + if (statut) { + const oldTrajet = await prisma.trajet.findUnique({ + where: { id: params.id }, + include: { + adherent: { + select: { + nom: true, + prenom: true, + }, + }, + }, + }); + + if (oldTrajet && oldTrajet.statut !== statut) { + const dateFormatted = new Date(trajet.date).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + let notificationType: 'trajet_cancelled' | 'trajet_completed' | null = null; + let notificationTitle = ''; + let notificationMessage = ''; + + if (statut === 'Annulé') { + notificationType = 'trajet_cancelled'; + notificationTitle = 'Trajet annulé'; + notificationMessage = `Le trajet pour ${trajet.adherent.prenom} ${trajet.adherent.nom} du ${dateFormatted} a été annulé`; + } else if (statut === 'Terminé') { + notificationType = 'trajet_completed'; + notificationTitle = 'Trajet terminé'; + notificationMessage = `Le trajet pour ${trajet.adherent.prenom} ${trajet.adherent.nom} du ${dateFormatted} est terminé`; + } + + if (notificationType) { + await createNotificationForAllUsers( + { + type: notificationType, + title: notificationTitle, + message: notificationMessage, + link: '/dashboard/calendrier', + }, + user.id + ); + } + } + } + return NextResponse.json(trajet); } catch (error) { console.error('Erreur lors de la mise à jour du trajet:', error); diff --git a/app/api/trajets/route.ts b/app/api/trajets/route.ts index 9d71873..aa5b4d3 100644 --- a/app/api/trajets/route.ts +++ b/app/api/trajets/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { getCurrentUser } from '@/lib/auth'; +import { createNotificationForAllUsers } from '@/lib/notifications'; // GET - Liste tous les trajets avec leurs relations export async function GET(request: NextRequest) { @@ -125,6 +126,25 @@ export async function POST(request: NextRequest) { }, }); + // Créer une notification pour tous les utilisateurs sauf celui qui a créé le trajet + const dateFormatted = new Date(date).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + await createNotificationForAllUsers( + { + type: 'trajet_created', + title: 'Nouveau trajet programmé', + message: `Trajet programmé pour ${trajet.adherent.prenom} ${trajet.adherent.nom} le ${dateFormatted}`, + link: '/dashboard/calendrier', + }, + user.id + ); + return NextResponse.json(trajet, { status: 201 }); } catch (error) { console.error('Erreur lors de la création du trajet:', error); diff --git a/app/error.tsx b/app/error.tsx index c17e01e..8c5bac8 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -67,7 +67,7 @@ export default function Error({ error, reset }: ErrorProps) { {/* Footer */}
- © 2025 MAD - Propulsé par LGX + © {new Date().getFullYear()} MAD - Propulsé par LGX
diff --git a/app/login/page.tsx b/app/login/page.tsx index 25cc25a..cd190be 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -39,7 +39,7 @@ export default async function LoginPage() { {/* Footer */}
- © 2025 MAD - Propulsé par LGX + © {new Date().getFullYear()} MAD - Propulsé par LGX
diff --git a/app/not-found.tsx b/app/not-found.tsx index a5cd143..33a5e86 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -44,7 +44,7 @@ export default function NotFound() { {/* Footer */}
- © 2025 MAD - Propulsé par LGX + © {new Date().getFullYear()} MAD - Propulsé par LGX
diff --git a/components/AdherentsTable.tsx b/components/AdherentsTable.tsx index a17a361..0a324d5 100644 --- a/components/AdherentsTable.tsx +++ b/components/AdherentsTable.tsx @@ -1132,31 +1132,31 @@ export default function AdherentsTable() { /> )} - {/* Modal vue détaillée */} + {/* Modal vue détaillée - Design épuré */} {viewingAdherent && (
-
- {/* Header */} -
-
-
-
+
+ {/* Header épuré */} +
+
+
+
{getInitials(viewingAdherent.nom, viewingAdherent.prenom)}
-

+

{viewingAdherent.prenom} {viewingAdherent.nom}

-

+

Informations détaillées de l'adhérent

@@ -1164,165 +1164,240 @@ export default function AdherentsTable() {
{/* Contenu scrollable */} -
- {/* Informations principales */} -
-

- Informations principales -

-
-
-
- Date de naissance -
-
- - - - {formatDate(viewingAdherent.dateNaissance)} -
-
- -
-
- Téléphone -
-
- +
+
+ {/* Actions rapides */} +
+ + + + + Appeler + + {viewingAdherent.telephoneSecondaire && ( + + - - {viewingAdherent.telephone} - -
-
- -
-
- Email -
- -
- -
-
- Adresse -
-
- - - - - {viewingAdherent.adresse} -
-
+ Téléphone secondaire + + )} + + + + + Envoyer un email +
-
- {/* Informations complémentaires */} -
-

- Informations complémentaires -

-
- {viewingAdherent.situation && ( -
-
- Situation -
-
- {viewingAdherent.situation} -
-
- )} - - {viewingAdherent.prescripteur && ( -
-
- Prescripteur -
-
- {viewingAdherent.prescripteur} -
-
- )} - - {viewingAdherent.facturation && ( -
-
- Facturation -
-
- {viewingAdherent.facturation} -
-
- )} - - {viewingAdherent.forfait && ( -
-
- Forfait -
-
- {viewingAdherent.forfait} -
-
- )} - - {viewingAdherent.telephoneSecondaire && ( -
-
- Téléphone secondaire -
-
- - +
+ {/* Carte Informations principales */} +
+
+ +

+ Informations principales +

- )} +
+
+
+ + + +
+
+

Date de naissance

+

{formatDate(viewingAdherent.dateNaissance)}

+
+
- {viewingAdherent.commentaire && ( -
-
- Commentaire +
+
+ + + +
+
-
-

{viewingAdherent.commentaire}

-
-
- )} - {viewingAdherent.instructions && ( -
-
- Instructions + {viewingAdherent.telephoneSecondaire && ( +
+
+ + + +
+
+

Téléphone secondaire

+ + {viewingAdherent.telephoneSecondaire} + +
+
+ )} + +
+
+ + + +
+
-
-

{viewingAdherent.instructions}

+ +
+
+ + + + +
+
+

Adresse

+

{viewingAdherent.adresse}

+
- )} +
+ + {/* Carte Informations complémentaires */} +
+
+
+ + + +
+

+ Informations complémentaires +

+
+
+ {viewingAdherent.situation && ( +
+
+ + + +
+
+

Situation

+

{viewingAdherent.situation}

+
+
+ )} + + {viewingAdherent.prescripteur && ( +
+
+ + + +
+
+

Prescripteur

+

{viewingAdherent.prescripteur}

+
+
+ )} + + {viewingAdherent.facturation && ( +
+
+ + + +
+
+

Facturation

+

{viewingAdherent.facturation}

+
+
+ )} + + {viewingAdherent.forfait && ( +
+
+ + + +
+
+

Forfait

+

{viewingAdherent.forfait}

+
+
+ )} + + {viewingAdherent.commentaire && ( +
+
+ + + +
+
+

Commentaire

+

{viewingAdherent.commentaire}

+
+
+ )} + + {viewingAdherent.instructions && ( +
+
+ + + +
+
+

Instructions

+

{viewingAdherent.instructions}

+
+
+ )} + + {!viewingAdherent.situation && !viewingAdherent.prescripteur && !viewingAdherent.facturation && !viewingAdherent.forfait && !viewingAdherent.commentaire && !viewingAdherent.instructions && ( +
+ Aucune information complémentaire +
+ )} +
+
- {/* Footer */} -
-
+ {/* Footer avec actions */} +
+
@@ -1331,12 +1406,12 @@ export default function AdherentsTable() { setViewingAdherent(null); handleEdit(viewingAdherent); }} - className="px-4 py-2 bg-lblue text-white text-sm font-medium rounded hover:bg-dblue transition-colors flex items-center gap-2" + className="px-6 py-2.5 bg-lblue text-white text-sm font-semibold rounded-lg hover:bg-dblue transition-colors flex items-center gap-2" > - + - Modifier + Modifier l'adhérent
diff --git a/components/ChauffeursTable.tsx b/components/ChauffeursTable.tsx index 5a0070d..8133984 100644 --- a/components/ChauffeursTable.tsx +++ b/components/ChauffeursTable.tsx @@ -145,7 +145,7 @@ export default function ChauffeursTable() { const getStatusColor = (status: string) => { switch (status) { case 'Disponible': - return 'bg-lgreen text-white'; + return 'bg-lblue text-white'; case 'Vacances': return 'bg-lblue text-white'; case 'Arrêt Maladie': @@ -1018,31 +1018,38 @@ export default function ChauffeursTable() { /> )} - {/* Modal vue détaillée */} + {/* Modal vue détaillée - Design épuré */} {viewingChauffeur && (
-
- {/* Header sobre */} -
-
-
-
+
+ {/* Header épuré */} +
+
+
+
{getInitials(viewingChauffeur.nom, viewingChauffeur.prenom)}
-

+

{viewingChauffeur.prenom} {viewingChauffeur.nom}

-

+

Informations détaillées du chauffeur

+ {viewingChauffeur.status && ( +
+ + {viewingChauffeur.status} + +
+ )}
@@ -1050,155 +1057,187 @@ export default function ChauffeursTable() {
{/* Contenu scrollable */} -
- {/* Section Informations personnelles */} -
-

- Informations personnelles -

-
-
-
- Date de naissance -
-
- - - - {formatDate(viewingChauffeur.dateNaissance)} -
-
- -
-
- Téléphone -
- -
- -
-
- Email -
- -
- -
-
- Adresse -
-
- - - - - {viewingChauffeur.adresse} -
-
+
+
+ {/* Actions rapides */} + -
- {/* Section Contrat */} -
-

- Informations contractuelles -

-
-
-
- Contrat d'heure -
-
- - - - {viewingChauffeur.heuresContrat}h -
-
- -
-
- Date de début -
-
- - - - {formatDate(viewingChauffeur.dateDebutContrat)} -
-
- - {viewingChauffeur.dateFinContrat && ( -
-
- Date de fin -
-
- - +
+ {/* Carte Informations personnelles */} +
+
+
+ + - {formatDate(viewingChauffeur.dateFinContrat)}
+

+ Informations personnelles +

- )} - - {viewingChauffeur.heuresRestantes !== undefined && ( -
-
- Heures restantes -
-
-
- - {viewingChauffeur.heuresRestantes}h / {viewingChauffeur.heuresContrat}h - +
+
+
+ + +
-
-
+
+

Date de naissance

+

{formatDate(viewingChauffeur.dateNaissance)}

+
+
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+ + + + +
+
+

Adresse

+

{viewingChauffeur.adresse}

- )} +
- {viewingChauffeur.status && ( -
-
- Status -
-
- - {viewingChauffeur.status} - + {/* Carte Informations contractuelles */} +
+
+
+ + +
+

+ Informations contractuelles +

- )} +
+
+
+ + + +
+
+

Contrat d'heure

+

{viewingChauffeur.heuresContrat}h

+
+
+ +
+
+ + + +
+
+

Date de début

+

{formatDate(viewingChauffeur.dateDebutContrat)}

+
+
+ + {viewingChauffeur.dateFinContrat && ( +
+
+ + + +
+
+

Date de fin

+

{formatDate(viewingChauffeur.dateFinContrat)}

+
+
+ )} + + {viewingChauffeur.heuresRestantes !== undefined && ( +
+
+

Heures restantes

+ + {viewingChauffeur.heuresRestantes}h / {viewingChauffeur.heuresContrat}h + +
+
+
+
+

+ {Math.round((viewingChauffeur.heuresRestantes / viewingChauffeur.heuresContrat) * 100)}% d'heures restantes +

+
+ )} +
+
- {/* Footer sobre */} -
-
+ {/* Footer avec actions */} +
+
@@ -1207,12 +1246,12 @@ export default function ChauffeursTable() { setViewingChauffeur(null); handleEdit(viewingChauffeur); }} - className="px-4 py-2 bg-lblue text-white text-sm font-medium rounded hover:bg-dblue transition-colors flex items-center gap-2" + className="px-6 py-2.5 bg-lblue text-white text-sm font-semibold rounded-lg hover:bg-dblue transition-colors flex items-center gap-2" > - + - Modifier + Modifier le chauffeur
diff --git a/components/DashboardLayout.tsx b/components/DashboardLayout.tsx index 0eacc2c..de35636 100644 --- a/components/DashboardLayout.tsx +++ b/components/DashboardLayout.tsx @@ -42,6 +42,29 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps } ); + // Récupérer les notifications + const { data: notificationsData, mutate: mutateNotifications } = useSWR<{ + notifications: Array<{ + id: string; + type: string; + title: string; + message: string; + read: boolean; + link: string | null; + createdAt: string; + }>; + unreadCount: number; + }>( + '/api/notifications', + fetcher, + { + refreshInterval: 3000, // Rafraîchir toutes les 3 secondes + } + ); + + const notifications = notificationsData?.notifications || []; + const unreadNotificationsCount = notificationsData?.unreadCount || 0; + // Récupérer les pages accessibles pour l'utilisateur const { data: userPagesData } = useSWR<{ pages: string[] }>( '/api/user/pages', @@ -222,7 +245,7 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps {/* Footer */}

- © 2025 MAD - Propulsé par LGX + © {new Date().getFullYear()} MAD - Propulsé par LGX

@@ -244,19 +267,105 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps {/* Badge de notification */} - + {unreadNotificationsCount > 0 && ( + + {unreadNotificationsCount > 99 ? '99+' : unreadNotificationsCount} + + )} {/* Dropdown Notifications */} {showNotifications && (
-
+

Notifications

+ {unreadNotificationsCount > 0 && ( + + )}
-
- Aucune notification -
+ {notifications.length === 0 ? ( +
+ Aucune notification +
+ ) : ( +
+ {notifications.map((notification) => ( + + ))} +
+ )}
)} diff --git a/components/ListeTrajets.tsx b/components/ListeTrajets.tsx index 95cf6f2..fbc4280 100644 --- a/components/ListeTrajets.tsx +++ b/components/ListeTrajets.tsx @@ -35,19 +35,44 @@ export default function ListeTrajets({ onTrajetCreated }: ListeTrajetsProps) { const [search, setSearch] = useState(''); const [showFilters, setShowFilters] = useState(false); const [filterStatut, setFilterStatut] = useState(''); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); const [showTrajetForm, setShowTrajetForm] = useState(false); useEffect(() => { fetchTrajets(); - }, []); + }, [startDate, endDate]); const fetchTrajets = async () => { setLoading(true); try { - const response = await fetch('/api/trajets?limit=10'); + const params = new URLSearchParams(); + + // Si des filtres de date sont appliqués, ne pas limiter les résultats + // Sinon, limiter à 10 pour afficher les derniers trajets créés + if (!startDate && !endDate) { + params.append('limit', '10'); + } + + if (startDate) { + // Ajouter l'heure au début de la journée (00:00:00) + const start = new Date(startDate); + start.setHours(0, 0, 0, 0); + params.append('startDate', start.toISOString()); + } + + if (endDate) { + // Ajouter l'heure à la fin de la journée (23:59:59) + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); + params.append('endDate', end.toISOString()); + } + + const response = await fetch(`/api/trajets?${params.toString()}`); if (response.ok) { const data = await response.json(); // L'API retourne déjà les trajets triés par date de création (plus récents en premier) + // ou par date du trajet si un filtre de date est appliqué setTrajets(data); } } catch (error) { @@ -127,7 +152,7 @@ export default function ListeTrajets({ onTrajetCreated }: ListeTrajetsProps) { - - - - +
+ {/* Filtre par plage de dates */} +
+
+ + {(startDate || endDate) && ( + + )} +
+
+
+ + setStartDate(e.target.value)} + className="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> +
+
+ + setEndDate(e.target.value)} + min={startDate || undefined} + className="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> +
+
+
+ + {/* Filtre par statut */} +
+ +
+ + + + + +
)} diff --git a/components/UniversProTable.tsx b/components/UniversProTable.tsx index d2818c8..8235c3c 100644 --- a/components/UniversProTable.tsx +++ b/components/UniversProTable.tsx @@ -862,31 +862,31 @@ export default function UniversProTable() { /> )} - {/* Modal vue détaillée */} + {/* Modal vue détaillée - Design épuré */} {viewingContact && (
-
- {/* Header */} -
-
-
-
+
+ {/* Header épuré */} +
+
+
+
{getInitials(viewingContact.nom, viewingContact.prenom)}
-

+

{viewingContact.prenom} {viewingContact.nom}

-

- Informations détaillées du contact +

+ Informations détaillées du contact professionnel

@@ -894,69 +894,123 @@ export default function UniversProTable() {
{/* Contenu scrollable */} -
-
-
-
- Téléphone -
-
- +
+
+ {/* Actions rapides */} + -
- -
-
- Email -
- + Envoyer un email +
-
-
- Adresse -
-
- - - - - {viewingContact.adresse} -
-
+
+ {/* Carte Informations de contact */} +
+
+
+ + + +
+

+ Informations de contact +

+
+
+
+
+ + + +
+ +
-
-
- Entreprise +
+
+ + + +
+ +
+ +
+
+ + + + +
+
+

Adresse

+

{viewingContact.adresse}

+
+
+
-
- - - - {viewingContact.nomEntreprise} + + {/* Carte Informations entreprise */} +
+
+
+ + + +
+

+ Informations entreprise +

+
+
+
+
+ + + +
+
+

Nom de l'entreprise

+

{viewingContact.nomEntreprise}

+
+
+
- {/* Footer */} -
-
+ {/* Footer avec actions */} +
+
@@ -965,12 +1019,12 @@ export default function UniversProTable() { setViewingContact(null); handleEdit(viewingContact); }} - className="px-4 py-2 bg-lblue text-white text-sm font-medium rounded hover:bg-dblue transition-colors flex items-center gap-2" + className="px-6 py-2.5 bg-lblue text-white text-sm font-semibold rounded-lg hover:bg-dblue transition-colors flex items-center gap-2" > - + - Modifier + Modifier le contact
diff --git a/lib/notifications.ts b/lib/notifications.ts new file mode 100644 index 0000000..ec8a1bc --- /dev/null +++ b/lib/notifications.ts @@ -0,0 +1,62 @@ +import { prisma } from './prisma'; + +export interface CreateNotificationParams { + userId: string; + type: 'message' | 'trajet_created' | 'trajet_cancelled' | 'trajet_completed'; + title: string; + message: string; + link?: string; +} + +/** + * Crée une notification pour un utilisateur + */ +export async function createNotification(params: CreateNotificationParams) { + try { + await prisma.notification.create({ + data: { + userId: params.userId, + type: params.type, + title: params.title, + message: params.message, + link: params.link || null, + }, + }); + } catch (error) { + console.error('Erreur lors de la création de la notification:', error); + } +} + +/** + * Crée des notifications pour tous les utilisateurs sauf celui qui a déclenché l'action + */ +export async function createNotificationForAllUsers( + params: Omit, + excludeUserId?: string +) { + try { + const users = await prisma.user.findMany({ + where: excludeUserId + ? { + id: { + not: excludeUserId, + }, + } + : undefined, + select: { + id: true, + }, + }); + + await Promise.all( + users.map((user) => + createNotification({ + ...params, + userId: user.id, + }) + ) + ); + } catch (error) { + console.error('Erreur lors de la création des notifications:', error); + } +} diff --git a/prisma/dev.db b/prisma/dev.db index 52fe9f9..248c8b1 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ad448a0..eadfab3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model User { updatedAt DateTime @updatedAt conversations ConversationParticipant[] sentMessages Message[] + notifications Notification[] } model Role { @@ -188,3 +189,19 @@ model AdherentOption { @@unique([type, value]) @@index([type]) } + +model Notification { + id String @id @default(cuid()) + userId String // Utilisateur destinataire de la notification + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + type String // "message", "trajet_created", "trajet_cancelled", "trajet_completed" + title String // Titre de la notification + message String // Message de la notification + read Boolean @default(false) // Indique si la notification a été lue + link String? // Lien optionnel vers la ressource (ex: /dashboard/messagerie, /dashboard/calendrier) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId, read]) + @@index([userId, createdAt]) +}