From 9fcceb264946bae135a5981d17e8b27e11562279 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 20 Feb 2026 09:22:12 +0100 Subject: [PATCH] Add candidateTeam field to evaluations; update related components and API endpoints for consistency --- prisma/dev.db | Bin 102400 -> 102400 bytes prisma/schema.prisma | 1 + prisma/seed.ts | 7 ++-- src/app/api/evaluations/[id]/route.ts | 25 ++++++------ src/app/api/evaluations/route.ts | 3 +- src/app/api/export/pdf/route.ts | 3 +- src/app/evaluations/[id]/page.tsx | 55 ++++++++++++++++---------- src/app/evaluations/new/page.tsx | 1 + src/app/page.tsx | 7 +++- src/components/CandidateForm.tsx | 13 ++++++ src/lib/export-utils.ts | 2 + 11 files changed, 79 insertions(+), 38 deletions(-) diff --git a/prisma/dev.db b/prisma/dev.db index e69bfe3d5eb2e0ef612e938d8e7d11acc8f9c038..81efa10e6c50add63dd64db27e6e545fcc427f07 100644 GIT binary patch delta 7776 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 J;VC*^_#a<2FP#7Y diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 96558e2..2c7d121 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,6 +46,7 @@ model Evaluation { id String @id @default(cuid()) candidateName String candidateRole String + candidateTeam String? // équipe du candidat evaluatorName String evaluationDate DateTime templateId String diff --git a/prisma/seed.ts b/prisma/seed.ts index 58319cb..96032c6 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -175,9 +175,9 @@ async function main() { }); const candidates = [ - { name: "Alice Chen", role: "Senior ML Engineer", evaluator: "Jean Dupont" }, - { name: "Bob Martin", role: "Data Scientist", evaluator: "Marie Curie" }, - { name: "Carol White", role: "AI Product Manager", evaluator: "Jean Dupont" }, + { name: "Alice Chen", role: "Senior ML Engineer", team: "Cars Front", evaluator: "Jean Dupont" }, + { name: "Bob Martin", role: "Data Scientist", team: "Cars Front", evaluator: "Marie Curie" }, + { name: "Carol White", role: "AI Product Manager", team: "Cars Data", evaluator: "Jean Dupont" }, ]; for (let i = 0; i < candidates.length; i++) { @@ -186,6 +186,7 @@ async function main() { data: { candidateName: c.name, candidateRole: c.role, + candidateTeam: c.team, evaluatorName: c.evaluator, evaluationDate: new Date(2025, 1, 15 + i), templateId: template.id, diff --git a/src/app/api/evaluations/[id]/route.ts b/src/app/api/evaluations/[id]/route.ts index 45dbe49..d752799 100644 --- a/src/app/api/evaluations/[id]/route.ts +++ b/src/app/api/evaluations/[id]/route.ts @@ -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 = {}; 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, + }); + } 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 }); } } diff --git a/src/app/api/evaluations/route.ts b/src/app/api/evaluations/route.ts index f9eb385..c0d81a1 100644 --- a/src/app/api/evaluations/route.ts +++ b/src/app/api/evaluations/route.ts @@ -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, diff --git a/src/app/api/export/pdf/route.ts b/src/app/api/export/pdf/route.ts index 5dbc7e2..b85b209 100644 --- a/src/app/api/export/pdf/route.ts +++ b/src/app/api/export/pdf/route.ts @@ -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); diff --git a/src/app/evaluations/[id]/page.tsx b/src/app/evaluations/[id]/page.tsx index a07080d..776bcf6 100644 --- a/src/app/evaluations/[id]/page.tsx +++ b/src/app/evaluations/[id]/page.tsx @@ -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() {

- {evaluation.candidateName} / {evaluation.candidateRole} + {evaluation.candidateName} + {evaluation.candidateTeam && ( + ({evaluation.candidateTeam}) + )} + / {evaluation.candidateRole}

+
+ + onChange("candidateTeam", e.target.value)} + className={inputClass} + disabled={disabled} + placeholder="Cars Front" + /> +