Add candidateTeam field to evaluations; update related components and API endpoints for consistency
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function NewEvaluationPage() {
|
||||
const [form, setForm] = useState({
|
||||
candidateName: "",
|
||||
candidateRole: "",
|
||||
candidateTeam: "",
|
||||
evaluatorName: "",
|
||||
evaluationDate: new Date().toISOString().split("T")[0],
|
||||
templateId: "",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? "",
|
||||
|
||||
Reference in New Issue
Block a user