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
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m29s
This commit is contained in:
@@ -51,36 +51,29 @@ interface DashboardClientProps {
|
|||||||
evaluations: EvalRow[];
|
evaluations: EvalRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardClient({ evaluations }: DashboardClientProps) {
|
type ViewMode = "full" | "team" | "table";
|
||||||
const [list, setList] = useState(evaluations);
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<EvalRow | null>(null);
|
|
||||||
|
|
||||||
return (
|
function groupByTeam(list: EvalRow[]): Map<string | null, EvalRow[]> {
|
||||||
<div>
|
const map = new Map<string | null, EvalRow[]>();
|
||||||
<div className="mb-6 flex items-center justify-between">
|
for (const e of list) {
|
||||||
<h1 className="font-mono text-lg font-medium text-zinc-800 dark:text-zinc-100">Évaluations</h1>
|
const key = e.candidateTeam ?? null;
|
||||||
<Link
|
const arr = map.get(key) ?? [];
|
||||||
href="/evaluations/new"
|
arr.push(e);
|
||||||
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"
|
map.set(key, arr);
|
||||||
>
|
}
|
||||||
+ nouvelle
|
return map;
|
||||||
</Link>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
{list.length === 0 ? (
|
function EvalCard({
|
||||||
<div className="py-12 text-center text-zinc-600 dark:text-zinc-500">
|
e,
|
||||||
Aucune évaluation.{" "}
|
onDelete,
|
||||||
<Link href="/evaluations/new" className="text-cyan-600 dark:text-cyan-400 hover:underline">
|
}: {
|
||||||
Créer
|
e: EvalRow;
|
||||||
</Link>
|
onDelete: (ev: React.MouseEvent) => void;
|
||||||
</div>
|
}) {
|
||||||
) : (
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{list.map((e) => {
|
|
||||||
const radarData = buildRadarData(e);
|
const radarData = buildRadarData(e);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={e.id}
|
|
||||||
href={`/evaluations/${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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -120,11 +113,7 @@ export function DashboardClient({ evaluations }: DashboardClientProps) {
|
|||||||
<span className="font-mono text-xs text-cyan-600 dark:text-cyan-400 group-hover:underline">→ ouvrir</span>
|
<span className="font-mono text-xs text-cyan-600 dark:text-cyan-400 group-hover:underline">→ ouvrir</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(ev) => {
|
onClick={onDelete}
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
setDeleteTarget(e);
|
|
||||||
}}
|
|
||||||
className="ml-auto font-mono text-xs text-red-500 hover:text-red-400"
|
className="ml-auto font-mono text-xs text-red-500 hover:text-red-400"
|
||||||
title="Supprimer"
|
title="Supprimer"
|
||||||
>
|
>
|
||||||
@@ -133,7 +122,210 @@ export function DashboardClient({ evaluations }: DashboardClientProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</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 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"
|
||||||
|
>
|
||||||
|
+ nouvelle
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<div className="py-12 text-center text-zinc-600 dark:text-zinc-500">
|
||||||
|
Aucune évaluation.{" "}
|
||||||
|
<Link href="/evaluations/new" className="text-cyan-600 dark:text-cyan-400 hover:underline">
|
||||||
|
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) => (
|
||||||
|
<EvalCard
|
||||||
|
key={e.id}
|
||||||
|
e={e}
|
||||||
|
onDelete={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
setDeleteTarget(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user