Implement evaluation grouping by team and enhance DashboardClient with view mode selection. Add EvalCard component for improved evaluation display, including radar chart visualization and delete functionality.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m29s

This commit is contained in:
Julien Froidefond
2026-02-20 14:11:52 +01:00
parent aab8a192d4
commit 8073321b0f

View File

@@ -51,14 +51,128 @@ interface DashboardClientProps {
evaluations: EvalRow[];
}
type ViewMode = "full" | "team" | "table";
function groupByTeam(list: EvalRow[]): Map<string | null, EvalRow[]> {
const map = new Map<string | null, EvalRow[]>();
for (const e of list) {
const key = e.candidateTeam ?? null;
const arr = map.get(key) ?? [];
arr.push(e);
map.set(key, arr);
}
return map;
}
function EvalCard({
e,
onDelete,
}: {
e: EvalRow;
onDelete: (ev: React.MouseEvent) => void;
}) {
const radarData = buildRadarData(e);
return (
<Link
href={`/evaluations/${e.id}`}
className="group flex flex-col overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-sm dark:shadow-none hover:border-cyan-500/50 dark:hover:border-cyan-500/30 transition-colors"
>
<div className="flex flex-1 flex-col p-4">
<div className="mb-3 flex items-start justify-between gap-2">
<div className="min-w-0">
<h3 className="truncate font-medium text-zinc-800 dark:text-zinc-100">{e.candidateName}</h3>
<p className="truncate text-sm text-zinc-600 dark:text-zinc-400">
{e.candidateRole}
{e.candidateTeam && ` · ${e.candidateTeam}`}
</p>
</div>
<span
className={`shrink-0 font-mono text-xs px-1.5 py-0.5 rounded ${
e.status === "submitted" ? "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400" : "bg-amber-500/20 text-amber-600 dark:text-amber-400"
}`}
>
{e.status === "submitted" ? "ok" : "draft"}
</span>
</div>
<div className="mb-3 flex flex-wrap gap-x-3 gap-y-0.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
<span>{e.evaluatorName}</span>
<span>{format(new Date(e.evaluationDate), "yyyy-MM-dd")}</span>
<span>{e.template?.name ?? ""}</span>
</div>
<div className="mt-auto min-h-[7rem]">
{radarData.length > 0 ? (
<RadarChart data={radarData} compact />
) : (
<div className="flex h-28 items-center justify-center rounded bg-zinc-50 dark:bg-zinc-700/30 font-mono text-xs text-zinc-400 dark:text-zinc-500">
pas de scores
</div>
)}
</div>
</div>
<div className="flex border-t border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-700/30 px-4 py-2">
<span className="font-mono text-xs text-cyan-600 dark:text-cyan-400 group-hover:underline"> ouvrir</span>
<button
type="button"
onClick={onDelete}
className="ml-auto font-mono text-xs text-red-500 hover:text-red-400"
title="Supprimer"
>
supprimer
</button>
</div>
</Link>
);
}
export function DashboardClient({ evaluations }: DashboardClientProps) {
const [list, setList] = useState(evaluations);
const [deleteTarget, setDeleteTarget] = useState<EvalRow | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>("full");
const grouped = viewMode === "team" ? groupByTeam(list) : null;
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<h1 className="font-mono text-lg font-medium text-zinc-800 dark:text-zinc-100">Évaluations</h1>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-zinc-500 dark:text-zinc-400">Vue :</span>
<div className="inline-flex rounded border border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800/50 p-0.5">
<button
type="button"
onClick={() => setViewMode("full")}
className={`rounded px-2.5 py-1 font-mono text-xs transition-colors ${
viewMode === "full"
? "bg-white dark:bg-zinc-700 text-zinc-800 dark:text-zinc-100 shadow-sm"
: "text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
}`}
>
full
</button>
<button
type="button"
onClick={() => setViewMode("team")}
className={`rounded px-2.5 py-1 font-mono text-xs transition-colors ${
viewMode === "team"
? "bg-white dark:bg-zinc-700 text-zinc-800 dark:text-zinc-100 shadow-sm"
: "text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
}`}
>
team
</button>
<button
type="button"
onClick={() => setViewMode("table")}
className={`rounded px-2.5 py-1 font-mono text-xs transition-colors ${
viewMode === "table"
? "bg-white dark:bg-zinc-700 text-zinc-800 dark:text-zinc-100 shadow-sm"
: "text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
}`}
>
tableau
</button>
</div>
</div>
<Link
href="/evaluations/new"
className="rounded border border-cyan-500/50 bg-cyan-500/10 px-3 py-1.5 font-mono text-xs text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500/20 transition-colors"
@@ -74,66 +188,144 @@ export function DashboardClient({ evaluations }: DashboardClientProps) {
Créer
</Link>
</div>
) : viewMode === "table" ? (
<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">
<div className="overflow-x-auto">
<table className="w-full min-w-[640px]">
<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 font-medium text-zinc-600 dark:text-zinc-400">
Candidat
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Rôle
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Équipe
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Évaluateur
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Date
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Template
</th>
<th className="px-4 py-2.5 text-left font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Statut
</th>
<th className="px-4 py-2.5 text-right font-mono text-xs font-medium text-zinc-600 dark:text-zinc-400">
Actions
</th>
</tr>
</thead>
<tbody>
{list.map((e) => (
<tr
key={e.id}
className="border-b border-zinc-200 dark:border-zinc-600/50 last:border-0 hover:bg-zinc-50 dark:hover:bg-zinc-700/30 transition-colors"
>
<td className="px-4 py-2.5">
<Link
href={`/evaluations/${e.id}`}
className="text-sm font-medium text-cyan-600 dark:text-cyan-400 hover:underline"
>
{e.candidateName}
</Link>
</td>
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">
{e.candidateRole}
</td>
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">
{e.candidateTeam ?? "—"}
</td>
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">
{e.evaluatorName}
</td>
<td className="px-4 py-2.5 font-mono text-xs text-zinc-600 dark:text-zinc-400">
{format(new Date(e.evaluationDate), "yyyy-MM-dd")}
</td>
<td className="px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-400">
{e.template?.name ?? ""}
</td>
<td className="px-4 py-2.5">
<span
className={`inline-block font-mono text-xs px-1.5 py-0.5 rounded ${
e.status === "submitted"
? "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400"
: "bg-amber-500/20 text-amber-600 dark:text-amber-400"
}`}
>
{e.status === "submitted" ? "ok" : "draft"}
</span>
</td>
<td className="px-4 py-2.5 text-right">
<span className="inline-flex items-center gap-2 font-mono text-xs">
<Link
href={`/evaluations/${e.id}`}
className="text-cyan-600 dark:text-cyan-400 hover:underline"
>
ouvrir
</Link>
<button
type="button"
onClick={() => setDeleteTarget(e)}
className="text-red-500 hover:text-red-400"
title="Supprimer"
>
supprimer
</button>
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : viewMode === "team" && grouped ? (
<div className="space-y-6">
{Array.from(grouped.entries())
.sort(([a], [b]) => {
if (a == null) return 1;
if (b == null) return -1;
return a.localeCompare(b);
})
.map(([team, evals]) => (
<div key={team ?? "__none__"}>
<h2 className="mb-3 font-mono text-sm font-medium text-zinc-600 dark:text-zinc-400">
{team ?? "Sans équipe"}
</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{evals.map((e) => (
<EvalCard
key={e.id}
e={e}
onDelete={(ev) => {
ev.preventDefault();
ev.stopPropagation();
setDeleteTarget(e);
}}
/>
))}
</div>
</div>
))}
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{list.map((e) => {
const radarData = buildRadarData(e);
return (
<Link
key={e.id}
href={`/evaluations/${e.id}`}
className="group flex flex-col overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-sm dark:shadow-none hover:border-cyan-500/50 dark:hover:border-cyan-500/30 transition-colors"
>
<div className="flex flex-1 flex-col p-4">
<div className="mb-3 flex items-start justify-between gap-2">
<div className="min-w-0">
<h3 className="truncate font-medium text-zinc-800 dark:text-zinc-100">{e.candidateName}</h3>
<p className="truncate text-sm text-zinc-600 dark:text-zinc-400">
{e.candidateRole}
{e.candidateTeam && ` · ${e.candidateTeam}`}
</p>
</div>
<span
className={`shrink-0 font-mono text-xs px-1.5 py-0.5 rounded ${
e.status === "submitted" ? "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400" : "bg-amber-500/20 text-amber-600 dark:text-amber-400"
}`}
>
{e.status === "submitted" ? "ok" : "draft"}
</span>
</div>
<div className="mb-3 flex flex-wrap gap-x-3 gap-y-0.5 font-mono text-xs text-zinc-500 dark:text-zinc-400">
<span>{e.evaluatorName}</span>
<span>{format(new Date(e.evaluationDate), "yyyy-MM-dd")}</span>
<span>{e.template?.name ?? ""}</span>
</div>
<div className="mt-auto min-h-[7rem]">
{radarData.length > 0 ? (
<RadarChart data={radarData} compact />
) : (
<div className="flex h-28 items-center justify-center rounded bg-zinc-50 dark:bg-zinc-700/30 font-mono text-xs text-zinc-400 dark:text-zinc-500">
pas de scores
</div>
)}
</div>
</div>
<div className="flex border-t border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-700/30 px-4 py-2">
<span className="font-mono text-xs text-cyan-600 dark:text-cyan-400 group-hover:underline"> ouvrir</span>
<button
type="button"
onClick={(ev) => {
ev.preventDefault();
ev.stopPropagation();
setDeleteTarget(e);
}}
className="ml-auto font-mono text-xs text-red-500 hover:text-red-400"
title="Supprimer"
>
supprimer
</button>
</div>
</Link>
);
})}
{list.map((e) => (
<EvalCard
key={e.id}
e={e}
onDelete={(ev) => {
ev.preventDefault();
ev.stopPropagation();
setDeleteTarget(e);
}}
/>
))}
</div>
)}