diff --git a/README.md b/README.md index 3b7bf04..4d44731 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,16 @@ Open [http://localhost:3000](http://localhost:3000). - **2 templates**: Full 15-dimensions, Short 8-dimensions - **Admin user**: `admin@peaksys.local` (mock auth) -## API Routes +## API Routes (restantes) + +Les mutations (create, update, delete, share, etc.) sont gérées par **Server Actions**. Routes API restantes : | Route | Method | Description | |-------|--------|-------------| -| `/api/evaluations` | GET, POST | List / create evaluations | -| `/api/evaluations/[id]` | GET, PUT | Get / update evaluation | -| `/api/templates` | GET | List templates | | `/api/export/csv?id=` | GET | Export evaluation as CSV | | `/api/export/pdf?id=` | GET | Export evaluation as PDF | -| `/api/auth` | GET, POST | Mock auth | -| `/api/ai/suggest-followups` | POST | AI follow-up suggestions (stub) | +| `/api/auth/*` | — | NextAuth | +| `/api/auth/signup` | POST | Inscription | ## Export cURL Examples diff --git a/src/actions/admin.ts b/src/actions/admin.ts new file mode 100644 index 0000000..32a7d49 --- /dev/null +++ b/src/actions/admin.ts @@ -0,0 +1,43 @@ +"use server"; + +import { auth } from "@/auth"; +import { prisma } from "@/lib/db"; +import { revalidatePath } from "next/cache"; + +export type ActionResult = { success: true; data?: T } | { success: false; error: string }; + +export async function setUserRole(userId: string, role: "admin" | "evaluator"): Promise { + const session = await auth(); + if (session?.user?.role !== "admin") return { success: false, error: "Forbidden" }; + + if (!role || !["admin", "evaluator"].includes(role)) { + return { success: false, error: "Rôle invalide (admin | evaluator)" }; + } + + try { + await prisma.user.update({ where: { id: userId }, data: { role } }); + revalidatePath("/admin"); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur" }; + } +} + +export async function deleteUser(userId: string): Promise { + const session = await auth(); + if (session?.user?.role !== "admin") return { success: false, error: "Forbidden" }; + + if (userId === session.user.id) { + return { success: false, error: "Impossible de supprimer votre propre compte" }; + } + + try { + await prisma.user.delete({ where: { id: userId } }); + revalidatePath("/admin"); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur" }; + } +} diff --git a/src/actions/evaluations.ts b/src/actions/evaluations.ts new file mode 100644 index 0000000..7274091 --- /dev/null +++ b/src/actions/evaluations.ts @@ -0,0 +1,190 @@ +"use server"; + +import { auth } from "@/auth"; +import { prisma } from "@/lib/db"; +import { canAccessEvaluation } from "@/lib/evaluation-access"; +import { getEvaluation } from "@/lib/server-data"; +import { revalidatePath } from "next/cache"; + +export type ActionResult = { success: true; data?: T } | { success: false; error: string }; + +export async function fetchEvaluation(id: string): Promise>>> { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const evaluation = await getEvaluation(id); + if (!evaluation) return { success: false, error: "Évaluation introuvable" }; + + return { success: true, data: evaluation }; +} + +export async function deleteEvaluation(id: string): Promise { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const hasAccess = await canAccessEvaluation(id, session.user.id, session.user.role === "admin"); + if (!hasAccess) return { success: false, error: "Accès refusé" }; + + try { + await prisma.evaluation.delete({ where: { id } }); + revalidatePath("/dashboard"); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur lors de la suppression" }; + } +} + +export async function createEvaluation(data: { + candidateName: string; + candidateRole: string; + candidateTeam?: string; + evaluationDate: string; + templateId: string; +}): Promise> { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const { candidateName, candidateRole, candidateTeam, evaluationDate, templateId } = data; + if (!candidateName || !candidateRole || !evaluationDate || !templateId) { + return { success: false, error: "Champs requis manquants" }; + } + + try { + const evaluatorName = session.user.name || session.user.email || "Évaluateur"; + + const template = await prisma.template.findUnique({ + where: { id: templateId }, + include: { dimensions: { orderBy: { orderIndex: "asc" } } }, + }); + if (!template) return { success: false, error: "Template introuvable" }; + + const evaluation = await prisma.evaluation.create({ + data: { + candidateName, + candidateRole, + candidateTeam: candidateTeam || null, + evaluatorName, + evaluatorId: session.user.id, + evaluationDate: new Date(evaluationDate), + templateId, + status: "draft", + }, + }); + + for (const dim of template.dimensions) { + await prisma.dimensionScore.create({ + data: { evaluationId: evaluation.id, dimensionId: dim.id }, + }); + } + + revalidatePath("/dashboard"); + return { success: true, data: { id: evaluation.id } }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur lors de la création" }; + } +} + +export interface UpdateEvaluationInput { + candidateName?: string; + candidateRole?: string; + candidateTeam?: string | null; + evaluatorName?: string; + evaluationDate?: string; + status?: string; + findings?: string | null; + recommendations?: string | null; + isPublic?: boolean; + dimensionScores?: { + dimensionId: string; + evaluationId: string; + score: number | null; + justification?: string | null; + examplesObserved?: string | null; + confidence?: string | null; + candidateNotes?: string | null; + }[]; +} + +export async function updateEvaluation(id: string, data: UpdateEvaluationInput): Promise { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const hasAccess = await canAccessEvaluation(id, session.user.id, session.user.role === "admin"); + if (!hasAccess) return { success: false, error: "Accès refusé" }; + + const existing = await prisma.evaluation.findUnique({ where: { id } }); + if (!existing) return { success: false, error: "Évaluation introuvable" }; + + try { + const { + candidateName, + candidateRole, + candidateTeam, + evaluatorName, + evaluationDate, + status, + findings, + recommendations, + isPublic, + dimensionScores, + } = data; + + const updateData: Record = {}; + if (candidateName != null) updateData.candidateName = candidateName; + if (candidateRole != null) updateData.candidateRole = candidateRole; + if (candidateTeam !== undefined) updateData.candidateTeam = candidateTeam; + if (evaluatorName != null) updateData.evaluatorName = evaluatorName; + 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; + if (typeof isPublic === "boolean") updateData.isPublic = isPublic; + + if (Object.keys(updateData).length > 0) { + await prisma.auditLog.create({ + data: { evaluationId: id, action: "updated", newValue: JSON.stringify(updateData) }, + }); + await prisma.evaluation.update({ where: { id }, data: updateData as Record }); + } + + if (dimensionScores && Array.isArray(dimensionScores)) { + for (const ds of dimensionScores) { + if (ds.dimensionId) { + await prisma.dimensionScore.upsert({ + where: { + evaluationId_dimensionId: { evaluationId: id, dimensionId: ds.dimensionId }, + }, + update: { + score: ds.score, + justification: ds.justification, + examplesObserved: ds.examplesObserved, + confidence: ds.confidence, + candidateNotes: ds.candidateNotes, + }, + create: { + evaluationId: id, + dimensionId: ds.dimensionId, + score: ds.score, + justification: ds.justification, + examplesObserved: ds.examplesObserved, + confidence: ds.confidence, + candidateNotes: ds.candidateNotes, + }, + }); + } + } + } + + revalidatePath(`/evaluations/${id}`); + revalidatePath("/dashboard"); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: e instanceof Error ? e.message : "Erreur lors de la sauvegarde" }; + } +} diff --git a/src/actions/password.ts b/src/actions/password.ts new file mode 100644 index 0000000..a77d3b4 --- /dev/null +++ b/src/actions/password.ts @@ -0,0 +1,45 @@ +"use server"; + +import bcrypt from "bcryptjs"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/db"; + +export type ActionResult = { success: true } | { success: false; error: string }; + +export async function changePassword( + currentPassword: string, + newPassword: string +): Promise { + const session = await auth(); + if (!session?.user?.id) return { success: false, error: "Non authentifié" }; + + if (!currentPassword || !newPassword) { + return { success: false, error: "Mot de passe actuel et nouveau requis" }; + } + if (newPassword.length < 8) { + return { success: false, error: "Le nouveau mot de passe doit faire au moins 8 caractères" }; + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { passwordHash: true }, + }); + if (!user?.passwordHash) { + return { success: false, error: "Compte sans mot de passe (connexion SSO)" }; + } + + const ok = await bcrypt.compare(String(currentPassword), user.passwordHash); + if (!ok) return { success: false, error: "Mot de passe actuel incorrect" }; + + try { + const passwordHash = await bcrypt.hash(String(newPassword), 10); + await prisma.user.update({ + where: { id: session.user.id }, + data: { passwordHash }, + }); + return { success: true }; + } catch (e) { + console.error("Password change error:", e); + return { success: false, error: "Erreur lors du changement de mot de passe" }; + } +} diff --git a/src/actions/share.ts b/src/actions/share.ts new file mode 100644 index 0000000..84fadcd --- /dev/null +++ b/src/actions/share.ts @@ -0,0 +1,66 @@ +"use server"; + +import { auth } from "@/auth"; +import { prisma } from "@/lib/db"; +import { canAccessEvaluation } from "@/lib/evaluation-access"; +import { revalidatePath } from "next/cache"; + +export type ActionResult = { success: true; data?: T } | { success: false; error: string }; + +export async function addShare(evaluationId: string, userId: string): Promise { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const hasAccess = await canAccessEvaluation( + evaluationId, + session.user.id, + session.user.role === "admin" + ); + if (!hasAccess) return { success: false, error: "Accès refusé" }; + + if (userId === session.user.id) return { success: false, error: "Vous avez déjà accès" }; + + const evaluation = await prisma.evaluation.findUnique({ + where: { id: evaluationId }, + select: { evaluatorId: true }, + }); + if (evaluation?.evaluatorId === userId) { + return { success: false, error: "L'évaluateur a déjà accès" }; + } + + try { + await prisma.evaluationShare.upsert({ + where: { evaluationId_userId: { evaluationId, userId } }, + create: { evaluationId, userId }, + update: {}, + }); + revalidatePath(`/evaluations/${evaluationId}`); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur" }; + } +} + +export async function removeShare(evaluationId: string, userId: string): Promise { + const session = await auth(); + if (!session?.user) return { success: false, error: "Non authentifié" }; + + const hasAccess = await canAccessEvaluation( + evaluationId, + session.user.id, + session.user.role === "admin" + ); + if (!hasAccess) return { success: false, error: "Accès refusé" }; + + try { + await prisma.evaluationShare.deleteMany({ + where: { evaluationId, userId }, + }); + revalidatePath(`/evaluations/${evaluationId}`); + return { success: true }; + } catch (e) { + console.error(e); + return { success: false, error: "Erreur" }; + } +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8759253..f87f4e4 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,193 +1,10 @@ -"use client"; +import { redirect } from "next/navigation"; +import { getTemplates, getAdminUsers } from "@/lib/server-data"; +import { AdminClient } from "@/components/AdminClient"; -import { useState, useEffect } from "react"; -import { format } from "date-fns"; -import { useSession } from "next-auth/react"; -import { ConfirmModal } from "@/components/ConfirmModal"; +export default async function AdminPage() { + const [templates, users] = await Promise.all([getTemplates(), getAdminUsers()]); + if (!users) redirect("/auth/login"); -interface Template { - id: string; - name: string; - dimensions: { id: string; title: string; orderIndex: number }[]; -} - -interface User { - id: string; - email: string; - name: string | null; - role: string; - createdAt: string; -} - -export default function AdminPage() { - const [templates, setTemplates] = useState([]); - const { data: session } = useSession(); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [updatingId, setUpdatingId] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); - - async function setRole(userId: string, role: "admin" | "evaluator") { - setUpdatingId(userId); - try { - const res = await fetch(`/api/admin/users/${userId}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ role }), - }); - if (res.ok) { - setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, role } : u))); - } else { - const data = await res.json().catch(() => ({})); - alert(data.error ?? "Erreur"); - } - } finally { - setUpdatingId(null); - } - } - - useEffect(() => { - Promise.all([ - fetch("/api/templates").then((r) => r.json()), - fetch("/api/admin/users").then((r) => r.json()), - ]) - .then(([templatesData, usersData]) => { - setTemplates(Array.isArray(templatesData) ? templatesData : []); - setUsers(Array.isArray(usersData) ? usersData : []); - }) - .catch(() => {}) - .finally(() => setLoading(false)); - }, []); - - if (loading) { - return
loading...
; - } - - return ( -
-

Admin

- -
-

Users

-
- - - - - - - - - - - - {users.map((u) => ( - - - - - - - - ))} - -
EmailNomRôleCréé le
{u.email}{u.name ?? "—"} - - {u.role} - - - {format(new Date(u.createdAt), "yyyy-MM-dd HH:mm")} - - - {u.role === "admin" ? ( - - ) : ( - - )} - {u.id !== session?.user?.id && ( - - )} - -
-
-
- - { - if (!deleteTarget) return; - const res = await fetch(`/api/admin/users/${deleteTarget.id}`, { method: "DELETE" }); - if (res.ok) { - setUsers((prev) => prev.filter((u) => u.id !== deleteTarget.id)); - setDeleteTarget(null); - } else { - const data = await res.json().catch(() => ({})); - alert(data.error ?? "Erreur"); - } - }} - onCancel={() => setDeleteTarget(null)} - /> - -
-

Modèles

-
- {templates.map((t) => ( -
-

{t.name}

-

- {t.dimensions.length} dim. -

-
    - {t.dimensions.slice(0, 5).map((d) => ( -
  • • {d.title}
  • - ))} - {t.dimensions.length > 5 && ( -
  • +{t.dimensions.length - 5}
  • - )} -
-
- ))} -
-
-
- ); + return ; } diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts deleted file mode 100644 index f76126b..0000000 --- a/src/app/api/admin/users/[id]/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -export async function PATCH( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - const session = await auth(); - if (session?.user?.role !== "admin") { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - const { id } = await params; - const body = await req.json(); - const { role } = body; - - if (!role || !["admin", "evaluator"].includes(role)) { - return NextResponse.json({ error: "Rôle invalide (admin | evaluator)" }, { status: 400 }); - } - - const user = await prisma.user.update({ - where: { id }, - data: { role }, - }); - return NextResponse.json(user); -} - -export async function DELETE( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - const session = await auth(); - if (session?.user?.role !== "admin") { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - const { id } = await params; - - if (id === session.user.id) { - return NextResponse.json({ error: "Impossible de supprimer votre propre compte" }, { status: 400 }); - } - - await prisma.user.delete({ where: { id } }); - return NextResponse.json({ ok: true }); -} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts deleted file mode 100644 index d1423d7..0000000 --- a/src/app/api/admin/users/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -export async function GET() { - const session = await auth(); - if (session?.user?.role !== "admin") { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - const users = await prisma.user.findMany({ - orderBy: { createdAt: "desc" }, - select: { - id: true, - email: true, - name: true, - role: true, - createdAt: true, - }, - }); - return NextResponse.json(users); -} diff --git a/src/app/api/evaluations/[id]/route.ts b/src/app/api/evaluations/[id]/route.ts deleted file mode 100644 index 802174e..0000000 --- a/src/app/api/evaluations/[id]/route.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { Prisma } from "@prisma/client"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -async function canAccessEvaluation( - evaluationId: string, - userId: string, - isAdmin: boolean, - readOnly = false -) { - if (isAdmin) return true; - const eval_ = await prisma.evaluation.findUnique({ - where: { id: evaluationId }, - select: { evaluatorId: true, isPublic: true, sharedWith: { select: { userId: true } } }, - }); - if (!eval_) return false; - if (eval_.evaluatorId === userId) return true; - if (eval_.sharedWith.some((s) => s.userId === userId)) return true; - if (readOnly && eval_.isPublic) return true; - return false; -} - -export async function GET( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id } = await params; - const evaluation = await prisma.evaluation.findUnique({ - where: { id }, - include: { - template: { - include: { - dimensions: { orderBy: { orderIndex: "asc" } }, - }, - }, - dimensionScores: { include: { dimension: true } }, - sharedWith: { include: { user: { select: { id: true, email: true, name: true } } } }, - }, - }); - - if (!evaluation) { - return NextResponse.json({ error: "Evaluation not found" }, { status: 404 }); - } - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin", - true // read-only: public evals accessibles en lecture - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - // Prisma ORM omits suggestedQuestions in some contexts — fetch via raw - const templateId = evaluation.templateId; - const dimsRaw = evaluation.template - ? ((await prisma.$queryRaw( - Prisma.sql`SELECT id, slug, title, rubric, "orderIndex", "suggestedQuestions" FROM "TemplateDimension" WHERE "templateId" = ${templateId} ORDER BY "orderIndex" ASC` - )) as { id: string; slug: string; title: string; rubric: string; orderIndex: number; suggestedQuestions: string | null }[]) - : []; - - const dimMap = new Map(dimsRaw.map((d) => [d.id, d])); - - const out = { - ...evaluation, - template: evaluation.template - ? { - ...evaluation.template, - dimensions: evaluation.template.dimensions.map((d) => { - const raw = dimMap.get(d.id); - return { - id: d.id, - slug: d.slug, - title: d.title, - rubric: d.rubric, - orderIndex: d.orderIndex, - suggestedQuestions: raw?.suggestedQuestions ?? d.suggestedQuestions, - }; - }), - } - : null, - dimensionScores: evaluation.dimensionScores.map((ds) => ({ - ...ds, - dimension: ds.dimension - ? { - id: ds.dimension.id, - slug: ds.dimension.slug, - title: ds.dimension.title, - rubric: ds.dimension.rubric, - suggestedQuestions: dimMap.get(ds.dimension.id)?.suggestedQuestions ?? ds.dimension.suggestedQuestions, - } - : null, - })), - }; - return NextResponse.json(out); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to fetch evaluation" }, { status: 500 }); - } -} - -export async function PUT( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id } = await params; - const body = await req.json(); - - const { candidateName, candidateRole, candidateTeam, evaluatorName, evaluationDate, status, findings, recommendations, dimensionScores, isPublic } = body; - - const existing = await prisma.evaluation.findUnique({ where: { id } }); - if (!existing) { - return NextResponse.json({ error: "Evaluation not found" }, { status: 404 }); - } - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin" - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - 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) { - 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; - if (typeof isPublic === "boolean") updateData.isPublic = isPublic; - - if (Object.keys(updateData).length > 0) { - await prisma.auditLog.create({ - data: { - evaluationId: id, - action: "updated", - newValue: JSON.stringify(updateData), - }, - }); - } - - 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) { - if (ds.dimensionId) { - await prisma.dimensionScore.upsert({ - where: { - evaluationId_dimensionId: { - evaluationId: id, - dimensionId: ds.dimensionId, - }, - }, - update: { - score: ds.score, - justification: ds.justification, - examplesObserved: ds.examplesObserved, - confidence: ds.confidence, - candidateNotes: ds.candidateNotes, - }, - create: { - evaluationId: id, - dimensionId: ds.dimensionId, - score: ds.score, - justification: ds.justification, - examplesObserved: ds.examplesObserved, - confidence: ds.confidence, - candidateNotes: ds.candidateNotes, - }, - }); - } - } - } - - const updated = await prisma.evaluation.findUnique({ - where: { id }, - include: { - template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } }, - dimensionScores: { include: { dimension: true } }, - }, - }); - - return NextResponse.json(updated); - } catch (e) { - console.error(e); - const msg = e instanceof Error ? e.message : "Failed to update evaluation"; - return NextResponse.json({ error: msg }, { status: 500 }); - } -} - -export async function DELETE( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id } = await params; - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin" - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - await prisma.evaluation.delete({ where: { id } }); - return NextResponse.json({ ok: true }); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to delete evaluation" }, { status: 500 }); - } -} diff --git a/src/app/api/evaluations/[id]/share/[userId]/route.ts b/src/app/api/evaluations/[id]/share/[userId]/route.ts deleted file mode 100644 index 5c164ec..0000000 --- a/src/app/api/evaluations/[id]/share/[userId]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -async function canAccessEvaluation(evaluationId: string, userId: string, isAdmin: boolean) { - if (isAdmin) return true; - const eval_ = await prisma.evaluation.findUnique({ - where: { id: evaluationId }, - select: { evaluatorId: true, sharedWith: { select: { userId: true } } }, - }); - if (!eval_) return false; - if (eval_.evaluatorId === userId) return true; - if (eval_.sharedWith.some((s) => s.userId === userId)) return true; - return false; -} - -export async function DELETE( - _req: NextRequest, - { params }: { params: Promise<{ id: string; userId: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id, userId } = await params; - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin" - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - await prisma.evaluationShare.deleteMany({ - where: { evaluationId: id, userId }, - }); - - return NextResponse.json({ ok: true }); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Erreur" }, { status: 500 }); - } -} diff --git a/src/app/api/evaluations/[id]/share/route.ts b/src/app/api/evaluations/[id]/share/route.ts deleted file mode 100644 index ca4232c..0000000 --- a/src/app/api/evaluations/[id]/share/route.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -async function canAccessEvaluation(evaluationId: string, userId: string, isAdmin: boolean) { - if (isAdmin) return true; - const eval_ = await prisma.evaluation.findUnique({ - where: { id: evaluationId }, - select: { evaluatorId: true, sharedWith: { select: { userId: true } } }, - }); - if (!eval_) return false; - if (eval_.evaluatorId === userId) return true; - if (eval_.sharedWith.some((s) => s.userId === userId)) return true; - return false; -} - -export async function GET( - _req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id } = await params; - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin" - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - const sharedWith = await prisma.evaluationShare.findMany({ - where: { evaluationId: id }, - include: { user: { select: { id: true, email: true, name: true } } }, - }); - return NextResponse.json(sharedWith); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Erreur" }, { status: 500 }); - } -} - -export async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { id } = await params; - const body = await req.json(); - const { email, userId } = body; - - if (!userId && !email) { - return NextResponse.json({ error: "userId ou email requis" }, { status: 400 }); - } - - let user; - if (userId && typeof userId === "string") { - user = await prisma.user.findUnique({ where: { id: userId } }); - } else if (email && typeof email === "string") { - user = await prisma.user.findUnique({ - where: { email: String(email).toLowerCase().trim() }, - }); - } - if (!user) { - return NextResponse.json({ error: "Utilisateur introuvable" }, { status: 404 }); - } - - const hasAccess = await canAccessEvaluation( - id, - session.user.id, - session.user.role === "admin" - ); - if (!hasAccess) { - return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); - } - - if (user.id === session.user.id) { - return NextResponse.json({ error: "Vous avez déjà accès" }, { status: 400 }); - } - - const evaluation = await prisma.evaluation.findUnique({ - where: { id }, - select: { evaluatorId: true }, - }); - if (evaluation?.evaluatorId === user.id) { - return NextResponse.json({ error: "L'évaluateur a déjà accès" }, { status: 400 }); - } - - await prisma.evaluationShare.upsert({ - where: { - evaluationId_userId: { evaluationId: id, userId: user.id }, - }, - create: { evaluationId: id, userId: user.id }, - update: {}, - }); - - return NextResponse.json({ ok: true, user: { id: user.id, email: user.email, name: user.name } }); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Erreur" }, { status: 500 }); - } -} diff --git a/src/app/api/evaluations/route.ts b/src/app/api/evaluations/route.ts deleted file mode 100644 index 57bb5a9..0000000 --- a/src/app/api/evaluations/route.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -export async function GET(req: NextRequest) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const { searchParams } = new URL(req.url); - const status = searchParams.get("status"); - const templateId = searchParams.get("templateId"); - - const isAdmin = session.user.role === "admin"; - const userId = session.user.id; - - const evaluations = await prisma.evaluation.findMany({ - where: { - ...(status && { status }), - ...(templateId && { templateId }), - ...(!isAdmin && { - OR: [ - { evaluatorId: userId }, - { sharedWith: { some: { userId } } }, - { isPublic: true }, - ], - }), - }, - include: { - template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } }, - dimensionScores: { include: { dimension: true } }, - }, - orderBy: { evaluationDate: "desc" }, - }); - - return NextResponse.json(evaluations); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to fetch evaluations" }, { status: 500 }); - } -} - -export async function POST(req: NextRequest) { - try { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const body = await req.json(); - const { candidateName, candidateRole, candidateTeam, evaluationDate, templateId } = body; - - if (!candidateName || !candidateRole || !evaluationDate || !templateId) { - return NextResponse.json( - { error: "Missing required fields: candidateName, candidateRole, evaluationDate, templateId" }, - { status: 400 } - ); - } - - const evaluatorName = session.user.name || session.user.email || "Évaluateur"; - - const template = await prisma.template.findUnique({ - where: { id: templateId }, - include: { dimensions: { orderBy: { orderIndex: "asc" } } }, - }); - if (!template) { - return NextResponse.json({ error: "Template not found" }, { status: 404 }); - } - - const evaluation = await prisma.evaluation.create({ - data: { - candidateName, - candidateRole, - candidateTeam: candidateTeam || null, - evaluatorName, - evaluatorId: session.user.id, - evaluationDate: new Date(evaluationDate), - templateId, - status: "draft", - }, - include: { - template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } }, - dimensionScores: { include: { dimension: true } }, - }, - }); - - // Create empty dimension scores for each template dimension - for (const dim of template.dimensions) { - await prisma.dimensionScore.create({ - data: { - evaluationId: evaluation.id, - dimensionId: dim.id, - }, - }); - } - - const updated = await prisma.evaluation.findUnique({ - where: { id: evaluation.id }, - include: { - template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } }, - dimensionScores: { include: { dimension: true } }, - }, - }); - - return NextResponse.json(updated); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to create evaluation" }, { status: 500 }); - } -} diff --git a/src/app/api/templates/data/route.ts b/src/app/api/templates/data/route.ts deleted file mode 100644 index d1ebd04..0000000 --- a/src/app/api/templates/data/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse } from "next/server"; -import { prisma } from "@/lib/db"; - -/** Returns templates in the canonical JSON format: { templates: [{ id, name, dimensions: [{ id, title, rubric }] }] } */ -export async function GET() { - try { - const templates = await prisma.template.findMany({ - include: { - dimensions: { orderBy: { orderIndex: "asc" } }, - }, - }); - const data = { - templates: templates.map((t) => ({ - id: t.id, - name: t.name, - dimensions: t.dimensions.map((d) => ({ - id: d.slug, - title: d.title, - rubric: d.rubric, - suggestedQuestions: d.suggestedQuestions, - })), - })), - }; - return NextResponse.json(data); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to fetch templates" }, { status: 500 }); - } -} diff --git a/src/app/api/templates/route.ts b/src/app/api/templates/route.ts deleted file mode 100644 index c69635f..0000000 --- a/src/app/api/templates/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse } from "next/server"; -import { Prisma } from "@prisma/client"; -import { prisma } from "@/lib/db"; - -export async function GET() { - try { - const templates = await prisma.template.findMany({ - include: { - dimensions: { orderBy: { orderIndex: "asc" } }, - }, - }); - // Prisma ORM omits suggestedQuestions — enrich via raw - const dimsRaw = (await prisma.$queryRaw( - Prisma.sql`SELECT id, "templateId", slug, title, rubric, "orderIndex", "suggestedQuestions" FROM "TemplateDimension" ORDER BY "templateId", "orderIndex"` - )) as { id: string; templateId: string; slug: string; title: string; rubric: string; orderIndex: number; suggestedQuestions: string | null }[]; - const dimMap = new Map(dimsRaw.map((d) => [d.id, d])); - const out = templates.map((t) => ({ - ...t, - dimensions: t.dimensions.map((d) => ({ - ...d, - suggestedQuestions: dimMap.get(d.id)?.suggestedQuestions ?? d.suggestedQuestions, - })), - })); - return NextResponse.json(out); - } catch (e) { - console.error(e); - return NextResponse.json({ error: "Failed to fetch templates" }, { status: 500 }); - } -} diff --git a/src/app/api/users/me/password/route.ts b/src/app/api/users/me/password/route.ts deleted file mode 100644 index dcf8b0d..0000000 --- a/src/app/api/users/me/password/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; -import bcrypt from "bcryptjs"; - -export async function PATCH(req: NextRequest) { - const session = await auth(); - if (!session?.user?.id) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - - try { - const { currentPassword, newPassword } = await req.json(); - if (!currentPassword || !newPassword) { - return NextResponse.json( - { error: "Mot de passe actuel et nouveau requis" }, - { status: 400 } - ); - } - if (newPassword.length < 8) { - return NextResponse.json( - { error: "Le nouveau mot de passe doit faire au moins 8 caractères" }, - { status: 400 } - ); - } - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - select: { passwordHash: true }, - }); - if (!user?.passwordHash) { - return NextResponse.json( - { error: "Compte sans mot de passe (connexion SSO)" }, - { status: 400 } - ); - } - - const ok = await bcrypt.compare(String(currentPassword), user.passwordHash); - if (!ok) { - return NextResponse.json( - { error: "Mot de passe actuel incorrect" }, - { status: 401 } - ); - } - - const passwordHash = await bcrypt.hash(String(newPassword), 10); - await prisma.user.update({ - where: { id: session.user.id }, - data: { passwordHash }, - }); - - return NextResponse.json({ ok: true }); - } catch (e) { - console.error("Password change error:", e); - return NextResponse.json( - { error: "Erreur lors du changement de mot de passe" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts deleted file mode 100644 index 74ecea4..0000000 --- a/src/app/api/users/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextResponse } from "next/server"; -import { auth } from "@/auth"; -import { prisma } from "@/lib/db"; - -/** Liste des utilisateurs (pour partage d'évaluations) — accessible à tout utilisateur connecté */ -export async function GET() { - const session = await auth(); - if (!session?.user) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - const users = await prisma.user.findMany({ - orderBy: { email: "asc" }, - select: { - id: true, - email: true, - name: true, - }, - }); - return NextResponse.json(users); -} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index a5a6cce..609fe86 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,167 +1,10 @@ -"use client"; +import { redirect } from "next/navigation"; +import { getEvaluations } from "@/lib/server-data"; +import { DashboardClient } from "@/components/DashboardClient"; -import { useState, useEffect } from "react"; -import Link from "next/link"; -import { format } from "date-fns"; -import { ConfirmModal } from "@/components/ConfirmModal"; -import { RadarChart } from "@/components/RadarChart"; +export default async function DashboardPage() { + const evaluations = await getEvaluations(); + if (!evaluations) redirect("/auth/login"); -interface Dimension { - id: string; - title: string; -} - -interface EvalRow { - id: string; - candidateName: string; - candidateRole: string; - candidateTeam?: string | null; - evaluatorName: string; - evaluationDate: string; - template?: { name: string; dimensions?: Dimension[] }; - status: string; - dimensionScores?: { dimensionId: string; score: number | null; dimension?: { title: string } }[]; -} - -function buildRadarData(e: EvalRow) { - const dimensions = e.template?.dimensions ?? []; - const scoreMap = new Map( - (e.dimensionScores ?? []).map((ds) => [ds.dimensionId, ds]) - ); - return 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 s = Number(score); - if (Number.isNaN(s) || s < 0 || s > 5) return null; - const title = dim.title ?? ""; - 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); -} - -export default function DashboardPage() { - const [evaluations, setEvaluations] = useState([]); - const [loading, setLoading] = useState(true); - const [deleteTarget, setDeleteTarget] = useState(null); - - useEffect(() => { - fetch("/api/evaluations") - .then((r) => r.json()) - .then(setEvaluations) - .catch(() => []) - .finally(() => setLoading(false)); - }, []); - - return ( -
-
-

Évaluations

- - + nouvelle - -
- - {loading ? ( -
loading...
- ) : evaluations.length === 0 ? ( -
- Aucune évaluation.{" "} - - Créer - -
- ) : ( -
- {evaluations.map((e) => { - const radarData = buildRadarData(e); - return ( - -
-
-
-

{e.candidateName}

-

- {e.candidateRole} - {e.candidateTeam && ` · ${e.candidateTeam}`} -

-
- - {e.status === "submitted" ? "ok" : "draft"} - -
-
- {e.evaluatorName} - {format(new Date(e.evaluationDate), "yyyy-MM-dd")} - {e.template?.name ?? ""} -
-
- {radarData.length > 0 ? ( - - ) : ( -
- pas de scores -
- )} -
-
-
- → ouvrir - -
- - ); - })} -
- )} - - { - if (!deleteTarget) return; - const res = await fetch(`/api/evaluations/${deleteTarget.id}`, { method: "DELETE" }); - if (res.ok) setEvaluations((prev) => prev.filter((x) => x.id !== deleteTarget.id)); - else alert("Erreur lors de la suppression"); - }} - onCancel={() => setDeleteTarget(null)} - /> -
- ); + return ; } diff --git a/src/app/evaluations/[id]/page.tsx b/src/app/evaluations/[id]/page.tsx index 26ebc09..ea808f9 100644 --- a/src/app/evaluations/[id]/page.tsx +++ b/src/app/evaluations/[id]/page.tsx @@ -1,215 +1,20 @@ -"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"; +import { redirect } from "next/navigation"; +import { getEvaluation, getTemplates, getUsers } from "@/lib/server-data"; +import { EvaluationEditor } from "@/components/EvaluationEditor"; -interface Dimension { - id: string; - slug: string; - title: string; - rubric: string; - suggestedQuestions?: string | null; +interface PageProps { + params: Promise<{ id: string }>; } -interface DimensionScore { - id: string; - dimensionId: string; - score: number | null; - justification: string | null; - examplesObserved: string | null; - confidence: string | null; - candidateNotes: string | null; - dimension: Dimension; -} +export default async function EvaluationDetailPage({ params }: PageProps) { + const { id } = await params; + const [evaluation, templates, users] = await Promise.all([ + getEvaluation(id), + getTemplates(), + getUsers(), + ]); -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 } }[]; - isPublic?: boolean; -} - -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, - isPublic: toSave.isPublic ?? false, - 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 (
@@ -221,234 +26,23 @@ export default function EvaluationDetailPage() { ); } - 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); + if (!users) redirect("/auth/login"); + + const templatesForEditor = templates.map((t) => ({ + id: t.id, + name: t.name, + dimensions: t.dimensions.map((d) => ({ + id: d.id, + suggestedQuestions: d.suggestedQuestions, + })), + })); 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 -

- -
-
- -