From dd16e05b47e015fa30e58a6e069ed0507eb5ee92 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 07:48:59 +0000 Subject: [PATCH] =?UTF-8?q?Demandes=20&=20messages=20:=20boutons=20d'actio?= =?UTF-8?q?n=20selon=20l'=C3=A9tat=20de=20la=20ligne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une demande approuvée gardait ses boutons « Approuver & créer » et « Rejeter », un message lu son « Marquer lu », un message archivé son « Archiver » : rien n'indiquait que l'action avait déjà été faite. Chaque ligne ne propose plus que les transitions qui ont du sens : - demande nouvelle → Approuver & créer / Rejeter - demande rejetée → Rouvrir - demande approuvée → aucun bouton (le compte existe) - message nouveau → Marquer lu / Archiver - message lu → Archiver - message archivé → Désarchiver Le masquage du bouton d'approbation vit dans `ApproveRequestForm` et non dans la page : après l'approbation, `router.refresh()` repasse la demande en « approuvée », et un test côté serveur démonterait le bloc d'identifiants à usage unique avant qu'il ait été lu. Deux gardes côté serveur couvrent l'onglet resté ouvert sur l'ancien état : `approveMembershipRequest` refuse une demande déjà traitée, et `setRequestStatus` ne touche plus une demande approuvée. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MrfrukBXnHtMwq5iT7DwAM --- src/app/backend/actions.ts | 17 +++++- .../backend/demandes/ApproveRequestForm.tsx | 15 ++++- src/app/backend/demandes/page.tsx | 57 +++++++++++++------ 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/app/backend/actions.ts b/src/app/backend/actions.ts index 96f5e49..a6581e6 100644 --- a/src/app/backend/actions.ts +++ b/src/app/backend/actions.ts @@ -4,7 +4,7 @@ import { randomInt } from "node:crypto"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, ne, sql } from "drizzle-orm"; import bcrypt from "bcryptjs"; import { signOut } from "@/auth"; import { getSession } from "@/lib/session"; @@ -1133,6 +1133,17 @@ export async function approveMembershipRequest( const [req] = await db.select().from(membershipRequests).where(eq(membershipRequests.id, id)); if (!req) throw new Error("Demande introuvable."); + // Le bouton disparaît dès qu'une demande est traitée, mais un onglet resté + // ouvert peut encore poster : on refuse ici plutôt que de créer un doublon. + if (req.status !== "new") { + return { + error: + req.status === "approved" + ? "Cette demande a déjà été approuvée." + : "Cette demande a été rejetée : rouvrez-la avant de l'approuver.", + }; + } + const email = (req.email ?? "").trim().toLowerCase(); if (!email) { return { @@ -1178,10 +1189,12 @@ export async function setRequestStatus(formData: FormData) { const id = Number(formData.get("id")); const status = String(formData.get("status") ?? ""); if (!id || !["new", "approved", "rejected"].includes(status)) return; + // Une demande approuvée a créé un compte : elle ne se rejoue pas depuis un + // onglet resté ouvert sur l'ancien état. await db .update(membershipRequests) .set({ status: status as "new" | "approved" | "rejected" }) - .where(eq(membershipRequests.id, id)); + .where(and(eq(membershipRequests.id, id), ne(membershipRequests.status, "approved"))); revalidatePath("/backend/demandes"); revalidatePath("/backend"); } diff --git a/src/app/backend/demandes/ApproveRequestForm.tsx b/src/app/backend/demandes/ApproveRequestForm.tsx index 0d00ce7..59af171 100644 --- a/src/app/backend/demandes/ApproveRequestForm.tsx +++ b/src/app/backend/demandes/ApproveRequestForm.tsx @@ -10,8 +10,19 @@ import { approveMembershipRequest, type CreatedMemberAccount } from "../actions" * Approuve une demande et affiche une seule fois les identifiants du compte * créé. On reste sur la page : une redirection ferait perdre le mot de passe, * qui n'est conservé nulle part. + * + * Le bouton ne s'affiche que pour une demande encore à traiter, mais c'est ce + * composant — et non la page — qui en décide : après l'approbation, + * `router.refresh()` renvoie la demande en « approuvée », et un masquage côté + * serveur démonterait le bloc d'identifiants avant qu'il ait été lu. */ -export function ApproveRequestForm({ requestId }: { requestId: number }) { +export function ApproveRequestForm({ + requestId, + status, +}: { + requestId: number; + status: string; +}) { const [created, setCreated] = useState(null); const [pending, setPending] = useState(false); const [error, setError] = useState(null); @@ -28,6 +39,8 @@ export function ApproveRequestForm({ requestId }: { requestId: number }) { ); } + if (status !== "new") return null; + return (
{ diff --git a/src/app/backend/demandes/page.tsx b/src/app/backend/demandes/page.tsx index c1be110..89780ee 100644 --- a/src/app/backend/demandes/page.tsx +++ b/src/app/backend/demandes/page.tsx @@ -72,13 +72,25 @@ export default async function DemandesPage() { + {/* Une demande traitée ne garde que la sortie qui a du sens : + « Rejeter » disparaît une fois l'adhérent créé, et un rejet + reste réversible tant qu'aucun compte n'existe. */}
- - - - - Rejeter - + + {r.status === "new" && ( +
+ + + Rejeter +
+ )} + {r.status === "rejected" && ( +
+ + + Rouvrir +
+ )}
{r.message && ( @@ -109,17 +121,30 @@ export default async function DemandesPage() { + {/* Chaque bouton fait avancer le message d'un cran : on ne + propose plus « Marquer lu » sur un message déjà lu ni + « Archiver » sur un message archivé. */}
-
- - - Marquer lu -
-
- - - Archiver -
+ {m.status === "new" && ( +
+ + + Marquer lu +
+ )} + {m.status !== "archived" ? ( +
+ + + Archiver +
+ ) : ( +
+ + + Désarchiver +
+ )}

{m.message}