Files
rikx-beheer/lib/db.ts
T
remco-vermeer 9f0bd87c40 Voeg Module 1 Projecten toe met project plan, Rikx calculatie en Rikx rondes
- 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>
2026-10-08 10:26:30 +02:00

165 lines
5.6 KiB
TypeScript

import { DatabaseSync } from "node:sqlite";
import path from "path";
const DB_PATH =
process.env.DB_PATH || path.join(process.cwd(), "data", "app.db");
let _db: DatabaseSync | null = null;
export function getDb(): DatabaseSync {
if (!_db) {
_db = new DatabaseSync(DB_PATH);
_db.exec("PRAGMA journal_mode = WAL");
_db.exec("PRAGMA foreign_keys = ON");
migrate(_db);
}
return _db;
}
function migrate(db: DatabaseSync) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'admin',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
dropLegacyQuoteTables(db);
db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'aanvraag',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
-- Rikx Ronde: investeringsronde (bv. van een gemeente) waarin projecten meedoen.
-- Rikx waarde en Rikx prijs gelden voor alle projecten in de ronde.
CREATE TABLE IF NOT EXISTS rikx_rondes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL UNIQUE,
rikx_waarde REAL,
rikx_prijs REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Expertscores per project voor de Rikx calculatie (Impactscore M).
-- Voorlopig handmatig ingevuld per expertnummer; Module 4 koppelt dit later aan expert-logins.
CREATE TABLE IF NOT EXISTS project_expert_scores (
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
expert INTEGER NOT NULL,
duur_impact REAL,
levensdomeinen REAL,
basissituatie REAL,
PRIMARY KEY (project_id, expert)
);
`);
// Velden uit _doc/Rikx rekenmethode uitleg.xlsx
// Stap "Project plan" (ingevuld door de sociaal ondernemer)
addColumnIfMissing(db, "projects", "projectkosten", "REAL");
addColumnIfMissing(db, "projects", "deelnemers_werk", "INTEGER");
addColumnIfMissing(db, "projects", "deelnemers_skills", "INTEGER");
addColumnIfMissing(db, "projects", "financiering_extern_bedrag", "REAL");
addColumnIfMissing(db, "projects", "financiering_extern_pct", "REAL");
// Overige project plan-velden als JSON; definities in lib/projectPlan.ts
addColumnIfMissing(db, "projects", "project_plan", "TEXT");
// Stap "Rikx calculatie" (alleen zichtbaar als rikx_calculatie_aan = 1)
addColumnIfMissing(db, "projects", "rikx_calculatie_aan", "INTEGER NOT NULL DEFAULT 0");
addColumnIfMissing(db, "projects", "bonus_malus", "TEXT");
addColumnIfMissing(db, "projects", "ronde_id", "INTEGER REFERENCES rikx_rondes(id) ON DELETE SET NULL");
db.exec("CREATE INDEX IF NOT EXISTS idx_projects_ronde ON projects(ronde_id)");
}
function addColumnIfMissing(db: DatabaseSync, table: string, column: string, definition: string) {
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
if (!columns.some((c) => c.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
}
}
// Ruimt de tabellen van het oude offerte-portaal op (clients, projects, quotes,
// quote_items). Lege tabellen worden verwijderd; bevat een tabel onverwacht
// gegevens, dan wordt die hernoemd naar legacy_<naam> zodat er niets verloren gaat.
function dropLegacyQuoteTables(db: DatabaseSync) {
const projectColumns = db.prepare("PRAGMA table_info(projects)").all() as { name: string }[];
if (!projectColumns.some((c) => c.name === "client_id")) return;
const tables = ["quote_items", "quotes", "projects", "clients"];
const existing = new Set(
(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all() as { name: string }[]).map(
(t) => t.name
)
);
db.exec("BEGIN");
try {
for (const table of tables) {
if (!existing.has(table)) continue;
const { n } = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number };
if (n === 0) {
db.exec(`DROP TABLE ${table}`);
} else {
console.warn(`Oude tabel ${table} bevat ${n} rijen; hernoemd naar legacy_${table}`);
db.exec(`ALTER TABLE ${table} RENAME TO legacy_${table}`);
}
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
throw err;
}
}
export type Project = {
id: number;
name: string;
status: string;
projectkosten: number | null;
deelnemers_werk: number | null;
deelnemers_skills: number | null;
financiering_extern_bedrag: number | null;
financiering_extern_pct: number | null;
project_plan: string | null;
rikx_calculatie_aan: number;
bonus_malus: string | null;
ronde_id: number | null;
created_at: string;
updated_at: string;
};
// Project met het label van de gekoppelde Rikx Ronde (voor overzichten)
export type ProjectListItem = Project & { ronde_label: string | null };
export type ProjectExpertScore = {
project_id: number;
expert: number;
duur_impact: number | null;
levensdomeinen: number | null;
basissituatie: number | null;
};
export type RikxRonde = {
id: number;
label: string;
rikx_waarde: number | null;
rikx_prijs: number | null;
created_at: string;
updated_at: string;
};
export function listRikxRondes(): RikxRonde[] {
return (
getDb().prepare("SELECT * FROM rikx_rondes ORDER BY created_at DESC, id DESC").all() as RikxRonde[]
).map((r) => ({ ...r }));
}