Add candidateTeam field to evaluations; update related components and API endpoints for consistency

This commit is contained in:
Julien Froidefond
2026-02-20 09:22:12 +01:00
parent f0c5d768db
commit 9fcceb2649
11 changed files with 79 additions and 38 deletions

Binary file not shown.

View File

@@ -46,6 +46,7 @@ model Evaluation {
id String @id @default(cuid()) id String @id @default(cuid())
candidateName String candidateName String
candidateRole String candidateRole String
candidateTeam String? // équipe du candidat
evaluatorName String evaluatorName String
evaluationDate DateTime evaluationDate DateTime
templateId String templateId String

View File

@@ -175,9 +175,9 @@ async function main() {
}); });
const candidates = [ const candidates = [
{ name: "Alice Chen", role: "Senior ML Engineer", evaluator: "Jean Dupont" }, { name: "Alice Chen", role: "Senior ML Engineer", team: "Cars Front", evaluator: "Jean Dupont" },
{ name: "Bob Martin", role: "Data Scientist", evaluator: "Marie Curie" }, { name: "Bob Martin", role: "Data Scientist", team: "Cars Front", evaluator: "Marie Curie" },
{ name: "Carol White", role: "AI Product Manager", evaluator: "Jean Dupont" }, { name: "Carol White", role: "AI Product Manager", team: "Cars Data", evaluator: "Jean Dupont" },
]; ];
for (let i = 0; i < candidates.length; i++) { for (let i = 0; i < candidates.length; i++) {
@@ -186,6 +186,7 @@ async function main() {
data: { data: {
candidateName: c.name, candidateName: c.name,
candidateRole: c.role, candidateRole: c.role,
candidateTeam: c.team,
evaluatorName: c.evaluator, evaluatorName: c.evaluator,
evaluationDate: new Date(2025, 1, 15 + i), evaluationDate: new Date(2025, 1, 15 + i),
templateId: template.id, templateId: template.id,

View File

@@ -80,7 +80,7 @@ export async function PUT(
const { id } = await params; const { id } = await params;
const body = await req.json(); const body = await req.json();
const { candidateName, candidateRole, evaluatorName, evaluationDate, status, findings, recommendations, dimensionScores } = body; const { candidateName, candidateRole, candidateTeam, evaluatorName, evaluationDate, status, findings, recommendations, dimensionScores } = body;
const existing = await prisma.evaluation.findUnique({ where: { id } }); const existing = await prisma.evaluation.findUnique({ where: { id } });
if (!existing) { if (!existing) {
@@ -90,8 +90,12 @@ export async function PUT(
const updateData: Record<string, unknown> = {}; const updateData: Record<string, unknown> = {};
if (candidateName != null) updateData.candidateName = candidateName; if (candidateName != null) updateData.candidateName = candidateName;
if (candidateRole != null) updateData.candidateRole = candidateRole; if (candidateRole != null) updateData.candidateRole = candidateRole;
if (candidateTeam !== undefined) updateData.candidateTeam = candidateTeam || null;
if (evaluatorName != null) updateData.evaluatorName = evaluatorName; if (evaluatorName != null) updateData.evaluatorName = evaluatorName;
if (evaluationDate != null) updateData.evaluationDate = new Date(evaluationDate); if (evaluationDate != null) {
const d = new Date(evaluationDate);
if (!isNaN(d.getTime())) updateData.evaluationDate = d;
}
if (status != null) updateData.status = status; if (status != null) updateData.status = status;
if (findings != null) updateData.findings = findings; if (findings != null) updateData.findings = findings;
if (recommendations != null) updateData.recommendations = recommendations; if (recommendations != null) updateData.recommendations = recommendations;
@@ -106,14 +110,12 @@ export async function PUT(
}); });
} }
const evaluation = await prisma.evaluation.update({ if (Object.keys(updateData).length > 0) {
where: { id }, await prisma.evaluation.update({
data: updateData, where: { id },
include: { data: updateData as Record<string, unknown>,
template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } }, });
dimensionScores: { include: { dimension: true } }, }
},
});
if (dimensionScores && Array.isArray(dimensionScores)) { if (dimensionScores && Array.isArray(dimensionScores)) {
for (const ds of dimensionScores) { for (const ds of dimensionScores) {
@@ -157,7 +159,8 @@ export async function PUT(
return NextResponse.json(updated); return NextResponse.json(updated);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return NextResponse.json({ error: "Failed to update evaluation" }, { status: 500 }); const msg = e instanceof Error ? e.message : "Failed to update evaluation";
return NextResponse.json({ error: msg }, { status: 500 });
} }
} }

View File

@@ -29,7 +29,7 @@ export async function GET(req: NextRequest) {
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const { candidateName, candidateRole, evaluatorName, evaluationDate, templateId } = body; const { candidateName, candidateRole, candidateTeam, evaluatorName, evaluationDate, templateId } = body;
if (!candidateName || !candidateRole || !evaluatorName || !evaluationDate || !templateId) { if (!candidateName || !candidateRole || !evaluatorName || !evaluationDate || !templateId) {
return NextResponse.json( return NextResponse.json(
@@ -50,6 +50,7 @@ export async function POST(req: NextRequest) {
data: { data: {
candidateName, candidateName,
candidateRole, candidateRole,
candidateTeam: candidateTeam || null,
evaluatorName, evaluatorName,
evaluationDate: new Date(evaluationDate), evaluationDate: new Date(evaluationDate),
templateId, templateId,

View File

@@ -28,7 +28,8 @@ export async function GET(req: NextRequest) {
doc.setFontSize(18); doc.setFontSize(18);
doc.text("Évaluation Maturité IA Gen", 14, 20); doc.text("Évaluation Maturité IA Gen", 14, 20);
doc.setFontSize(10); doc.setFontSize(10);
doc.text(`Candidat : ${evaluation.candidateName} | Rôle : ${evaluation.candidateRole}`, 14, 28); const teamStr = evaluation.candidateTeam ? ` | Équipe : ${evaluation.candidateTeam}` : "";
doc.text(`Candidat : ${evaluation.candidateName} | Rôle : ${evaluation.candidateRole}${teamStr}`, 14, 28);
doc.text(`Évaluateur : ${evaluation.evaluatorName} | Date : ${format(evaluation.evaluationDate, "yyyy-MM-dd")}`, 14, 34); doc.text(`Évaluateur : ${evaluation.evaluatorName} | Date : ${format(evaluation.evaluationDate, "yyyy-MM-dd")}`, 14, 34);
doc.text(`Modèle : ${evaluation.template?.name ?? ""} | Statut : ${evaluation.status === "submitted" ? "Soumise" : "Brouillon"}`, 14, 40); doc.text(`Modèle : ${evaluation.template?.name ?? ""} | Statut : ${evaluation.status === "submitted" ? "Soumise" : "Brouillon"}`, 14, 40);

View File

@@ -32,6 +32,7 @@ interface Evaluation {
id: string; id: string;
candidateName: string; candidateName: string;
candidateRole: string; candidateRole: string;
candidateTeam?: string | null;
evaluatorName: string; evaluatorName: string;
evaluationDate: string; evaluationDate: string;
templateId: string; templateId: string;
@@ -70,10 +71,10 @@ export default function EvaluationDetailPage() {
const tmpl = templatesData.find((t: { id: string }) => t.id === evalData.templateId); const tmpl = templatesData.find((t: { id: string }) => t.id === evalData.templateId);
if (tmpl?.dimensions?.length) { if (tmpl?.dimensions?.length) {
const dimMap = new Map(tmpl.dimensions.map((d: { id: string }) => [d.id, d])); const dimMap = new Map(tmpl.dimensions.map((d: { id: string }) => [d.id, d]));
evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string }) => ({ evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string; suggestedQuestions?: string | null }) => ({
...d, ...d,
suggestedQuestions: d.suggestedQuestions ?? dimMap.get(d.id)?.suggestedQuestions, suggestedQuestions: d.suggestedQuestions ?? dimMap.get(d.id)?.suggestedQuestions,
})); }));
} }
} }
} catch { } catch {
@@ -117,26 +118,32 @@ export default function EvaluationDetailPage() {
const scores = e.dimensionScores.map((ds) => const scores = e.dimensionScores.map((ds) =>
ds.dimensionId === dimensionId ? { ...ds, ...data } : ds ds.dimensionId === dimensionId ? { ...ds, ...data } : ds
); );
return { ...e, dimensionScores: scores }; const next = { ...e, dimensionScores: scores };
if (data.score !== undefined) {
setTimeout(() => handleSave(next, { skipRefresh: true }), 0);
}
return next;
}); });
}; };
const handleSave = async () => { const handleSave = async (evalOverride?: Evaluation | null, options?: { skipRefresh?: boolean }) => {
if (!evaluation) return; const toSave = evalOverride ?? evaluation;
if (!toSave) return;
setSaving(true); setSaving(true);
try { try {
const res = await fetch(`/api/evaluations/${id}`, { const res = await fetch(`/api/evaluations/${id}`, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
candidateName: evaluation.candidateName, candidateName: toSave.candidateName,
candidateRole: evaluation.candidateRole, candidateRole: toSave.candidateRole,
evaluatorName: evaluation.evaluatorName, candidateTeam: toSave.candidateTeam ?? null,
evaluationDate: evaluation.evaluationDate, evaluatorName: toSave.evaluatorName,
status: evaluation.status, evaluationDate: typeof toSave.evaluationDate === "string" ? toSave.evaluationDate : new Date(toSave.evaluationDate).toISOString(),
findings: evaluation.findings, status: toSave.status,
recommendations: evaluation.recommendations, findings: toSave.findings,
dimensionScores: (evaluation.dimensionScores ?? []).map((ds) => ({ recommendations: toSave.recommendations,
dimensionScores: (toSave.dimensionScores ?? []).map((ds) => ({
dimensionId: ds.dimensionId, dimensionId: ds.dimensionId,
evaluationId: id, evaluationId: id,
score: ds.score, score: ds.score,
@@ -148,11 +155,14 @@ export default function EvaluationDetailPage() {
}), }),
}); });
if (res.ok) { if (res.ok) {
fetchEval(); if (!options?.skipRefresh) fetchEval();
} else { } else {
const data = await res.json(); const data = await res.json().catch(() => ({}));
alert(data.error ?? "Save failed"); alert(data.error ?? `Save failed (${res.status})`);
} }
} catch (err) {
console.error("Save error:", err);
alert("Erreur lors de la sauvegarde");
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -203,11 +213,15 @@ export default function EvaluationDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="font-mono text-base font-medium text-zinc-800 dark:text-zinc-100"> <h1 className="font-mono text-base font-medium text-zinc-800 dark:text-zinc-100">
{evaluation.candidateName} <span className="text-zinc-500">/</span> {evaluation.candidateRole} {evaluation.candidateName}
{evaluation.candidateTeam && (
<span className="text-zinc-500"> ({evaluation.candidateTeam})</span>
)}
<span className="text-zinc-500"> / </span> {evaluation.candidateRole}
</h1> </h1>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleSave} onClick={() => handleSave()}
disabled={saving} disabled={saving}
className="rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-100 dark:bg-zinc-700 px-3 py-1.5 font-mono text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50" className="rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-100 dark:bg-zinc-700 px-3 py-1.5 font-mono text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50"
> >
@@ -233,6 +247,7 @@ export default function EvaluationDetailPage() {
<CandidateForm <CandidateForm
candidateName={evaluation.candidateName} candidateName={evaluation.candidateName}
candidateRole={evaluation.candidateRole} candidateRole={evaluation.candidateRole}
candidateTeam={evaluation.candidateTeam ?? ""}
evaluatorName={evaluation.evaluatorName} evaluatorName={evaluation.evaluatorName}
evaluationDate={evaluation.evaluationDate.split("T")[0]} evaluationDate={evaluation.evaluationDate.split("T")[0]}
templateId={evaluation.templateId} templateId={evaluation.templateId}

View File

@@ -12,6 +12,7 @@ export default function NewEvaluationPage() {
const [form, setForm] = useState({ const [form, setForm] = useState({
candidateName: "", candidateName: "",
candidateRole: "", candidateRole: "",
candidateTeam: "",
evaluatorName: "", evaluatorName: "",
evaluationDate: new Date().toISOString().split("T")[0], evaluationDate: new Date().toISOString().split("T")[0],
templateId: "", templateId: "",

View File

@@ -9,6 +9,7 @@ interface EvalRow {
id: string; id: string;
candidateName: string; candidateName: string;
candidateRole: string; candidateRole: string;
candidateTeam?: string | null;
evaluatorName: string; evaluatorName: string;
evaluationDate: string; evaluationDate: string;
template?: { name: string }; template?: { name: string };
@@ -45,6 +46,7 @@ export default function DashboardPage() {
<thead> <thead>
<tr className="border-b border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-700/80"> <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">Candidat</th> <th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Candidat</th>
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Équipe</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">Rôle</th>
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Évaluateur</th> <th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Évaluateur</th>
<th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Date</th> <th className="px-4 py-2.5 text-left font-mono text-xs text-zinc-600 dark:text-zinc-400">Date</th>
@@ -56,13 +58,13 @@ export default function DashboardPage() {
<tbody> <tbody>
{loading ? ( {loading ? (
<tr> <tr>
<td colSpan={7} className="px-4 py-12 text-center font-mono text-xs text-zinc-600 dark:text-zinc-500"> <td colSpan={8} className="px-4 py-12 text-center font-mono text-xs text-zinc-600 dark:text-zinc-500">
loading... loading...
</td> </td>
</tr> </tr>
) : evaluations.length === 0 ? ( ) : evaluations.length === 0 ? (
<tr> <tr>
<td colSpan={7} className="px-4 py-12 text-center text-zinc-600 dark:text-zinc-500"> <td colSpan={8} className="px-4 py-12 text-center text-zinc-600 dark:text-zinc-500">
Aucune évaluation.{" "} Aucune évaluation.{" "}
<Link href="/evaluations/new" className="text-cyan-600 dark:text-cyan-400 hover:underline"> <Link href="/evaluations/new" className="text-cyan-600 dark:text-cyan-400 hover:underline">
Créer Créer
@@ -73,6 +75,7 @@ export default function DashboardPage() {
evaluations.map((e) => ( evaluations.map((e) => (
<tr key={e.id} className="border-b border-zinc-200 dark:border-zinc-600/50 hover:bg-zinc-50 dark:hover:bg-zinc-700/50 transition-colors"> <tr key={e.id} className="border-b border-zinc-200 dark:border-zinc-600/50 hover:bg-zinc-50 dark:hover:bg-zinc-700/50 transition-colors">
<td className="px-4 py-2.5 text-sm font-medium text-zinc-800 dark:text-zinc-100">{e.candidateName}</td> <td className="px-4 py-2.5 text-sm font-medium text-zinc-800 dark:text-zinc-100">{e.candidateName}</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.candidateRole}</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.evaluatorName}</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"> <td className="px-4 py-2.5 font-mono text-xs text-zinc-600 dark:text-zinc-400">

View File

@@ -3,6 +3,7 @@
interface CandidateFormProps { interface CandidateFormProps {
candidateName: string; candidateName: string;
candidateRole: string; candidateRole: string;
candidateTeam?: string;
evaluatorName: string; evaluatorName: string;
evaluationDate: string; evaluationDate: string;
templateId: string; templateId: string;
@@ -20,6 +21,7 @@ const labelClass = "mb-0.5 block text-xs font-medium text-zinc-600 dark:text-zin
export function CandidateForm({ export function CandidateForm({
candidateName, candidateName,
candidateRole, candidateRole,
candidateTeam = "",
evaluatorName, evaluatorName,
evaluationDate, evaluationDate,
templateId, templateId,
@@ -52,6 +54,17 @@ export function CandidateForm({
placeholder="ML Engineer" placeholder="ML Engineer"
/> />
</div> </div>
<div className="min-w-[140px]">
<label className={labelClass}>Équipe</label>
<input
type="text"
value={candidateTeam}
onChange={(e) => onChange("candidateTeam", e.target.value)}
className={inputClass}
disabled={disabled}
placeholder="Cars Front"
/>
</div>
<div className="min-w-[120px]"> <div className="min-w-[120px]">
<label className={labelClass}>Évaluateur</label> <label className={labelClass}>Évaluateur</label>
<input <input

View File

@@ -52,6 +52,7 @@ export function evaluationToCsvRows(evalData: EvaluationWithScores): string[][]
rows.push([ rows.push([
"candidateName", "candidateName",
"candidateRole", "candidateRole",
"candidateTeam",
"evaluatorName", "evaluatorName",
"evaluationDate", "evaluationDate",
"template", "template",
@@ -66,6 +67,7 @@ export function evaluationToCsvRows(evalData: EvaluationWithScores): string[][]
rows.push([ rows.push([
evalData.candidateName, evalData.candidateName,
evalData.candidateRole, evalData.candidateRole,
(evalData as { candidateTeam?: string | null }).candidateTeam ?? "",
evalData.evaluatorName, evalData.evaluatorName,
evalData.evaluationDate.toISOString().split("T")[0], evalData.evaluationDate.toISOString().split("T")[0],
evalData.template?.name ?? "", evalData.template?.name ?? "",