175 lines
7.1 KiB
TypeScript
175 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { format } from "date-fns";
|
|
import { setUserRole, deleteUser } from "@/actions/admin";
|
|
import { useSession } from "next-auth/react";
|
|
import { ConfirmModal } from "@/components/ConfirmModal";
|
|
|
|
interface Template {
|
|
id: string;
|
|
name: string;
|
|
dimensions: { id: string; title: string; orderIndex: number }[];
|
|
}
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
role: string;
|
|
createdAt: Date | string;
|
|
}
|
|
|
|
interface AdminClientProps {
|
|
templates: Template[];
|
|
users: User[];
|
|
}
|
|
|
|
export function AdminClient({ templates, users: initialUsers }: AdminClientProps) {
|
|
const { data: session } = useSession();
|
|
const [users, setUsers] = useState(initialUsers);
|
|
const [updatingId, setUpdatingId] = useState<string | null>(null);
|
|
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
|
|
|
async function handleSetRole(userId: string, role: "admin" | "evaluator") {
|
|
setUpdatingId(userId);
|
|
try {
|
|
const result = await setUserRole(userId, role);
|
|
if (result.success) {
|
|
setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, role } : u)));
|
|
} else {
|
|
alert(result.error);
|
|
}
|
|
} finally {
|
|
setUpdatingId(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="mb-6 font-mono text-lg font-medium text-zinc-800 dark:text-zinc-200">Admin</h1>
|
|
|
|
<section>
|
|
<h2 className="mb-4 font-mono text-xs text-zinc-600 dark:text-zinc-500">Users</h2>
|
|
<div className="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-sm dark:shadow-none">
|
|
<table className="min-w-full">
|
|
<thead>
|
|
<tr className="border-b border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-700/80">
|
|
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Email</th>
|
|
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Nom</th>
|
|
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Rôle</th>
|
|
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Créé le</th>
|
|
<th className="px-4 py-2.5 text-right font-mono text-xs text-zinc-600 dark:text-zinc-400">—</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => (
|
|
<tr key={u.id} className="border-b border-zinc-200 dark:border-zinc-600/50 last:border-0">
|
|
<td className="px-4 py-2.5 text-sm text-zinc-800 dark:text-zinc-200">{u.email}</td>
|
|
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">{u.name ?? "—"}</td>
|
|
<td className="px-4 py-2.5">
|
|
<span
|
|
className={`font-mono text-xs px-1.5 py-0.5 rounded ${
|
|
u.role === "admin" ? "bg-cyan-500/20 text-cyan-600 dark:text-cyan-400" : "bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400"
|
|
}`}
|
|
>
|
|
{u.role}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-2.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
|
|
{format(new Date(u.createdAt), "yyyy-MM-dd HH:mm")}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-right">
|
|
<span className="inline-flex items-center gap-2">
|
|
{u.role === "admin" ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSetRole(u.id, "evaluator")}
|
|
disabled={updatingId === u.id}
|
|
className="font-mono text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 disabled:opacity-50"
|
|
title="Rétrograder en évaluateur"
|
|
>
|
|
{updatingId === u.id ? "..." : "rétrograder"}
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSetRole(u.id, "admin")}
|
|
disabled={updatingId === u.id}
|
|
className="font-mono text-xs text-cyan-600 dark:text-cyan-400 hover:text-cyan-500 disabled:opacity-50"
|
|
title="Promouvoir admin"
|
|
>
|
|
{updatingId === u.id ? "..." : "promouvoir admin"}
|
|
</button>
|
|
)}
|
|
{u.id !== session?.user?.id && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setDeleteTarget(u)}
|
|
className="font-mono text-xs text-red-500 hover:text-red-400"
|
|
title="Supprimer"
|
|
>
|
|
supprimer
|
|
</button>
|
|
)}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<ConfirmModal
|
|
isOpen={!!deleteTarget}
|
|
title="Supprimer l'utilisateur"
|
|
message={
|
|
deleteTarget
|
|
? `Supprimer ${deleteTarget.name || deleteTarget.email} ? Les évaluations créées par cet utilisateur resteront (évaluateur mis à null).`
|
|
: ""
|
|
}
|
|
confirmLabel="Supprimer"
|
|
cancelLabel="Annuler"
|
|
variant="danger"
|
|
onConfirm={async () => {
|
|
if (!deleteTarget) return;
|
|
const result = await deleteUser(deleteTarget.id);
|
|
if (result.success) {
|
|
setUsers((prev) => prev.filter((u) => u.id !== deleteTarget.id));
|
|
setDeleteTarget(null);
|
|
} else {
|
|
alert(result.error);
|
|
}
|
|
}}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
|
|
<section className="mt-8">
|
|
<h2 className="mb-4 font-mono text-xs text-zinc-600 dark:text-zinc-500">Modèles</h2>
|
|
<div className="space-y-3">
|
|
{templates.map((t) => (
|
|
<div
|
|
key={t.id}
|
|
className="rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 p-4 shadow-sm dark:shadow-none"
|
|
>
|
|
<h3 className="font-medium text-zinc-800 dark:text-zinc-200">{t.name}</h3>
|
|
<p className="mt-1 font-mono text-xs text-zinc-600 dark:text-zinc-500">
|
|
{t.dimensions.length} dim.
|
|
</p>
|
|
<ul className="mt-2 space-y-0.5 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
|
{t.dimensions.slice(0, 5).map((d) => (
|
|
<li key={d.id}>• {d.title}</li>
|
|
))}
|
|
{t.dimensions.length > 5 && (
|
|
<li className="text-zinc-600 dark:text-zinc-500">+{t.dimensions.length - 5}</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|