9f0bd87c40
- Projectenpagina met pipeline (standaard) en tabelweergave, filters op status, Rikx Ronde en zoeken - Projectpagina in stappen: Project plan (velden uit de Rikx-aanvraag) en Rikx calculatie achter een schuif, live berekend volgens de rekenmethode - Rikx rondes beheren (label, Rikx waarde, Rikx prijs) en koppelen aan projecten - Oranje Rikx-menubalk met menu per module, gedeeld door alle pagina's - Oude offerte-tabellen opgeruimd: leeg wordt verwijderd, met data hernoemd naar legacy_* - CLAUDE.md bijgewerkt naar de huidige stand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import Link from "next/link";
|
|
import { redirect } from "next/navigation";
|
|
import { getSession } from "../../lib/auth";
|
|
import { getDb, listRikxRondes, type ProjectListItem } from "../../lib/db";
|
|
import { PROJECT_STATUSES, isProjectStatus } from "../../lib/projectStatus";
|
|
import { AppHeader } from "../AppHeader";
|
|
import { AutoSubmitSelect } from "./AutoSubmitSelect";
|
|
import { ProjectPipeline } from "./ProjectPipeline";
|
|
import { ProjectTable } from "./ProjectTable";
|
|
|
|
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 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-rikx-orange";
|
|
const labelClass = "block text-sm text-zinc-700 dark:text-zinc-300 mb-1";
|
|
|
|
export default async function ProjectenPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ q?: string; status?: string; ronde?: string; weergave?: string }>;
|
|
}) {
|
|
const session = await getSession();
|
|
if (!session) redirect("/login");
|
|
|
|
const params = await searchParams;
|
|
const q = (params.q ?? "").trim();
|
|
const status = isProjectStatus(params.status) ? params.status : "";
|
|
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! : "";
|
|
// Pipeline is altijd de standaardweergave; de tabel alleen als die expliciet gekozen is.
|
|
const view = params.weergave === "tabel" ? "tabel" : "pipeline";
|
|
|
|
const where: string[] = [];
|
|
const args: (string | number)[] = [];
|
|
if (q) {
|
|
where.push("p.name LIKE ? ESCAPE '\\'");
|
|
args.push(`%${q.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
}
|
|
if (status) {
|
|
where.push("p.status = ?");
|
|
args.push(status);
|
|
}
|
|
if (ronde === "geen") {
|
|
where.push("p.ronde_id IS NULL");
|
|
} else if (ronde) {
|
|
where.push("p.ronde_id = ?");
|
|
args.push(Number(ronde));
|
|
}
|
|
const projects = getDb()
|
|
.prepare(
|
|
`SELECT p.*, r.label AS ronde_label
|
|
FROM projects p LEFT JOIN rikx_rondes r ON r.id = p.ronde_id
|
|
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
ORDER BY p.created_at DESC, p.id DESC`
|
|
)
|
|
.all(...args) as ProjectListItem[];
|
|
const isFiltered = Boolean(q || status || ronde);
|
|
|
|
function viewHref(target: "pipeline" | "tabel") {
|
|
const sp = new URLSearchParams();
|
|
if (target === "tabel") sp.set("weergave", "tabel");
|
|
if (status) sp.set("status", status);
|
|
if (ronde) sp.set("ronde", ronde);
|
|
if (q) sp.set("q", q);
|
|
const qs = sp.toString();
|
|
return qs ? `/projecten?${qs}` : "/projecten";
|
|
}
|
|
|
|
const toggleClass = (isActive: boolean) =>
|
|
isActive
|
|
? "rounded-lg bg-zinc-900 dark:bg-zinc-100 px-5 py-2.5 text-sm font-medium text-white dark:text-zinc-900"
|
|
: "rounded-lg px-5 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors";
|
|
|
|
return (
|
|
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
|
<AppHeader email={session.email} active="projecten" />
|
|
|
|
<main className="max-w-[96rem] mx-auto px-4 sm:px-6 py-6 flex flex-col gap-6">
|
|
<div className="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 sm:p-5 flex flex-wrap items-end gap-4">
|
|
<nav aria-label="Weergave" className="flex items-center gap-1">
|
|
<Link href={viewHref("pipeline")} className={toggleClass(view === "pipeline")} aria-current={view === "pipeline" ? "page" : undefined}>
|
|
Pipeline
|
|
</Link>
|
|
<Link href={viewHref("tabel")} className={toggleClass(view === "tabel")} aria-current={view === "tabel" ? "page" : undefined}>
|
|
Tabel
|
|
</Link>
|
|
</nav>
|
|
|
|
<form action="/projecten" className="contents">
|
|
{view === "tabel" && <input type="hidden" name="weergave" value="tabel" />}
|
|
<div className="w-full sm:w-56">
|
|
<label htmlFor="filter-status" className={labelClass}>
|
|
Status
|
|
</label>
|
|
<AutoSubmitSelect id="filter-status" name="status" defaultValue={status} className={fieldClass}>
|
|
<option value="">Alle</option>
|
|
{PROJECT_STATUSES.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.label}
|
|
</option>
|
|
))}
|
|
</AutoSubmitSelect>
|
|
</div>
|
|
<div className="w-full sm:w-56">
|
|
<label htmlFor="filter-ronde" className={labelClass}>
|
|
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>
|
|
<div className="w-full sm:w-auto sm:flex-1 sm:min-w-56">
|
|
<label htmlFor="filter-q" className={labelClass}>
|
|
Zoeken
|
|
</label>
|
|
<input
|
|
id="filter-q"
|
|
type="search"
|
|
name="q"
|
|
defaultValue={q}
|
|
placeholder="Projectnaam"
|
|
className={fieldClass}
|
|
/>
|
|
</div>
|
|
</form>
|
|
|
|
<Link
|
|
href="/projecten/nieuw"
|
|
className="rounded-lg bg-rikx-maroon hover:bg-rikx-maroon-light px-5 py-2.5 text-sm font-medium text-white transition-colors whitespace-nowrap"
|
|
>
|
|
+ Project toevoegen
|
|
</Link>
|
|
</div>
|
|
|
|
{isFiltered && (
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<p className="text-zinc-500">
|
|
{projects.length} {projects.length === 1 ? "project" : "projecten"} gevonden
|
|
</p>
|
|
<Link
|
|
href={view === "tabel" ? "/projecten?weergave=tabel" : "/projecten"}
|
|
className="text-rikx-orange-dark hover:underline"
|
|
>
|
|
Filters wissen
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{view === "pipeline" ? (
|
|
<ProjectPipeline projects={projects} status={status} />
|
|
) : (
|
|
<ProjectTable projects={projects} isFiltered={isFiltered} />
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|