"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { CandidateForm } from "@/components/CandidateForm"; import { DimensionCard } from "@/components/DimensionCard"; import { RadarChart } from "@/components/RadarChart"; import { ExportModal } from "@/components/ExportModal"; import { ShareModal } from "@/components/ShareModal"; import { ConfirmModal } from "@/components/ConfirmModal"; import { generateFindings, generateRecommendations, computeAverageScore } from "@/lib/export-utils"; interface Dimension { id: string; slug: string; title: string; rubric: string; suggestedQuestions?: string | null; } interface DimensionScore { id: string; dimensionId: string; score: number | null; justification: string | null; examplesObserved: string | null; confidence: string | null; candidateNotes: string | null; dimension: Dimension; } interface Evaluation { id: string; candidateName: string; candidateRole: string; candidateTeam?: string | null; evaluatorName: string; evaluatorId?: string | null; evaluationDate: string; templateId: string; template: { id: string; name: string; dimensions: Dimension[] }; status: string; findings: string | null; recommendations: string | null; dimensionScores: DimensionScore[]; sharedWith?: { id: string; user: { id: string; email: string; name: string | null } }[]; } export default function EvaluationDetailPage() { const params = useParams(); const router = useRouter(); const id = params.id as string; const [evaluation, setEvaluation] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]); const [exportOpen, setExportOpen] = useState(false); const [shareOpen, setShareOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [collapseAllTrigger, setCollapseAllTrigger] = useState(0); const [users, setUsers] = useState<{ id: string; email: string; name: string | null }[]>([]); const fetchEval = useCallback(() => { setLoading(true); Promise.all([ fetch(`/api/evaluations/${id}`).then((r) => r.json()), fetch("/api/templates").then((r) => r.json()), fetch("/api/users").then((r) => r.json()), ]) .then(([evalData, templatesData, usersData]) => { setTemplates(Array.isArray(templatesData) ? templatesData : []); setUsers(Array.isArray(usersData) ? usersData : []); if (evalData?.error) { setEvaluation(null); return; } try { if (evalData?.template?.dimensions?.length > 0 && Array.isArray(templatesData)) { 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; suggestedQuestions?: string | null }) => [d.id, d])); evalData.template.dimensions = evalData.template.dimensions.map((d: { id: string; suggestedQuestions?: string | null }) => ({ ...d, suggestedQuestions: d.suggestedQuestions ?? (dimMap.get(d.id) as { suggestedQuestions?: string | null } | undefined)?.suggestedQuestions, })); } } } catch { /* merge failed, use evalData as-is */ } setEvaluation({ ...evalData, dimensionScores: evalData.dimensionScores ?? [] }); }) .catch(() => setEvaluation(null)) .finally(() => setLoading(false)); }, [id]); useEffect(() => { fetchEval(); }, [fetchEval]); // Draft backup to localStorage (debounced, for offline resilience) useEffect(() => { if (!evaluation || !id) return; const t = setTimeout(() => { try { localStorage.setItem( `eval-draft-${id}`, JSON.stringify({ ...evaluation, evaluationDate: evaluation.evaluationDate }) ); } catch { /* ignore */ } }, 2000); return () => clearTimeout(t); }, [evaluation, id]); const handleFormChange = (field: string, value: string) => { if (!evaluation) return; setEvaluation((e) => (e ? { ...e, [field]: value } : null)); }; const handleScoreChange = (dimensionId: string, data: Partial) => { if (!evaluation) return; setEvaluation((e) => { if (!e) return null; const existing = e.dimensionScores.find((ds) => ds.dimensionId === dimensionId); const dim = e.template?.dimensions?.find((d) => d.id === dimensionId); const scores = existing ? e.dimensionScores.map((ds) => ds.dimensionId === dimensionId ? { ...ds, ...data } : ds ) : [ ...e.dimensionScores, { id: `temp-${dimensionId}`, dimensionId, score: (data as { score?: number }).score ?? null, justification: (data as { justification?: string }).justification ?? null, examplesObserved: (data as { examplesObserved?: string }).examplesObserved ?? null, confidence: (data as { confidence?: string }).confidence ?? null, candidateNotes: (data as { candidateNotes?: string }).candidateNotes ?? null, dimension: dim ?? { id: dimensionId, slug: "", title: "", rubric: "" }, }, ]; const next = { ...e, dimensionScores: scores }; if (data.score !== undefined) { setTimeout(() => handleSave(next, { skipRefresh: true }), 0); } return next; }); }; 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: 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, justification: ds.justification, examplesObserved: ds.examplesObserved, confidence: ds.confidence, candidateNotes: ds.candidateNotes, })), }), }); if (res.ok) { if (!options?.skipRefresh) fetchEval(); } else { 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); } }; const handleGenerateFindings = () => { if (!evaluation) return; const findings = generateFindings(evaluation.dimensionScores ?? []); const recommendations = generateRecommendations(evaluation.dimensionScores ?? []); setEvaluation((e) => (e ? { ...e, findings, recommendations } : null)); }; const allFives = evaluation?.dimensionScores?.every( (ds) => ds.score === 5 && (!ds.justification || ds.justification.trim() === "") ); const showAllFivesWarning = allFives && evaluation?.status === "submitted"; if (loading) { return
loading...
; } if (!evaluation) { return (
Évaluation introuvable.{" "} ← dashboard
); } const dimensions = evaluation.template?.dimensions ?? []; const dimensionScores = evaluation.dimensionScores ?? []; const scoreMap = new Map(dimensionScores.map((ds) => [ds.dimensionId, ds])); const radarData = dimensions .filter((dim) => !(dim.title ?? "").startsWith("[Optionnel]")) .map((dim) => { const ds = scoreMap.get(dim.id); const score = ds?.score; if (score == null) return null; const title = dim.title ?? ""; const s = Number(score); if (Number.isNaN(s) || s < 0 || s > 5) return null; return { dimension: title.length > 12 ? title.slice(0, 12) + "…" : title, score: s, fullMark: 5, }; }) .filter((d): d is { dimension: string; score: number; fullMark: number } => d != null); const avgScore = computeAverageScore(dimensionScores); return (

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

{showAllFivesWarning && (
⚠ Tous les scores = 5 sans justification
)}

Session

Dimensions

{dimensions.map((dim, i) => (
))}

Synthèse

Moyenne {avgScore.toFixed(1)}/5