Demandes d'adhésion : actions selon le statut, double approbation impossible

Une demande approuvée gardait ses boutons « Approuver & créer » et « Rejeter »,
alors que l'adhérent et son compte existaient déjà. La ligne n'affiche plus
que le lien vers la fiche créée (retrouvée par l'e-mail) ; une demande
nouvelle garde ses deux actions, une demande rejetée peut encore être
approuvée.

Côté serveur, approveMembershipRequest refuse une demande déjà approuvée et
setRequestStatus ne réécrit plus le statut d'une demande approuvée : un
double clic ou un onglet resté ouvert ne peut plus créer un second compte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
This commit is contained in:
Claude committed 2026-09-06 09:22:25 +00:00
1 parent 23d1d14a49
commit 86e7e28020
2 files changed
+50 -9

No files matched your search

+6
View File
@@ -1163,6 +1163,9 @@ export async function approveMembershipRequest(formData: FormData): Promise<Crea
const [req] = await db.select().from(membershipRequests).where(eq(membershipRequests.id, id));
if (!req) throw new Error("Demande introuvable.");
// Garde serveur : un double clic ou un onglet resté ouvert ne doit jamais
// créer un second compte pour la même demande.
if (req.status === "approved") throw new Error("Cette demande a déjà été approuvée : l'adhérent existe.");
const email = (req.email ?? "").trim().toLowerCase();
if (!email) {
@@ -1206,6 +1209,9 @@ 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 produit un adhérent : son statut ne se réécrit plus.
const [current] = await db.select({ status: membershipRequests.status }).from(membershipRequests).where(eq(membershipRequests.id, id));
if (!current || current.status === "approved") return;
await db
.update(membershipRequests)
.set({ status: status as "new" | "approved" | "rejected" })
+44 -9
View File
@@ -1,8 +1,9 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { desc } from "drizzle-orm";
import { desc, inArray } from "drizzle-orm";
import { getSession } from "@/lib/session";
import { db } from "@/db";
import { contactMessages, membershipRequests } from "@/db/schema";
import { contactMessages, members, membershipRequests } from "@/db/schema";
import { can } from "@/lib/rbac";
import { setContactStatus, setRequestStatus } from "../actions";
import { ApproveRequestForm } from "./ApproveRequestForm";
@@ -52,6 +53,21 @@ export default async function DemandesPage() {
db.select().from(contactMessages).orderBy(desc(contactMessages.createdAt)),
]);
// Demandes approuvées : on retrouve la fiche créée par son e-mail, pour
// proposer un lien vers l'adhérent plutôt que des boutons déjà consommés.
const approvedEmails = requests
.filter((r) => r.status === "approved" && r.email)
.map((r) => (r.email ?? "").trim().toLowerCase())
.filter(Boolean);
const memberByEmail = new Map<string, number>();
if (approvedEmails.length > 0) {
const rows = await db
.select({ id: members.id, email: members.email })
.from(members)
.where(inArray(members.email, approvedEmails));
for (const row of rows) memberByEmail.set(row.email.trim().toLowerCase(), row.id);
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 26 }}>
{/* ---- Demandes d'adhésion ---- */}
@@ -72,13 +88,32 @@ export default async function DemandesPage() {
</div>
</div>
<StatusPill map={REQ_STATUS} status={r.status} />
<div style={{ display: "flex", gap: 8 }}>
<ApproveRequestForm requestId={r.id} />
<form action={setRequestStatus}>
<input type="hidden" name="id" value={r.id} />
<input type="hidden" name="status" value="rejected" />
<ActionBtn color="#d8472b">Rejeter</ActionBtn>
</form>
{/* Les actions dépendent du statut : une demande approuvée a déjà
produit son adhérent, on ne propose plus que d'ouvrir sa fiche. */}
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{r.status === "approved" ? (
(() => {
const memberId = memberByEmail.get((r.email ?? "").trim().toLowerCase());
return memberId ? (
<Link href={`/backend/adherents/${memberId}`} style={{ color: "#2C6FB3", fontWeight: 700, fontSize: 12.5, textDecoration: "none", whiteSpace: "nowrap" }}>
Ouvrir la fiche adhérent →
</Link>
) : (
<span style={{ fontSize: 12.5, color: "#a99c82" }}>Adhérent créé</span>
);
})()
) : (
<>
<ApproveRequestForm requestId={r.id} />
{r.status === "new" && (
<form action={setRequestStatus}>
<input type="hidden" name="id" value={r.id} />
<input type="hidden" name="status" value="rejected" />
<ActionBtn color="#d8472b">Rejeter</ActionBtn>
</form>
)}
</>
)}
</div>
</div>
{r.message && (