830 lines
40 KiB
JavaScript
830 lines
40 KiB
JavaScript
/*
|
||
* Express-Server der Projektübersicht: liefert die statischen Frontend-Dateien aus
|
||
* 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: Serverport (PORT-Env) und die gültigen Abteilungsordner
|
||
const PORT = process.env.PORT || 3003;
|
||
const VALID_DEPARTMENTS = new Set(['ELT','HLS','ARCH']);
|
||
|
||
// Globale Middleware: CORS, JSON-Body-Limit (2 MB) und statisches Serving von public/ ohne Cache für Skripte/Styles
|
||
app.use(require('cors')());
|
||
app.use(express.json({ limit: '2mb' }));
|
||
app.use(express.static(path.join(__dirname, 'public'), {
|
||
setHeaders: (res, path) => {
|
||
if(path.endsWith('.js') || path.endsWith('.css') || path.endsWith('.html')){
|
||
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||
res.set('Pragma', 'no-cache');
|
||
res.set('Expires', '0');
|
||
}
|
||
}
|
||
}));
|
||
|
||
// 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 });
|
||
|
||
// 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';
|
||
}
|
||
|
||
// ---------- 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;
|
||
}
|
||
|
||
// 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);
|
||
|
||
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 (?, ?, ?, ?)`);
|
||
|
||
(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);
|
||
});
|
||
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
|
||
app.get('/health', (req,res)=> res.json({ok:true}));
|
||
|
||
// Route: Liefert die Hauptseite Test.html
|
||
app.get('/', (req,res)=>{
|
||
res.sendFile(path.join(__dirname, 'public', 'Test.html'));
|
||
});
|
||
|
||
// Gültige Tab-Slugs für Deep-Link-Routen (muss zu TAB_SLUGS in js/globals.js passen)
|
||
const VALID_TAB_SLUGS = new Set(['Projektübersicht','Auslastung','Projekte','Projektbeteiligte']);
|
||
// Slug der Projektplan-Seite (muss zu PLAN_SLUG in js/globals.js passen)
|
||
const PLAN_SLUG = 'Projektplan';
|
||
|
||
// Route: Deep-Links je Abteilung + Tab, z. B. "/HLS/Auslastung" oder
|
||
// "/ELT/Projektübersicht". Liefert wie "/" die Hauptseite; unbekannte
|
||
// Abteilungen/Tabs werden abgelehnt (404), damit keine Test.html auf beliebigen
|
||
// Pfaden ausgeliefert wird.
|
||
app.get('/:department/:tab', (req, res) => {
|
||
const { department, tab } = req.params;
|
||
if(!VALID_DEPARTMENTS.has(department)) return res.status(404).send('Nicht gefunden');
|
||
if(tab === PLAN_SLUG){
|
||
// Projektplan-Seite (GANTT), z. B. "/HLS/Projektplan?weekLabel=..."
|
||
return res.sendFile(path.join(__dirname, 'public', 'project-plan.html'));
|
||
}
|
||
if(!VALID_TAB_SLUGS.has(tab)) return res.status(404).send('Nicht gefunden');
|
||
res.sendFile(path.join(__dirname, 'public', 'Test.html'));
|
||
});
|
||
|
||
// Route: Projektplan-Seite mit Projektname im Pfad, z. B.
|
||
// "/HLS/Projektplan/Gymnasium%20Hemer?weekLabel=..." – das Pfad-Segment dient
|
||
// nur der lesbaren Adresse, die Identifikation erfolgt über weekLabel/projNr.
|
||
app.get('/:department/Projektplan/:projName', (req, res) => {
|
||
if(!VALID_DEPARTMENTS.has(req.params.department)) return res.status(404).send('Nicht gefunden');
|
||
res.sendFile(path.join(__dirname, 'public', 'project-plan.html'));
|
||
});
|
||
|
||
// Serverstart mit Fehlerbehandlung (z.B. Port bereits belegt)
|
||
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'){
|
||
console.error(`Port ${PORT} ist bereits belegt. Bitte einen anderen PORT verwenden, z.B. PORT=3004 npm start`);
|
||
}
|
||
process.exit(1);
|
||
});
|