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 */}
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 */}
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 */}
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 */}
+
-
-
-
-
-
-
- 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 && (
+
+ )}
+
+
-
-
{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)}
-
-
-
-
-
-
-
-
-
- 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 */}
@@ -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) {