Files
rikx-beheer/app/waardebepaling/page.tsx
T
remco-vermeer 53bf667f54 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>
2026-10-08 14:42:22 +02:00

103 lines
4.3 KiB
TypeScript

import { redirect } from "next/navigation";
import { requireAdmin } from "../../lib/auth";
import { getDb, listRikxRondes, type Project } from "../../lib/db";
import { listBeoordelingen } from "../../lib/experts";
import { listAccountNames } from "../../lib/platform-db";
import { rikxInputFromProject } from "../../lib/rikxCalculatie";
import { AppHeader } from "../AppHeader";
import { AutoSubmitSelect } from "../projecten/AutoSubmitSelect";
import { WaardebepalingTable, type WaardebepalingRow } from "./WaardebepalingTable";
// Alle projecten met status Waardebepaling, gerangschikt op Kosten/impact van laag naar hoog (laag = efficiënt).
export default async function WaardebepalingPage({
searchParams,
}: {
searchParams: Promise<{ ronde?: string }>;
}) {
const session = await requireAdmin();
const params = await searchParams;
const rondes = listRikxRondes();
// "geen" = projecten zonder ronde; anders het id van een bestaande ronde
const ronde =
params.ronde === "geen" || rondes.some((r) => String(r.id) === params.ronde) ? params.ronde! : "";
const db = getDb();
const where = ["status = 'waardebepaling'"];
const args: number[] = [];
if (ronde === "geen") {
where.push("ronde_id IS NULL");
} else if (ronde) {
where.push("ronde_id = ?");
args.push(Number(ronde));
}
const projects = (db.prepare(`SELECT * FROM projects WHERE ${where.join(" AND ")} ORDER BY id`).all(...args) as Project[]).map(
(p) => ({ ...p })
);
const scores = listBeoordelingen(projects.map((p) => p.id));
const impactmakers = new Map(listAccountNames().map((a) => [a.id, a.name]));
const rows: WaardebepalingRow[] = projects.map((p) => {
const projectRonde = rondes.find((r) => r.id === p.ronde_id) ?? null;
return {
id: p.id,
name: p.name,
impactmaker: p.impactmaker_account_id ? impactmakers.get(p.impactmaker_account_id) ?? null : null,
ronde: projectRonde ? { id: projectRonde.id, label: projectRonde.label, rikxWaarde: projectRonde.rikx_waarde } : null,
input: rikxInputFromProject(
p,
scores.filter((s) => s.project_id === p.id),
projectRonde
),
};
});
const fieldClass =
"w-full h-10 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-rikx-orange";
return (
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
<AppHeader email={session.email} active="projecten" />
<main className="max-w-6xl mx-auto px-4 sm:px-6 py-8 flex flex-col gap-6">
<div>
<h1 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">Waardebepaling projecten</h1>
<p className="text-sm text-zinc-500 mt-1">
Projecten met status Waardebepaling, gerangschikt op kosten/impact (laagste eerst).
</p>
</div>
<WaardebepalingTable
rows={rows}
toon75Procent={ronde !== "" && ronde !== "geen"}
filter={
// `contents`: het formulier zelf neemt geen ruimte in, zodat het filter in dezelfde rij
// komt te staan als het Rikx Waarde-veld (zie WaardebepalingTable)
// autoComplete="off": de browser mag de keuze niet herstellen na "Terug", anders toont
// de dropdown een andere ronde dan de lijst
<form action="/waardebepaling" autoComplete="off" className="contents">
<div className="w-full sm:w-64">
<label htmlFor="filter-ronde" className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
Rikx Ronde
</label>
<AutoSubmitSelect id="filter-ronde" name="ronde" defaultValue={ronde} className={fieldClass}>
<option value="">Alle</option>
{rondes.map((r) => (
<option key={r.id} value={r.id}>
{r.label}
</option>
))}
<option value="geen">Zonder ronde</option>
</AutoSubmitSelect>
</div>
</form>
}
emptyMessage={
ronde ? "Geen projecten in waardebepaling voor deze ronde." : "Er zijn geen projecten in waardebepaling."
}
/>
</main>
</div>
);
}