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>
This commit is contained in:
@@ -26,49 +26,139 @@ function migrate(db: DatabaseSync) {
|
||||
role TEXT NOT NULL DEFAULT 'admin',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
address TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
dropLegacyQuoteTables(db);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
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 TABLE IF NOT EXISTS quotes (
|
||||
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,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id),
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
notes TEXT,
|
||||
sent_at TEXT,
|
||||
accepted_at TEXT,
|
||||
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'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS quote_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
quote_id INTEGER NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
|
||||
description TEXT NOT NULL,
|
||||
quantity REAL NOT NULL DEFAULT 1,
|
||||
unit TEXT,
|
||||
unit_price REAL NOT NULL DEFAULT 0,
|
||||
total REAL GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
-- 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 }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user