Voeg Module 4 experts, waardebepalingspagina en projectverbeteringen toe

Experts (Module 4)
- Pagina Experts (CRM): experts uitnodigen per e-mail (zonder SMTP wordt de
  link getoond), rondes koppelen, opnieuw uitnodigen, verwijderen en
  "Inloggen als" (alleen lezen) om mee te kijken
- Uitnodigingspagina waar een expert zijn wachtwoord instelt (token 7 dagen
  geldig, eenmalig, alleen als hash opgeslagen)
- Expertpagina: projecten in waardebepaling uit de eigen rondes, project plan
  lezen en eigen beoordeling geven (3 scores, advies Bonus/Malus, toelichting)
- Rollen admin/expert: alle beheerpagina's en -acties controleren de rol
- Rikx calculatie gebruikt de beoordelingen van de experts in plaats van de
  handmatige expertkolommen; experts koppelen ook vanaf de rondepagina

Waardebepaling projecten
- Nieuwe pagina met projecten in waardebepaling op kosten/impact (laag naar
  hoog), filter op ronde, Rikx Waarde-veld dat live doorrekent en opslaat in
  de ronde, en een 75%-lijn bij een gekozen ronde

Projecten
- Rikx calculatie als tab, zichtbaar vanaf status Waardebepaling, met keuze
  rekenmethode (Arbeid; Jeugd nog niet beschikbaar) en beoordeling
  goedgekeurd/afgekeurd met reden
- Impactmaker gekoppeld aan een sociaal ondernemer
- Opslaan-knop bovenaan; ongeldige velden op een verborgen tab worden getoond
- Project toevoegen maakt direct een project aan; aparte pagina verwijderd
- Waarderingssystematiek Rikx juni 2023 toegevoegd aan _doc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:42:22 +02:00
parent 9f0bd87c40
commit 53bf667f54
41 changed files with 2075 additions and 357 deletions
+25 -11
View File
@@ -1,9 +1,12 @@
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { getSession } from "../../../lib/auth";
import { getDb, listRikxRondes, type Project, type ProjectExpertScore } from "../../../lib/db";
import { requireAdmin } from "../../../lib/auth";
import { getDb, listRikxRondes, type Project } from "../../../lib/db";
import { listBeoordelingen } from "../../../lib/experts";
import { projectStatusBadgeClass, projectStatusLabel } from "../../../lib/projectStatus";
import { listAccountNames } from "../../../lib/platform-db";
import { AppHeader } from "../../AppHeader";
import { PROJECT_FORM_ID } from "./formId";
import { ProjectDetailForm } from "./ProjectDetailForm";
export default async function ProjectPage({
@@ -11,28 +14,25 @@ export default async function ProjectPage({
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ error?: string; opgeslagen?: string }>;
searchParams: Promise<{ error?: string; opgeslagen?: string; tab?: string }>;
}) {
const session = await getSession();
if (!session) redirect("/login");
const session = await requireAdmin();
const { id } = await params;
const { error, opgeslagen } = await searchParams;
const { error, opgeslagen, tab } = await searchParams;
const db = getDb();
const row = db.prepare("SELECT * FROM projects WHERE id = ?").get(Number(id)) as Project | undefined;
if (!row) notFound();
// node:sqlite levert objecten met null-prototype; spreiden maakt ze doorgeefbaar aan client components.
const project = { ...row };
const expertScores = (
db.prepare("SELECT * FROM project_expert_scores WHERE project_id = ? ORDER BY expert").all(project.id) as ProjectExpertScore[]
).map((s) => ({ ...s }));
const beoordelingen = listBeoordelingen([project.id]);
const message =
error === "missing"
? { tone: "error", text: "Vul een projectnaam in." }
: error === "invalid"
? { tone: "error", text: "Niet opgeslagen: controleer de getallen (percentage 0–100, expertscores 1–10, geen negatieve waarden)." }
? { tone: "error", text: "Niet opgeslagen: controleer de getallen (percentage 0–100, geen negatieve waarden)." }
: error === "datum"
? { tone: "error", text: "Niet opgeslagen: controleer de datums (de einddatum mag niet vóór de startdatum liggen)." }
: opgeslagen
@@ -57,6 +57,14 @@ export default async function ProjectPage({
>
{projectStatusLabel(project.status)}
</span>
{/* Verstuurt het projectformulier hieronder (form-attribuut) */}
<button
type="submit"
form={PROJECT_FORM_ID}
className="ml-auto rounded-lg bg-rikx-maroon hover:bg-rikx-maroon-light px-5 py-2 text-sm font-medium text-white transition-colors"
>
Opslaan
</button>
</div>
{message && (
<p
@@ -69,7 +77,13 @@ export default async function ProjectPage({
{message.text}
</p>
)}
<ProjectDetailForm project={project} expertScores={expertScores} rondes={listRikxRondes()} />
<ProjectDetailForm
project={project}
beoordelingen={beoordelingen}
rondes={listRikxRondes()}
impactmakers={listAccountNames()}
initialTab={tab === "calculatie" ? "calculatie" : "plan"}
/>
</main>
</div>
);