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>
165 lines
5.6 KiB
TypeScript
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 }));
|
|
}
|