diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts index b489e3b..b0591ea 100644 --- a/app/api/auth/me/route.ts +++ b/app/api/auth/me/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { getCurrentUser } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; // GET - Récupérer l'utilisateur actuel export async function GET() { @@ -8,7 +9,26 @@ export async function GET() { if (!user) { return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); } - return NextResponse.json(user); + + // Récupérer l'utilisateur avec son rôle + const userWithRole = await prisma.user.findUnique({ + where: { id: user.id }, + select: { + id: true, + email: true, + name: true, + roleId: true, + role: { + select: { + id: true, + name: true, + description: true, + }, + }, + }, + }); + + return NextResponse.json(userWithRole); } catch (error) { console.error('Erreur lors de la récupération de l\'utilisateur:', error); return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 }); diff --git a/app/api/dashboard/stats/route.ts b/app/api/dashboard/stats/route.ts new file mode 100644 index 0000000..fcee248 --- /dev/null +++ b/app/api/dashboard/stats/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Récupérer les statistiques du dashboard +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const now = new Date(); + const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); + + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); + + const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); + + const startOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + const endOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999); + + // 1. Participations du mois (montant total des trajets validés/terminés + nombre de factures) + // Pour l'instant, on considère qu'un trajet terminé = une facture + // Montant estimé : 6.80€ par trajet (valeur moyenne basée sur l'image) + const trajetsMois = await prisma.trajet.findMany({ + where: { + archived: false, + statut: { + in: ['Terminé', 'Validé'], + }, + date: { + gte: startOfMonth, + lte: endOfMonth, + }, + }, + }); + + const montantMoyenParTrajet = 6.80; // Montant moyen par trajet en euros + const participationsMois = trajetsMois.length * montantMoyenParTrajet; + const nombreFactures = trajetsMois.length; + + // 2. Trajets aujourd'hui + const trajetsAujourdhui = await prisma.trajet.count({ + where: { + archived: false, + date: { + gte: startOfToday, + lte: endOfToday, + }, + }, + }); + + // Trajets hier pour comparaison + const trajetsHier = await prisma.trajet.count({ + where: { + archived: false, + date: { + gte: startOfYesterday, + lte: endOfYesterday, + }, + }, + }); + + const differenceAujourdhui = trajetsAujourdhui - trajetsHier; + + // 3. Trajets réalisés ce mois (terminés) + const trajetsRealisesMois = await prisma.trajet.count({ + where: { + archived: false, + statut: 'Terminé', + date: { + gte: startOfMonth, + lte: endOfMonth, + }, + }, + }); + + // Trajets réalisés le mois dernier pour comparaison + const trajetsRealisesMoisDernier = await prisma.trajet.count({ + where: { + archived: false, + statut: 'Terminé', + date: { + gte: startOfLastMonth, + lte: endOfLastMonth, + }, + }, + }); + + const pourcentageEvolution = trajetsRealisesMoisDernier > 0 + ? Math.round(((trajetsRealisesMois - trajetsRealisesMoisDernier) / trajetsRealisesMoisDernier) * 100) + : trajetsRealisesMois > 0 ? 100 : 0; + + // 4. Chauffeurs actifs (disponibles) + const totalChauffeurs = await prisma.chauffeur.count(); + const chauffeursActifs = await prisma.chauffeur.count({ + where: { + status: 'Disponible', + }, + }); + + return NextResponse.json({ + participationsMois: { + montant: participationsMois, + nombreFactures: nombreFactures, + }, + trajetsAujourdhui: { + nombre: trajetsAujourdhui, + difference: differenceAujourdhui, + }, + trajetsRealisesMois: { + nombre: trajetsRealisesMois, + pourcentageEvolution: pourcentageEvolution, + }, + chauffeursActifs: { + nombre: chauffeursActifs, + total: totalChauffeurs, + }, + }); + } catch (error) { + console.error('Erreur lors de la récupération des statistiques:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/permissions/route.ts b/app/api/permissions/route.ts new file mode 100644 index 0000000..52449ab --- /dev/null +++ b/app/api/permissions/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Liste toutes les permissions disponibles +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const permissions = await prisma.permission.findMany({ + orderBy: { + name: 'asc', + }, + }); + + return NextResponse.json(permissions); + } catch (error) { + console.error('Erreur lors de la récupération des permissions:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// POST - Créer une nouvelle permission +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 { name, description } = body; + + if (!name) { + return NextResponse.json( + { error: 'Le nom de la permission est requis' }, + { status: 400 } + ); + } + + // Vérifier si la permission existe déjà + const existing = await prisma.permission.findUnique({ + where: { name }, + }); + + if (existing) { + return NextResponse.json( + { error: 'Cette permission existe déjà' }, + { status: 400 } + ); + } + + const permission = await prisma.permission.create({ + data: { + name, + description: description || null, + }, + }); + + return NextResponse.json(permission, { status: 201 }); + } catch (error) { + console.error('Erreur lors de la création de la permission:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/roles/[id]/route.ts b/app/api/roles/[id]/route.ts new file mode 100644 index 0000000..b5f2428 --- /dev/null +++ b/app/api/roles/[id]/route.ts @@ -0,0 +1,205 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Récupérer un rôle avec ses permissions +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 role = await prisma.role.findUnique({ + where: { id: params.id }, + include: { + permissions: { + include: { + permission: true, + }, + }, + _count: { + select: { + users: true, + }, + }, + }, + }); + + if (!role) { + return NextResponse.json( + { error: 'Rôle non trouvé' }, + { status: 404 } + ); + } + + return NextResponse.json(role); + } catch (error) { + console.error('Erreur lors de la récupération du rôle:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// PUT - Modifier un rôle +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, description, permissionIds } = body; + + const role = await prisma.role.findUnique({ + where: { id: params.id }, + }); + + if (!role) { + return NextResponse.json( + { error: 'Rôle non trouvé' }, + { status: 404 } + ); + } + + // Vérifier si le nouveau nom existe déjà (si changé) + if (name && name !== role.name) { + const existing = await prisma.role.findUnique({ + where: { name }, + }); + + if (existing) { + return NextResponse.json( + { error: 'Ce nom de rôle existe déjà' }, + { status: 400 } + ); + } + } + + // Mettre à jour le rôle + const updatedRole = await prisma.role.update({ + where: { id: params.id }, + data: { + name: name || role.name, + description: description !== undefined ? description : role.description, + }, + include: { + permissions: { + include: { + permission: true, + }, + }, + _count: { + select: { + users: true, + }, + }, + }, + }); + + // Mettre à jour les permissions si fournies + if (permissionIds !== undefined) { + // Supprimer toutes les permissions actuelles + await prisma.rolePermission.deleteMany({ + where: { roleId: params.id }, + }); + + // Ajouter les nouvelles permissions + if (permissionIds.length > 0) { + await prisma.rolePermission.createMany({ + data: permissionIds.map((permissionId: string) => ({ + roleId: params.id, + permissionId, + })), + }); + } + + // Récupérer le rôle mis à jour avec les nouvelles permissions + const roleWithPermissions = await prisma.role.findUnique({ + where: { id: params.id }, + include: { + permissions: { + include: { + permission: true, + }, + }, + _count: { + select: { + users: true, + }, + }, + }, + }); + + return NextResponse.json(roleWithPermissions); + } + + return NextResponse.json(updatedRole); + } catch (error) { + console.error('Erreur lors de la modification du rôle:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// DELETE - Supprimer un rôle +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 role = await prisma.role.findUnique({ + where: { id: params.id }, + include: { + _count: { + select: { + users: true, + }, + }, + }, + }); + + if (!role) { + return NextResponse.json( + { error: 'Rôle non trouvé' }, + { status: 404 } + ); + } + + // Vérifier si le rôle est utilisé + if (role._count.users > 0) { + return NextResponse.json( + { error: 'Ce rôle est attribué à des utilisateurs. Veuillez d\'abord retirer le rôle de ces utilisateurs.' }, + { status: 400 } + ); + } + + await prisma.role.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erreur lors de la suppression du rôle:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/roles/route.ts b/app/api/roles/route.ts new file mode 100644 index 0000000..be8d81e --- /dev/null +++ b/app/api/roles/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Liste tous les rôles avec leurs permissions +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const roles = await prisma.role.findMany({ + include: { + permissions: { + include: { + permission: true, + }, + }, + _count: { + select: { + users: true, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); + + return NextResponse.json(roles); + } catch (error) { + console.error('Erreur lors de la récupération des rôles:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// POST - Créer un nouveau rôle +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 { name, description, permissionIds } = body; + + if (!name) { + return NextResponse.json( + { error: 'Le nom du rôle est requis' }, + { status: 400 } + ); + } + + // Vérifier si le rôle existe déjà + const existing = await prisma.role.findUnique({ + where: { name }, + }); + + if (existing) { + return NextResponse.json( + { error: 'Ce rôle existe déjà' }, + { status: 400 } + ); + } + + // Créer le rôle avec ses permissions + const role = await prisma.role.create({ + data: { + name, + description: description || null, + permissions: permissionIds && permissionIds.length > 0 + ? { + create: permissionIds.map((permissionId: string) => ({ + permissionId, + })), + } + : undefined, + }, + include: { + permissions: { + include: { + permission: true, + }, + }, + _count: { + select: { + users: true, + }, + }, + }, + }); + + return NextResponse.json(role, { status: 201 }); + } catch (error) { + console.error('Erreur lors de la création du rôle:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/settings/adherent-options/[id]/route.ts b/app/api/settings/adherent-options/[id]/route.ts new file mode 100644 index 0000000..78446da --- /dev/null +++ b/app/api/settings/adherent-options/[id]/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// PUT - Modifier une option +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 { value, order } = body; + + if (!value) { + return NextResponse.json( + { error: 'La valeur est requise' }, + { status: 400 } + ); + } + + // Vérifier si l'option existe + const existing = await prisma.adherentOption.findUnique({ + where: { id: params.id }, + }); + + if (!existing) { + return NextResponse.json( + { error: 'Option non trouvée' }, + { status: 404 } + ); + } + + // Vérifier si une autre option avec le même type et valeur existe + const duplicate = await prisma.adherentOption.findFirst({ + where: { + type: existing.type, + value, + id: { not: params.id }, + }, + }); + + if (duplicate) { + return NextResponse.json( + { error: 'Cette option existe déjà' }, + { status: 400 } + ); + } + + const option = await prisma.adherentOption.update({ + where: { id: params.id }, + data: { + value, + order: order !== undefined ? order : existing.order, + }, + }); + + return NextResponse.json(option); + } catch (error) { + console.error('Erreur lors de la modification de l\'option:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// DELETE - Supprimer une option +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 option = await prisma.adherentOption.findUnique({ + where: { id: params.id }, + }); + + if (!option) { + return NextResponse.json( + { error: 'Option non trouvée' }, + { status: 404 } + ); + } + + await prisma.adherentOption.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erreur lors de la suppression de l\'option:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/settings/adherent-options/route.ts b/app/api/settings/adherent-options/route.ts new file mode 100644 index 0000000..8786bf5 --- /dev/null +++ b/app/api/settings/adherent-options/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Récupérer toutes les options par type +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 type = searchParams.get('type'); // "situation", "prescripteur", "facturation" + + const where: any = {}; + if (type) { + where.type = type; + } + + const options = await prisma.adherentOption.findMany({ + where, + orderBy: [ + { type: 'asc' }, + { order: 'asc' }, + { value: 'asc' }, + ], + }); + + // Grouper par type + const grouped = options.reduce((acc, option) => { + if (!acc[option.type]) { + acc[option.type] = []; + } + acc[option.type].push(option); + return acc; + }, {} as Record); + + return NextResponse.json(type ? options : grouped); + } catch (error) { + console.error('Erreur lors de la récupération des options:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// POST - Créer une nouvelle option +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 { type, value, order } = body; + + if (!type || !value) { + return NextResponse.json( + { error: 'Le type et la valeur sont requis' }, + { status: 400 } + ); + } + + if (!['situation', 'prescripteur', 'facturation'].includes(type)) { + return NextResponse.json( + { error: 'Type invalide. Doit être: situation, prescripteur ou facturation' }, + { status: 400 } + ); + } + + // Vérifier si l'option existe déjà + const existing = await prisma.adherentOption.findFirst({ + where: { + type, + value, + }, + }); + + if (existing) { + return NextResponse.json( + { error: 'Cette option existe déjà' }, + { status: 400 } + ); + } + + const option = await prisma.adherentOption.create({ + data: { + type, + value, + order: order || 0, + }, + }); + + return NextResponse.json(option, { status: 201 }); + } catch (error) { + console.error('Erreur lors de la création de l\'option:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/user/pages/route.ts b/app/api/user/pages/route.ts new file mode 100644 index 0000000..8572d4f --- /dev/null +++ b/app/api/user/pages/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUser } from '@/lib/auth'; +import { getUserAccessiblePages } from '@/lib/permissions'; + +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const accessiblePages = await getUserAccessiblePages(user.id); + return NextResponse.json({ pages: accessiblePages }); + } catch (error) { + console.error('Erreur lors de la récupération des pages accessibles:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/users/[id]/role/route.ts b/app/api/users/[id]/role/route.ts new file mode 100644 index 0000000..22d946c --- /dev/null +++ b/app/api/users/[id]/role/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// PUT - Attribuer ou modifier le rôle d'un utilisateur +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const currentUser = await getCurrentUser(); + if (!currentUser) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { roleId } = body; + + const user = await prisma.user.findUnique({ + where: { id: params.id }, + }); + + if (!user) { + return NextResponse.json( + { error: 'Utilisateur non trouvé' }, + { status: 404 } + ); + } + + // Vérifier que le rôle existe si fourni + if (roleId) { + const role = await prisma.role.findUnique({ + where: { id: roleId }, + }); + + if (!role) { + return NextResponse.json( + { error: 'Rôle non trouvé' }, + { status: 404 } + ); + } + } + + // Mettre à jour le rôle de l'utilisateur + const updatedUser = await prisma.user.update({ + where: { id: params.id }, + data: { + roleId: roleId || null, + }, + select: { + id: true, + email: true, + name: true, + roleId: true, + role: { + select: { + id: true, + name: true, + description: true, + }, + }, + createdAt: true, + }, + }); + + return NextResponse.json(updatedUser); + } catch (error) { + console.error('Erreur lors de l\'attribution du rôle:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/users/[id]/route.ts b/app/api/users/[id]/route.ts new file mode 100644 index 0000000..e0892cc --- /dev/null +++ b/app/api/users/[id]/route.ts @@ -0,0 +1,93 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; +import bcrypt from 'bcryptjs'; + +// DELETE - Supprimer un utilisateur +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const currentUser = await getCurrentUser(); + if (!currentUser) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + // Empêcher la suppression de son propre compte + if (currentUser.id === params.id) { + return NextResponse.json( + { error: 'Vous ne pouvez pas supprimer votre propre compte' }, + { status: 400 } + ); + } + + const user = await prisma.user.findUnique({ + where: { id: params.id }, + }); + + if (!user) { + return NextResponse.json( + { error: 'Utilisateur non trouvé' }, + { status: 404 } + ); + } + + await prisma.user.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Erreur lors de la suppression de l\'utilisateur:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// PUT - Réinitialiser le mot de passe d'un utilisateur +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const currentUser = await getCurrentUser(); + if (!currentUser) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { action } = body; + + if (action === 'reset-password') { + // Générer un nouveau mot de passe aléatoire + const newPassword = Math.random().toString(36).slice(-12) + Math.random().toString(36).slice(-12); + const hashedPassword = await bcrypt.hash(newPassword, 10); + + await prisma.user.update({ + where: { id: params.id }, + data: { + password: hashedPassword, + }, + }); + + return NextResponse.json({ + success: true, + newPassword, // Retourner le mot de passe en clair pour l'affichage + }); + } + + return NextResponse.json( + { error: 'Action non reconnue' }, + { status: 400 } + ); + } catch (error) { + console.error('Erreur lors de la réinitialisation du mot de passe:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/users/route.ts b/app/api/users/route.ts index a6ec533..2adba1b 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -2,7 +2,7 @@ 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) +// GET - Liste tous les utilisateurs export async function GET(request: NextRequest) { try { const user = await getCurrentUser(); @@ -10,33 +10,32 @@ export async function GET(request: NextRequest) { 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, + roleId: true, + role: { + select: { + id: true, + name: true, + description: true, + }, + }, createdAt: true, }, orderBy: { - name: 'asc', + createdAt: 'desc', }, }); 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 }); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); } } diff --git a/app/dashboard/adherents/page.tsx b/app/dashboard/adherents/page.tsx index 68c0de0..0fa8b24 100644 --- a/app/dashboard/adherents/page.tsx +++ b/app/dashboard/adherents/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; import AdherentsTable from '@/components/AdherentsTable'; @@ -10,6 +11,11 @@ export default async function AdherentsPage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/adherents'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/app/dashboard/archives/page.tsx b/app/dashboard/archives/page.tsx index 337c539..850fb9e 100644 --- a/app/dashboard/archives/page.tsx +++ b/app/dashboard/archives/page.tsx @@ -1,4 +1,5 @@ import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import { redirect } from 'next/navigation'; import DashboardLayout from '@/components/DashboardLayout'; import ArchivesTrajets from '@/components/ArchivesTrajets'; @@ -9,6 +10,11 @@ export default async function ArchivesPage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/archives'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/app/dashboard/calendrier/page.tsx b/app/dashboard/calendrier/page.tsx index b579e5a..c60fde8 100644 --- a/app/dashboard/calendrier/page.tsx +++ b/app/dashboard/calendrier/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; import CalendrierPageContent from '@/components/CalendrierPageContent'; @@ -10,6 +11,11 @@ export default async function CalendrierPage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/calendrier'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/app/dashboard/chauffeurs/page.tsx b/app/dashboard/chauffeurs/page.tsx index 8ede46f..92be56d 100644 --- a/app/dashboard/chauffeurs/page.tsx +++ b/app/dashboard/chauffeurs/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; import ChauffeursTable from '@/components/ChauffeursTable'; @@ -10,6 +11,11 @@ export default async function ChauffeursPage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/chauffeurs'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/app/dashboard/messagerie/page.tsx b/app/dashboard/messagerie/page.tsx index 51b9223..1be7437 100644 --- a/app/dashboard/messagerie/page.tsx +++ b/app/dashboard/messagerie/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; import Messagerie from '@/components/Messagerie'; @@ -10,6 +11,11 @@ export default async function MessageriePage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/messagerie'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 9d8b118..15704e9 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,6 +1,8 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; +import DashboardContent from '@/components/DashboardContent'; export default async function DashboardPage() { const user = await getCurrentUser(); @@ -9,46 +11,15 @@ export default async function DashboardPage() { redirect('/login'); } + // Vérifier les permissions + const hasAccess = await hasPageAccess(user.id, '/dashboard'); + if (!hasAccess) { + redirect('/dashboard/parametres'); // Rediriger vers une page accessible ou afficher une erreur + } + return ( -
-

- Content de vous revoir {user.name || user.email} -

- -

- Bienvenue sur votre tableau de bord. -

- -
-
-

- Bienvenue -

-

- {user.name || user.email} -

-
- -
-

- Statistiques -

-

- Contenu à venir -

-
- -
-

- Activité récente -

-

- Contenu à venir -

-
-
-
+
); } diff --git a/app/dashboard/parametres/configuration/page.tsx b/app/dashboard/parametres/configuration/page.tsx new file mode 100644 index 0000000..cab437e --- /dev/null +++ b/app/dashboard/parametres/configuration/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from 'next/navigation'; +import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; +import DashboardLayout from '@/components/DashboardLayout'; +import ConfigurationContent from '@/components/ConfigurationContent'; + +export default async function ConfigurationPage() { + const user = await getCurrentUser(); + + if (!user) { + redirect('/login'); + } + + const hasAccess = await hasPageAccess(user.id, '/dashboard/parametres/configuration'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + + return ( + + + + ); +} diff --git a/app/dashboard/parametres/page.tsx b/app/dashboard/parametres/page.tsx new file mode 100644 index 0000000..aa90dcc --- /dev/null +++ b/app/dashboard/parametres/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from 'next/navigation'; +import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; +import DashboardLayout from '@/components/DashboardLayout'; +import ParametresContent from '@/components/ParametresContent'; + +export default async function ParametresPage() { + const user = await getCurrentUser(); + + if (!user) { + redirect('/login'); + } + + const hasAccess = await hasPageAccess(user.id, '/dashboard/parametres'); + if (!hasAccess) { + redirect('/login'); + } + + return ( + + + + ); +} diff --git a/app/dashboard/univers-pro/page.tsx b/app/dashboard/univers-pro/page.tsx index c4023f1..ad19621 100644 --- a/app/dashboard/univers-pro/page.tsx +++ b/app/dashboard/univers-pro/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; import DashboardLayout from '@/components/DashboardLayout'; import UniversProTable from '@/components/UniversProTable'; @@ -10,6 +11,11 @@ export default async function UniversProPage() { redirect('/login'); } + const hasAccess = await hasPageAccess(user.id, '/dashboard/univers-pro'); + if (!hasAccess) { + redirect('/dashboard/parametres'); + } + return (
diff --git a/components/AdherentForm.tsx b/components/AdherentForm.tsx index b364bba..5fda6ff 100644 --- a/components/AdherentForm.tsx +++ b/components/AdherentForm.tsx @@ -25,6 +25,15 @@ interface AdherentFormProps { export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { const [loading, setLoading] = useState(false); + const [options, setOptions] = useState<{ + situation: Array<{ id: string; value: string }>; + prescripteur: Array<{ id: string; value: string }>; + facturation: Array<{ id: string; value: string }>; + }>({ + situation: [], + prescripteur: [], + facturation: [], + }); const [formData, setFormData] = useState({ nom: '', prenom: '', @@ -40,6 +49,26 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { instructions: '', }); + useEffect(() => { + fetchOptions(); + }, []); + + const fetchOptions = async () => { + try { + const response = await fetch('/api/settings/adherent-options'); + if (response.ok) { + const data = await response.json(); + setOptions({ + situation: data.situation || [], + prescripteur: data.prescripteur || [], + facturation: data.facturation || [], + }); + } + } catch (error) { + console.error('Erreur lors du chargement des options:', error); + } + }; + useEffect(() => { if (adherent) { const dateNaissance = new Date(adherent.dateNaissance); @@ -281,11 +310,11 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent appearance-none bg-white" > - - - - - + {options.situation.map((option) => ( + + ))}
@@ -312,10 +341,11 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent appearance-none bg-white" > - - - - + {options.prescripteur.map((option) => ( + + ))}
@@ -342,9 +372,11 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent appearance-none bg-white" > - - - + {options.facturation.map((option) => ( + + ))}
diff --git a/components/ConfigurationContent.tsx b/components/ConfigurationContent.tsx new file mode 100644 index 0000000..fe3c598 --- /dev/null +++ b/components/ConfigurationContent.tsx @@ -0,0 +1,1130 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { useNotification } from './NotificationProvider'; +import { AVAILABLE_PAGES } from '@/lib/pages'; + +interface AdherentOption { + id: string; + type: 'situation' | 'prescripteur' | 'facturation'; + value: string; + order: number; +} + +interface OptionsByType { + situation: AdherentOption[]; + prescripteur: AdherentOption[]; + facturation: AdherentOption[]; +} + +export default function ConfigurationContent() { + const router = useRouter(); + const { showNotification } = useNotification(); + const [activeConfigSection, setActiveConfigSection] = useState<'adherents' | 'comptes' | 'roles' | null>('adherents'); + const [options, setOptions] = useState({ + situation: [], + prescripteur: [], + facturation: [], + }); + const [loading, setLoading] = useState(true); + const [editingId, setEditingId] = useState(null); + const [editingValue, setEditingValue] = useState(''); + const [newValue, setNewValue] = useState>({ + situation: '', + prescripteur: '', + facturation: '', + }); + + const fetchOptions = useCallback(async () => { + setLoading(true); + try { + const response = await fetch('/api/settings/adherent-options'); + if (response.ok) { + const data = await response.json(); + setOptions({ + situation: data.situation || [], + prescripteur: data.prescripteur || [], + facturation: data.facturation || [], + }); + } + } catch (error) { + console.error('Erreur lors du chargement des options:', error); + showNotification('Erreur lors du chargement des options', 'error'); + } finally { + setLoading(false); + } + }, [showNotification]); + + useEffect(() => { + fetchOptions(); + }, [fetchOptions]); + + const handleAdd = useCallback(async (type: 'situation' | 'prescripteur' | 'facturation') => { + setNewValue((current) => { + const value = (current[type] || '').trim(); + if (!value) { + showNotification('Veuillez entrer une valeur', 'error'); + return current; + } + + // Appel API asynchrone + (async () => { + try { + const response = await fetch('/api/settings/adherent-options', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type, + value, + }), + }); + + if (response.ok) { + setNewValue((prev) => ({ ...prev, [type]: '' })); + await fetchOptions(); + showNotification('Option ajoutée avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de l\'ajout', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de l\'ajout', 'error'); + } + })(); + + return current; + }); + }, [showNotification, fetchOptions]); + + const handleEdit = (option: AdherentOption) => { + setEditingId(option.id); + setEditingValue(option.value); + }; + + const handleSaveEdit = async (id: string, type: 'situation' | 'prescripteur' | 'facturation') => { + const value = editingValue.trim(); + if (!value) { + showNotification('Veuillez entrer une valeur', 'error'); + return; + } + + try { + const response = await fetch(`/api/settings/adherent-options/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ value }), + }); + + if (response.ok) { + setEditingId(null); + setEditingValue(''); + await fetchOptions(); + showNotification('Option modifiée avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la modification', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la modification', 'error'); + } + }; + + const handleCancelEdit = () => { + setEditingId(null); + setEditingValue(''); + }; + + const handleDelete = async (id: string) => { + if (!confirm('Êtes-vous sûr de vouloir supprimer cette option ?')) { + return; + } + + try { + const response = await fetch(`/api/settings/adherent-options/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + await fetchOptions(); + showNotification('Option supprimée avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la suppression', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la suppression', 'error'); + } + }; + + const OptionCard = ({ + type, + label, + icon, + }: { + type: 'situation' | 'prescripteur' | 'facturation'; + label: string; + icon: React.ReactNode; + }) => { + const typeOptions = options[type] || []; + + return ( +
+
+
+ {icon} +
+

{label}

+ + {typeOptions.length} option{typeOptions.length > 1 ? 's' : ''} + +
+ + {/* Liste des options */} +
+ {loading ? ( +
Chargement...
+ ) : typeOptions.length === 0 ? ( +
+ Aucune option configurée +
+ ) : ( + typeOptions.map((option) => ( +
+ {editingId === option.id ? ( + <> + setEditingValue(e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSaveEdit(option.id, type); + } else if (e.key === 'Escape') { + handleCancelEdit(); + } + }} + autoFocus + /> + + + + ) : ( + <> + + {option.value} + + + + + )} +
+ )) + )} +
+ + {/* Formulaire d'ajout */} +
+ { + setNewValue((prev) => ({ ...prev, [type]: e.target.value })); + }} + placeholder={`Ajouter une nouvelle ${label.toLowerCase()}`} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAdd(type); + } + }} + /> + +
+
+ ); + }; + + // Composant pour la gestion des comptes + const GestionComptesContent = () => { + const [users, setUsers] = useState>([]); + const [roles, setRoles] = useState>([]); + const [loadingUsers, setLoadingUsers] = useState(true); + const [selectedUser, setSelectedUser] = useState(null); + const [showPasswordModal, setShowPasswordModal] = useState(false); + const [newPassword, setNewPassword] = useState(null); + + useEffect(() => { + fetchUsers(); + fetchRoles(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const fetchRoles = async () => { + try { + const response = await fetch('/api/roles'); + if (response.ok) { + const data = await response.json(); + setRoles(data); + } + } catch (error) { + console.error('Erreur lors du chargement des rôles:', error); + } + }; + + const fetchUsers = async () => { + setLoadingUsers(true); + try { + const response = await fetch('/api/users'); + if (response.ok) { + const data = await response.json(); + setUsers(data); + } + } catch (error) { + console.error('Erreur lors du chargement des utilisateurs:', error); + showNotification('Erreur lors du chargement des utilisateurs', 'error'); + } finally { + setLoadingUsers(false); + } + }; + + const handleDeleteUser = async (userId: string) => { + if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.')) { + return; + } + + try { + const response = await fetch(`/api/users/${userId}`, { + method: 'DELETE', + }); + + if (response.ok) { + await fetchUsers(); + showNotification('Utilisateur supprimé avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la suppression', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la suppression', 'error'); + } + }; + + const handleResetPassword = async (userId: string) => { + try { + const response = await fetch(`/api/users/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'reset-password' }), + }); + + if (response.ok) { + const data = await response.json(); + setSelectedUser(userId); + setNewPassword(data.newPassword); + setShowPasswordModal(true); + showNotification('Mot de passe réinitialisé avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la réinitialisation', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la réinitialisation', 'error'); + } + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: 'long', + year: 'numeric', + }); + }; + + const getUserInitials = (name: string | null, email: string) => { + if (name) { + const names = name.split(' '); + if (names.length >= 2) { + return `${names[0].charAt(0)}${names[1].charAt(0)}`.toUpperCase(); + } + return name.charAt(0).toUpperCase(); + } + return email.charAt(0).toUpperCase(); + }; + + return ( + <> +
+
+

Gestion des comptes

+

+ Gérez tous les comptes utilisateurs de la plateforme +

+
+ +
+ {loadingUsers ? ( +
Chargement...
+ ) : users.length === 0 ? ( +
Aucun utilisateur trouvé
+ ) : ( +
+ {users.map((user) => ( +
+
+ {/* Avatar */} +
+ {getUserInitials(user.name, user.email)} +
+ + {/* Informations */} +
+
+

+ {user.name || 'Sans nom'} +

+ {user.role && ( + + {user.role.name} + + )} +
+

{user.email}

+

+ Inscrit le {formatDate(user.createdAt)} +

+
+ + {/* Actions */} +
+ + + +
+
+
+ ))} +
+ )} +
+
+ + {/* Modal pour afficher le nouveau mot de passe */} + {showPasswordModal && newPassword && ( +
+
+
+

Nouveau mot de passe généré

+ +
+
+

+ Le nouveau mot de passe pour {users.find(u => u.id === selectedUser)?.email} est : +

+
+ {newPassword} +
+

+ ⚠️ Copiez ce mot de passe maintenant, il ne sera plus affiché après la fermeture de cette fenêtre. +

+
+ +
+
+ )} + + ); + }; + + // Composant pour la gestion des rôles + const GestionRolesContent = () => { + const [roles, setRoles] = useState; + _count: { + users: number; + }; + }>>([]); + const [loading, setLoading] = useState(true); + const [showRoleForm, setShowRoleForm] = useState(false); + const [editingRole, setEditingRole] = useState(null); + const [roleFormData, setRoleFormData] = useState({ + name: '', + description: '', + pageRoutes: [] as string[], + }); + + useEffect(() => { + fetchRoles(); + }, []); + + const fetchRoles = async () => { + setLoading(true); + try { + const response = await fetch('/api/roles'); + if (response.ok) { + const data = await response.json(); + setRoles(data); + } + } catch (error) { + console.error('Erreur lors du chargement des rôles:', error); + showNotification('Erreur lors du chargement des rôles', 'error'); + } finally { + setLoading(false); + } + }; + + const handleCreateRole = async () => { + if (!roleFormData.name.trim()) { + showNotification('Le nom du rôle est requis', 'error'); + return; + } + + try { + // Créer les permissions pour les pages sélectionnées si elles n'existent pas + const permissionIds: string[] = []; + for (const route of roleFormData.pageRoutes) { + const page = AVAILABLE_PAGES.find(p => p.route === route); + if (page) { + // Vérifier si la permission existe, sinon la créer + const permResponse = await fetch('/api/permissions'); + if (permResponse.ok) { + const existingPerms = await permResponse.json(); + let perm = existingPerms.find((p: any) => p.name === route); + + if (!perm) { + // Créer la permission + const createPermResponse = await fetch('/api/permissions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: route, + description: page.description, + }), + }); + if (createPermResponse.ok) { + perm = await createPermResponse.json(); + } + } + + if (perm) { + permissionIds.push(perm.id); + } + } + } + } + + const response = await fetch('/api/roles', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: roleFormData.name, + description: roleFormData.description || null, + permissionIds, + }), + }); + + if (response.ok) { + setShowRoleForm(false); + setRoleFormData({ name: '', description: '', pageRoutes: [] }); + await fetchRoles(); + showNotification('Rôle créé avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la création', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la création', 'error'); + } + }; + + const handleEditRole = (role: typeof roles[0]) => { + setEditingRole(role.id); + setRoleFormData({ + name: role.name, + description: role.description || '', + pageRoutes: role.permissions.map(p => p.permission.name).filter(name => + AVAILABLE_PAGES.some(page => page.route === name) + ), + }); + setShowRoleForm(true); + }; + + const handleUpdateRole = async () => { + if (!editingRole || !roleFormData.name.trim()) { + showNotification('Le nom du rôle est requis', 'error'); + return; + } + + try { + // Créer les permissions pour les pages sélectionnées si elles n'existent pas + const permissionIds: string[] = []; + for (const route of roleFormData.pageRoutes) { + const page = AVAILABLE_PAGES.find(p => p.route === route); + if (page) { + // Vérifier si la permission existe, sinon la créer + const permResponse = await fetch('/api/permissions'); + if (permResponse.ok) { + const existingPerms = await permResponse.json(); + let perm = existingPerms.find((p: any) => p.name === route); + + if (!perm) { + // Créer la permission + const createPermResponse = await fetch('/api/permissions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: route, + description: page.description, + }), + }); + if (createPermResponse.ok) { + perm = await createPermResponse.json(); + } + } + + if (perm) { + permissionIds.push(perm.id); + } + } + } + } + + const response = await fetch(`/api/roles/${editingRole}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: roleFormData.name, + description: roleFormData.description || null, + permissionIds, + }), + }); + + if (response.ok) { + setShowRoleForm(false); + setEditingRole(null); + setRoleFormData({ name: '', description: '', pageRoutes: [] }); + await fetchRoles(); + showNotification('Rôle modifié avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la modification', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la modification', 'error'); + } + }; + + const handleDeleteRole = async (roleId: string) => { + if (!confirm('Êtes-vous sûr de vouloir supprimer ce rôle ?')) { + return; + } + + try { + const response = await fetch(`/api/roles/${roleId}`, { + method: 'DELETE', + }); + + if (response.ok) { + await fetchRoles(); + showNotification('Rôle supprimé avec succès', 'success'); + } else { + const error = await response.json(); + showNotification(error.error || 'Erreur lors de la suppression', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la suppression', 'error'); + } + }; + + const handleCancelForm = () => { + setShowRoleForm(false); + setEditingRole(null); + setRoleFormData({ name: '', description: '', pageRoutes: [] }); + }; + + return ( + <> +
+
+
+

Gestion des rôles

+

+ Créez et gérez les rôles avec leurs permissions +

+
+ +
+ + {loading ? ( +
Chargement...
+ ) : roles.length === 0 ? ( +
+ Aucun rôle créé. Cliquez sur "Nouveau rôle" pour commencer. +
+ ) : ( +
+ {roles.map((role) => ( +
+
+
+
+

{role.name}

+ + {role._count.users} utilisateur{role._count.users > 1 ? 's' : ''} + +
+ {role.description && ( +

{role.description}

+ )} +
+ {role.permissions.length === 0 ? ( + Aucune page accessible + ) : ( + role.permissions + .filter(rp => AVAILABLE_PAGES.some(page => page.route === rp.permission.name)) + .map((rp) => { + const page = AVAILABLE_PAGES.find(p => p.route === rp.permission.name); + return ( + + {page?.label || rp.permission.name} + + ); + }) + )} +
+
+
+ + +
+
+
+ ))} +
+ )} +
+ + {/* Modal formulaire rôle */} + {showRoleForm && ( +
+
+
+
+

+ {editingRole ? 'Modifier le rôle' : 'Nouveau rôle'} +

+

+ {editingRole ? 'Modifiez les informations du rôle' : 'Créez un nouveau rôle et sélectionnez les pages accessibles'} +

+
+ +
+ +
+
+ + setRoleFormData({ ...roleFormData, name: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + placeholder="Ex: Administrateur, Éditeur..." + /> +
+ +
+ +