234 lines
7.7 KiB
TypeScript
234 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface Feedback {
|
|
id: string;
|
|
rating: number;
|
|
comment: string | null;
|
|
createdAt: string;
|
|
event: {
|
|
id: string;
|
|
name: string;
|
|
date: string;
|
|
type: string;
|
|
};
|
|
user: {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
};
|
|
}
|
|
|
|
interface EventStatistics {
|
|
eventId: string;
|
|
eventName: string;
|
|
eventDate: string | null;
|
|
eventType: string | null;
|
|
averageRating: number;
|
|
feedbackCount: number;
|
|
}
|
|
|
|
export default function FeedbackManagement() {
|
|
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
|
const [statistics, setStatistics] = useState<EventStatistics[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchFeedbacks();
|
|
}, []);
|
|
|
|
const fetchFeedbacks = async () => {
|
|
try {
|
|
const response = await fetch("/api/admin/feedback");
|
|
if (!response.ok) {
|
|
setError("Erreur lors du chargement des feedbacks");
|
|
return;
|
|
}
|
|
const data = await response.json();
|
|
setFeedbacks(data.feedbacks || []);
|
|
setStatistics(data.statistics || []);
|
|
} catch {
|
|
setError("Erreur lors du chargement des feedbacks");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getEventTypeLabel = (type: string) => {
|
|
switch (type) {
|
|
case "ATELIER":
|
|
return "Atelier";
|
|
case "KATA":
|
|
return "Kata";
|
|
case "PRESENTATION":
|
|
return "Présentation";
|
|
case "LEARNING_HOUR":
|
|
return "Learning Hour";
|
|
default:
|
|
return type;
|
|
}
|
|
};
|
|
|
|
const renderStars = (rating: number) => {
|
|
return (
|
|
<div className="flex items-center gap-0.5 sm:gap-1">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<span
|
|
key={star}
|
|
className={`text-sm sm:text-lg ${
|
|
star <= rating ? "text-pixel-gold" : "text-gray-600"
|
|
}`}
|
|
>
|
|
★
|
|
</span>
|
|
))}
|
|
<span className="text-gray-400 text-xs sm:text-sm ml-1 sm:ml-2">
|
|
({rating}/5)
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const filteredFeedbacks = selectedEvent
|
|
? feedbacks.filter((f) => f.event.id === selectedEvent)
|
|
: feedbacks;
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg p-4 sm:p-8">
|
|
<p className="text-gray-400 text-center text-sm">Chargement...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4 sm:space-y-6">
|
|
{/* Statistiques par événement */}
|
|
{statistics.length > 0 && (
|
|
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg p-3 sm:p-6">
|
|
<h3 className="text-pixel-gold font-bold text-base sm:text-lg mb-4 break-words">
|
|
Statistiques par événement
|
|
</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
|
{statistics.map((stat) => (
|
|
<div
|
|
key={stat.eventId}
|
|
className={`bg-black/40 border rounded p-3 sm:p-4 cursor-pointer transition ${
|
|
selectedEvent === stat.eventId
|
|
? "border-pixel-gold bg-pixel-gold/10"
|
|
: "border-pixel-gold/30 hover:border-pixel-gold/50"
|
|
}`}
|
|
onClick={() =>
|
|
setSelectedEvent(
|
|
selectedEvent === stat.eventId ? null : stat.eventId
|
|
)
|
|
}
|
|
>
|
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 mb-2">
|
|
<h4 className="text-white font-semibold text-xs sm:text-sm break-words">
|
|
{stat.eventName}
|
|
</h4>
|
|
<span className="text-pixel-gold text-[10px] sm:text-xs uppercase whitespace-nowrap flex-shrink-0">
|
|
{stat.eventType && getEventTypeLabel(stat.eventType)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
{renderStars(Math.round(stat.averageRating))}
|
|
</div>
|
|
<div className="text-gray-400 text-[10px] sm:text-xs">
|
|
Moyenne: {stat.averageRating.toFixed(2)}/5
|
|
</div>
|
|
<div className="text-gray-400 text-[10px] sm:text-xs">
|
|
{stat.feedbackCount} feedback
|
|
{stat.feedbackCount > 1 ? "s" : ""}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{selectedEvent && (
|
|
<button
|
|
onClick={() => setSelectedEvent(null)}
|
|
className="mt-4 text-pixel-gold text-xs sm:text-sm hover:text-orange-400 transition"
|
|
>
|
|
Voir tous les feedbacks
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Liste des feedbacks */}
|
|
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg p-3 sm:p-6">
|
|
<h3 className="text-pixel-gold font-bold text-base sm:text-lg mb-4 break-words">
|
|
{selectedEvent
|
|
? `Feedbacks pour: ${
|
|
statistics.find((s) => s.eventId === selectedEvent)?.eventName
|
|
}`
|
|
: "Tous les feedbacks"}
|
|
</h3>
|
|
|
|
{error && (
|
|
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-3 sm:px-4 py-2 sm:py-3 rounded text-xs sm:text-sm mb-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{filteredFeedbacks.length === 0 ? (
|
|
<p className="text-gray-400 text-center py-8 text-sm">
|
|
Aucun feedback pour le moment
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{filteredFeedbacks.map((feedback) => (
|
|
<div
|
|
key={feedback.id}
|
|
className="bg-black/40 border border-pixel-gold/20 rounded p-3 sm:p-4"
|
|
>
|
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 mb-2">
|
|
<h4 className="text-white font-semibold text-sm sm:text-base break-words">
|
|
{feedback.user.username}
|
|
</h4>
|
|
<span className="text-gray-500 text-[10px] sm:text-xs break-all">
|
|
{feedback.user.email}
|
|
</span>
|
|
</div>
|
|
<div className="text-pixel-gold text-xs sm:text-sm font-semibold mb-2 break-words">
|
|
{feedback.event.name}
|
|
</div>
|
|
<div className="text-gray-500 text-[10px] sm:text-xs mb-2">
|
|
{new Date(feedback.createdAt).toLocaleDateString(
|
|
"fr-FR",
|
|
{
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
{renderStars(feedback.rating)}
|
|
</div>
|
|
</div>
|
|
{feedback.comment && (
|
|
<div className="mt-3 pt-3 border-t border-pixel-gold/20">
|
|
<p className="text-gray-300 text-xs sm:text-sm whitespace-pre-wrap break-words">
|
|
{feedback.comment}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|