Enhance project setup with Prisma, new scripts, and dependencies; update README for clarity and add API routes; improve layout and styling for better user experience

This commit is contained in:
Julien Froidefond
2026-02-20 09:12:37 +01:00
parent 4ecd13a93a
commit f0c5d768db
33 changed files with 4277 additions and 107 deletions

View File

@@ -0,0 +1,176 @@
import { NextRequest, NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/db";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const evaluation = await prisma.evaluation.findUnique({
where: { id },
include: {
template: {
include: {
dimensions: { orderBy: { orderIndex: "asc" } },
},
},
dimensionScores: { include: { dimension: true } },
},
});
if (!evaluation) {
return NextResponse.json({ error: "Evaluation not found" }, { status: 404 });
}
// 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 { id } = await params;
const body = await req.json();
const { candidateName, candidateRole, evaluatorName, evaluationDate, status, findings, recommendations, dimensionScores } = body;
const existing = await prisma.evaluation.findUnique({ where: { id } });
if (!existing) {
return NextResponse.json({ error: "Evaluation not found" }, { status: 404 });
}
const updateData: Record<string, unknown> = {};
if (candidateName != null) updateData.candidateName = candidateName;
if (candidateRole != null) updateData.candidateRole = candidateRole;
if (evaluatorName != null) updateData.evaluatorName = evaluatorName;
if (evaluationDate != null) updateData.evaluationDate = new Date(evaluationDate);
if (status != null) updateData.status = status;
if (findings != null) updateData.findings = findings;
if (recommendations != null) updateData.recommendations = recommendations;
if (Object.keys(updateData).length > 0) {
await prisma.auditLog.create({
data: {
evaluationId: id,
action: "updated",
newValue: JSON.stringify(updateData),
},
});
}
const evaluation = await prisma.evaluation.update({
where: { id },
data: updateData,
include: {
template: { include: { dimensions: { orderBy: { orderIndex: "asc" } } } },
dimensionScores: { include: { dimension: true } },
},
});
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);
return NextResponse.json({ error: "Failed to update evaluation" }, { status: 500 });
}
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
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 });
}
}

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const status = searchParams.get("status");
const templateId = searchParams.get("templateId");
const evaluations = await prisma.evaluation.findMany({
where: {
...(status && { status }),
...(templateId && { templateId }),
},
include: {
template: true,
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 body = await req.json();
const { candidateName, candidateRole, evaluatorName, evaluationDate, templateId } = body;
if (!candidateName || !candidateRole || !evaluatorName || !evaluationDate || !templateId) {
return NextResponse.json(
{ error: "Missing required fields: candidateName, candidateRole, evaluatorName, evaluationDate, templateId" },
{ status: 400 }
);
}
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,
evaluatorName,
evaluationDate: new Date(evaluationDate),
templateId,
status: "draft",
},
include: {
template: true,
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: true,
dimensionScores: { include: { dimension: true } },
},
});
return NextResponse.json(updated);
} catch (e) {
console.error(e);
return NextResponse.json({ error: "Failed to create evaluation" }, { status: 500 });
}
}