Unnötige Datein gelöscht. DB normalisiert
This commit is contained in:
1 parent
da64472251
commit
4e083bee73
32 files changed
+971
-5542
No files matched your search
@@ -1,19 +1,32 @@
|
||||
/*
|
||||
* Express-Server der Projektübersicht: liefert die statischen Frontend-Dateien aus
|
||||
* public/ (Test.html unter "/") aus und stellt den Frontend-Modulen unter public/js/
|
||||
* eine CSV-API zum Laden und Speichern der Sitzungsdaten in sessions/ bereit.
|
||||
* Standardport ist 3003, überschreibbar über die Umgebungsvariable PORT.
|
||||
* Endpunkte: GET /, GET /health, GET /load-all, POST /save-csv, POST /delete-csv.
|
||||
* Die Daten werden als UTF-8-CSV mit BOM geschrieben; ungültige Abteilungen fallen
|
||||
* auf ELT zurück, Dateinamen werden über safeName() streng validiert.
|
||||
* public/ (Test.html unter "/") und stellt den Frontend-Modulen unter public/js/
|
||||
* eine JSON-API zum Laden und Speichern der Sitzungsdaten bereit. Die Daten liegen
|
||||
* in einer SQLite-Datenbank (node:sqlite, Node >= 22.13) unter sessions/projektuebersicht.db.
|
||||
*
|
||||
* Das Schema ist voll relational normalisiert: statt eines grossen JSON-Dokuments
|
||||
* pro Woche gibt es eigene Tabellen für Wochen-Metadaten, Wochen-Personen, Rollen,
|
||||
* Anwesenheit, Projekte, LPH-Werte, Projekt-Zuordnungen, Projektpläne, TODOs,
|
||||
* Meilensteine, Abhängigkeiten sowie die globalen Personen inkl. Rollen und
|
||||
* Abwesenheiten. Die HTTP-API bleibt davon unberührt: Das Frontend sendet und
|
||||
* erhält weiterhin komplette Wochen- und Personen-Objekte (JSON); der Server
|
||||
* verteilt sie beim Speichern auf die Tabellen und setzt sie beim Laden wieder
|
||||
* Zusammen (Serializer/Deserializer).
|
||||
*
|
||||
* Eine bestehende Datenbank aus dem alten Ein-Dokument-pro-Woche-Format wird
|
||||
* automatisch auf das relationale Schema umgestellt.
|
||||
*
|
||||
* Standardport ist 3003, überschreibbar über die Umgebungsvariable PORT.
|
||||
* Endpunkte: GET /, GET /health, GET /load-all, POST /save-week, POST /save-people,
|
||||
* POST /delete-week. Ungültige Abteilungen fallen auf ELT zurück.
|
||||
*/
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const app = express();
|
||||
|
||||
// Wichtige Konstanten: UTF-8-BOM für CSV-Dateien, Serverport (PORT-Env) und die gültigen Abteilungsordner
|
||||
const BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
|
||||
// Wichtige Konstanten: Serverport (PORT-Env) und die gültigen Abteilungsordner
|
||||
const PORT = process.env.PORT || 3003;
|
||||
const VALID_DEPARTMENTS = new Set(['ELT','HLS','ARCH']);
|
||||
|
||||
@@ -30,87 +43,743 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
||||
}
|
||||
}));
|
||||
|
||||
// Datenverzeichnis sessions/ bei Bedarf anlegen
|
||||
// Datenverzeichnis sessions/ bei Bedarf anlegen (bind-mounted in Docker, enthält die DB)
|
||||
const SESSIONS_DIR = path.join(__dirname, 'sessions');
|
||||
if(!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
||||
|
||||
// Erlaubt ausschliesslich sichere CSV-Dateinamen im Format <Name>.csv
|
||||
function safeName(name){
|
||||
const raw = String(name || '');
|
||||
// Pfad-Trennzeichen und Verzeichnisnavigation ablehnen
|
||||
if(/[\\/]|\.\./.test(raw)) return null;
|
||||
const base = path.basename(raw);
|
||||
if(!base || base.toLowerCase() === '.csv') return null;
|
||||
const safe = base.replace(/[^a-zA-Z0-9_\-\.]/g,'_');
|
||||
if(!/^([a-zA-Z0-9_\-]+)\.csv$/i.test(safe)) return null;
|
||||
return safe;
|
||||
// SQLite-Datenbank öffnen (wird angelegt, falls nicht vorhanden). Der Pfad ist
|
||||
// über die Umgebungsvariable DB_PATH übersteuerbar (für Tests/Backups), sonst
|
||||
// standardmäßig sessions/projektuebersicht.db.
|
||||
const DB_PATH = process.env.DB_PATH ? path.resolve(process.env.DB_PATH) : path.join(SESSIONS_DIR, 'projektuebersicht.db');
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
db.exec('PRAGMA journal_mode = WAL;');
|
||||
db.exec('PRAGMA foreign_keys = ON;');
|
||||
|
||||
// ---------- Schema (voll relational normalisiert) ----------
|
||||
|
||||
// Der serialisierte Datenbestand einer Woche besteht aus folgenden Entitäten:
|
||||
// - weeks: Metadaten (id, label, date, locked)
|
||||
// - week_people / week_people_roles: personen-Kopien pro Woche
|
||||
// - attendance: Anwesenheit pro Woche/Person
|
||||
// - projects: Projekt-Metadaten pro Woche
|
||||
// - project_lph: LPH-Fortschrittswerte pro Projekt
|
||||
// - project_assign: Zuordnungen (Haupt/Optional) pro Projekt/Person/Rolle
|
||||
// - project_plan: Plan-Kopf (start, end, progress) pro Projekt
|
||||
// - plan_people: im Plan beteiligte Personen
|
||||
// - plan_todos / plan_todo_assignees / plan_todo_deps: TODO-Listen
|
||||
// - plan_milestones / plan_milestone_deps: Meilensteine
|
||||
// - people / person_roles / person_absences: globale Personen pro Abteilung
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
|
||||
department TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
week_id TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
locked INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, label)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people_roles (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS attendance (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
present INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS projects (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
nr TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
"group" TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
order_value INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_lph (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_assign (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
state INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aufwand (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
value REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, lph, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aktuelle_lp (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_plan (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '',
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_people (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todos (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '',
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_assignees (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_deps (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestones (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
position TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestone_deps (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL,
|
||||
depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS people (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_roles (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_absences (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
absence TEXT NOT NULL,
|
||||
PRIMARY KEY (department, person_id, absence)
|
||||
)`);
|
||||
|
||||
// Erlaubt ausschliesslich die gültigen Abteilungen (ELT|HLS|ARCH, sonst ELT)
|
||||
function departmentKey(department){
|
||||
return (typeof department === 'string' && VALID_DEPARTMENTS.has(department)) ? department : 'ELT';
|
||||
}
|
||||
|
||||
// Liefert den Zielordner für eine Abteilung (nur ELT|HLS|ARCH, sonst ELT) und legt ihn bei Bedarf an
|
||||
function departmentDir(department){
|
||||
const dept = (typeof department === 'string' && VALID_DEPARTMENTS.has(department)) ? department : 'ELT';
|
||||
const dir = path.join(SESSIONS_DIR, dept);
|
||||
if(!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
// ---------- Migration: altes Ein-Dokument-Format -> relationales Schema ----------
|
||||
|
||||
// hasLegacyWeeksTable: erkennt das alte Schema an der Daten-Spalte in `weeks`.
|
||||
function hasLegacyWeeksTable(){
|
||||
const cols = db.prepare("SELECT name FROM pragma_table_info('weeks') WHERE name = 'data'").all();
|
||||
return cols.length > 0;
|
||||
}
|
||||
|
||||
// Route: Speichert eine CSV-Datei (mit UTF-8-BOM) für eine Abteilung im sessions/-Ordner
|
||||
app.post('/save-csv', (req, res) => {
|
||||
const { filename, csv, department } = req.body || {};
|
||||
if(!filename || typeof csv !== 'string') return res.status(400).json({ ok: false, error: 'Missing filename or csv' });
|
||||
const safe = safeName(filename);
|
||||
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' });
|
||||
const dir = departmentDir(department);
|
||||
const out = path.join(dir, safe);
|
||||
const data = Buffer.concat([BOM, Buffer.from(csv, 'utf8')]);
|
||||
fs.writeFile(out, data, (err) => {
|
||||
if(err){ console.error('Write error', err); return res.status(500).json({ ok:false, error: String(err) }); }
|
||||
console.log('Saved CSV:', out);
|
||||
return res.json({ ok: true, path: out });
|
||||
});
|
||||
});
|
||||
// serializeWeek: zerlegt ein Wochen-Objekt (JSON vom Frontend) in die relationalen
|
||||
// Tabellen. Wird in einer Transaktion vom Aufrufer verwendet (deleteWeekData +
|
||||
// serializeWeek). Ersetzt alle Zeilen der Woche.
|
||||
function serializeWeek(department, week){
|
||||
const weekLabel = String(week.label || '');
|
||||
const upsertWeek = db.prepare(`INSERT INTO weeks (department, label, week_id, date, locked) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(department, label) DO UPDATE SET week_id = excluded.week_id, date = excluded.date, locked = excluded.locked`);
|
||||
upsertWeek.run(department, weekLabel, String(week.id || ''), String(week.date || ''), week.locked ? 1 : 0);
|
||||
|
||||
// Route: Löscht eine CSV-Datei einer Abteilung aus dem sessions/-Ordner (fehlende Datei wird toleriert)
|
||||
app.post('/delete-csv', (req, res) => {
|
||||
const { filename, department } = req.body || {};
|
||||
if(!filename) return res.status(400).json({ ok: false, error: 'Missing filename' });
|
||||
const safe = safeName(filename);
|
||||
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' });
|
||||
const dir = departmentDir(department);
|
||||
const out = path.join(dir, safe);
|
||||
fs.unlink(out, (err) => {
|
||||
if(err){
|
||||
if(err.code === 'ENOENT') {
|
||||
console.log('CSV-Datei nicht gefunden:', out);
|
||||
return res.json({ ok: true, missing: true });
|
||||
}
|
||||
console.error('Delete error', err);
|
||||
return res.status(500).json({ ok:false, error: String(err) });
|
||||
}
|
||||
console.log('Deleted CSV:', out);
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
const insWeekPerson = db.prepare(`INSERT INTO week_people (department, week_label, person_id, name, full_name, array_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insWeekPersonRole = db.prepare(`INSERT OR IGNORE INTO week_people_roles (department, week_label, person_id, role, role_order)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insAttendance = db.prepare(`INSERT OR REPLACE INTO attendance (department, week_label, person_id, present)
|
||||
VALUES (?, ?, ?, ?)`);
|
||||
|
||||
// Route: Lädt alle Sitzungs-CSV-Dateien einer Abteilung plus _people.csv (nach Dateiname sortiert)
|
||||
app.get('/load-all', (req, res) => {
|
||||
const dir = departmentDir(req.query.department);
|
||||
fs.readdir(dir, (err, files) => {
|
||||
if(err){ console.error('Read error', err); return res.status(500).json({ ok:false, error: String(err) }); }
|
||||
const csvFiles = files.filter(f => f.toLowerCase().endsWith('.csv'));
|
||||
const result = { ok: true, sessions: [], people: null };
|
||||
csvFiles.forEach(filename => {
|
||||
if(filename.toLowerCase() === '_people.csv'){
|
||||
result.people = fs.readFileSync(path.join(dir, filename), 'utf8');
|
||||
return;
|
||||
}
|
||||
const content = fs.readFileSync(path.join(dir, filename), 'utf8');
|
||||
result.sessions.push({ filename, content });
|
||||
(Array.isArray(week.people) ? week.people : []).forEach((p, idx) => {
|
||||
insWeekPerson.run(department, weekLabel, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insWeekPersonRole.run(department, weekLabel, String(p.id || ''), String(r || ''), ridx));
|
||||
});
|
||||
const att = week.attendance || {};
|
||||
Object.keys(att).forEach(pid => {
|
||||
insAttendance.run(department, weekLabel, pid, att[pid] ? 1 : 0);
|
||||
});
|
||||
|
||||
const insProject = db.prepare(`INSERT INTO projects (department, week_label, project_id, nr, name, status, "group", array_order, order_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insLph = db.prepare(`INSERT OR REPLACE INTO project_lph (department, week_label, project_id, lph, value)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insAssign = db.prepare(`INSERT OR REPLACE INTO project_assign (department, week_label, project_id, person_id, role, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insAufwand = db.prepare(`INSERT OR REPLACE INTO project_aufwand (department, week_label, project_id, lph, role, value)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insAktuelleLp = db.prepare(`INSERT OR IGNORE INTO project_aktuelle_lp (department, week_label, project_id, lph)
|
||||
VALUES (?, ?, ?, ?)`);
|
||||
const insPlan = db.prepare(`INSERT OR REPLACE INTO project_plan (department, week_label, project_id, start, end, progress)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insPlanPerson = db.prepare(`INSERT OR IGNORE INTO plan_people (department, week_label, project_id, person_id, array_order)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insTodo = db.prepare(`INSERT INTO plan_todos (department, week_label, project_id, todo_id, title, start, end, done, progress, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insTodoAssignee = db.prepare(`INSERT OR IGNORE INTO plan_todo_assignees (department, week_label, project_id, todo_id, person_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (department, week_label, project_id, todo_id, depends_on_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insMilestone = db.prepare(`INSERT INTO plan_milestones (department, week_label, project_id, milestone_id, name, date, position, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
|
||||
(Array.isArray(week.projects) ? week.projects : []).forEach((p, idx) => {
|
||||
const projId = String(p.id || '');
|
||||
insProject.run(department, weekLabel, projId, String(p.nr || ''), String(p.name || ''),
|
||||
String(p.status || ''), String(p.group || ''), idx, typeof p.order === 'number' ? p.order : idx);
|
||||
const lph = p.lph || {};
|
||||
Object.keys(lph).forEach(l => insLph.run(department, weekLabel, projId, String(l || ''), String(lph[l] === undefined ? '' : lph[l])));
|
||||
const assign = p.assign || {};
|
||||
Object.keys(assign).forEach(key => {
|
||||
// Key-Format: "personId|Rolle"
|
||||
const sep = key.indexOf('|');
|
||||
const pid = sep >= 0 ? key.slice(0, sep) : key;
|
||||
const role = sep >= 0 ? key.slice(sep + 1) : '';
|
||||
insAssign.run(department, weekLabel, projId, pid, role, Number(assign[key]) || 0);
|
||||
});
|
||||
// Sortiere Sitzungen nach Dateiname, damit die Reihenfolge stabil ist
|
||||
result.sessions.sort((a,b) => a.filename.localeCompare(b.filename));
|
||||
res.json(result);
|
||||
const aufwand = p.aufwandProLP || {};
|
||||
Object.keys(aufwand).forEach(lph => {
|
||||
const roles = aufwand[lph] || {};
|
||||
Object.keys(roles).forEach(role => {
|
||||
const v = Number(roles[role]);
|
||||
if(!isNaN(v)) insAufwand.run(department, weekLabel, projId, String(lph || ''), String(role || ''), v);
|
||||
});
|
||||
});
|
||||
(Array.isArray(p.aktuelleLP) ? p.aktuelleLP : []).forEach(lph => {
|
||||
insAktuelleLp.run(department, weekLabel, projId, String(lph || ''));
|
||||
});
|
||||
const plan = p.plan || null;
|
||||
if(plan){
|
||||
insPlan.run(department, weekLabel, projId, String(plan.start || ''), String(plan.end || ''), Number(plan.progress) || 0);
|
||||
(Array.isArray(plan.people) ? plan.people : []).forEach((pid, pidx) => insPlanPerson.run(department, weekLabel, projId, String(pid || ''), pidx));
|
||||
(Array.isArray(plan.todos) ? plan.todos : []).forEach((t, tidx) => {
|
||||
insTodo.run(department, weekLabel, projId, String(t.id || ''), String(t.title || ''),
|
||||
String(t.start || ''), String(t.end || ''), t.done ? 1 : 0, Number(t.progress) || 0, tidx);
|
||||
(Array.isArray(t.assignees) ? t.assignees : []).forEach(pid => insTodoAssignee.run(department, weekLabel, projId, String(t.id || ''), String(pid || '')));
|
||||
(Array.isArray(t.dependsOn) ? t.dependsOn : []).forEach(did => insTodoDep.run(department, weekLabel, projId, String(t.id || ''), String(did || '')));
|
||||
});
|
||||
(Array.isArray(plan.milestones) ? plan.milestones : []).forEach((m, midx) => {
|
||||
insMilestone.run(department, weekLabel, projId, String(m.id || ''), String(m.name || ''), String(m.date || ''), String(m.position || ''), midx);
|
||||
(Array.isArray(m.dependsOn) ? m.dependsOn : []).forEach(did => insMilestoneDep.run(department, weekLabel, projId, String(m.id || ''), String(did || '')));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteWeekData: entfernt alle relationalen Zeilen einer Woche (Abteilung + Label)
|
||||
function deleteWeekData(department, weekLabel){
|
||||
const tables = ['week_people_roles','week_people','attendance','project_lph','project_assign',
|
||||
'project_aufwand','project_aktuelle_lp',
|
||||
'project_plan','plan_people','plan_todos','plan_todo_assignees','plan_todo_deps',
|
||||
'plan_milestones','plan_milestone_deps','projects'];
|
||||
tables.forEach(t => {
|
||||
db.prepare(`DELETE FROM ${t} WHERE department = ? AND week_label = ?`).run(department, weekLabel);
|
||||
});
|
||||
// Die Wochen-Tabelle selbst nutzt `label` statt `week_label`
|
||||
db.prepare('DELETE FROM weeks WHERE department = ? AND label = ?').run(department, weekLabel);
|
||||
}
|
||||
|
||||
// saveWeekRelational: speichert eine Woche vollständig relational (Transaktion)
|
||||
function saveWeekRelational(department, week){
|
||||
const weekLabel = String(week.label || '');
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
deleteWeekData(department, weekLabel);
|
||||
serializeWeek(department, week);
|
||||
db.exec('COMMIT');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// serializeGlobalPeople: zerlegt die globale Personenliste (JSON-Array) in die
|
||||
// Tabellen people/person_roles/person_absences. Ersetzt die gesamte Abteilungsliste.
|
||||
function serializeGlobalPeople(department, people){
|
||||
const del = db.prepare(`DELETE FROM people WHERE department = ?`);
|
||||
const delRoles = db.prepare(`DELETE FROM person_roles WHERE department = ?`);
|
||||
const delAbsences = db.prepare(`DELETE FROM person_absences WHERE department = ?`);
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
del.run(department); delRoles.run(department); delAbsences.run(department);
|
||||
const insPerson = db.prepare(`INSERT INTO people (department, person_id, name, full_name, email, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insRole = db.prepare(`INSERT OR IGNORE INTO person_roles (department, person_id, role, role_order) VALUES (?, ?, ?, ?)`);
|
||||
const insAbsence = db.prepare(`INSERT OR IGNORE INTO person_absences (department, person_id, absence) VALUES (?, ?, ?)`);
|
||||
people.forEach((p, idx) => {
|
||||
insPerson.run(department, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), String(p.email || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insRole.run(department, String(p.id || ''), String(r || ''), ridx));
|
||||
(Array.isArray(p.absences) ? p.absences : []).forEach(a => insAbsence.run(department, String(p.id || ''), String(a || '')));
|
||||
});
|
||||
db.exec('COMMIT');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// loadGlobalPeople: setzt die globale Personenliste einer Abteilung aus den
|
||||
// Tabellen people/person_roles/person_absences wieder zusammen. Rückgabe: Array.
|
||||
function loadGlobalPeople(department){
|
||||
const people = db.prepare('SELECT * FROM people WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const roles = db.prepare('SELECT * FROM person_roles WHERE department = ? ORDER BY role_order').all(department);
|
||||
const absences = db.prepare('SELECT * FROM person_absences WHERE department = ?').all(department);
|
||||
const rolesByPerson = {};
|
||||
const absByPerson = {};
|
||||
roles.forEach(r => { (rolesByPerson[r.person_id] = rolesByPerson[r.person_id] || []).push(r.role); });
|
||||
absences.forEach(a => { (absByPerson[a.person_id] = absByPerson[a.person_id] || []).push(a.absence); });
|
||||
return people.map(p => ({
|
||||
id: p.person_id,
|
||||
name: p.name,
|
||||
fullName: p.full_name,
|
||||
email: p.email,
|
||||
role: rolesByPerson[p.person_id] || [],
|
||||
absences: absByPerson[p.person_id] || [],
|
||||
}));
|
||||
}
|
||||
|
||||
// loadWeeks: setzt alle Wochen einer Abteilung aus den relationalen Tabellen
|
||||
// wieder zu kompletten Wochen-Objekten zusammen (Format wie vom Frontend erwartet).
|
||||
// Rückgabe: Array von Wochen-Objekten, sortiert nach Label.
|
||||
function loadWeeks(department){
|
||||
const weeks = db.prepare('SELECT * FROM weeks WHERE department = ? ORDER BY label').all(department);
|
||||
const weekPeople = db.prepare('SELECT * FROM week_people WHERE department = ? ORDER BY array_order').all(department);
|
||||
const weekRoles = db.prepare('SELECT * FROM week_people_roles WHERE department = ? ORDER BY role_order').all(department);
|
||||
const attendance = db.prepare('SELECT * FROM attendance WHERE department = ?').all(department);
|
||||
const projects = db.prepare('SELECT * FROM projects WHERE department = ? ORDER BY array_order').all(department);
|
||||
const lph = db.prepare('SELECT * FROM project_lph WHERE department = ?').all(department);
|
||||
const assign = db.prepare('SELECT * FROM project_assign WHERE department = ?').all(department);
|
||||
const aufwand = db.prepare('SELECT * FROM project_aufwand WHERE department = ?').all(department);
|
||||
const aktuelleLp = db.prepare('SELECT * FROM project_aktuelle_lp WHERE department = ?').all(department);
|
||||
const plans = db.prepare('SELECT * FROM project_plan WHERE department = ?').all(department);
|
||||
const planPeople = db.prepare('SELECT * FROM plan_people WHERE department = ? ORDER BY array_order').all(department);
|
||||
const todos = db.prepare('SELECT * FROM plan_todos WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const todoAssignees = db.prepare('SELECT * FROM plan_todo_assignees WHERE department = ?').all(department);
|
||||
const todoDeps = db.prepare('SELECT * FROM plan_todo_deps WHERE department = ?').all(department);
|
||||
const milestones = db.prepare('SELECT * FROM plan_milestones WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const milestoneDeps = db.prepare('SELECT * FROM plan_milestone_deps WHERE department = ?').all(department);
|
||||
|
||||
// Index-Karten für schnellen Zugriff
|
||||
const weekKey = (label) => department + '|' + label;
|
||||
const projKey = (label, pid) => department + '|' + label + '|' + pid;
|
||||
const todoKey = (label, pid, tid) => department + '|' + label + '|' + pid + '|' + tid;
|
||||
const msKey = (label, pid, mid) => department + '|' + label + '|' + pid + '|' + mid;
|
||||
|
||||
const weekPeopleMap = {};
|
||||
const weekRolesMap = {};
|
||||
const attendanceMap = {};
|
||||
weekPeople.forEach(r => {
|
||||
const k = weekKey(r.week_label);
|
||||
(weekPeopleMap[k] = weekPeopleMap[k] || [])[r.array_order] = { id: r.person_id, name: r.name, fullName: r.full_name };
|
||||
});
|
||||
weekRoles.forEach(r => {
|
||||
const k = weekKey(r.week_label);
|
||||
const arr = (weekRolesMap[k] = weekRolesMap[k] || {});
|
||||
(arr[r.person_id] = arr[r.person_id] || []).push(r.role);
|
||||
});
|
||||
attendance.forEach(r => {
|
||||
attendanceMap[weekKey(r.week_label) + '|' + r.person_id] = !!r.present;
|
||||
});
|
||||
|
||||
const projectsMap = {};
|
||||
projects.forEach(r => {
|
||||
projectsMap[projKey(r.week_label, r.project_id)] = {
|
||||
id: r.project_id, nr: r.nr, name: r.name, status: r.status, group: r.group,
|
||||
lph: {}, assign: {}, order: r.order_value, plan: null,
|
||||
_arrayOrder: r.array_order,
|
||||
};
|
||||
});
|
||||
lph.forEach(r => { const p = projectsMap[projKey(r.week_label, r.project_id)]; if(p) p.lph[r.lph] = r.value; });
|
||||
assign.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p) p.assign[r.person_id + (r.role ? '|' + r.role : '')] = r.state;
|
||||
});
|
||||
aufwand.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p){ p.aufwandProLP = p.aufwandProLP || {}; (p.aufwandProLP[r.lph] = p.aufwandProLP[r.lph] || {})[r.role] = r.value; }
|
||||
});
|
||||
aktuelleLp.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p){ p.aktuelleLP = p.aktuelleLP || []; if(!p.aktuelleLP.includes(r.lph)) p.aktuelleLP.push(r.lph); }
|
||||
});
|
||||
|
||||
const planMap = {};
|
||||
plans.forEach(r => { planMap[projKey(r.week_label, r.project_id)] = { start: r.start, end: r.end, progress: r.progress, people: [], todos: [], milestones: [] }; });
|
||||
planPeople.forEach(r => {
|
||||
const plan = planMap[projKey(r.week_label, r.project_id)];
|
||||
if(plan) plan.people.push(r.person_id);
|
||||
});
|
||||
|
||||
const todoMap = {};
|
||||
const todoAssigneeMap = {};
|
||||
const todoDepMap = {};
|
||||
todos.forEach(r => {
|
||||
const k = todoKey(r.week_label, r.project_id, r.todo_id);
|
||||
todoMap[k] = { id: r.todo_id, title: r.title, start: r.start, end: r.end, done: !!r.done, dependsOn: [], assignees: [], progress: r.progress };
|
||||
});
|
||||
todoAssignees.forEach(r => {
|
||||
const t = todoMap[todoKey(r.week_label, r.project_id, r.todo_id)];
|
||||
if(t) t.assignees.push(r.person_id);
|
||||
});
|
||||
todoDeps.forEach(r => {
|
||||
const t = todoMap[todoKey(r.week_label, r.project_id, r.todo_id)];
|
||||
if(t) t.dependsOn.push(r.depends_on_id);
|
||||
});
|
||||
|
||||
const msMap = {};
|
||||
const msDepMap = {};
|
||||
milestones.forEach(r => {
|
||||
const k = msKey(r.week_label, r.project_id, r.milestone_id);
|
||||
msMap[k] = { id: r.milestone_id, name: r.name, date: r.date, position: r.position, dependsOn: [] };
|
||||
});
|
||||
milestoneDeps.forEach(r => {
|
||||
const m = msMap[msKey(r.week_label, r.project_id, r.milestone_id)];
|
||||
if(m) m.dependsOn.push(r.depends_on_id);
|
||||
});
|
||||
|
||||
// TODO- und Meilenstein-Objekte in die Projekt-Pläne einsortieren
|
||||
Object.keys(todoMap).forEach(k => {
|
||||
const parts = k.split('|');
|
||||
const plan = planMap[department + '|' + parts[1] + '|' + parts[2]];
|
||||
if(plan) plan.todos.push(todoMap[k]);
|
||||
});
|
||||
Object.keys(msMap).forEach(k => {
|
||||
const parts = k.split('|');
|
||||
const plan = planMap[department + '|' + parts[1] + '|' + parts[2]];
|
||||
if(plan) plan.milestones.push(msMap[k]);
|
||||
});
|
||||
|
||||
// Wochen-Objekte zusammensetzen
|
||||
return weeks.map(w => {
|
||||
const wkey = weekKey(w.label);
|
||||
const people = (weekPeopleMap[wkey] || []).filter(Boolean).map(p => {
|
||||
const roles = (weekRolesMap[wkey] || {})[p.id] || [];
|
||||
const full = Object.assign({}, p);
|
||||
if(roles.length) full.role = roles;
|
||||
return full;
|
||||
});
|
||||
const att = {};
|
||||
(weekPeopleMap[wkey] || []).filter(Boolean).forEach(p => {
|
||||
const present = attendanceMap[wkey + '|' + p.id];
|
||||
if(present !== undefined) att[p.id] = present;
|
||||
});
|
||||
const weekProjects = Object.keys(projectsMap)
|
||||
.filter(k => k.startsWith(department + '|' + w.label + '|'))
|
||||
.map(k => {
|
||||
const p = projectsMap[k];
|
||||
const plan = planMap[k];
|
||||
if(plan) p.plan = plan;
|
||||
return p;
|
||||
})
|
||||
.sort((a,b) => a._arrayOrder - b._arrayOrder)
|
||||
.map(({ _arrayOrder, ...rest }) => rest);
|
||||
return {
|
||||
id: w.week_id, label: w.label, date: w.date,
|
||||
people, projects: weekProjects, attendance: att, locked: !!w.locked,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Migration: altes Ein-Dokument-Format -> relationales Schema ----------
|
||||
|
||||
// migrateLegacySchema: überführt eine bestehende Datenbank aus dem alten Format
|
||||
// (eine `weeks`-Tabelle mit JSON-`data`-Spalte sowie `people` mit JSON-`data`)
|
||||
// in das relationale Schema. Wird einmalig beim Start ausgeführt.
|
||||
function migrateLegacySchema(){
|
||||
if(!hasLegacyWeeksTable()) return;
|
||||
console.log('Migriere altes Wochen-Dokument-Format in relationales Schema …');
|
||||
|
||||
// Alte Wochen-Dokumente lesen
|
||||
const legacyWeeks = db.prepare('SELECT department, label, data FROM weeks').all();
|
||||
// Alte globale Personen-Dokumente lesen (eine JSON-Array-Zeile pro Abteilung)
|
||||
const legacyPeopleRows = [];
|
||||
const hasLegacyPeople = db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='people' AND sql LIKE '%data%'").get().c > 0;
|
||||
if(hasLegacyPeople){
|
||||
legacyPeopleRows.push(...db.prepare('SELECT department, data FROM people').all());
|
||||
}
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
// Alte Tabellen entfernen (Reihenfolge: erst Kind-Tabellen, dann Stamm)
|
||||
db.exec(`DROP TABLE IF EXISTS week_people_roles; DROP TABLE IF EXISTS week_people;
|
||||
DROP TABLE IF EXISTS attendance; DROP TABLE IF EXISTS project_lph;
|
||||
DROP TABLE IF EXISTS project_assign; DROP TABLE IF EXISTS project_aufwand;
|
||||
DROP TABLE IF EXISTS project_aktuelle_lp; DROP TABLE IF EXISTS project_plan;
|
||||
DROP TABLE IF EXISTS plan_people; DROP TABLE IF EXISTS plan_todos;
|
||||
DROP TABLE IF EXISTS plan_todo_assignees; DROP TABLE IF EXISTS plan_todo_deps;
|
||||
DROP TABLE IF EXISTS plan_milestones; DROP TABLE IF EXISTS plan_milestone_deps;
|
||||
DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS weeks;
|
||||
DROP TABLE IF EXISTS people; DROP TABLE IF EXISTS person_roles; DROP TABLE IF EXISTS person_absences;`);
|
||||
|
||||
// Neue Tabellen anlegen (Schema-DDL oben ausführen)
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
|
||||
department TEXT NOT NULL, label TEXT NOT NULL, week_id TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '', locked INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, label)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '', full_name TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people_roles (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL, role_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS attendance (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
present INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS projects (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
nr TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT '',
|
||||
"group" TEXT NOT NULL DEFAULT '', array_order INTEGER NOT NULL DEFAULT 0,
|
||||
order_value INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_lph (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, value TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_assign (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT '', state INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aufwand (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, role TEXT NOT NULL DEFAULT '', value REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, lph, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aktuelle_lp (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_plan (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
start TEXT NOT NULL DEFAULT '', end TEXT NOT NULL DEFAULT '', progress INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_people (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL, array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todos (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '', done INTEGER NOT NULL DEFAULT 0, progress INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, project_id, todo_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_assignees (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_deps (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestones (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', date TEXT NOT NULL DEFAULT '',
|
||||
position TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestone_deps (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL, depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS people (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_roles (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_absences (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, absence TEXT NOT NULL,
|
||||
PRIMARY KEY (department, person_id, absence)
|
||||
)`);
|
||||
|
||||
// Alte Dokumente einlesen und relational speichern. Die Tabellen wurden
|
||||
// gerade neu angelegt und sind leer; `serializeWeek` genügt (kein eigener
|
||||
// Transaktions-Wrapper, da die äußere BEGIN/COMMIT der Migration aktiv ist).
|
||||
legacyWeeks.forEach(row => {
|
||||
let week;
|
||||
try { week = JSON.parse(row.data); } catch(e){ console.error('Kaputte Woche bei Migration:', row.label, e.message); return; }
|
||||
if(!week || !week.label) return;
|
||||
serializeWeek(row.department, week);
|
||||
});
|
||||
// Globale Personen aus dem alten Dokument-Format migrieren. serializeGlobalPeople
|
||||
// startet selbst eine Transaktion, daher werden die Zeilen hier direkt in die
|
||||
// neuen Tabellen eingefügt (serializeGlobalPeople ist in Transaktionen nicht
|
||||
// aufrufbar, weil BEGIN innerhalb BEGIN nicht erlaubt ist).
|
||||
legacyPeopleRows.forEach(row => {
|
||||
let people;
|
||||
try { people = JSON.parse(row.data); } catch(e){ console.error('Kaputte Personen-Liste bei Migration:', row.department, e.message); return; }
|
||||
if(!Array.isArray(people)) return;
|
||||
const insPerson = db.prepare(`INSERT INTO people (department, person_id, name, full_name, email, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insRole = db.prepare(`INSERT OR IGNORE INTO person_roles (department, person_id, role, role_order) VALUES (?, ?, ?, ?)`);
|
||||
const insAbsence = db.prepare(`INSERT OR IGNORE INTO person_absences (department, person_id, absence) VALUES (?, ?, ?)`);
|
||||
people.forEach((p, idx) => {
|
||||
insPerson.run(row.department, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), String(p.email || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insRole.run(row.department, String(p.id || ''), String(r || ''), ridx));
|
||||
(Array.isArray(p.absences) ? p.absences : []).forEach(a => insAbsence.run(row.department, String(p.id || ''), String(a || '')));
|
||||
});
|
||||
});
|
||||
db.exec('COMMIT');
|
||||
console.log('Migration abgeschlossen (' + legacyWeeks.length + ' Wochen).');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
console.error('Migration fehlgeschlagen:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Migration beim Serverstart ausführen
|
||||
migrateLegacySchema();
|
||||
|
||||
// Route: Speichert eine komplette Woche (als JSON-Objekt) für eine Abteilung
|
||||
app.post('/save-week', (req, res) => {
|
||||
const { department, week } = req.body || {};
|
||||
if(!week || typeof week.label !== 'string' || !week.label) return res.status(400).json({ ok: false, error: 'Missing week or label' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
saveWeekRelational(dept, week);
|
||||
console.log('Saved week:', dept + ' / ' + week.label);
|
||||
return res.json({ ok: true });
|
||||
} catch(err){
|
||||
console.error('Save week error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Speichert die globale Personenliste einer Abteilung (als JSON-Array)
|
||||
app.post('/save-people', (req, res) => {
|
||||
const { department, people } = req.body || {};
|
||||
if(!Array.isArray(people)) return res.status(400).json({ ok: false, error: 'Missing people array' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
serializeGlobalPeople(dept, people);
|
||||
console.log('Saved people:', dept, '(' + people.length + ' Personen)');
|
||||
return res.json({ ok: true });
|
||||
} catch(err){
|
||||
console.error('Save people error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Löscht eine Woche einer Abteilung (fehlende Woche wird toleriert)
|
||||
app.post('/delete-week', (req, res) => {
|
||||
const { department, label } = req.body || {};
|
||||
if(!label) return res.status(400).json({ ok: false, error: 'Missing label' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
const row = db.prepare('SELECT 1 FROM weeks WHERE department = ? AND label = ?').get(dept, String(label));
|
||||
const existed = !!row;
|
||||
deleteWeekData(dept, String(label));
|
||||
console.log('Deleted week:', dept + ' / ' + label, existed ? '' : '(nicht vorhanden)');
|
||||
return res.json({ ok: true, deleted: existed });
|
||||
} catch(err){
|
||||
console.error('Delete week error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Lädt alle Wochen plus globale Personenliste einer Abteilung.
|
||||
// Liefert strukturierte JSON-Objekte (aus den relationalen Tabellen
|
||||
// zusammengesetzt); nach Label sortiert.
|
||||
app.get('/load-all', (req, res) => {
|
||||
try {
|
||||
const dept = departmentKey(req.query.department);
|
||||
const weeks = loadWeeks(dept);
|
||||
const people = loadGlobalPeople(dept);
|
||||
res.json({ ok: true, weeks, people });
|
||||
} catch(err){
|
||||
console.error('Load error', err);
|
||||
res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Healthcheck-Endpunkt für Docker/Überwachung
|
||||
@@ -150,7 +819,7 @@ app.get('/:department/Projektplan/:projName', (req, res) => {
|
||||
});
|
||||
|
||||
// Serverstart mit Fehlerbehandlung (z.B. Port bereits belegt)
|
||||
const server = app.listen(PORT, ()=> console.log(`CSV-Session server running on http://localhost:${PORT}`));
|
||||
const server = app.listen(PORT, ()=> console.log(`Projektübersicht server running on http://localhost:${PORT}`));
|
||||
server.on('error', (err) => {
|
||||
console.error('Server konnte nicht starten:', err.message);
|
||||
if(err.code === 'EADDRINUSE'){
|
||||
|
||||
Reference in new issue
Block a user