Author SHA1 Message Date
Claude 86e7e28020 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
2026-09-06 09:22:25 +00:00
2 changed files with 50 additions and 9 deletions

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)); const [req] = await db.select().from(membershipRequests).where(eq(membershipRequests.id, id));
if (!req) throw new Error("Demande introuvable."); 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(); const email = (req.email ?? "").trim().toLowerCase();
if (!email) { if (!email) {
@@ -1206,6 +1209,9 @@ export async function setRequestStatus(formData: FormData) {
const id = Number(formData.get("id")); const id = Number(formData.get("id"));
const status = String(formData.get("status") ?? ""); const status = String(formData.get("status") ?? "");
if (!id || !["new", "approved", "rejected"].includes(status)) return; 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 await db
.update(membershipRequests) .update(membershipRequests)
.set({ status: status as "new" | "approved" | "rejected" }) .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 { redirect } from "next/navigation";
import { desc } from "drizzle-orm"; import { desc, inArray } from "drizzle-orm";
import { getSession } from "@/lib/session"; import { getSession } from "@/lib/session";
import { db } from "@/db"; import { db } from "@/db";
import { contactMessages, membershipRequests } from "@/db/schema"; import { contactMessages, members, membershipRequests } from "@/db/schema";
import { can } from "@/lib/rbac"; import { can } from "@/lib/rbac";
import { setContactStatus, setRequestStatus } from "../actions"; import { setContactStatus, setRequestStatus } from "../actions";
import { ApproveRequestForm } from "./ApproveRequestForm"; import { ApproveRequestForm } from "./ApproveRequestForm";
@@ -52,6 +53,21 @@ export default async function DemandesPage() {
db.select().from(contactMessages).orderBy(desc(contactMessages.createdAt)), 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 ( return (
<div style={{ display: "flex", flexDirection: "column", gap: 26 }}> <div style={{ display: "flex", flexDirection: "column", gap: 26 }}>
{/* ---- Demandes d'adhésion ---- */} {/* ---- Demandes d'adhésion ---- */}
@@ -72,13 +88,32 @@ export default async function DemandesPage() {
</div> </div>
</div> </div>
<StatusPill map={REQ_STATUS} status={r.status} /> <StatusPill map={REQ_STATUS} status={r.status} />
<div style={{ display: "flex", gap: 8 }}> {/* Les actions dépendent du statut : une demande approuvée a déjà
<ApproveRequestForm requestId={r.id} /> produit son adhérent, on ne propose plus que d'ouvrir sa fiche. */}
<form action={setRequestStatus}> <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input type="hidden" name="id" value={r.id} /> {r.status === "approved" ? (
<input type="hidden" name="status" value="rejected" /> (() => {
<ActionBtn color="#d8472b">Rejeter</ActionBtn> const memberId = memberByEmail.get((r.email ?? "").trim().toLowerCase());
</form> 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>
</div> </div>
{r.message && ( {r.message && (