From bfaf30ee2648de000f28b99fe16f5bbe96a8b544 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 15 Dec 2025 22:19:58 +0100 Subject: [PATCH] Add admin challenge management features: Implement functions for canceling and reactivating challenges, enhance error handling, and update the ChallengeManagement component to support these actions. Update API to retrieve all challenge statuses for admin and improve UI to display active challenges count. --- actions/admin/challenges.ts | 68 ++++++++++ app/api/admin/challenges/route.ts | 8 +- app/api/challenges/active-count/route.ts | 23 ++++ components/admin/ChallengeManagement.tsx | 136 +++++++++++++++++--- components/challenges/ChallengesSection.tsx | 4 +- components/navigation/ChallengeBadge.tsx | 72 +++++++++++ components/navigation/Navigation.tsx | 35 ++--- components/navigation/NavigationWrapper.tsx | 14 +- services/challenges/challenge.service.ts | 83 +++++++++++- 9 files changed, 390 insertions(+), 53 deletions(-) create mode 100644 app/api/challenges/active-count/route.ts create mode 100644 components/navigation/ChallengeBadge.tsx diff --git a/actions/admin/challenges.ts b/actions/admin/challenges.ts index 6fbafc5..dcf32c5 100644 --- a/actions/admin/challenges.ts +++ b/actions/admin/challenges.ts @@ -166,3 +166,71 @@ export async function deleteChallenge(challengeId: string) { }; } } + +export async function adminCancelChallenge(challengeId: string) { + try { + await checkAdminAccess(); + + const challenge = await challengeService.adminCancelChallenge(challengeId); + + revalidatePath("/admin"); + revalidatePath("/challenges"); + + return { + success: true, + message: "Défi annulé avec succès", + data: challenge, + }; + } catch (error) { + console.error("Admin cancel challenge error:", error); + + if (error instanceof ValidationError) { + return { success: false, error: error.message }; + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message }; + } + if (error instanceof Error && error.message.includes("Accès refusé")) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de l'annulation du défi", + }; + } +} + +export async function reactivateChallenge(challengeId: string) { + try { + await checkAdminAccess(); + + const challenge = await challengeService.reactivateChallenge(challengeId); + + revalidatePath("/admin"); + revalidatePath("/challenges"); + + return { + success: true, + message: "Défi réactivé avec succès", + data: challenge, + }; + } catch (error) { + console.error("Reactivate challenge error:", error); + + if (error instanceof ValidationError) { + return { success: false, error: error.message }; + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message }; + } + if (error instanceof Error && error.message.includes("Accès refusé")) { + return { success: false, error: error.message }; + } + + return { + success: false, + error: "Une erreur est survenue lors de la réactivation du défi", + }; + } +} diff --git a/app/api/admin/challenges/route.ts b/app/api/admin/challenges/route.ts index 5c4316b..4411522 100644 --- a/app/api/admin/challenges/route.ts +++ b/app/api/admin/challenges/route.ts @@ -11,12 +11,8 @@ export async function GET() { return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); } - // Récupérer tous les défis (PENDING et ACCEPTED) pour l'admin - const allChallenges = await challengeService.getAllChallenges(); - // Filtrer pour ne garder que PENDING et ACCEPTED - const challenges = allChallenges.filter( - (c) => c.status === "PENDING" || c.status === "ACCEPTED" - ); + // Récupérer tous les défis pour l'admin (PENDING, ACCEPTED, CANCELLED, COMPLETED, REJECTED) + const challenges = await challengeService.getAllChallenges(); return NextResponse.json(challenges); } catch (error) { diff --git a/app/api/challenges/active-count/route.ts b/app/api/challenges/active-count/route.ts new file mode 100644 index 0000000..871e717 --- /dev/null +++ b/app/api/challenges/active-count/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { challengeService } from "@/services/challenges/challenge.service"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ count: 0 }); + } + + const count = await challengeService.getActiveChallengesCount( + session.user.id + ); + + return NextResponse.json({ count }); + } catch (error) { + console.error("Error fetching active challenges count:", error); + return NextResponse.json({ count: 0 }); + } +} + diff --git a/components/admin/ChallengeManagement.tsx b/components/admin/ChallengeManagement.tsx index c714369..98542a6 100644 --- a/components/admin/ChallengeManagement.tsx +++ b/components/admin/ChallengeManagement.tsx @@ -6,6 +6,8 @@ import { rejectChallenge, updateChallenge, deleteChallenge, + adminCancelChallenge, + reactivateChallenge, } from "@/actions/admin/challenges"; import { Button, Card, Input, Textarea, Alert } from "@/components/ui"; import { Avatar } from "@/components/ui"; @@ -178,6 +180,44 @@ export default function ChallengeManagement() { }); }; + const handleCancel = async (challengeId: string) => { + if (!confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) { + return; + } + + startTransition(async () => { + const result = await adminCancelChallenge(challengeId); + + if (result.success) { + setSuccessMessage("Défi annulé avec succès"); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors de l'annulation"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + + const handleReactivate = async (challengeId: string) => { + if (!confirm("Êtes-vous sûr de vouloir réactiver ce défi ?")) { + return; + } + + startTransition(async () => { + const result = await reactivateChallenge(challengeId); + + if (result.success) { + setSuccessMessage("Défi réactivé avec succès"); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors de la réactivation"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + if (loading) { return (
Chargement...
@@ -185,15 +225,18 @@ export default function ChallengeManagement() { } if (challenges.length === 0) { - return ( -
- Aucun défi en attente -
- ); + return
Aucun défi
; } const acceptedChallenges = challenges.filter((c) => c.status === "ACCEPTED"); const pendingChallenges = challenges.filter((c) => c.status === "PENDING"); + const cancelledChallenges = challenges.filter( + (c) => c.status === "CANCELLED" + ); + const completedChallenges = challenges.filter( + (c) => c.status === "COMPLETED" + ); + const rejectedChallenges = challenges.filter((c) => c.status === "REJECTED"); return (
@@ -208,15 +251,41 @@ export default function ChallengeManagement() { )}
- {acceptedChallenges.length} défi - {acceptedChallenges.length > 1 ? "s" : ""} en attente de validation + {acceptedChallenges.length > 0 && ( + + {acceptedChallenges.length} défi + {acceptedChallenges.length > 1 ? "s" : ""} en attente de désignation + du gagnant + + )} {pendingChallenges.length > 0 && ( - + 0 ? "ml-2" : ""}> • {pendingChallenges.length} défi {pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation )} + {cancelledChallenges.length > 0 && ( + + • {cancelledChallenges.length} défi + {cancelledChallenges.length > 1 ? "s" : ""} annulé + {cancelledChallenges.length > 1 ? "s" : ""} + + )} + {completedChallenges.length > 0 && ( + + • {completedChallenges.length} défi + {completedChallenges.length > 1 ? "s" : ""} complété + {completedChallenges.length > 1 ? "s" : ""} + + )} + {rejectedChallenges.length > 0 && ( + + • {rejectedChallenges.length} défi + {rejectedChallenges.length > 1 ? "s" : ""} rejeté + {rejectedChallenges.length > 1 ? "s" : ""} + + )}
{challenges.map((challenge) => ( @@ -260,13 +329,27 @@ export default function ChallengeManagement() { - {challenge.status === "ACCEPTED" - ? "Accepté" - : "En attente d'acceptation"} + {challenge.status === "PENDING" + ? "En attente d'acceptation" + : challenge.status === "ACCEPTED" + ? "En cours - En attente de désignation du gagnant" + : challenge.status === "COMPLETED" + ? "Complété" + : challenge.status === "CANCELLED" + ? "Annulé" + : challenge.status === "REJECTED" + ? "Rejeté" + : challenge.status}
{challenge.acceptedAt && ( @@ -291,7 +374,28 @@ export default function ChallengeManagement() { variant="primary" size="sm" > - Valider/Rejeter + Désigner le gagnant + + )} + {challenge.status !== "CANCELLED" && + challenge.status !== "COMPLETED" && ( + + )} + {challenge.status === "CANCELLED" && ( + )}