All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 7m6s
- Nouvelle action updateDimensionScore pour sauvegarder un seul champ en base sans envoyer tout le formulaire - DimensionCard : blur sur notes, justification, exemples, confiance → upsert ciblé + bordure violette 800ms - CandidateForm : même pattern sur tous les champs du cartouche - Bouton save passe aussi en violet (cohérence visuelle) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
7.3 KiB
TypeScript
215 lines
7.3 KiB
TypeScript
"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<T = void> = { success: true; data?: T } | { success: false; error: string };
|
|
|
|
export async function fetchEvaluation(id: string): Promise<ActionResult<Awaited<ReturnType<typeof getEvaluation>>>> {
|
|
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<ActionResult> {
|
|
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<ActionResult<{ id: string }>> {
|
|
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 updateDimensionScore(
|
|
evaluationId: string,
|
|
dimensionId: string,
|
|
data: { score?: number | null; justification?: string | null; examplesObserved?: string | null; confidence?: string | null; candidateNotes?: string | null }
|
|
): Promise<ActionResult> {
|
|
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.dimensionScore.upsert({
|
|
where: { evaluationId_dimensionId: { evaluationId, dimensionId } },
|
|
update: data,
|
|
create: { evaluationId, dimensionId, ...data },
|
|
});
|
|
revalidatePath(`/evaluations/${evaluationId}`);
|
|
return { success: true };
|
|
} catch (e) {
|
|
return { success: false, error: e instanceof Error ? e.message : "Erreur" };
|
|
}
|
|
}
|
|
|
|
export async function updateEvaluation(id: string, data: UpdateEvaluationInput): Promise<ActionResult> {
|
|
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<string, unknown> = {};
|
|
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<string, unknown> });
|
|
}
|
|
|
|
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" };
|
|
}
|
|
}
|