355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
export default function AdminUsers({ users }: { users: any[] }) {
|
|
const router = useRouter();
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [filter, setFilter] = useState<"ALL" | "MUAY" | "YOGA">("ALL");
|
|
|
|
const filtered = users.filter((u) => {
|
|
if (filter === "MUAY") return u.isMuayThai;
|
|
if (filter === "YOGA") return u.isYoga;
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-bold">
|
|
👥 Adhérents ({filtered.length})
|
|
</h2>
|
|
<button
|
|
onClick={() => setShowCreate(!showCreate)}
|
|
className="btn-primary !px-4 !py-2 text-sm"
|
|
>
|
|
{showCreate ? "Fermer" : "+ Nouveau compte"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{(
|
|
[
|
|
["ALL", "Tous"],
|
|
["MUAY", "🥊 Muay Thai"],
|
|
["YOGA", "🧘 Yoga"],
|
|
] as const
|
|
).map(([val, label]) => (
|
|
<button
|
|
key={val}
|
|
onClick={() => setFilter(val)}
|
|
className={`rounded-xl px-4 py-2 text-sm font-semibold transition-colors ${
|
|
filter === val
|
|
? "bg-primary-600 text-white"
|
|
: "bg-white/5 text-white/60"
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{showCreate && <CreateUserForm onDone={() => { setShowCreate(false); router.refresh(); }} />}
|
|
|
|
<div className="space-y-3">
|
|
{filtered.length === 0 && (
|
|
<p className="card text-center text-sm text-white/50">
|
|
Aucun adhérent.
|
|
</p>
|
|
)}
|
|
{filtered.map((user) => (
|
|
<MemberCard key={user.id} user={user} onUpdate={() => router.refresh()} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CreateUserForm({ onDone }: { onDone: () => void }) {
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [role, setRole] = useState("MEMBER");
|
|
const [isMuayThai, setIsMuayThai] = useState(true);
|
|
const [isYoga, setIsYoga] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
setError("");
|
|
const res = await fetch("/api/admin/users", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name,
|
|
email,
|
|
password,
|
|
role,
|
|
isMuayThai: role === "MEMBER" ? isMuayThai : false,
|
|
isYoga: role === "MEMBER" ? isYoga : false,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
setSaving(false);
|
|
if (!res.ok) {
|
|
setError(data.error || "Erreur");
|
|
return;
|
|
}
|
|
onDone();
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="card space-y-4 border-primary-500/30">
|
|
<h3 className="font-semibold">✨ Créer un compte</h3>
|
|
<div>
|
|
<label className="label">Nom complet</label>
|
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Jean Dupont" required />
|
|
</div>
|
|
<div>
|
|
<label className="label">Email</label>
|
|
<input type="email" className="input" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="jean@gmail.com" required />
|
|
</div>
|
|
<div>
|
|
<label className="label">Mot de passe initial</label>
|
|
<input className="input" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Mot de passe temporaire" required />
|
|
</div>
|
|
<div>
|
|
<label className="label">Rôle</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setRole("MEMBER")}
|
|
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
|
|
role === "MEMBER"
|
|
? "bg-primary-600 text-white"
|
|
: "bg-white/5 text-white/60"
|
|
}`}
|
|
>
|
|
👤 Membre
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setRole("ADMIN")}
|
|
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
|
|
role === "ADMIN"
|
|
? "bg-amber-600 text-white"
|
|
: "bg-white/5 text-white/60"
|
|
}`}
|
|
>
|
|
⭐ Admin
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{role === "MEMBER" && (
|
|
<div className="flex gap-3">
|
|
<Checkbox label="🥊 Muay Thai" checked={isMuayThai} onChange={setIsMuayThai} />
|
|
<Checkbox label="🧘 Yoga" checked={isYoga} onChange={setIsYoga} />
|
|
</div>
|
|
)}
|
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
<button type="submit" className="btn-primary w-full" disabled={saving}>
|
|
{saving ? "Création..." : "✅ Créer le compte"}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function Checkbox({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
|
|
return (
|
|
<label className="flex cursor-pointer items-center gap-2 rounded-xl bg-white/5 px-4 py-2 text-sm">
|
|
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="h-4 w-4 accent-primary-600" />
|
|
{label}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function MemberCard({ user, onUpdate }: { user: any; onUpdate: () => void }) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const [editName, setEditName] = useState(user.name);
|
|
const [editPassword, setEditPassword] = useState("");
|
|
const [editRole, setEditRole] = useState(user.role);
|
|
const [error, setError] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const certStatus = user.certificates?.find((c: any) => c.status !== "REJECTED")?.status;
|
|
|
|
async function updateUser(data: any) {
|
|
setSaving(true);
|
|
setError("");
|
|
const res = await fetch(`/api/admin/users/${user.id}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
setSaving(false);
|
|
if (res.ok) {
|
|
setEditPassword("");
|
|
onUpdate();
|
|
} else {
|
|
const d = await res.json().catch(() => ({}));
|
|
setError(d.error || "Erreur lors de la mise à jour");
|
|
}
|
|
}
|
|
|
|
async function deleteUser() {
|
|
if (!confirm(`Supprimer le compte de ${user.name} ?`)) return;
|
|
const res = await fetch(`/api/admin/users/${user.id}`, {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ id: user.id }),
|
|
});
|
|
if (res.ok) onUpdate();
|
|
}
|
|
|
|
const isAdmin = user.role === "ADMIN";
|
|
const canDelete = !isAdmin;
|
|
|
|
return (
|
|
<div className="card">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={`flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold ${
|
|
isAdmin
|
|
? "bg-amber-500/20 text-amber-300"
|
|
: "bg-primary-600/20 text-primary-300"
|
|
}`}
|
|
>
|
|
{user.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold">{user.name}</p>
|
|
<p className="text-xs text-white/50">{user.email}</p>
|
|
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
{isAdmin && (
|
|
<span className="rounded-full bg-amber-500/20 px-2 py-0.5 text-xs text-amber-300">
|
|
⭐ Admin
|
|
</span>
|
|
)}
|
|
{user.isMuayThai && (
|
|
<span className="rounded-full bg-primary-600/20 px-2 py-0.5 text-xs text-primary-300">
|
|
🥊 Muay Thai
|
|
</span>
|
|
)}
|
|
{user.isYoga && (
|
|
<span className="rounded-full bg-emerald-600/20 px-2 py-0.5 text-xs text-emerald-300">
|
|
🧘 Yoga
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setExpanded(!expanded)}
|
|
className="rounded-xl bg-white/5 px-3 py-2 text-sm text-white/60"
|
|
>
|
|
{expanded ? "✕" : "⚙️"}
|
|
</button>
|
|
</div>
|
|
|
|
{certStatus && (
|
|
<div className="mt-3 flex items-center justify-between rounded-xl bg-white/5 px-3 py-2 text-xs">
|
|
<span className="text-white/60">Certificat médical</span>
|
|
<CertBadge status={certStatus} />
|
|
</div>
|
|
)}
|
|
|
|
{user.yoga && (
|
|
<div className="mt-2 flex items-center justify-between rounded-xl bg-white/5 px-3 py-2 text-xs">
|
|
<span className="text-white/60">Séances yoga restantes</span>
|
|
<span className="font-bold text-emerald-300">
|
|
{user.yoga.sessionsRemaining}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{expanded && (
|
|
<div className="mt-4 space-y-3 border-t border-white/10 pt-4">
|
|
<h4 className="text-sm font-semibold">
|
|
⚙️ Configuration — {isAdmin ? "compte admin" : "compte membre"}
|
|
</h4>
|
|
|
|
<div>
|
|
<label className="label">Nom</label>
|
|
<input className="input" value={editName} onChange={(e) => setEditName(e.target.value)} />
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Rôle</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditRole("MEMBER")}
|
|
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
|
|
editRole === "MEMBER"
|
|
? "bg-primary-600 text-white"
|
|
: "bg-white/5 text-white/60"
|
|
}`}
|
|
>
|
|
👤 Membre
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditRole("ADMIN")}
|
|
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
|
|
editRole === "ADMIN"
|
|
? "bg-amber-600 text-white"
|
|
: "bg-white/5 text-white/60"
|
|
}`}
|
|
>
|
|
⭐ Admin
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Réinitialiser le mot de passe</label>
|
|
<input
|
|
className="input"
|
|
value={editPassword}
|
|
onChange={(e) => setEditPassword(e.target.value)}
|
|
placeholder="Nouveau mot de passe (laisser vide pour ne pas changer)"
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
const data: any = { name: editName, role: editRole };
|
|
if (editPassword) data.password = editPassword;
|
|
updateUser(data);
|
|
}}
|
|
className="btn-primary flex-1 !py-2 text-sm"
|
|
disabled={saving}
|
|
>
|
|
{saving ? "Enregistrement..." : "💾 Enregistrer"}
|
|
</button>
|
|
{canDelete && (
|
|
<button onClick={deleteUser} className="btn-danger" title="Supprimer">
|
|
🗑️
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CertBadge({ status }: { status: string }) {
|
|
switch (status) {
|
|
case "VALIDATED":
|
|
return <span className="font-bold text-emerald-300">✓ Validé</span>;
|
|
case "PENDING":
|
|
return <span className="font-bold text-amber-300">⏳ En attente</span>;
|
|
case "REJECTED":
|
|
return <span className="font-bold text-red-300">✗ Rejeté</span>;
|
|
default:
|
|
return null;
|
|
}
|
|
} |