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:
179
services/events/event-feedback.service.ts
Normal file
179
services/events/event-feedback.service.ts
Normal 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();
|
||||
134
services/events/event-registration.service.ts
Normal file
134
services/events/event-registration.service.ts
Normal 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();
|
||||
278
services/events/event.service.ts
Normal file
278
services/events/event.service.ts
Normal 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();
|
||||
Reference in New Issue
Block a user