From 3bd43e777ec540bc3152794686289e8aee8d99a0 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 10 Dec 2025 06:11:32 +0100 Subject: [PATCH] Implement event feedback functionality: Add EventFeedback model to Prisma schema, enabling users to submit ratings and comments for events. Update EventsPageSection and AdminPanel components to support feedback management, including UI for submitting feedback and viewing existing feedbacks. Refactor registration logic to retrieve all user registrations for improved feedback handling. --- app/api/admin/feedback/route.ts | 92 + app/api/events/[id]/route.ts | 31 + app/api/feedback/[eventId]/route.ts | 108 ++ app/events/page.tsx | 33 +- app/feedback/[eventId]/page.tsx | 259 +++ components/AdminPanel.tsx | 22 +- components/EventsPageSection.tsx | 19 +- components/FeedbackManagement.tsx | 229 +++ prisma/generated/prisma/browser.ts | 5 + prisma/generated/prisma/client.ts | 5 + prisma/generated/prisma/internal/class.ts | 14 +- .../prisma/internal/prismaNamespace.ts | 91 +- .../prisma/internal/prismaNamespaceBrowser.ts | 14 + prisma/generated/prisma/models.ts | 1 + prisma/generated/prisma/models/Event.ts | 134 ++ .../generated/prisma/models/EventFeedback.ts | 1588 +++++++++++++++++ prisma/generated/prisma/models/User.ts | 166 ++ .../migration.sql | 22 + prisma/schema.prisma | 18 + 19 files changed, 2818 insertions(+), 33 deletions(-) create mode 100644 app/api/admin/feedback/route.ts create mode 100644 app/api/events/[id]/route.ts create mode 100644 app/api/feedback/[eventId]/route.ts create mode 100644 app/feedback/[eventId]/page.tsx create mode 100644 components/FeedbackManagement.tsx create mode 100644 prisma/generated/prisma/models/EventFeedback.ts create mode 100644 prisma/migrations/20251210120000_add_event_feedback/migration.sql diff --git a/app/api/admin/feedback/route.ts b/app/api/admin/feedback/route.ts new file mode 100644 index 0000000..938351d --- /dev/null +++ b/app/api/admin/feedback/route.ts @@ -0,0 +1,92 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { Role } from "@/prisma/generated/prisma/client"; + +export async function GET() { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + if (session.user.role !== Role.ADMIN) { + return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); + } + + // 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", + }, + }); + + // 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, + }; + }); + + return NextResponse.json({ + feedbacks, + statistics: statsWithDetails, + }); + } catch (error) { + console.error("Error fetching feedbacks:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des feedbacks" }, + { status: 500 } + ); + } +} diff --git a/app/api/events/[id]/route.ts b/app/api/events/[id]/route.ts new file mode 100644 index 0000000..a407f73 --- /dev/null +++ b/app/api/events/[id]/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + + const event = await prisma.event.findUnique({ + where: { id }, + }); + + if (!event) { + return NextResponse.json( + { error: "Événement introuvable" }, + { status: 404 } + ); + } + + return NextResponse.json(event); + } catch (error) { + console.error("Error fetching event:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération de l'événement" }, + { status: 500 } + ); + } +} + diff --git a/app/api/feedback/[eventId]/route.ts b/app/api/feedback/[eventId]/route.ts new file mode 100644 index 0000000..150b622 --- /dev/null +++ b/app/api/feedback/[eventId]/route.ts @@ -0,0 +1,108 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function POST( + request: Request, + { params }: { params: Promise<{ eventId: string }> } +) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + const { eventId } = await params; + const body = await request.json(); + const { rating, comment } = body; + + // Valider la note (1-5) + if (!rating || rating < 1 || rating > 5) { + return NextResponse.json( + { error: "La note doit être entre 1 et 5" }, + { status: 400 } + ); + } + + // Vérifier que l'événement existe + const event = await prisma.event.findUnique({ + where: { id: eventId }, + }); + + if (!event) { + return NextResponse.json( + { error: "Événement introuvable" }, + { status: 404 } + ); + } + + // Créer ou mettre à jour le feedback (unique par utilisateur/événement) + const feedback = await prisma.eventFeedback.upsert({ + where: { + userId_eventId: { + userId: session.user.id, + eventId, + }, + }, + update: { + rating, + comment: comment || null, + }, + create: { + userId: session.user.id, + eventId, + rating, + comment: comment || null, + }, + }); + + return NextResponse.json({ success: true, feedback }); + } catch (error) { + console.error("Error saving feedback:", error); + return NextResponse.json( + { error: "Erreur lors de l'enregistrement du feedback" }, + { status: 500 } + ); + } +} + +export async function GET( + request: Request, + { params }: { params: Promise<{ eventId: string }> } +) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + const { eventId } = await params; + + // Récupérer le feedback de l'utilisateur pour cet événement + const feedback = await prisma.eventFeedback.findUnique({ + where: { + userId_eventId: { + userId: session.user.id, + eventId, + }, + }, + include: { + event: { + select: { + id: true, + name: true, + date: true, + }, + }, + }, + }); + + return NextResponse.json({ feedback }); + } catch (error) { + console.error("Error fetching feedback:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération du feedback" }, + { status: 500 } + ); + } +} diff --git a/app/events/page.tsx b/app/events/page.tsx index 824e88d..643a6ad 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -27,28 +27,19 @@ export default async function EventsPage() { const initialRegistrations: Record = {}; if (session?.user?.id) { - const upcomingEvents = events.filter( - (e) => calculateEventStatus(e.date) === "UPCOMING" - ); - const eventIds = upcomingEvents.map((e) => e.id); + // Récupérer toutes les inscriptions (passées et à venir) pour permettre le feedback + const allRegistrations = await prisma.eventRegistration.findMany({ + where: { + userId: session.user.id, + }, + select: { + eventId: true, + }, + }); - if (eventIds.length > 0) { - const registrations = await prisma.eventRegistration.findMany({ - where: { - userId: session.user.id, - eventId: { - in: eventIds, - }, - }, - select: { - eventId: true, - }, - }); - - registrations.forEach((reg) => { - initialRegistrations[reg.eventId] = true; - }); - } + allRegistrations.forEach((reg) => { + initialRegistrations[reg.eventId] = true; + }); } return ( diff --git a/app/feedback/[eventId]/page.tsx b/app/feedback/[eventId]/page.tsx new file mode 100644 index 0000000..667c61f --- /dev/null +++ b/app/feedback/[eventId]/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { useState, useEffect, type FormEvent } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter, useParams } from "next/navigation"; +import Navigation from "@/components/Navigation"; +import { useBackgroundImage } from "@/hooks/usePreferences"; + +interface Event { + id: string; + name: string; + date: string; + description: string; +} + +interface Feedback { + id: string; + rating: number; + comment: string | null; +} + +export default function FeedbackPage() { + const { status } = useSession(); + const router = useRouter(); + const params = useParams(); + const eventId = params?.eventId as string; + const backgroundImage = useBackgroundImage("home", "/got-2.jpg"); + + const [event, setEvent] = useState(null); + const [existingFeedback, setExistingFeedback] = useState(null); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + const [rating, setRating] = useState(0); + const [comment, setComment] = useState(""); + + const fetchEventAndFeedback = async () => { + try { + // Récupérer l'événement + const eventResponse = await fetch(`/api/events/${eventId}`); + if (!eventResponse.ok) { + setError("Événement introuvable"); + setLoading(false); + return; + } + const eventData = await eventResponse.json(); + setEvent(eventData); + + // Récupérer le feedback existant si disponible + const feedbackResponse = await fetch(`/api/feedback/${eventId}`); + if (feedbackResponse.ok) { + const feedbackData = await feedbackResponse.json(); + if (feedbackData.feedback) { + setExistingFeedback(feedbackData.feedback); + setRating(feedbackData.feedback.rating); + setComment(feedbackData.feedback.comment || ""); + } + } + } catch { + setError("Erreur lors du chargement des données"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (status === "unauthenticated") { + router.push(`/login?redirect=/feedback/${eventId}`); + return; + } + + if (status === "authenticated" && eventId) { + fetchEventAndFeedback(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status, eventId, router]); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + setSuccess(false); + + if (rating === 0) { + setError("Veuillez sélectionner une note"); + return; + } + + setSubmitting(true); + + try { + const response = await fetch(`/api/feedback/${eventId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + rating, + comment: comment.trim() || null, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + setError(data.error || "Erreur lors de l'enregistrement"); + return; + } + + setSuccess(true); + setExistingFeedback(data.feedback); + + // Rediriger après 2 secondes + setTimeout(() => { + router.push("/events"); + }, 2000); + } catch { + setError("Erreur lors de l'enregistrement"); + } finally { + setSubmitting(false); + } + }; + + if (status === "loading" || loading) { + return ( +
+ +
+
Chargement...
+
+
+ ); + } + + if (!event) { + return ( +
+ +
+
Événement introuvable
+
+
+ ); + } + + return ( +
+ +
+ {/* Background Image */} +
+
+
+ + {/* Feedback Form */} +
+
+

+ + FEEDBACK + +

+

+ {existingFeedback + ? "Modifier votre feedback pour" + : "Donnez votre avis sur"} +

+

+ {event.name} +

+ + {success && ( +
+ Feedback enregistré avec succès ! Redirection... +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ {/* Rating */} +
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+

+ {rating > 0 && `${rating}/5`} +

+
+ + {/* Comment */} +
+ +