From 2273b816f10f9b1472f3f439ba193a18d405ee07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Poncin?= Date: Wed, 24 Jun 2026 17:55:15 +0200 Subject: [PATCH 1/4] refactor:challenge creation & invalidation --- .../AdminChallenge/adminChalengeList.tsx | 21 +- .../AdminChallenge/adminChallengeEditor.tsx | 12 +- .../adminChallengeValidatedList.tsx | 242 +++++++++++++++--- 3 files changed, 230 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx index 2b1661d..27fe698 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx @@ -1,6 +1,6 @@ import { CheckCircle2, Edit, Search, Trash2 } from "lucide-react"; import { useMemo, useState } from "react"; -import Select, { type SingleValue } from "react-select"; +import Select from "react-select"; import Swal from "sweetalert2"; import { type Challenge } from "../../../interfaces/challenge.interface"; @@ -62,6 +62,11 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact } }; + const handleValidationCLick = async (c: Challenge) => { + setShowValidationFormForId(c.id); + setValidationType(c.category.toLowerCase() as ValidationTarget); + } + const handleValidate = async () => { if (!showValidationFormForId || !validationType || !selectedTargetId) return; @@ -145,7 +150,7 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact Supprimer + {showUnvalidationFormForId === group.challenge_id && ( + + + +

❌ Invalider le challenge

+ + setSelectedTargetId(Number(option?.value))} + onChange={(options) => setSelectedTargetIds(options.map((o) => Number(o.value)))} options={users.map((u: User) => ({ value: u.userId, label: `${u.firstName} ${u.lastName}`, @@ -176,8 +179,9 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact {validationType === "team" && ( setSelectedTargetId(Number(option?.value))} + onChange={(options) => setSelectedTargetIds(options.map((o) => Number(o.value)))} options={factions.map((f: Faction) => ({ value: f.factionId, label: f.name, @@ -207,7 +212,7 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact onClick={() => { setShowValidationFormForId(null); setValidationType(null); - setSelectedTargetId(null); + setSelectedTargetIds([]); }} className="bg-gray-400 hover:bg-gray-500 text-white" > diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeAddPointsForm.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeAddPointsForm.tsx index 1fa1e03..6a27268 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeAddPointsForm.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeAddPointsForm.tsx @@ -67,7 +67,7 @@ export const AdminChallengeAddPointsForm = () => { return (
- + 🎯 Ajouter des points à une faction diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeEditor.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeEditor.tsx index 73dd80b..b1d5a84 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeEditor.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeEditor.tsx @@ -72,7 +72,7 @@ const ChallengeEditor = ({ editingChallenge, setEditingChallenge, refreshChallen }; return ( - + {editingChallenge ? "✏️ Modifier Challenge" : "🛠️ Créer Challenge"} diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx index c49e727..7420b64 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx @@ -1,6 +1,6 @@ -import { Search } from "lucide-react"; +import { Search, Trash2, X } from "lucide-react"; import { useMemo, useState } from "react"; -import Select from "react-select"; +import Select from "react-select"; import Swal from "sweetalert2"; import { type ValidatedChallenge } from "../../../interfaces/challenge.interface"; @@ -205,7 +205,7 @@ export const AdminValidatedChallengesList = ({ }; return ( -
+
@@ -226,94 +226,88 @@ export const AdminValidatedChallengesList = ({
{/* Grille */} - {filtered.length === 0 ? ( -

Aucun challenge validé trouvé.

- ) : ( -
- {groupedValidatedChallenges.map((group) => ( -
-
-

{group.challenge_name}

-

{group.challenge_categorie}

-

{group.challenge_description}

-
+
+ {filtered.length > 0 ? ( + groupedValidatedChallenges.map((group) => ( + + +
+

{group.challenge_name}

+

{group.challenge_description}

+

Catégorie : {group.challenge_categorie}

+

Points : {group.points}

+

Validé le : {new Date(group.validated_at).toLocaleDateString()}

+
-
-

Points : {group.points}

-

- Validé le : {new Date(group.validated_at).toLocaleDateString()} -

-
+
+

Destinataires :

+ {group.recipients.length > 0 ? ( +
    + {group.recipients.map((recipient) => ( +
  • {recipient.label}
  • + ))} +
+ ) : ( +

Aucun destinataire connu.

+ )} +
-
-

Destinataires :

- {group.recipients.length > 0 ? ( -
    - {group.recipients.map((recipient) => ( -
  • {recipient.label}
  • - ))} -
- ) : ( -

Aucun destinataire connu.

- )} -
- - - {showUnvalidationFormForId === group.challenge_id && ( - - - -

❌ Invalider le challenge

+
+ +
- + setSelectedUnvalidationTargetIds(options.map((o) => Number(o.value))) + } + options={group.recipients.map((recipient) => ({ + value: recipient.id, + label: recipient.label, + }))} + /> - - - -
- - - )} -
- ))} -
- )} +
+ + + +
+ + + )} + + + )) + ) : ( +

Aucun challenge validé trouvé.

+ )} +
From bf1234df48d059b6706d3810a7d7389ce58431fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Poncin?= Date: Thu, 25 Jun 2026 10:57:35 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20"d=C3=A9valider=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/tent.controller.ts | 4 +-- backend/src/services/tent.service.ts | 2 +- backend/src/utils/emailtemplates.ts | 2 +- .../adminChallengeValidatedList.tsx | 26 +++++++++---------- frontend/src/components/Admin/adminTent.tsx | 6 ++--- .../src/services/requests/tent.service.ts | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index 5dd067b..2f87a81 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -90,7 +90,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { to: [user1.email, user2.email], subject: confirmed ? "🎉 Votre tente a été validée !" - : "⛺ Votre tente a été dévalidée", + : "⛺ Votre tente a été invalidée", text: "", // optionnel html: htmlEmail, }; @@ -101,7 +101,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { Ok(res, { msg: confirmed ? "Tente validée et email envoyé." - : "Tente dévalidée et email envoyé.", + : "Tente invalidée et email envoyé.", }); } catch (err: any) { console.error(err); diff --git a/backend/src/services/tent.service.ts b/backend/src/services/tent.service.ts index ed9b32b..5f02fce 100644 --- a/backend/src/services/tent.service.ts +++ b/backend/src/services/tent.service.ts @@ -131,5 +131,5 @@ export const toggleTentConfirmation = async ( ) ); - return { success: true, message: confirmed ? "Tente validée." : "Tente dévalidée." }; + return { success: true, message: confirmed ? "Tente validée." : "Tente invalidée." }; }; diff --git a/backend/src/utils/emailtemplates.ts b/backend/src/utils/emailtemplates.ts index 309068e..3d53f57 100644 --- a/backend/src/utils/emailtemplates.ts +++ b/backend/src/utils/emailtemplates.ts @@ -341,7 +341,7 @@ export const templateNotifyTentConfirmation = `

La tente entre {{user1}} et {{user2}} a été - {{#if confirmed}}validée{{else}}dévalidée{{/if}} + {{#if confirmed}}validée{{else}}invalidée{{/if}} .

diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx index 7420b64..a136943 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx @@ -102,23 +102,23 @@ export const AdminValidatedChallengesList = ({ const handleUnvalidate = async ( ) => { const confirm = await Swal.fire({ - title: "Confirmer la dévalidation ?", + title: "Confirmer la invalidation ?", text: "Cette action retirera la validation du challenge.", icon: "warning", showCancelButton: true, confirmButtonColor: "#e3342f", cancelButtonColor: "#6b7280", - confirmButtonText: "Oui, dévalider", + confirmButtonText: "Oui, invalider", cancelButtonText: "Annuler", }); if (!confirm.isConfirmed) return; for (const targetId of selectedUnvalidationTargetIds) { - console.log("Dévalidation du challenge pour l'ID cible :", targetId); + console.log("Invalidation du challenge pour l'ID cible :", targetId); console.log(validatedChallenges); const challengeToUnvalidate = validatedChallenges.find( (c) => c.challenge_id === showUnvalidationFormForId ); - console.log("Challenge à dévalider :", challengeToUnvalidate); + console.log("Challenge à invalider :", challengeToUnvalidate); if (!challengeToUnvalidate) { Swal.fire({ icon: "error", title: "Erreur", text: "Challenge non trouvé." }); return; @@ -130,7 +130,7 @@ export const AdminValidatedChallengesList = ({ teamId: challengeToUnvalidate.target_team_id ?? 0, userId: challengeToUnvalidate.target_user_id ?? 0, }); - console.log("Réponse de la dévalidation :", res); + console.log("Réponse de la invalidation :", res); validatedChallenges = validatedChallenges.filter( (c) => c.challenge_id === showUnvalidationFormForId && @@ -139,14 +139,14 @@ export const AdminValidatedChallengesList = ({ c.target_faction_id !== targetId ); } catch { - Swal.fire({ icon: "error", title: "Erreur", text: "Impossible de dévalider ce challenge." }); + Swal.fire({ icon: "error", title: "Erreur", text: "Impossible de invalider ce challenge." }); } } Swal.fire({ icon: "success", - title: "Dévalidé", - text: "Les challenges ont été dévalidés.", + title: "Invalidé", + text: "Les challenges ont été invalidés.", timer: 1800, showConfirmButton: false, }); @@ -157,13 +157,13 @@ export const AdminValidatedChallengesList = ({ const handleUnvalidateAll = async () => { const confirm = await Swal.fire({ - title: "Confirmer la dévalidation ?", + title: "Confirmer la invalidation ?", text: "Cette action retirera la validation du challenge.", icon: "warning", showCancelButton: true, confirmButtonColor: "#e3342f", cancelButtonColor: "#6b7280", - confirmButtonText: "Oui, dévalider", + confirmButtonText: "Oui, invalider", cancelButtonText: "Annuler", }); @@ -185,7 +185,7 @@ export const AdminValidatedChallengesList = ({ Swal.fire({ icon: "error", title: "Erreur", - text: "Impossible de dévalider un ou plusieurs challenges.", + text: "Impossible de invalider un ou plusieurs challenges.", }); return; } @@ -193,8 +193,8 @@ export const AdminValidatedChallengesList = ({ Swal.fire({ icon: "success", - title: "Dévalidé", - text: "Les challenges ont été dévalidés.", + title: "Invalidé", + text: "Les challenges ont été invalidés.", timer: 1800, showConfirmButton: false, }); diff --git a/frontend/src/components/Admin/adminTent.tsx b/frontend/src/components/Admin/adminTent.tsx index 644492d..df4ecf6 100644 --- a/frontend/src/components/Admin/adminTent.tsx +++ b/frontend/src/components/Admin/adminTent.tsx @@ -41,7 +41,7 @@ export const TentAdmin = () => { }; const handleToggle = async (pair: TentPair) => { - const action = pair.confirmed ? "dévalider" : "valider"; + const action = pair.confirmed ? "invalider" : "valider"; const confirm = await Swal.fire({ title: '⚠️ Confirmation requise', @@ -73,7 +73,7 @@ export const TentAdmin = () => { Swal.fire( "Succès ✅", - `La tente a bien été ${pair.confirmed ? "dévalidée" : "validée"}.`, + `La tente a bien été ${pair.confirmed ? "invalidée" : "validée"}.`, "success" ); @@ -194,7 +194,7 @@ export const TentAdmin = () => { : "bg-green-600 hover:bg-green-700 text-white" } > - {pair.confirmed ? "❌ Dévalider" : "✅ Valider"} + {pair.confirmed ? "❌ Invalidée" : "✅ Validée"} diff --git a/frontend/src/services/requests/tent.service.ts b/frontend/src/services/requests/tent.service.ts index 945fe85..83709f8 100644 --- a/frontend/src/services/requests/tent.service.ts +++ b/frontend/src/services/requests/tent.service.ts @@ -24,7 +24,7 @@ export const getAllTentPairs = async () => { return response.data; }; -// Valider ou dévalider une tente (admin) +// Valider ou invalider une tente (admin) export const toggleTentConfirmation = async ( userId1: number, userId2: number, From 19b36d4ef8a64599bf859707a75f6e8444685e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Poncin?= Date: Wed, 1 Jul 2026 09:17:42 +0200 Subject: [PATCH 4/4] fix: error message --- .../AdminChallenge/adminChalengeList.tsx | 2 +- .../services/requests/challenge.service.ts | 56 ++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx index 4395fb1..c3cccf5 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx @@ -95,7 +95,7 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact Swal.fire({ icon: "error", title: "Erreur ❌", - text: "Impossible de valider ce challenge. Réessaie plus tard. (cette erreur peut survenir si le challenge a déjà été validé pour cet utilisateur/équipe/faction)", + text: err as string, }); } } diff --git a/frontend/src/services/requests/challenge.service.ts b/frontend/src/services/requests/challenge.service.ts index 5a90014..9bf8a91 100644 --- a/frontend/src/services/requests/challenge.service.ts +++ b/frontend/src/services/requests/challenge.service.ts @@ -1,5 +1,27 @@ +import { type AxiosError } from "axios"; + import api from "../api"; +interface ApiErrorResponse { + message?: string; + error?: string; +} + +type ApiErrorWithStatus = Error & { status?: number }; + +const buildApiError = (error: unknown): ApiErrorWithStatus => { + const axiosError = error as AxiosError; + const message = + axiosError.response?.data?.message || + axiosError.response?.data?.error || + axiosError.message || + "Une erreur est survenue lors de la requête."; + + const apiError = new Error(message) as ApiErrorWithStatus; + apiError.status = axiosError.response?.status; + return apiError; +}; + // Récupérer tous les challenges (utilisateur ou admin, en fonction du token) export const getAllChallenges = async () => { const response = await api.get("challenge/user/challenges"); @@ -44,12 +66,16 @@ export const validateChallenge = async ({ type: "user" | "team" | "faction"; targetId: number; }) => { - const response = await api.post("challenge/admin/validate", { - challengeId, - type, - targetId, - }); - return response.data; + try { + const response = await api.post("challenge/admin/validate", { + challengeId, + type, + targetId, + }); + return response.data; + } catch (error) { + throw buildApiError(error); + } }; export const unvalidateChallenge = async ({ @@ -63,13 +89,17 @@ export const unvalidateChallenge = async ({ teamId: number; userId: number; }) => { - const response = await api.post("challenge/admin/unvalidate", { - challengeId, - factionId, - teamId, - userId - }); - return response.data; + try { + const response = await api.post("challenge/admin/unvalidate", { + challengeId, + factionId, + teamId, + userId + }); + return response.data; + } catch (error) { + throw buildApiError(error); + } }; // Supprimer un challenge (ADMIN uniquement)