Added few configurations & mores
This commit is contained in:
@@ -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 });
|
||||
|
||||
131
app/api/dashboard/stats/route.ts
Normal file
131
app/api/dashboard/stats/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
74
app/api/permissions/route.ts
Normal file
74
app/api/permissions/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
205
app/api/roles/[id]/route.ts
Normal file
205
app/api/roles/[id]/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
106
app/api/roles/route.ts
Normal file
106
app/api/roles/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
106
app/api/settings/adherent-options/[id]/route.ts
Normal file
106
app/api/settings/adherent-options/[id]/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
105
app/api/settings/adherent-options/route.ts
Normal file
105
app/api/settings/adherent-options/route.ts
Normal file
@@ -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<string, typeof options>);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
21
app/api/user/pages/route.ts
Normal file
21
app/api/user/pages/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
74
app/api/users/[id]/role/route.ts
Normal file
74
app/api/users/[id]/role/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
93
app/api/users/[id]/route.ts
Normal file
93
app/api/users/[id]/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-6">
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-8">
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-8">
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-6">
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="h-full">
|
||||
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-semibold text-cblack mb-1">
|
||||
Content de vous revoir <span className="text-dyellow">{user.name || user.email}</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-cgray mb-6">
|
||||
Bienvenue sur votre tableau de bord.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Bienvenue
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{user.name || user.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Statistiques
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Contenu à venir
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Activité récente
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Contenu à venir
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardContent userName={user.name} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
24
app/dashboard/parametres/configuration/page.tsx
Normal file
24
app/dashboard/parametres/configuration/page.tsx
Normal file
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<ConfigurationContent />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
24
app/dashboard/parametres/page.tsx
Normal file
24
app/dashboard/parametres/page.tsx
Normal file
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<ParametresContent />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-6">
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<option value="">Sélectionner une situation</option>
|
||||
<option value="Actif">Actif</option>
|
||||
<option value="Retraité">Retraité</option>
|
||||
<option value="Chômeur">Chômeur</option>
|
||||
<option value="Étudiant">Étudiant</option>
|
||||
<option value="Autre">Autre</option>
|
||||
{options.situation.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -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"
|
||||
>
|
||||
<option value="">Sélectionner un prescripteur</option>
|
||||
<option value="Médecin">Médecin</option>
|
||||
<option value="Assistante sociale">Assistante sociale</option>
|
||||
<option value="CCAS">CCAS</option>
|
||||
<option value="Autre">Autre</option>
|
||||
{options.prescripteur.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -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"
|
||||
>
|
||||
<option value="">Sélectionner un mode de facturation</option>
|
||||
<option value="Direct">Direct</option>
|
||||
<option value="CCAS">CCAS</option>
|
||||
<option value="Autre organisme">Autre organisme</option>
|
||||
{options.facturation.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
1130
components/ConfigurationContent.tsx
Normal file
1130
components/ConfigurationContent.tsx
Normal file
File diff suppressed because it is too large
Load Diff
483
components/DashboardContent.tsx
Normal file
483
components/DashboardContent.tsx
Normal file
@@ -0,0 +1,483 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TrajetForm from './TrajetForm';
|
||||
import AdherentForm from './AdherentForm';
|
||||
import TrajetDetailModal from './TrajetDetailModal';
|
||||
|
||||
interface Stats {
|
||||
participationsMois: {
|
||||
montant: number;
|
||||
nombreFactures: number;
|
||||
};
|
||||
trajetsAujourdhui: {
|
||||
nombre: number;
|
||||
difference: number;
|
||||
};
|
||||
trajetsRealisesMois: {
|
||||
nombre: number;
|
||||
pourcentageEvolution: number;
|
||||
};
|
||||
chauffeursActifs: {
|
||||
nombre: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Trajet {
|
||||
id: string;
|
||||
date: string;
|
||||
adresseDepart: string;
|
||||
adresseArrivee: string;
|
||||
commentaire?: string | null;
|
||||
statut: string;
|
||||
adherent: {
|
||||
id: string;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
telephone: string;
|
||||
email: string;
|
||||
};
|
||||
chauffeur?: {
|
||||
id: string;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
telephone: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface DashboardContentProps {
|
||||
userName?: string | null;
|
||||
}
|
||||
|
||||
export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
const router = useRouter();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [trajetsRecents, setTrajetsRecents] = useState<Trajet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showTrajetForm, setShowTrajetForm] = useState(false);
|
||||
const [showAdherentForm, setShowAdherentForm] = useState(false);
|
||||
const [selectedTrajet, setSelectedTrajet] = useState<Trajet | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
fetchTrajetsRecents();
|
||||
}, []);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/stats');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des statistiques:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTrajetsRecents = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/trajets?limit=3');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTrajetsRecents(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des trajets récents:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const getInitials = (nom: string, prenom: string) => {
|
||||
return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase();
|
||||
};
|
||||
|
||||
const getStatusColor = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'Validé':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'Terminé':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'En cours':
|
||||
return 'bg-blue-100 text-blue-700';
|
||||
case 'Planifié':
|
||||
return 'bg-gray-100 text-gray-700';
|
||||
case 'Annulé':
|
||||
return 'bg-red-100 text-red-700';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusDotColor = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'Validé':
|
||||
case 'Terminé':
|
||||
return 'bg-green-500';
|
||||
case 'En cours':
|
||||
return 'bg-blue-500';
|
||||
case 'Planifié':
|
||||
return 'bg-yellow-500';
|
||||
case 'Annulé':
|
||||
return 'bg-red-500';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getChauffeurNumber = (chauffeur: { prenom: string; nom: string } | null | undefined) => {
|
||||
if (!chauffeur) return null;
|
||||
// Simple hash pour obtenir un numéro de chauffeur (1-5)
|
||||
const hash = (chauffeur.prenom + chauffeur.nom).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return (hash % 5) + 1;
|
||||
};
|
||||
|
||||
// Montant estimé par trajet (basé sur l'image)
|
||||
const getMontantTrajet = () => {
|
||||
return 6.80; // Montant moyen
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-8">
|
||||
{/* En-tête */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
Content de vous revoir <span className="text-dyellow">{userName || 'Utilisateur'}</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Bienvenue sur votre tableau de bord.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Statistiques */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{/* Participations du mois */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-dyellow/20 to-dyellow/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg className="w-6 h-6 text-dyellow" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 mb-2 font-medium uppercase tracking-wide">Participations du mois</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{stats ? `${stats.participationsMois.montant.toFixed(2).replace('.', ',')}€` : '0,00€'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-medium">
|
||||
{stats ? `${stats.participationsMois.nombreFactures} ${stats.participationsMois.nombreFactures > 1 ? 'Factures' : 'Facture'}` : '0 Facture'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trajets Aujourd'hui */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-dyellow/20 to-dyellow/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg className="w-6 h-6 text-dyellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 mb-2 font-medium uppercase tracking-wide">Trajets Aujourd'hui</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{stats ? stats.trajetsAujourdhui.nombre : 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-medium">
|
||||
{stats && stats.trajetsAujourdhui.difference !== 0
|
||||
? `${stats.trajetsAujourdhui.difference > 0 ? '+' : ''}${stats.trajetsAujourdhui.difference} vs hier`
|
||||
: 'Aucun changement'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trajets réalisés ce mois */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-dyellow/20 to-dyellow/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg className="w-6 h-6 text-dyellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 mb-2 font-medium uppercase tracking-wide">Trajets réalisés ce mois</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{stats ? stats.trajetsRealisesMois.nombre : 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-medium">
|
||||
{stats && stats.trajetsRealisesMois.pourcentageEvolution !== 0
|
||||
? `${stats.trajetsRealisesMois.pourcentageEvolution > 0 ? '+' : ''}${stats.trajetsRealisesMois.pourcentageEvolution}% vs mois dernier`
|
||||
: 'Aucun changement'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chauffeurs actifs */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-dyellow/20 to-dyellow/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg className="w-6 h-6 text-dyellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 mb-2 font-medium uppercase tracking-wide">Chauffeurs actifs</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{stats ? stats.chauffeursActifs.nombre : 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-medium">
|
||||
{stats ? `Sur ${stats.chauffeursActifs.total} total` : 'Sur 0 total'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Rapides et Trajets Récents côte à côte */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Actions Rapides */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-gray-900">Actions Rapides</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => setShowTrajetForm(true)}
|
||||
className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:border-lblue hover:shadow-lg transition-all duration-300 text-left group relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-lblue/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-lblue/15 to-lblue/5 flex items-center justify-center mb-4 group-hover:scale-110 group-hover:from-lblue/25 group-hover:to-lblue/15 transition-all duration-300">
|
||||
<svg className="w-6 h-6 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-gray-900 group-hover:text-lblue transition-colors">Nouveau trajet</h3>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAdherentForm(true)}
|
||||
className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:border-lblue hover:shadow-lg transition-all duration-300 text-left group relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-lblue/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-lblue/15 to-lblue/5 flex items-center justify-center mb-4 group-hover:scale-110 group-hover:from-lblue/25 group-hover:to-lblue/15 transition-all duration-300">
|
||||
<svg className="w-6 h-6 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-gray-900 group-hover:text-lblue transition-colors">Nouvel adhérent</h3>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/dashboard/factures')}
|
||||
className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 hover:border-lblue hover:shadow-lg transition-all duration-300 text-left group relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-lblue/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-lblue/15 to-lblue/5 flex items-center justify-center mb-4 group-hover:scale-110 group-hover:from-lblue/25 group-hover:to-lblue/15 transition-all duration-300">
|
||||
<svg className="w-6 h-6 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-gray-900 group-hover:text-lblue transition-colors">Nouvelle facture</h3>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100 opacity-60 cursor-not-allowed">
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-gray-400">Bientôt ?</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trajets Récents */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-gray-900">Trajets Récents</h2>
|
||||
<button
|
||||
onClick={fetchTrajetsRecents}
|
||||
className="text-sm text-lblue hover:text-dblue font-medium flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-500">Chargement...</div>
|
||||
) : trajetsRecents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Aucun trajet créé récemment
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 space-y-3">
|
||||
{trajetsRecents.map((trajet) => (
|
||||
<div
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="p-4 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar adhérent */}
|
||||
<div className="w-12 h-12 rounded-full bg-lgreen flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
|
||||
{/* Informations principales */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{formatDate(trajet.date)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{formatTime(trajet.date)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded ${
|
||||
trajet.statut === 'Terminé'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: trajet.statut === 'Validé'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adresses */}
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Départ:</span>{' '}
|
||||
<span className="text-gray-900">{trajet.adresseDepart}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Arrivée:</span>{' '}
|
||||
<span className="text-gray-900">{trajet.adresseArrivee}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chauffeur */}
|
||||
<div className="flex items-center gap-4 mt-3">
|
||||
{trajet.chauffeur ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-lblue flex items-center justify-center text-white text-xs font-semibold">
|
||||
{getInitials(trajet.chauffeur.nom, trajet.chauffeur.prenom)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">Chauffeur</div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-sm text-orange-600 font-medium">
|
||||
Aucun chauffeur assigné
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Commentaire */}
|
||||
{trajet.commentaire && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-500 italic">{trajet.commentaire}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{showTrajetForm && (
|
||||
<TrajetForm
|
||||
onClose={() => setShowTrajetForm(false)}
|
||||
onSuccess={() => {
|
||||
setShowTrajetForm(false);
|
||||
fetchTrajetsRecents();
|
||||
fetchStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showAdherentForm && (
|
||||
<AdherentForm
|
||||
adherent={null}
|
||||
onClose={() => {
|
||||
setShowAdherentForm(false);
|
||||
fetchStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedTrajet && (
|
||||
<TrajetDetailModal
|
||||
trajet={selectedTrajet}
|
||||
onClose={() => setSelectedTrajet(null)}
|
||||
onUpdate={() => {
|
||||
fetchTrajetsRecents();
|
||||
fetchStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
roleId?: string | null;
|
||||
}
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
@@ -41,8 +42,18 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps
|
||||
}
|
||||
);
|
||||
|
||||
// Récupérer les pages accessibles pour l'utilisateur
|
||||
const { data: userPagesData } = useSWR<{ pages: string[] }>(
|
||||
'/api/user/pages',
|
||||
fetcher
|
||||
);
|
||||
|
||||
const accessiblePages = userPagesData?.pages || [];
|
||||
|
||||
// Calculer le nombre total de messages non lus
|
||||
const totalUnreadCount = conversations?.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0) || 0;
|
||||
const totalUnreadCount = Array.isArray(conversations)
|
||||
? conversations.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0)
|
||||
: 0;
|
||||
|
||||
const getUserInitials = () => {
|
||||
if (user.name) {
|
||||
@@ -173,6 +184,15 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps
|
||||
const isMessagerie = item.href === '/dashboard/messagerie';
|
||||
const showBadge = isMessagerie && totalUnreadCount > 0;
|
||||
|
||||
// Vérifier si l'utilisateur a accès à cette page
|
||||
// Si accessiblePages est vide (chargement), afficher tous les liens
|
||||
// Sinon, vérifier si la page est dans la liste
|
||||
const hasAccess = accessiblePages.length === 0 || accessiblePages.includes(item.href);
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
@@ -271,19 +291,7 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowProfileMenu(false);
|
||||
// TODO: Navigate to profile page
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Mon profil
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowProfileMenu(false);
|
||||
// TODO: Navigate to settings
|
||||
router.push('/dashboard/parametres');
|
||||
}}
|
||||
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
|
||||
371
components/ParametresContent.tsx
Normal file
371
components/ParametresContent.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useNotification } from './NotificationProvider';
|
||||
import useSWR from 'swr';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
roleId: string | null;
|
||||
role: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface AdherentOption {
|
||||
id: string;
|
||||
type: 'situation' | 'prescripteur' | 'facturation';
|
||||
value: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface OptionsByType {
|
||||
situation: AdherentOption[];
|
||||
prescripteur: AdherentOption[];
|
||||
facturation: AdherentOption[];
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export default function ParametresContent() {
|
||||
const { showNotification } = useNotification();
|
||||
const { data: user } = useSWR<User>('/api/auth/me', fetcher);
|
||||
const [activeConfigSection, setActiveConfigSection] = useState<'adherents' | null>(null);
|
||||
const [options, setOptions] = useState<OptionsByType>({
|
||||
situation: [],
|
||||
prescripteur: [],
|
||||
facturation: [],
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingValue, setEditingValue] = useState('');
|
||||
const [newValue, setNewValue] = useState<Record<string, string>>({
|
||||
situation: '',
|
||||
prescripteur: '',
|
||||
facturation: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchOptions();
|
||||
}, []);
|
||||
|
||||
const fetchOptions = 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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async (type: 'situation' | 'prescripteur' | 'facturation') => {
|
||||
const value = newValue[type].trim();
|
||||
if (!value) {
|
||||
showNotification('Veuillez entrer une valeur', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
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({ ...newValue, [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');
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{label}</h3>
|
||||
<span className="ml-auto px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600">
|
||||
{typeOptions.length} option{typeOptions.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Liste des options */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{loading ? (
|
||||
<div className="text-center py-4 text-gray-500">Chargement...</div>
|
||||
) : typeOptions.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500 text-sm">
|
||||
Aucune option configurée
|
||||
</div>
|
||||
) : (
|
||||
typeOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 transition-colors"
|
||||
>
|
||||
{editingId === option.id ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={editingValue}
|
||||
onChange={(e) => setEditingValue(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm 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
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(option.id, type)}
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-lgreen rounded-lg hover:bg-dgreen transition-colors"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 text-sm font-medium text-gray-900">
|
||||
{option.value}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleEdit(option)}
|
||||
className="px-3 py-1.5 text-xs font-medium text-lblue bg-lblue/10 rounded-lg hover:bg-lblue/20 transition-colors"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(option.id)}
|
||||
className="px-3 py-1.5 text-xs font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition-colors"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Formulaire d'ajout */}
|
||||
<div className="flex items-center gap-2 pt-4 border-t border-gray-200">
|
||||
<input
|
||||
type="text"
|
||||
value={newValue[type]}
|
||||
onChange={(e) => setNewValue({ ...newValue, [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 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAdd(type);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAdd(type)}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-lblue rounded-lg hover:bg-dblue transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getUserInitials = () => {
|
||||
if (user?.name) {
|
||||
const names = user.name.split(' ');
|
||||
if (names.length >= 2) {
|
||||
return `${names[0].charAt(0)}${names[1].charAt(0)}`.toUpperCase();
|
||||
}
|
||||
return user.name.charAt(0).toUpperCase();
|
||||
}
|
||||
return user?.email?.charAt(0).toUpperCase() || 'U';
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Paramètres</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Gérez votre profil et configurez la plateforme
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Colonne de gauche - Profil/Compte */}
|
||||
<div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-lblue to-dblue flex items-center justify-center mx-auto mb-4 shadow-lg">
|
||||
<span className="text-white text-2xl font-bold">{getUserInitials()}</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-1">
|
||||
{user?.name || 'Utilisateur'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">{user?.email}</p>
|
||||
</div>
|
||||
|
||||
<button className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 rounded-lg text-left transition-colors border border-gray-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span className="font-medium text-gray-900">Modifier le compte</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colonne de droite - Configuration de la plateforme (visible uniquement pour Admin) */}
|
||||
{(user?.role?.name === 'Admin' || user?.role?.name === 'Administrateur') && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => router.push('/dashboard/parametres/configuration')}
|
||||
className="w-full bg-white rounded-xl shadow-sm border border-gray-100 p-6 hover:shadow-md hover:border-lblue transition-all text-left group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-xl bg-lblue/10 flex items-center justify-center group-hover:bg-lblue/20 transition-colors">
|
||||
<svg className="w-8 h-8 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-1 group-hover:text-lblue transition-colors">
|
||||
Configuration
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Configurez les paramètres de la plateforme
|
||||
</p>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-gray-400 group-hover:text-lblue transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
lib/pages.ts
Normal file
55
lib/pages.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// Liste des pages disponibles dans l'application avec leurs permissions
|
||||
export const AVAILABLE_PAGES = [
|
||||
{
|
||||
route: '/dashboard',
|
||||
label: 'Tableau de Board',
|
||||
description: 'Page principale du tableau de bord',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/calendrier',
|
||||
label: 'Calendrier',
|
||||
description: 'Gestion des trajets et planning',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/chauffeurs',
|
||||
label: 'Chauffeurs',
|
||||
description: 'Gestion des chauffeurs',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/adherents',
|
||||
label: 'Adhérents',
|
||||
description: 'Gestion des adhérents',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/univers-pro',
|
||||
label: 'Univers Pro',
|
||||
description: 'Base de données des contacts professionnels',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/messagerie',
|
||||
label: 'Messagerie',
|
||||
description: 'Système de messagerie',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/factures',
|
||||
label: 'Factures',
|
||||
description: 'Gestion des factures',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/archives',
|
||||
label: 'Archives',
|
||||
description: 'Archives des trajets',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/parametres',
|
||||
label: 'Paramètres',
|
||||
description: 'Paramètres utilisateur',
|
||||
},
|
||||
{
|
||||
route: '/dashboard/parametres/configuration',
|
||||
label: 'Configuration',
|
||||
description: 'Configuration de la plateforme',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type PageRoute = typeof AVAILABLE_PAGES[number]['route'];
|
||||
88
lib/permissions.ts
Normal file
88
lib/permissions.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { prisma } from './prisma';
|
||||
import { AVAILABLE_PAGES } from './pages';
|
||||
|
||||
/**
|
||||
* Vérifie si un utilisateur a accès à une page donnée
|
||||
*/
|
||||
export async function hasPageAccess(userId: string, pageRoute: string): Promise<boolean> {
|
||||
try {
|
||||
// La page Paramètres est toujours accessible à tous les utilisateurs
|
||||
if (pageRoute === '/dashboard/parametres') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Si l'utilisateur n'a pas de rôle, permettre l'accès à toutes les pages
|
||||
// (pour permettre la transition et la configuration initiale)
|
||||
if (!user || !user.role) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier si le rôle a la permission pour cette page
|
||||
const hasPermission = user.role.permissions.some(
|
||||
(rp) => rp.permission.name === pageRoute
|
||||
);
|
||||
|
||||
return hasPermission;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des permissions:', error);
|
||||
// En cas d'erreur, permettre l'accès pour éviter de bloquer l'utilisateur
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les pages accessibles pour un utilisateur
|
||||
*/
|
||||
export async function getUserAccessiblePages(userId: string): Promise<string[]> {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Si l'utilisateur n'a pas de rôle, retourner toutes les pages disponibles
|
||||
// (pour permettre la transition et la configuration initiale)
|
||||
if (!user || !user.role) {
|
||||
return AVAILABLE_PAGES.map(page => page.route);
|
||||
}
|
||||
|
||||
// La page Paramètres est toujours accessible
|
||||
const pages: string[] = ['/dashboard/parametres'];
|
||||
|
||||
// Ajouter les pages accessibles via le rôle
|
||||
const rolePages = user.role.permissions
|
||||
.map((rp) => rp.permission.name)
|
||||
.filter((route) => AVAILABLE_PAGES.some((page) => page.route === route));
|
||||
|
||||
return [...pages, ...rolePages];
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des pages accessibles:', error);
|
||||
// En cas d'erreur, retourner toutes les pages pour éviter de bloquer l'utilisateur
|
||||
return AVAILABLE_PAGES.map(page => page.route);
|
||||
}
|
||||
}
|
||||
27
lib/withPageAccess.tsx
Normal file
27
lib/withPageAccess.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
import { hasPageAccess } from '@/lib/permissions';
|
||||
|
||||
/**
|
||||
* HOC pour protéger une page avec vérification des permissions
|
||||
*/
|
||||
export async function withPageAccess(
|
||||
pageRoute: string,
|
||||
PageComponent: React.ComponentType<any>
|
||||
) {
|
||||
return async function ProtectedPage(props: any) {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
const hasAccess = await hasPageAccess(user.id, pageRoute);
|
||||
if (!hasAccess) {
|
||||
// Rediriger vers la première page accessible ou paramètres
|
||||
redirect('/dashboard/parametres');
|
||||
}
|
||||
|
||||
return <PageComponent {...props} />;
|
||||
};
|
||||
}
|
||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
@@ -15,12 +15,46 @@ model User {
|
||||
email String @unique
|
||||
password String
|
||||
name String?
|
||||
roleId String?
|
||||
role Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
conversations ConversationParticipant[]
|
||||
sentMessages Message[]
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
users User[]
|
||||
permissions RolePermission[]
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
roles RolePermission[]
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
id String @id @default(cuid())
|
||||
roleId String
|
||||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
permissionId String
|
||||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([roleId, permissionId])
|
||||
@@index([roleId])
|
||||
@@index([permissionId])
|
||||
}
|
||||
|
||||
model Chauffeur {
|
||||
id String @id @default(cuid())
|
||||
nom String
|
||||
@@ -140,3 +174,15 @@ model MessageFile {
|
||||
|
||||
@@index([messageId])
|
||||
}
|
||||
|
||||
model AdherentOption {
|
||||
id String @id @default(cuid())
|
||||
type String // "situation", "prescripteur", "facturation"
|
||||
value String // La valeur de l'option
|
||||
order Int @default(0) // Ordre d'affichage
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([type, value])
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user