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

@@ -0,0 +1,179 @@
import { prisma } from "../database";
import type { EventFeedback, Prisma } from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError } from "../errors";
import { eventService } from "./event.service";
export interface CreateOrUpdateFeedbackInput {
rating: number;
comment?: string | null;
}
export interface FeedbackStatistics {
eventId: string;
eventName: string;
eventDate: Date | null;
eventType: string | null;
averageRating: number;
feedbackCount: number;
}
/**
* Service de gestion des feedbacks sur les événements
*/
export class EventFeedbackService {
/**
* Crée ou met à jour un feedback
*/
async createOrUpdateFeedback(
userId: string,
eventId: string,
data: CreateOrUpdateFeedbackInput
): Promise<EventFeedback> {
return prisma.eventFeedback.upsert({
where: {
userId_eventId: {
userId,
eventId,
},
},
update: {
rating: data.rating,
comment: data.comment || null,
},
create: {
userId,
eventId,
rating: data.rating,
comment: data.comment || null,
},
});
}
/**
* Récupère le feedback d'un utilisateur pour un événement
*/
async getUserFeedback(
userId: string,
eventId: string
): Promise<EventFeedback | null> {
return prisma.eventFeedback.findUnique({
where: {
userId_eventId: {
userId,
eventId,
},
},
include: {
event: {
select: {
id: true,
name: true,
date: true,
},
},
},
});
}
/**
* Récupère tous les feedbacks (pour admin)
*/
async getAllFeedbacks(options?: {
include?: Prisma.EventFeedbackInclude;
orderBy?: Prisma.EventFeedbackOrderByWithRelationInput;
}): Promise<EventFeedback[]> {
return prisma.eventFeedback.findMany({
include: options?.include || {
event: {
select: {
id: true,
name: true,
date: true,
type: true,
},
},
user: {
select: {
id: true,
username: true,
email: true,
},
},
},
orderBy: options?.orderBy || { createdAt: "desc" },
});
}
/**
* Récupère les statistiques de feedback par événement
*/
async getFeedbackStatistics(): Promise<FeedbackStatistics[]> {
// 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
return 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,
};
});
}
/**
* Valide et crée/met à jour un feedback avec toutes les règles métier
*/
async validateAndCreateFeedback(
userId: string,
eventId: string,
data: { rating: number; comment?: string | null }
): Promise<EventFeedback> {
// Valider la note (1-5)
if (!data.rating || data.rating < 1 || data.rating > 5) {
throw new ValidationError("La note doit être entre 1 et 5", "rating");
}
// Vérifier que l'événement existe
const event = await eventService.getEventById(eventId);
if (!event) {
throw new NotFoundError("Événement");
}
// Créer ou mettre à jour le feedback
return this.createOrUpdateFeedback(userId, eventId, {
rating: data.rating,
comment: data.comment || null,
});
}
}
export const eventFeedbackService = new EventFeedbackService();

View File

@@ -0,0 +1,134 @@
import { prisma } from "../database";
import type { EventRegistration } from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError, ConflictError } from "../errors";
import { eventService } from "./event.service";
import { calculateEventStatus } from "@/lib/eventStatus";
/**
* Service de gestion des inscriptions aux événements
*/
export class EventRegistrationService {
/**
* Inscrit un utilisateur à un événement
*/
async registerUserToEvent(
userId: string,
eventId: string
): Promise<EventRegistration> {
return prisma.eventRegistration.create({
data: {
userId,
eventId,
},
});
}
/**
* Désinscrit un utilisateur d'un événement
*/
async unregisterUserFromEvent(
userId: string,
eventId: string
): Promise<void> {
await prisma.eventRegistration.deleteMany({
where: {
userId,
eventId,
},
});
}
/**
* Vérifie si un utilisateur est inscrit à un événement
*/
async checkUserRegistration(
userId: string,
eventId: string
): Promise<boolean> {
const registration = await prisma.eventRegistration.findUnique({
where: {
userId_eventId: {
userId,
eventId,
},
},
});
return !!registration;
}
/**
* Récupère l'inscription d'un utilisateur à un événement
*/
async getUserRegistration(
userId: string,
eventId: string
): Promise<EventRegistration | null> {
return prisma.eventRegistration.findUnique({
where: {
userId_eventId: {
userId,
eventId,
},
},
});
}
/**
* Récupère toutes les inscriptions d'un utilisateur
*/
async getUserRegistrations(userId: string): Promise<EventRegistration[]> {
return prisma.eventRegistration.findMany({
where: {
userId,
},
select: {
eventId: true,
},
});
}
/**
* Récupère le nombre d'inscriptions pour un événement
*/
async getEventRegistrationsCount(eventId: string): Promise<number> {
const count = await prisma.eventRegistration.count({
where: {
eventId,
},
});
return count;
}
/**
* Valide et inscrit un utilisateur à un événement avec toutes les règles métier
*/
async validateAndRegisterUser(
userId: string,
eventId: string
): Promise<EventRegistration> {
// Vérifier que l'événement existe
const event = await eventService.getEventById(eventId);
if (!event) {
throw new NotFoundError("Événement");
}
// Vérifier que l'événement est à venir
const eventStatus = calculateEventStatus(event.date);
if (eventStatus !== "UPCOMING") {
throw new ValidationError(
"Vous ne pouvez vous inscrire qu'aux événements à venir"
);
}
// Vérifier si l'utilisateur est déjà inscrit
const isRegistered = await this.checkUserRegistration(userId, eventId);
if (isRegistered) {
throw new ConflictError("Vous êtes déjà inscrit à cet événement");
}
// Créer l'inscription
return this.registerUserToEvent(userId, eventId);
}
}
export const eventRegistrationService = new EventRegistrationService();

View File

@@ -0,0 +1,278 @@
import { prisma } from "../database";
import type {
Event,
EventType,
Prisma,
} from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError } from "../errors";
import { calculateEventStatus } from "@/lib/eventStatus";
export interface CreateEventInput {
date: Date;
name: string;
description: string;
type: EventType;
room?: string | null;
time?: string | null;
maxPlaces?: number | null;
}
export interface UpdateEventInput {
date?: Date;
name?: string;
description?: string;
type?: EventType;
room?: string | null;
time?: string | null;
maxPlaces?: number | null;
}
export interface EventWithRegistrationsCount extends Event {
registrationsCount: number;
}
/**
* Service de gestion des événements
*/
export class EventService {
/**
* Récupère tous les événements
*/
async getAllEvents(options?: {
orderBy?: Prisma.EventOrderByWithRelationInput;
take?: number;
}): Promise<Event[]> {
return prisma.event.findMany({
orderBy: options?.orderBy || { date: "asc" },
...(options?.take && { take: options.take }),
});
}
/**
* Récupère un événement par son ID
*/
async getEventById(id: string): Promise<Event | null> {
return prisma.event.findUnique({
where: { id },
});
}
/**
* Récupère les événements à venir
*/
async getUpcomingEvents(limit: number = 3): Promise<Event[]> {
const now = new Date();
return prisma.event.findMany({
where: {
date: {
gte: now,
},
},
orderBy: {
date: "asc",
},
take: limit,
});
}
/**
* Crée un nouvel événement
*/
async createEvent(data: CreateEventInput): Promise<Event> {
return prisma.event.create({
data: {
date: data.date,
name: data.name,
description: data.description,
type: data.type,
room: data.room || null,
time: data.time || null,
maxPlaces: data.maxPlaces ? parseInt(String(data.maxPlaces)) : null,
},
});
}
/**
* Met à jour un événement
*/
async updateEvent(id: string, data: UpdateEventInput): Promise<Event> {
const updateData: Prisma.EventUpdateInput = {};
if (data.date !== undefined) {
updateData.date = data.date;
}
if (data.name !== undefined) {
updateData.name = data.name;
}
if (data.description !== undefined) {
updateData.description = data.description;
}
if (data.type !== undefined) {
updateData.type = data.type;
}
if (data.room !== undefined) {
updateData.room = data.room || null;
}
if (data.time !== undefined) {
updateData.time = data.time || null;
}
if (data.maxPlaces !== undefined) {
updateData.maxPlaces = data.maxPlaces
? parseInt(String(data.maxPlaces))
: null;
}
return prisma.event.update({
where: { id },
data: updateData,
});
}
/**
* Supprime un événement
*/
async deleteEvent(id: string): Promise<void> {
await prisma.event.delete({
where: { id },
});
}
/**
* Récupère les événements avec le nombre d'inscriptions (pour admin)
*/
async getEventsWithRegistrationsCount(): Promise<
EventWithRegistrationsCount[]
> {
const events = await prisma.event.findMany({
orderBy: {
date: "desc",
},
include: {
_count: {
select: {
registrations: true,
},
},
},
});
return events.map((event) => ({
...event,
registrationsCount: event._count.registrations,
}));
}
/**
* Récupère les événements avec leur statut calculé (pour admin)
*/
async getEventsWithStatus(): Promise<
Array<
EventWithRegistrationsCount & { status: "UPCOMING" | "LIVE" | "PAST" }
>
> {
const events = await this.getEventsWithRegistrationsCount();
return events.map((event) => ({
...event,
status: calculateEventStatus(event.date),
}));
}
/**
* Valide et crée un événement avec toutes les règles métier
*/
async validateAndCreateEvent(data: {
date: string | Date;
name: string;
description: string;
type: string;
room?: string | null;
time?: string | null;
maxPlaces?: string | number | null;
}): Promise<Event> {
// Validation des champs requis
if (!data.date || !data.name || !data.description || !data.type) {
throw new ValidationError("Tous les champs sont requis");
}
// Convertir et valider la date
const eventDate =
typeof data.date === "string" ? new Date(data.date) : data.date;
if (isNaN(eventDate.getTime())) {
throw new ValidationError("Format de date invalide", "date");
}
// Valider le type d'événement
if (!Object.values(EventType).includes(data.type as EventType)) {
throw new ValidationError("Type d'événement invalide", "type");
}
// Créer l'événement
return this.createEvent({
date: eventDate,
name: data.name,
description: data.description,
type: data.type as EventType,
room: data.room || null,
time: data.time || null,
maxPlaces: data.maxPlaces ? parseInt(String(data.maxPlaces)) : null,
});
}
/**
* Valide et met à jour un événement avec toutes les règles métier
*/
async validateAndUpdateEvent(
id: string,
data: {
date?: string | Date;
name?: string;
description?: string;
type?: string;
room?: string | null;
time?: string | null;
maxPlaces?: string | number | null;
}
): Promise<Event> {
// Vérifier que l'événement existe
const existingEvent = await this.getEventById(id);
if (!existingEvent) {
throw new NotFoundError("Événement");
}
const updateData: UpdateEventInput = {};
// Valider et convertir la date si fournie
if (data.date !== undefined) {
const eventDate =
typeof data.date === "string" ? new Date(data.date) : data.date;
if (isNaN(eventDate.getTime())) {
throw new ValidationError("Format de date invalide", "date");
}
updateData.date = eventDate;
}
// Valider le type si fourni
if (data.type !== undefined) {
if (!Object.values(EventType).includes(data.type as EventType)) {
throw new ValidationError("Type d'événement invalide", "type");
}
updateData.type = data.type as EventType;
}
// Autres champs
if (data.name !== undefined) updateData.name = data.name;
if (data.description !== undefined)
updateData.description = data.description;
if (data.room !== undefined) updateData.room = data.room || null;
if (data.time !== undefined) updateData.time = data.time || null;
if (data.maxPlaces !== undefined) {
updateData.maxPlaces = data.maxPlaces
? parseInt(String(data.maxPlaces))
: null;
}
return this.updateEvent(id, updateData);
}
}
export const eventService = new EventService();