Refactor EvaluationEditor component by removing unused Link import. Enhance ExportModal to include Confluence export option and update export-utils with functions for parsing questions and rubrics, and generating Confluence markup for evaluations.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
This commit is contained in:
39
src/app/api/export/confluence/route.ts
Normal file
39
src/app/api/export/confluence/route.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
import { evaluationToConfluenceMarkup } from "@/lib/export-utils";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const id = searchParams.get("id");
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Evaluation id required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluation = await prisma.evaluation.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
template: true,
|
||||||
|
dimensionScores: { include: { dimension: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!evaluation) {
|
||||||
|
return NextResponse.json({ error: "Evaluation not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const markup = evaluationToConfluenceMarkup(
|
||||||
|
evaluation as Parameters<typeof evaluationToConfluenceMarkup>[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
return new NextResponse(markup, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Content-Disposition": `attachment; filename="guide-entretien-${id}.md"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: "Export failed" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { updateEvaluation, deleteEvaluation, fetchEvaluation } from "@/actions/evaluations";
|
import { updateEvaluation, deleteEvaluation, fetchEvaluation } from "@/actions/evaluations";
|
||||||
import { CandidateForm } from "@/components/CandidateForm";
|
import { CandidateForm } from "@/components/CandidateForm";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function ExportModal({ isOpen, onClose, evaluationId }: ExportModalProps)
|
|||||||
const base = typeof window !== "undefined" ? window.location.origin : "";
|
const base = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
const csvUrl = `${base}/api/export/csv?id=${evaluationId}`;
|
const csvUrl = `${base}/api/export/csv?id=${evaluationId}`;
|
||||||
const pdfUrl = `${base}/api/export/pdf?id=${evaluationId}`;
|
const pdfUrl = `${base}/api/export/pdf?id=${evaluationId}`;
|
||||||
|
const confluenceUrl = `${base}/api/export/confluence?id=${evaluationId}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -37,6 +38,14 @@ export function ExportModal({ isOpen, onClose, evaluationId }: ExportModalProps)
|
|||||||
>
|
>
|
||||||
pdf
|
pdf
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href={confluenceUrl}
|
||||||
|
download
|
||||||
|
className="rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-100 dark:bg-zinc-700 px-4 py-2 text-center font-mono text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||||
|
title="Guide d'entretien (questions + grille) au format Confluence wiki markup"
|
||||||
|
>
|
||||||
|
confluence
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -5,6 +5,28 @@ export interface EvaluationWithScores extends Evaluation {
|
|||||||
dimensionScores: (DimensionScore & { dimension: TemplateDimension })[];
|
dimensionScores: (DimensionScore & { dimension: TemplateDimension })[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse suggestedQuestions JSON array */
|
||||||
|
function parseQuestions(s: string | null | undefined): string[] {
|
||||||
|
if (!s) return [];
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(s) as unknown;
|
||||||
|
return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse rubric "1:X;2:Y;..." into labels */
|
||||||
|
function parseRubric(rubric: string): string[] {
|
||||||
|
if (rubric === "1-5" || !rubric) return ["1", "2", "3", "4", "5"];
|
||||||
|
const labels: string[] = [];
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
const m = rubric.match(new RegExp(`${i}:([^;]+)`));
|
||||||
|
labels.push(m ? m[1].trim() : String(i));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
/** Compute average score across dimensions (1-5 scale) */
|
/** Compute average score across dimensions (1-5 scale) */
|
||||||
export function computeAverageScore(scores: { score: number | null }[]): number {
|
export function computeAverageScore(scores: { score: number | null }[]): number {
|
||||||
const valid = scores.filter((s) => s.score != null && s.score >= 1 && s.score <= 5);
|
const valid = scores.filter((s) => s.score != null && s.score >= 1 && s.score <= 5);
|
||||||
@@ -81,3 +103,66 @@ export function evaluationToCsvRows(evalData: EvaluationWithScores): string[][]
|
|||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONFIDENCE_LABELS: Record<string, string> = {
|
||||||
|
low: "Faible",
|
||||||
|
med: "Moyenne",
|
||||||
|
high: "Haute",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Convert evaluation template (dimensions + questions + rubric) to Markdown for Confluence paste */
|
||||||
|
export function evaluationToConfluenceMarkup(evalData: EvaluationWithScores): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`# Guide d'entretien — ${evalData.template?.name ?? "Évaluation"}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`**Candidat:** ${evalData.candidateName} | **Rôle:** ${evalData.candidateRole}`);
|
||||||
|
if (evalData.candidateTeam) lines.push(`**Équipe:** ${evalData.candidateTeam}`);
|
||||||
|
lines.push(`**Évaluateur:** ${evalData.evaluatorName} | **Date:** ${evalData.evaluationDate.toISOString().split("T")[0]}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("## Système de notation");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Chaque dimension est notée de **1** (faible) à **5** (expert). La grille ci-dessous détaille les critères par niveau.");
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
for (const ds of evalData.dimensionScores) {
|
||||||
|
const dim = ds.dimension;
|
||||||
|
const title = (dim as { title?: string; name?: string }).title ?? (dim as { name?: string }).name ?? "";
|
||||||
|
lines.push(`## ${title}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
const questions = parseQuestions((dim as { suggestedQuestions?: string | null }).suggestedQuestions);
|
||||||
|
if (questions.length > 0) {
|
||||||
|
lines.push("### Questions suggérées");
|
||||||
|
questions.forEach((q, i) => {
|
||||||
|
lines.push(`${i + 1}. ${q}`);
|
||||||
|
});
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rubricLabels = parseRubric((dim as { rubric?: string }).rubric ?? "");
|
||||||
|
if (rubricLabels.length > 0) {
|
||||||
|
lines.push("### Grille");
|
||||||
|
rubricLabels.forEach((label, i) => {
|
||||||
|
lines.push(`- ${i + 1}: ${label}`);
|
||||||
|
});
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("### Notes évaluateur");
|
||||||
|
if (ds.score != null) {
|
||||||
|
lines.push(`- **Score:** ${ds.score}/5`);
|
||||||
|
if (ds.confidence)
|
||||||
|
lines.push(`- **Confiance:** ${CONFIDENCE_LABELS[ds.confidence] ?? ds.confidence}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`- **Score:** _à compléter_`);
|
||||||
|
lines.push(`- **Confiance:** _à compléter_`);
|
||||||
|
}
|
||||||
|
lines.push(`- **Notes candidat:** ${ds.candidateNotes ?? "_à compléter_"}`);
|
||||||
|
lines.push(`- **Justification:** ${ds.justification ?? "_à compléter_"}`);
|
||||||
|
lines.push(`- **Exemples:** ${ds.examplesObserved ?? "_à compléter_"}`);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user