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:
@@ -0,0 +1,102 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user