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

View File

@@ -80,7 +80,7 @@ export async function PUT(
const { id } = await params;
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 } });
if (!existing) {
@@ -90,8 +90,12 @@ export async function PUT(
const updateData: Record<string, unknown> = {};
if (candidateName != null) updateData.candidateName = candidateName;
if (candidateRole != null) updateData.candidateRole = candidateRole;
if (candidateTeam !== undefined) updateData.candidateTeam = candidateTeam || null;
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 (findings != null) updateData.findings = findings;
if (recommendations != null) updateData.recommendations = recommendations;
@@ -106,14 +110,12 @@ export async function PUT(
});
}
const evaluation = await prisma.evaluation.update({
where: { id },
data: updateData,
include: {
template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } },
dimensionScores: { include: { dimension: true } },
},
});
if (Object.keys(updateData).length > 0) {
await prisma.evaluation.update({
where: { id },
data: updateData as Record<string, unknown>,
});
}
if (dimensionScores && Array.isArray(dimensionScores)) {
for (const ds of dimensionScores) {
@@ -157,7 +159,8 @@ export async function PUT(
return NextResponse.json(updated);
} catch (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) {
try {
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) {
return NextResponse.json(
@@ -50,6 +50,7 @@ export async function POST(req: NextRequest) {
data: {
candidateName,
candidateRole,
candidateTeam: candidateTeam || null,
evaluatorName,
evaluationDate: new Date(evaluationDate),
templateId,

View File

@@ -28,7 +28,8 @@ export async function GET(req: NextRequest) {
doc.setFontSize(18);
doc.text("Évaluation Maturité IA Gen", 14, 20);
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(`Modèle : ${evaluation.template?.name ?? ""} | Statut : ${evaluation.status === "submitted" ? "Soumise" : "Brouillon"}`, 14, 40);

View File

@@ -32,6 +32,7 @@ interface Evaluation {
id: string;
candidateName: string;
candidateRole: string;
candidateTeam?: string | null;
evaluatorName: string;
evaluationDate: string;
templateId: string;
@@ -70,10 +71,10 @@ export default function EvaluationDetailPage() {
const tmpl = templatesData.find((t: { id: string }) => t.id === evalData.templateId);
if (tmpl?.dimensions?.length) {
const dimMap = new Map(tmpl.dimensions.map((d: { id: string }) => [d.id, d]));
evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string }) => ({
...d,
suggestedQuestions: d.suggestedQuestions ?? dimMap.get(d.id)?.suggestedQuestions,
}));
evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string; suggestedQuestions?: string | null }) => ({
...d,
suggestedQuestions: d.suggestedQuestions ?? dimMap.get(d.id)?.suggestedQuestions,
}));
}
}
} catch {
@@ -117,26 +118,32 @@ export default function EvaluationDetailPage() {
const scores = e.dimensionScores.map((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 () => {
if (!evaluation) return;
const handleSave = async (evalOverride?: Evaluation | null, options?: { skipRefresh?: boolean }) => {
const toSave = evalOverride ?? evaluation;
if (!toSave) return;
setSaving(true);
try {
const res = await fetch(`/api/evaluations/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
candidateName: evaluation.candidateName,
candidateRole: evaluation.candidateRole,
evaluatorName: evaluation.evaluatorName,
evaluationDate: evaluation.evaluationDate,
status: evaluation.status,
findings: evaluation.findings,
recommendations: evaluation.recommendations,
dimensionScores: (evaluation.dimensionScores ?? []).map((ds) => ({
candidateName: toSave.candidateName,
candidateRole: toSave.candidateRole,
candidateTeam: toSave.candidateTeam ?? null,
evaluatorName: toSave.evaluatorName,
evaluationDate: typeof toSave.evaluationDate === "string" ? toSave.evaluationDate : new Date(toSave.evaluationDate).toISOString(),
status: toSave.status,
findings: toSave.findings,
recommendations: toSave.recommendations,
dimensionScores: (toSave.dimensionScores ?? []).map((ds) => ({
dimensionId: ds.dimensionId,
evaluationId: id,
score: ds.score,
@@ -148,11 +155,14 @@ export default function EvaluationDetailPage() {
}),
});
if (res.ok) {
fetchEval();
if (!options?.skipRefresh) fetchEval();
} else {
const data = await res.json();
alert(data.error ?? "Save failed");
const data = await res.json().catch(() => ({}));
alert(data.error ?? `Save failed (${res.status})`);
}
} catch (err) {
console.error("Save error:", err);
alert("Erreur lors de la sauvegarde");
} finally {
setSaving(false);
}
@@ -203,11 +213,15 @@ export default function EvaluationDetailPage() {
<div className="space-y-6">
<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">
{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>
<div className="flex gap-2">
<button
onClick={handleSave}
onClick={() => handleSave()}
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"
>
@@ -233,6 +247,7 @@ export default function EvaluationDetailPage() {
<CandidateForm
candidateName={evaluation.candidateName}
candidateRole={evaluation.candidateRole}
candidateTeam={evaluation.candidateTeam ?? ""}
evaluatorName={evaluation.evaluatorName}
evaluationDate={evaluation.evaluationDate.split("T")[0]}
templateId={evaluation.templateId}

View File

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

View File

@@ -9,6 +9,7 @@ interface EvalRow {
id: string;
candidateName: string;
candidateRole: string;
candidateTeam?: string | null;
evaluatorName: string;
evaluationDate: string;
template?: { name: string };
@@ -45,6 +46,7 @@ export default function DashboardPage() {
<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">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">Évaluateur</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>
{loading ? (
<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...
</td>
</tr>
) : evaluations.length === 0 ? (
<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.{" "}
<Link href="/evaluations/new" className="text-cyan-600 dark:text-cyan-400 hover:underline">
Créer
@@ -73,6 +75,7 @@ export default function DashboardPage() {
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">
<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.evaluatorName}</td>
<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 {
candidateName: string;
candidateRole: string;
candidateTeam?: string;
evaluatorName: string;
evaluationDate: 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({
candidateName,
candidateRole,
candidateTeam = "",
evaluatorName,
evaluationDate,
templateId,
@@ -52,6 +54,17 @@ export function CandidateForm({
placeholder="ML Engineer"
/>
</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]">
<label className={labelClass}>Évaluateur</label>
<input

View File

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