Refactor event handling and user management: Replace direct database calls with service layer methods for events, user profiles, and preferences, enhancing code organization and maintainability. Update API routes to utilize new services for event registration, feedback, and user statistics, ensuring a consistent approach across the application.

This commit is contained in:
Julien Froidefond
2025-12-12 16:19:13 +01:00
parent fd095246a3
commit 494ac3f503
34 changed files with 1795 additions and 1096 deletions

View File

@@ -1,7 +1,8 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { Role, EventType } from "@/prisma/generated/prisma/client";
import { eventService } from "@/services/events/event.service";
import { Role } from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError } from "@/services/errors";
export async function PUT(
request: Request,
@@ -19,63 +20,27 @@ export async function PUT(
const { date, name, description, type, room, time, maxPlaces } = body;
// Le statut est ignoré s'il est fourni, il sera calculé automatiquement
// Vérifier que l'événement existe
const existingEvent = await prisma.event.findUnique({
where: { id },
});
if (!existingEvent) {
return NextResponse.json(
{ error: "Événement non trouvé" },
{ status: 404 }
);
}
const updateData: {
date?: Date;
name?: string;
description?: string;
type?: EventType;
room?: string | null;
time?: string | null;
maxPlaces?: number | null;
} = {};
if (date !== undefined) {
const eventDate = new Date(date);
if (isNaN(eventDate.getTime())) {
return NextResponse.json(
{ error: "Format de date invalide" },
{ status: 400 }
);
}
updateData.date = eventDate;
}
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (type !== undefined) {
if (!Object.values(EventType).includes(type)) {
return NextResponse.json(
{ error: "Type d'événement invalide" },
{ status: 400 }
);
}
updateData.type = type as EventType;
}
// Le statut est toujours calculé automatiquement, on ignore s'il est fourni
if (room !== undefined) updateData.room = room || null;
if (time !== undefined) updateData.time = time || null;
if (maxPlaces !== undefined)
updateData.maxPlaces = maxPlaces ? parseInt(maxPlaces) : null;
const event = await prisma.event.update({
where: { id },
data: updateData,
const event = await eventService.validateAndUpdateEvent(id, {
date,
name,
description,
type,
room,
time,
maxPlaces,
});
return NextResponse.json(event);
} catch (error) {
console.error("Error updating event:", error);
if (error instanceof ValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
if (error instanceof NotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}
return NextResponse.json(
{ error: "Erreur lors de la mise à jour de l'événement" },
{ status: 500 }
@@ -97,9 +62,7 @@ export async function DELETE(
const { id } = await params;
// Vérifier que l'événement existe
const existingEvent = await prisma.event.findUnique({
where: { id },
});
const existingEvent = await eventService.getEventById(id);
if (!existingEvent) {
return NextResponse.json(
@@ -108,9 +71,7 @@ export async function DELETE(
);
}
await prisma.event.delete({
where: { id },
});
await eventService.deleteEvent(id);
return NextResponse.json({ success: true });
} catch (error) {

View File

@@ -1,8 +1,8 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { Role, EventType } from "@/prisma/generated/prisma/client";
import { calculateEventStatus } from "@/lib/eventStatus";
import { eventService } from "@/services/events/event.service";
import { Role } from "@/prisma/generated/prisma/client";
import { ValidationError } from "@/services/errors";
export async function GET() {
try {
@@ -12,34 +12,22 @@ export async function GET() {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
const events = await prisma.event.findMany({
orderBy: {
date: "desc",
},
include: {
_count: {
select: {
registrations: true,
},
},
},
});
const events = await eventService.getEventsWithStatus();
// Transformer les données pour inclure le nombre d'inscriptions
// Le statut est calculé automatiquement en fonction de la date
// Transformer les données pour la sérialisation
const eventsWithCount = events.map((event) => ({
id: event.id,
date: event.date.toISOString(),
name: event.name,
description: event.description,
type: event.type,
status: calculateEventStatus(event.date),
status: event.status,
room: event.room,
time: event.time,
maxPlaces: event.maxPlaces,
createdAt: event.createdAt.toISOString(),
updatedAt: event.updatedAt.toISOString(),
registrationsCount: event._count.registrations,
registrationsCount: event.registrationsCount,
}));
return NextResponse.json(eventsWithCount);
@@ -63,45 +51,24 @@ export async function POST(request: Request) {
const body = await request.json();
const { date, name, description, type, room, time, maxPlaces } = body;
if (!date || !name || !description || !type) {
return NextResponse.json(
{ error: "Tous les champs sont requis" },
{ status: 400 }
);
}
// Convertir la date string en Date object
const eventDate = new Date(date);
if (isNaN(eventDate.getTime())) {
return NextResponse.json(
{ error: "Format de date invalide" },
{ status: 400 }
);
}
// Valider les enums
if (!Object.values(EventType).includes(type)) {
return NextResponse.json(
{ error: "Type d'événement invalide" },
{ status: 400 }
);
}
const event = await prisma.event.create({
data: {
date: eventDate,
name,
description,
type: type as EventType,
room: room || null,
time: time || null,
maxPlaces: maxPlaces ? parseInt(maxPlaces) : null,
},
const event = await eventService.validateAndCreateEvent({
date,
name,
description,
type,
room,
time,
maxPlaces,
});
return NextResponse.json(event);
} catch (error) {
console.error("Error creating event:", error);
if (error instanceof ValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json(
{ error: "Erreur lors de la création de l'événement" },
{ status: 500 }

View File

@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { eventFeedbackService } from "@/services/events/event-feedback.service";
import { Role } from "@/prisma/generated/prisma/client";
export async function GET() {
@@ -15,72 +15,14 @@ export async function GET() {
}
// Récupérer tous les feedbacks avec les détails de l'événement et de l'utilisateur
const feedbacks = await prisma.eventFeedback.findMany({
include: {
event: {
select: {
id: true,
name: true,
date: true,
type: true,
},
},
user: {
select: {
id: true,
username: true,
email: true,
},
},
},
orderBy: {
createdAt: "desc",
},
});
const feedbacks = await eventFeedbackService.getAllFeedbacks();
// Calculer les statistiques par événement
const eventStats = await prisma.eventFeedback.groupBy({
by: ["eventId"],
_avg: {
rating: true,
},
_count: {
id: true,
},
});
// Récupérer les détails des événements pour les stats
const eventIds = eventStats.map((stat) => stat.eventId);
const events = await prisma.event.findMany({
where: {
id: {
in: eventIds,
},
},
select: {
id: true,
name: true,
date: true,
type: true,
},
});
// Combiner les stats avec les détails des événements
const statsWithDetails = eventStats.map((stat) => {
const event = events.find((e) => e.id === stat.eventId);
return {
eventId: stat.eventId,
eventName: event?.name || "Événement supprimé",
eventDate: event?.date || null,
eventType: event?.type || null,
averageRating: stat._avg.rating || 0,
feedbackCount: stat._count.id,
};
});
const statistics = await eventFeedbackService.getFeedbackStatistics();
return NextResponse.json({
feedbacks,
statistics: statsWithDetails,
statistics,
});
} catch (error) {
console.error("Error fetching feedbacks:", error);

View File

@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
import { Role } from "@/prisma/generated/prisma/client";
export async function GET() {
@@ -11,22 +11,8 @@ export async function GET() {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
// Récupérer les préférences globales du site
let sitePreferences = await prisma.sitePreferences.findUnique({
where: { id: "global" },
});
// Si elles n'existent pas, créer une entrée par défaut
if (!sitePreferences) {
sitePreferences = await prisma.sitePreferences.create({
data: {
id: "global",
homeBackground: null,
eventsBackground: null,
leaderboardBackground: null,
},
});
}
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
return NextResponse.json(sitePreferences);
} catch (error) {
@@ -49,26 +35,10 @@ export async function PUT(request: Request) {
const body = await request.json();
const { homeBackground, eventsBackground, leaderboardBackground } = body;
const preferences = await prisma.sitePreferences.upsert({
where: { id: "global" },
update: {
homeBackground:
homeBackground === "" ? null : (homeBackground ?? undefined),
eventsBackground:
eventsBackground === "" ? null : (eventsBackground ?? undefined),
leaderboardBackground:
leaderboardBackground === ""
? null
: (leaderboardBackground ?? undefined),
},
create: {
id: "global",
homeBackground: homeBackground === "" ? null : (homeBackground ?? null),
eventsBackground:
eventsBackground === "" ? null : (eventsBackground ?? null),
leaderboardBackground:
leaderboardBackground === "" ? null : (leaderboardBackground ?? null),
},
const preferences = await sitePreferencesService.updateSitePreferences({
homeBackground,
eventsBackground,
leaderboardBackground,
});
return NextResponse.json(preferences);

View File

@@ -1,7 +1,13 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { userService } from "@/services/users/user.service";
import { userStatsService } from "@/services/users/user-stats.service";
import { Role } from "@/prisma/generated/prisma/client";
import {
ValidationError,
NotFoundError,
ConflictError,
} from "@/services/errors";
export async function PUT(
request: Request,
@@ -18,157 +24,26 @@ export async function PUT(
const body = await request.json();
const { username, avatar, hpDelta, xpDelta, score, level, role } = body;
// Récupérer l'utilisateur actuel
const user = await prisma.user.findUnique({
where: { id },
});
if (!user) {
return NextResponse.json(
{ error: "Utilisateur non trouvé" },
{ status: 404 }
);
}
// Calculer les nouvelles valeurs
let newHp = user.hp;
let newXp = user.xp;
let newLevel = user.level;
let newMaxXp = user.maxXp;
// Appliquer les changements de HP
if (hpDelta !== undefined) {
newHp = Math.max(0, Math.min(user.maxHp, user.hp + hpDelta));
}
// Appliquer les changements de XP
if (xpDelta !== undefined) {
newXp = user.xp + xpDelta;
newLevel = user.level;
newMaxXp = user.maxXp;
// Gérer le niveau up si nécessaire (quand on ajoute de l'XP)
if (newXp >= newMaxXp && newXp > 0) {
while (newXp >= newMaxXp) {
newXp -= newMaxXp;
newLevel += 1;
// Augmenter le maxXp pour le prochain niveau (formule simple)
newMaxXp = Math.floor(newMaxXp * 1.2);
}
}
// Gérer le niveau down si nécessaire (quand on enlève de l'XP)
if (newXp < 0 && newLevel > 1) {
while (newXp < 0 && newLevel > 1) {
newLevel -= 1;
// Calculer le maxXp du niveau précédent
newMaxXp = Math.floor(newMaxXp / 1.2);
newXp += newMaxXp;
}
// S'assurer que l'XP ne peut pas être négative
newXp = Math.max(0, newXp);
}
// S'assurer que le niveau minimum est 1
if (newLevel < 1) {
newLevel = 1;
newXp = 0;
}
}
// Appliquer les changements directs (username, avatar, score, level, role)
const updateData: {
hp: number;
xp: number;
level: number;
maxXp: number;
username?: string;
avatar?: string | null;
score?: number;
role?: Role;
} = {
hp: newHp,
xp: newXp,
level: newLevel,
maxXp: newMaxXp,
};
// Validation et mise à jour du username
// Valider username si fourni
if (username !== undefined) {
if (typeof username !== "string" || username.trim().length === 0) {
return NextResponse.json(
{ error: "Le nom d'utilisateur ne peut pas être vide" },
{ status: 400 }
);
}
if (username.length < 3 || username.length > 20) {
return NextResponse.json(
{
error:
"Le nom d'utilisateur doit contenir entre 3 et 20 caractères",
},
{ status: 400 }
);
}
// Vérifier si le username est déjà pris par un autre utilisateur
const existingUser = await prisma.user.findFirst({
where: {
username: username.trim(),
NOT: { id },
},
});
if (existingUser) {
return NextResponse.json(
{ error: "Ce nom d'utilisateur est déjà pris" },
{ status: 400 }
);
}
updateData.username = username.trim();
}
// Mise à jour de l'avatar
if (avatar !== undefined) {
updateData.avatar = avatar || null;
}
if (score !== undefined) {
updateData.score = Math.max(0, score);
}
if (level !== undefined) {
// Si le niveau est modifié directement, utiliser cette valeur
const targetLevel = Math.max(1, level);
updateData.level = targetLevel;
// Recalculer le maxXp pour le nouveau niveau
// Formule: maxXp = 5000 * (1.2 ^ (level - 1))
let calculatedMaxXp = 5000;
for (let i = 1; i < targetLevel; i++) {
calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2);
}
updateData.maxXp = calculatedMaxXp;
// Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP)
if (targetLevel !== user.level && xpDelta === undefined) {
updateData.xp = 0;
try {
await userService.validateAndUpdateUserProfile(id, { username });
} catch (error) {
if (
error instanceof ValidationError ||
error instanceof ConflictError
) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
throw error;
}
}
if (role !== undefined) {
if (role === "ADMIN" || role === "USER") {
updateData.role = role as Role;
}
}
// Mettre à jour l'utilisateur
const updatedUser = await prisma.user.update({
where: { id },
data: updateData,
select: {
// Mettre à jour stats et profil
const updatedUser = await userStatsService.updateUserStatsAndProfile(
id,
{ username, avatar, hpDelta, xpDelta, score, level, role },
{
id: true,
username: true,
email: true,
@@ -180,8 +55,8 @@ export async function PUT(
xp: true,
maxXp: true,
avatar: true,
},
});
}
);
return NextResponse.json(updatedUser);
} catch (error) {
@@ -206,34 +81,19 @@ export async function DELETE(
const { id } = await params;
// Vérifier que l'utilisateur existe
const user = await prisma.user.findUnique({
where: { id },
});
if (!user) {
return NextResponse.json(
{ error: "Utilisateur non trouvé" },
{ status: 404 }
);
}
// Empêcher la suppression de soi-même
if (user.id === session.user.id) {
return NextResponse.json(
{ error: "Vous ne pouvez pas supprimer votre propre compte" },
{ status: 400 }
);
}
// Supprimer l'utilisateur (les relations seront supprimées en cascade)
await prisma.user.delete({
where: { id },
});
await userService.validateAndDeleteUser(id, session.user.id);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting user:", error);
if (error instanceof ValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
if (error instanceof NotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}
return NextResponse.json(
{ error: "Erreur lors de la suppression de l'utilisateur" },
{ status: 500 }

View File

@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { userService } from "@/services/users/user.service";
import { Role } from "@/prisma/generated/prisma/client";
export async function GET() {
@@ -12,7 +12,10 @@ export async function GET() {
}
// Récupérer tous les utilisateurs avec leurs stats
const users = await prisma.user.findMany({
const users = await userService.getAllUsers({
orderBy: {
score: "desc",
},
select: {
id: true,
username: true,
@@ -27,9 +30,6 @@ export async function GET() {
avatar: true,
createdAt: true,
},
orderBy: {
score: "desc",
},
});
return NextResponse.json(users);