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_ 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 })); }