Files
got-gaming/components/admin/FeedbackManagement.tsx

363 lines
13 KiB
TypeScript

"use client";
import { useState } from "react";
import {
addFeedbackBonusPoints,
markFeedbackAsRead,
} from "@/actions/admin/feedback";
import { Button } from "@/components/ui";
import Avatar from "@/components/ui/Avatar";
interface Feedback {
id: string;
rating: number;
comment: string | null;
isRead: boolean;
createdAt: string;
event: {
id: string;
name: string;
date: string;
type: string;
};
user: {
id: string;
username: string;
email: string;
avatar: string | null;
score: number;
};
}
interface EventStatistics {
eventId: string;
eventName: string;
eventDate: string | null;
eventType: string | null;
averageRating: number;
feedbackCount: number;
}
interface FeedbackManagementProps {
initialFeedbacks: Feedback[];
initialStatistics: EventStatistics[];
}
export default function FeedbackManagement({
initialFeedbacks,
initialStatistics,
}: FeedbackManagementProps) {
const [feedbacks, setFeedbacks] = useState<Feedback[]>(initialFeedbacks);
const [statistics, setStatistics] = useState<EventStatistics[]>(initialStatistics);
const [error, setError] = useState("");
const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
const [addingPoints, setAddingPoints] = useState<Record<string, boolean>>(
{}
);
const [markingRead, setMarkingRead] = useState<Record<string, boolean>>({});
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");
}
};
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 handleAddPoints = async (userId: string, points: number) => {
const key = `${userId}-${points}`;
setAddingPoints((prev) => ({ ...prev, [key]: true }));
setError("");
try {
const result = await addFeedbackBonusPoints(userId, points);
if (result.success) {
// Rafraîchir les données pour voir les nouveaux scores
await fetchFeedbacks();
// Rafraîchir le score dans le header si l'utilisateur est connecté
window.dispatchEvent(new Event("refreshUserScore"));
} else {
setError(result.error || "Erreur lors de l'ajout des points");
}
} catch {
setError("Erreur lors de l'ajout des points");
} finally {
setAddingPoints((prev) => ({ ...prev, [key]: false }));
}
};
const handleMarkAsRead = async (feedbackId: string, isRead: boolean) => {
setMarkingRead((prev) => ({ ...prev, [feedbackId]: true }));
setError("");
try {
const result = await markFeedbackAsRead(feedbackId, isRead);
if (result.success) {
// Rafraîchir les données pour voir le nouveau statut
await fetchFeedbacks();
} else {
setError(result.error || "Erreur lors de la mise à jour");
}
} catch {
setError("Erreur lors de la mise à jour");
} finally {
setMarkingRead((prev) => ({ ...prev, [feedbackId]: false }));
}
};
const filteredFeedbacks = (selectedEvent
? feedbacks.filter((f) => f.event.id === selectedEvent)
: feedbacks
).sort((a, b) => {
// Trier : non lus en premier, puis par date décroissante
if (a.isRead !== b.isRead) {
return a.isRead ? 1 : -1;
}
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
});
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 rounded p-3 sm:p-4 ${
feedback.isRead
? "border-pixel-gold/20 opacity-75"
: "border-pixel-gold/50 bg-pixel-gold/5"
}`}
>
<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">
{/* En-tête utilisateur avec avatar */}
<div className="flex items-center gap-2 sm:gap-3 mb-3">
<Avatar
src={feedback.user.avatar}
username={feedback.user.username}
size="md"
borderClassName="border-pixel-gold/30"
/>
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 mb-1">
<h4 className="text-white font-semibold text-sm sm:text-base break-words">
{feedback.user.username}
</h4>
<span className="text-pixel-gold font-bold text-xs sm:text-sm">
{feedback.user.score.toLocaleString("fr-FR")} pts
</span>
</div>
<span className="text-gray-500 text-[10px] sm:text-xs break-all">
{feedback.user.email}
</span>
</div>
</div>
<div className="flex items-center gap-2 mb-2">
<div className="text-pixel-gold text-xs sm:text-sm font-semibold break-words">
{feedback.event.name}
</div>
{!feedback.isRead && (
<span className="bg-pixel-gold/20 text-pixel-gold text-[10px] px-1.5 py-0.5 rounded uppercase font-semibold">
Non lu
</span>
)}
</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 flex-col items-end gap-2 flex-shrink-0">
{renderStars(feedback.rating)}
<Button
variant={feedback.isRead ? "secondary" : "success"}
size="sm"
onClick={() =>
handleMarkAsRead(feedback.id, !feedback.isRead)
}
disabled={markingRead[feedback.id]}
className="text-xs whitespace-nowrap"
>
{markingRead[feedback.id]
? "..."
: feedback.isRead
? "Marquer non lu"
: "Marquer lu"}
</Button>
</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>
)}
{/* Boutons pour ajouter des points bonus */}
<div className="mt-3 pt-3 border-t border-pixel-gold/20 flex flex-wrap gap-2">
<span className="text-gray-400 text-xs sm:text-sm mr-2">
Points bonus:
</span>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 10)}
disabled={addingPoints[`${feedback.user.id}-10`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-10`] ? "..." : "+10"}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 100)}
disabled={addingPoints[`${feedback.user.id}-100`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-100`] ? "..." : "+100"}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 1000)}
disabled={addingPoints[`${feedback.user.id}-1000`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-1000`] ? "..." : "+1000"}
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}