Unnötige Datein gelöscht. DB normalisiert

This commit is contained in:
erik committed 2026-08-14 15:03:24 +02:00
1 parent da64472251
commit 4e083bee73
32 files changed
+971 -5542

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
# SQLite-Datenbank (Laufzeitartefakt in sessions/)
sessions/*.db
sessions/*.db-wal
sessions/*.db-shm
# macOS-Artefakte
.DS_Store
+71 -47
View File
@@ -4,39 +4,38 @@
Single-page app for tracking engineering projects across weekly sessions
("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build
step, no bundler, no tests. Data is persisted as semicolon-separated CSVs
under `sessions/`.
step, no bundler, no tests. Data is persisted in a SQLite database
(`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`.
Two HTML entrypoints, both served statically by Express:
- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte,
Projektbeteiligte). Served at `/`.
- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new
tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules
(`globals`, `utils`, `ui`, `date-picker`, `csv`, `server`, `gantt`) plus the
(`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the
plan modules under `js/plan/` — NOT the full main module set.
All UI strings/comments/CSV content are German. Preserve German in
All UI strings/comments are German. Preserve German in
user-facing text.
## File structure
```
.
├── server.js # Express server + CSV API
├── server.js # Express server + SQLite API
├── package.json # deps: express, cors only
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile
├── Docker/Dockerfile # node:20-alpine
├── Docker/Dockerfile # node:22-alpine (nötig für node:sqlite)
├── .dockerignore
├── .vscode/extensions.json
├── public/
│ ├── Test.html # main app (served at /)
│ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey-Quelle
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
│ │ ├── server.js # baseUrl, sendWeekToServer, sendPeopleToServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
@@ -62,8 +61,8 @@ user-facing text.
│ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link
│ ├── styles.css
│ └── dein_bild.ico
└── sessions/ # data; bind-mounted into Docker container
├── ELT/ HLS/ ARCH/ # one folder per department
└── sessions/ # SQLite-DB; bind-mounted into Docker container
└── projektuebersicht.db # node:sqlite database (WAL mode)
```
## Running
@@ -93,31 +92,55 @@ docker compose up --build # Docker variant (compose file is at repo root)
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with
`_people.csv` content in `people`; sessions sorted by filename.
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV.
- `POST /delete-csv {filename, department}`.
- `GET /load-all?department=` → `{weeks:[weekObj], people:[personObj]}` with the
global people list in `people`; weeks are full JSON week objects (id, label,
date, locked, people, attendance, projects), sorted by label.
- `POST /save-week {department, week}` → upserts one week (JSON object, keyed by
department + label). Used by `sendWeekToServer()`.
- `POST /save-people {department, people}` → replaces the department's global
people list (JSON array).
- `POST /delete-week {department, label}` → deletes the week; missing label
yields 400, missing week tolerates with `deleted:false`.
- Valid departments hard-coded as `ELT|HLS|ARCH` in `server.js`
(`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in
`DEPARTMENTS` in `js/globals.js`.
- `safeName()` strictly validates filenames (path traversal protection);
static assets get `Cache-Control: no-store`.
- Static assets get `Cache-Control: no-store`.
- JSON body limit is 2 MB.
## CSV format
## Persistence (SQLite)
- Delimiter `;`, UTF-8 with BOM. Hand-editing is possible but risky; the parser
reconstructs objects from the fixed column layout.
- Week files `<Label>.csv` (e.g. `KW_30___2026.csv` — `filenameForWeek()`
replaces non-word chars with `_`). Row layout is positional:
`WOCHE` (label, date, locked flag) → `PERSONEN` (`name|role|fullName` per
column) → header row → `ANWESENHEIT` → one row per project.
- `_people.csv` per department: `NAME;Rollen;Voller Name;E-Mail;Abwesenheiten`
(absences as JSON). People are global per department; new people are copied
into the current week. Historical weeks keep their own person copies, so
deleting a global person leaves old sessions unaffected no matter what.
- Don't change the column layout without updating both `weekToCsv()` and
`parseSessionCsv()` (`js/csv.js`), which must stay inverse of each other.
- Data lives in `sessions/projektuebersicht.db` (`node:sqlite` `DatabaseSync`,
WAL mode, `PRAGMA foreign_keys = ON`), created automatically. The schema is
**fully relational** (18 tables) instead of one big JSON document per week:
- `weeks` (department, label, week_id, date, locked; PK `(department,label)`)
- `week_people` / `week_people_roles` — person copies per week (`array_order`
keeps the list order, `role_order` the role array order)
- `attendance` — présence per week/person
- `projects` (`array_order` = position in the JSON array, `order_value` = the
`p.order` field, which is only unique *within a group*, not globally!)
- `project_lph`, `project_assign` (person_id|role keys split into columns),
`project_aufwand` (LP effort per role), `project_aktuelle_lp`
- `project_plan`, `plan_people`, `plan_todos` / `plan_todo_assignees` /
`plan_todo_deps`, `plan_milestones` (`position` column!) /
`plan_milestone_deps`
- `people` / `person_roles` / `person_absences` — global people per department
- The HTTP API stays JSON-shaped: `serializeWeek()` /
`serializeGlobalPeople()` split incoming week/people objects across the
tables inside a transaction, `loadWeeks()` / `loadGlobalPeople()`
(re)assemble them on read. `saveWeekRelational()` deletes the week's rows
first (`deleteWeekData()`, note `weeks` uses `label`, all child tables
`week_label`).
- Migration on every start (idempotent):
`migrateLegacySchema()` converts a DB from the old single-JSON-document format
(detected via a `data` column in `weeks`): it reads the documents, drops all
tables, recreates the relational schema and inserts everything in one
transaction.
- The DB file sits in `sessions/`, so the existing Docker bind-mount
`./sessions:/app/sessions` keeps it persistent. The path is overridable via
the `DB_PATH` environment variable (used for tests), production default is
`sessions/projektuebersicht.db`.
- Project/person IDs are stable across reloads; only week-level fields are
queryable columns.
## Frontend architecture
@@ -125,28 +148,28 @@ docker compose up --build # Docker variant (compose file is at repo root)
`public/js/`; **all functions are globals on `window`**. Two HTML files include
them via `<script src>` tags; load order in the HTML defines dependencies,
since there is no bundler:
- `Test.html` loads `globals → utils → ui → date-picker → csv → server →
- `Test.html` loads `globals → utils → ui → date-picker → server →
store → weeks` (shared modules), then the tab-specific folders `board →
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
init`. See the exact list in `Test.html`.
- `project-plan.html` loads only `globals → utils → ui → date-picker → csv →
- `project-plan.html` loads only `globals → utils → ui → date-picker →
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
- Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all
project-plan page state (week, project, zoom level, drag/`modal`-state),
`db`/`currentDepartment` live in `js/globals.js`.
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
- Project IDs are regenerated on every CSV load, so projects are addressed
cross-page by `department` + week `label` + `projNr` (+ readable projName path
segment) via URL params: `openProjectPlan()` builds
`/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`, `findProjectForPlan`
resolves them.
- Person IDs are stable across reloads via `personIdForName(name)` →
`p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus
`_people.csv` — not just the changed week. Exception: `js/plan/plan-state.js`
`savePlan()` bypasses `save()` and writes only the current week's CSV directly
via `sendCsvToServer()`.
- Project/person IDs are stable across reloads because the server splits and
reassembles week objects deterministically (JSON IDs are kept in the DB
columns); projects are addressed cross-page by `department` + week `label` +
`projNr` (+ readable projName path segment) via URL params:
`openProjectPlan()` builds `/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`,
`findProjectForPlan` resolves them. Person IDs follow
`personIdForName(name)` → `p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks (via
`sendWeekToServer()`) plus the people list (via `sendPeopleToServer()`) — not
just the changed week. Exception: `js/plan/plan-state.js` `savePlan()` bypasses
`save()` and writes only the current week directly via `sendWeekToServer()`.
- `switchDepartment()` cancels a pending save debounce before switching, and
`load()` discards stale `/load-all` responses after a department switch
(frozen `requestedDepartment`), so late responses can never render or persist
@@ -185,9 +208,10 @@ docker compose up --build # Docker variant (compose file is at repo root)
- `docker compose up --build` from the repo root. Compose maps `3003:3003`,
bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`.
- Dockerfile: `node:20-alpine`, `npm ci --production`, runs as non-root `node`,
HEALTHCHECK on `/health`. `.dockerignore` excludes `Docker/*` (except
`Docker/Dockerfile`), `node_modules/`, `.git`, `npm-debug.log`.
- Dockerfile: `node:22-alpine` (nötig für node:sqlite), `npm ci --production`,
runs as non-root `node`, HEALTHCHECK on `/health`. `.dockerignore` excludes
`Docker/*` (except `Docker/Dockerfile`), `node_modules/`, `.git`,
`npm-debug.log`.
## Conventions & verification
@@ -195,4 +219,4 @@ docker compose up --build # Docker variant (compose file is at repo root)
`http://192.168.3.187:3000/erik/projektuebersicht.git`.
- No linter, formatter, typecheck, or test suite. Manual verification only:
start server, open `http://localhost:3003`, exercises the relevant tab,
confirm CSV changes in `sessions/`.
confirm SQLite changes (the DB file `sessions/projektuebersicht.db` grows).
+4 -2
View File
@@ -1,5 +1,7 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine
# node:22-alpine, weil node:sqlite (SQLite-Persistenz in server.js) erst ab
# Node 22.13 (ohne Flag) stabil verfügbar ist.
FROM node:22-alpine
WORKDIR /app
@@ -11,7 +13,7 @@ RUN npm ci --production
COPY server.js ./
COPY public ./public
# Sessions-Ordner fuer die CSV-Daten sicherstellen
# Sessions-Ordner fuer die SQLite-Datenbank sicherstellen
RUN mkdir -p /app/sessions && chown -R node:node /app
USER node
+17 -26
View File
@@ -5,7 +5,7 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- einem Frontend in `Test.html`, `styles.css` und den JS-Modulen unter `public/js/`
- einem einfachen Node.js-Server in `server.js`
- einem Datenspeicher in CSV-Dateien unter `sessions/`
- einem Datenspeicher in einer SQLite-Datenbank (`node:sqlite`) unter `sessions/projektuebersicht.db`
- einem Docker-Setup unter `Docker/`
## Datei- und Komponentenübersicht
@@ -17,9 +17,10 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- stellt statische Dateien (`Test.html`, die Module unter `js/`, `styles.css`, etc.) bereit
- implementiert Endpunkte:
- `GET /` → liefert `Test.html`
- `GET /load-all` → lädt alle CSV-Dateien aus dem aktiven `sessions/<DEPARTMENT>/`-Ordner inkl. `_people.csv`
- `POST /save-csv` → speichert eine CSV-Datei (Dateiname wird validiert)
- `POST /delete-csv` → löscht eine CSV-Datei
- `GET /load-all?department=…` → lädt alle Sitzungen plus globale Personenliste einer Abteilung
- `POST /save-week {department, week}` → speichert/aktualisiert eine Sitzung
- `POST /save-people {department, people}` → ersetzt die globale Personenliste einer Abteilung
- `POST /delete-week {department, label}` → löscht eine Sitzung
- `GET /health` → Gesundheitscheck
- legt das `sessions`-Verzeichnis beim Start an, falls es nicht existiert
- beendet sich mit aussagekräftiger Fehlermeldung, wenn der Port belegt ist
@@ -33,16 +34,15 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- `public/js/` (Module)
- Die Client-Logik ist in kleine klassische Script-Dateien aufgeteilt (alle Funktionen global auf `window`):
`globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `csv.js`, `server.js`, `store.js`, `weeks.js`,
`globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `server.js`, `store.js`, `weeks.js`,
`board.js`, `people.js`, `projects.js`, `load.js`, `gantt.js`, `showmode.js`, `tabs-auth.js`, `pdf.js`,
`init.js` sowie die Plan-Module unter `js/plan/` (`plan-state`, `plan-build-gantt`, `plan-gantt`,
`plan-drag`, `plan-modals`, `plan-schedule`).
- rendert das Projektboard, die Projekt- und Personenverwaltung sowie die Auslastungsansicht
- lädt Daten über den Server und speichert Änderungen zurück als CSV
- lädt Daten über den Server und speichert Änderungen als JSON-Objekte in die SQLite-Datenbank zurück
- verwendet `currentDepartment` zur Trennung von Abteilungen (`ELT`, `HLS`, `ARCH`)
- speichert:
- `sessions/<DEPT>/<WEEK>.csv`
- `sessions/<DEPT>/_people.csv` (globale Personenliste pro Abteilung)
- alle Sitzungen und die globale Personenliste pro Abteilung in `sessions/projektuebersicht.db`
- sortiert Sitzungen nach Kalenderwoche, nicht alphabetisch
- sperrt historische Sitzungen automatisch (bis auf die chronologisch letzte)
- Personen sind global pro Abteilung verwaltet; historische Sitzungen behalten ihre lokalen Personenkopien
@@ -91,27 +91,18 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
## Speicherorte und Datenfluss
### Speicherung
- Alle persistierten Daten werden als CSV-Dateien in `sessions/` gespeichert.
- Innerhalb von `sessions/` existieren Abteilungsordner:
- `sessions/ELT/`
- `sessions/HLS/`
- `sessions/ARCH/`
- Jede Abteilung enthält mindestens:
- `_people.csv` → globale Personenliste mit Rollen und vollständigem Namen
- `KW_XX___YYYY.csv` oder vergleichbare Sitzungsdateien → Sitzungsdaten mit Projekten, Zuordnungen und Anwesenheit
- Alle persistierten Daten werden in einer SQLite-Datenbank (`node:sqlite`, `DatabaseSync`, WAL-Modus) unter `sessions/projektuebersicht.db` gespeichert.
- Innerhalb von `sessions/` liegen ausschließlich die SQLite-Datenbankdateien (`projektuebersicht.db` + WAL/SHM).
### Wo wird der Server erstellt?
- Der Server wird in `server.js` erstellt.
- `server.js` startet einen Express-Server auf Port `3003` oder dem Wert von `process.env.PORT`.
- Das Frontend ruft diesen Server über `window.location.origin` an.
### Wo werden die Dateien gespeichert?
- Das Frontend schreibt über den Server in das Dateisystem.
- In `js/store.js` ruft die Anwendung beim Speichern `/save-csv` auf.
- Der Server legt CSV-Dateien unter `sessions/<DEPARTMENT>/` ab.
- Beispiel:
- `sessions/ELT/KW_32___2026.csv`
- `sessions/ELT/_people.csv`
### Wo werden die Daten gespeichert?
- Das Frontend schreibt über den Server in die SQLite-Datenbank.
- In `js/store.js` ruft die Anwendung beim Speichern `/save-week` (Sitzungen) bzw. `/save-people` (globale Personen) auf.
- Der Server schreibt die relationalen Tabellen in `sessions/projektuebersicht.db` (eine Transaktion pro Sitzung, siehe `serializeWeek()`/`loadWeeks()` in `server.js`).
## Betrieb
@@ -141,8 +132,8 @@ http://localhost:3003
```
## Wichtige Hinweise
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/`.
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/` (für die SQLite-Datenbank und das Docker-Bind-Mount).
- Die Docker-Compose-Konfiguration mountet `./sessions` in den Container. Achte darauf, dass der relative Pfad im Kontext korrekt ist, wenn du `docker compose` aus einem anderen Verzeichnis startest.
- `js/store.js` und `js/server.js` arbeiten mit Abteilungsparametern und speichern Daten abteilungsweise in separaten Unterordnern.
- `js/store.js` und `js/server.js` arbeiten mit Abteilungsparametern und speichern Daten abteilungsweise getrennt.
- Die Passwortfunktion ist nur ein einfacher Client-Mechanismus im Frontend und kein echter Sicherheitsmechanismus.
- Globale Personen werden in `_people.csv` verwaltet. Wird eine Person global gelöscht, bleibt sie in bereits existierenden Sitzungen sichtbar.
- Globale Personen werden pro Abteilung in der Datenbank verwaltet. Wird eine Person global gelöscht, bleibt sie in bereits existierenden Sitzungen sichtbar (Wochen-Personenkopien).
BIN
View File
Binary file not shown.
+1 -2
View File
@@ -5,7 +5,7 @@
und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im
seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet
styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js,
js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, danach die
js/date-picker.js, js/server.js, js/store.js, js/weeks.js, danach die
tab-spezifischen Module js/board/board.js, js/people/people.js,
js/projects/projects.js, js/load/load.js sowie js/gantt.js, js/showmode.js,
js/tabs-auth.js, js/pdf.js und js/init.js; crypto-js und pdfmake kommen von einem
@@ -227,7 +227,6 @@
<script src="/js/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.js?v=5"></script>
<script src="/js/csv.js?v=5"></script>
<script src="/js/server.js?v=5"></script>
<script src="/js/store.js?v=5"></script>
<script src="/js/weeks.js?v=5"></script>
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -53,7 +53,7 @@ function formatStatus(command, value){
}
// peopleByRole liegt jetzt im gemeinsamen Modul js/utils.js (benötigt auch
// csv.js für die Projektplan-Seite, die board.js nicht lädt).
// die Projektplan-Seite, die board.js nicht lädt).
// roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role`
@@ -85,7 +85,7 @@ function renderBoard(){
let h2 = `<tr>`;
ROLES.forEach(r=>{
if(grouped[r].length===0) h2 += `<th class="rot ${roleClass(r)}">–</th>`;
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${entry.person.name}</th>`);
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${esc(entry.person.name)}</th>`);
});
LPHS.forEach(l=> h2 += `<th class="rot">${l}</th>`);
h2 += `</tr>`;
-351
View File
@@ -1,351 +0,0 @@
// -----------------------------
// Projektübersicht — CSV-Serialisierung & Parsing
// Hinweis: weekToCsv() und parseSessionCsv() sind invers zueinander.
// Das Spaltenlayout darf nicht geändert werden, ohne beide Funktionen anzupassen.
// -----------------------------
// Diese Datei enthält das gesamte CSV-Handling (Serialisieren und Parsen) und
// wird nach js/date-picker.js von beiden HTML-Einstiegspunkten geladen. Sie
// stellt die globalen Funktionen weekToCsv/parseSessionCsv, das Personen-CSV
// (globalPeopleToCsv/parseGlobalPeopleCsv) sowie Helfer wie weekSortKey und
// decodeWeekLabel bereit. Hauptnutzer sind js/store.js (Laden/Speichern aller
// Sitzungen), js/weeks.js (Wochen-Klonen) und js/plan/plan-state.js (savePlan),
// die die CSV-Zeichenketten über js/server.js austauschen.
// filenameForWeek: leitet aus dem Wochenlabel einen Dateinamen ab
// (alle Nicht-Wort-Zeichen werden durch _ ersetzt) und hängt .csv an.
// Parameter: label (String). Rückgabe: String wie 'KW_30___2026.csv'.
function filenameForWeek(label){
return (label.replace(/[^\w]/g,'_'))+'.csv';
}
// csvEscape: maskiert ein CSV-Feld, falls es Sonderzeichen (;, ", Zeilenumbruch) enthält
// Parameter: v (beliebiger Wert). Rückgabe: String, bei Bedarf in Anführungszeichen.
function csvEscape(v){
const s = (v === null || v === undefined ? '' : String(v));
if(/[;"\n\r]/.test(s)) return '"' + s.replace(/"/g,'""') + '"';
return s;
}
// globalPeopleToCsv: serialisiert die globalen Personen (aller Abteilungen) als CSV-Text
// Parameter: people (Array von Person-Objekten). Rückgabe: UTF-8-BOM-CSV-String.
function globalPeopleToCsv(people){
const rows = [['NAME', 'Rollen', 'Voller Name', 'E-Mail', 'Abwesenheiten']];
people.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(p => {
const absences = Array.isArray(p.absences) ? JSON.stringify(p.absences) : '';
rows.push([p.name, (p.role || []).join(','), p.fullName || '', p.email || '', absences]);
});
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
}
// parseGlobalPeopleCsv: parst den _people.csv-Inhalt zurück in Person-Objekte
// Parameter: text (String). Rückgabe: Array von Person-Objekten (mit stabiler ID).
function parseGlobalPeopleCsv(text){
if(!text) return [];
const rows = parseCsvText(text);
if(!rows.length) return [];
const people = [];
rows.slice(1).forEach(row => {
const name = (row[0] || '').trim();
if(!name) return;
const roles = String(row[1] || 'Planung').split(',').map(x => x.trim()).filter(Boolean);
const fullName = (row[2] || '').trim() || name;
const email = (row[3] || '').trim();
let absences = [];
if(row[4]){
try {
const parsed = JSON.parse(row[4]);
if(Array.isArray(parsed)) absences = parsed;
} catch(e) {
// Ignoriere fehlerhafte JSON-Daten
}
}
people.push({ id: personIdForName(name), name, fullName, email, role: roles, absences });
});
return people;
}
// weekToCsv: serialisiert eine komplette Sitzung (Woche) in das feste CSV-Spaltenlayout
// Parameter: w (Wochen-Objekt mit label, date, locked, people, attendance, projects).
// Rückgabe: UTF-8-BOM-CSV-String. Invers zu parseSessionCsv().
function weekToCsv(w){
// allPeople enthält für das Board jede (Person, Rolle)-Kombination einmal.
const grouped = peopleByRole(w);
const allPeople = [];
ROLES.forEach(r => grouped[r].forEach(entry => allPeople.push(entry)));
const rows = [];
// Metadaten-Zeile: Label, Datum und Sperrstatus der Sitzung
rows.push(['WOCHE', w.label || '', w.date || '', w.locked ? '1' : '']);
// PERSONEN-Zeile: Pro Spalte Name|Rolle|Voller Name, damit beim Laden
// die Zuordnung pro Rolle eindeutig bleibt und der volle Name erhalten ist.
// Der volle Name wird immer als dritter Teil gespeichert (auch leer),
// damit er beim Bearbeiten nicht versehentlich verloren geht.
const peopleMeta = allPeople.map(e => `${e.person.name}|${e.role}|${e.person.fullName || ''}`);
rows.push(['PERSONEN', '', '', ...peopleMeta, ...LPHS.map(() => ''), '','','','','']);
rows.push(['Pr.-Nr.','Bezeichnung','Status', ...allPeople.map(e => e.person.name), ...LPHS, 'Gruppe','Index','Plan','Aufwand','Aktive Phasen']);
rows.push(['ANWESENHEIT','', ...allPeople.map(e => w.attendance && w.attendance[e.person.id] ? 'X' : ''), ...LPHS.map(() => ''),'','','','','']);
w.projects.forEach(p => {
const arr = [p.nr || '', p.name || '', p.status || ''];
allPeople.forEach(entry => {
const s = p.assign[assignKey(entry.person.id, entry.role)];
arr.push(s === 1 ? 'X' : s === 2 ? '(X)' : '');
});
LPHS.forEach(l => {
const v = p.lph[l];
arr.push(v !== undefined && v !== '' ? String(v).replace(/%/g,'') + '%' : '');
});
arr.push(p.group || '');
arr.push(p.order !== undefined ? String(p.order) : '');
arr.push(p.plan ? JSON.stringify(p.plan) : '');
arr.push(p.aufwandProLP ? JSON.stringify(p.aufwandProLP) : '');
arr.push(p.aktuelleLP ? JSON.stringify(p.aktuelleLP) : '');
rows.push(arr);
});
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
}
// parseCsvText: entfernt ein (mehrfach kodiertes) BOM und zerlegt den Text in Zeilen-Arrays
// Parameter: text (String). Rückgabe: Array aus Zeilen, jede ein Feld-Array.
function parseCsvText(text){
// Manche ältere CSV-Dateien enthalten ein UTF-8-BOM gefolgt von einem
// literalen "" (dreifach kodiert). Beides am Anfang entfernen.
return text.replace(/^[\ufeff\u00EF\u00BB\u00BF]+/,'').split(/\r?\n/).filter(line => line.trim() !== '').map(parseCsvLine);
}
// Toleranter CSV-Zeilenparser. Versteht sauber gequotetes CSV (wie es
// csvEscape/weekToCsv erzeugen). Semikolons innerhalb eines quoted Feldes sind
// Feldinhalt und kein Trennzeichen – so bleibt weekToCsv()/parseSessionCsv()
// auch bei Status-Texten, die mit ";" enden, invers.
// parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array
// Parameter: line (String). Rückgabe: Array der Felder.
function parseCsvLine(line){
const result = [];
let current = '';
let inQuotes = false;
let hasUnclosedQuotes = false;
let i = 0;
while(i < line.length){
const c = line[i];
const n = line[i+1];
if(inQuotes){
if(c === '"' && n === '"'){
current += '"'; i += 2;
} else if(c === '"' && (n === ';' || n === undefined)){
result.push(current);
current = '';
inQuotes = false;
i += (n === ';' ? 2 : 1);
} else {
current += c; i++;
}
} else {
if(c === ';'){
result.push(current);
current = ''; i++;
} else if(c === '"'){
inQuotes = true; i++;
} else {
current += c; i++;
}
}
}
if(inQuotes) hasUnclosedQuotes = true;
result.push(current);
if(hasUnclosedQuotes){
console.warn('CSV-Zeile enthält nicht geschlossene Anführungszeichen:', line.slice(0,80));
}
return result;
}
// decodeWeekLabel: rekonstruiert das Wochenlabel aus einem Dateinamen
// (ersetzt _ durch Leerzeichen und ergänzt das Layout "KW XX / YYYY").
// Parameter: filename (String). Rückgabe: Label-String.
function decodeWeekLabel(filename){
const base = filename.replace(/\.csv$/i,'');
let label = base.replace(/_+/g,' ').trim();
if(/^KW \d+ \d+$/.test(label)){
label = label.replace(/^KW (\d+) (\d+)$/, 'KW $1 / $2');
}
return label || base;
}
// Erzeugt einen Sortierschlüssel aus einem Wochenlabel, sodass z.B.
// "KW 9 / 2026" vor "KW 10 / 2026" kommt.
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
function weekSortKey(label){
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
if(m){
const kw = m[1].padStart(2,'0');
return `${m[2]}-${kw}`;
}
return String(label || '').toLowerCase();
}
// parseSessionCsv: parst die CSV-Zeilen einer Sitzung zurück in ein Wochen-Objekt
// Parameter: filename (String), rows (Array von Feld-Arrays aus parseCsvText).
// Rückgabe: Wochen-Objekt {id, label, date, people, projects, attendance, locked}
// oder null bei leerer Eingabe. Invers zu weekToCsv().
function parseSessionCsv(filename, rows){
if(!rows || rows.length === 0) return null;
let label, date, headerRowIdx = 0, attendanceRowIdx = 1, projectStartIdx = 2;
let locked = false;
let metaPeopleRow = null;
if(rows[0][0] === 'WOCHE'){
label = rows[0][1] || decodeWeekLabel(filename);
date = rows[0][2] || '';
locked = (rows[0][3] || '').trim() === '1';
if(rows[1] && rows[1][0] === 'PERSONEN'){
metaPeopleRow = rows[1];
headerRowIdx = 2; attendanceRowIdx = 3;
} else {
headerRowIdx = 1; attendanceRowIdx = 2;
}
} else {
label = decodeWeekLabel(filename);
date = '';
}
projectStartIdx = attendanceRowIdx + 1;
const header = rows[headerRowIdx];
if(!header || header.length < 4) return { id: uid(), label, date, people: [], projects: [], attendance: {} };
const lphStart = header.indexOf('LPH 1');
const groupIdx = header.indexOf('Gruppe');
const orderIdx = header.indexOf('Index');
const planIdx = header.indexOf('Plan');
const aufwandIdx = header.indexOf('Aufwand');
const aktiveIdx = header.indexOf('Aktive Phasen');
const peopleCols = header.slice(3, lphStart === -1 ? header.length : lphStart);
const people = [];
const colMap = [];
const personMeta = metaPeopleRow ? metaPeopleRow.slice(3, lphStart === -1 ? metaPeopleRow.length : lphStart) : [];
const personByName = {};
const nameOccurrences = {};
peopleCols.forEach((name, idx) => {
const n = (name || '').trim();
if(!n){ colMap.push(null); return; }
let roles = ['Planung'];
let fullName = n;
if(personMeta[idx]){
const parts = String(personMeta[idx]).split('|');
const rawRoles = parts[1] || '';
const parsedRoles = rawRoles.split(',').map(x => x.trim()).filter(x => ROLES.includes(x));
if(parsedRoles.length > 0) roles = parsedRoles;
// Voller Name ist alles ab dem zweiten Pipe; damit sind auch Pipe-Zeichen
// im Namen möglich. Leere Strings werden bewusst übernommen.
if(parts.length >= 3) fullName = parts.slice(2).join('|').trim();
} else {
const occurrence = nameOccurrences[n] || 0;
roles = [ROLES[Math.min(occurrence, ROLES.length - 1)]];
nameOccurrences[n] = occurrence + 1;
}
// Gleiche Person (gleiches Kürzel) wird im Board pro Rolle mehrfach
// dargestellt, soll aber im Datenmodell nur einmal existieren.
let person = personByName[n];
if(!person){
person = { id: personIdForName(n), name: n, fullName: fullName, role: [] };
personByName[n] = person;
people.push(person);
}
// Überschreibe den vollen Namen mit dem Wert aus der CSV; die CSV ist
// hier die führende Datenquelle für den vollständigen Namen.
person.fullName = fullName;
// Sammle alle bekannten Rollen der Person; die aktuelle Spaltenrolle
// wird für die Zuordnung über colMap verwendet.
roles.forEach(r => { if(!person.role.includes(r)) person.role.push(r); });
colMap.push({ person, role: roles[0] });
});
const projects = [];
for(let rIdx = projectStartIdx; rIdx < rows.length; rIdx++){
const row = rows[rIdx];
if(row.length < 4) continue;
const nr = row[0] || '';
const name = row[1] || '';
const status = row[2] || '';
if(!nr.trim() && !name.trim()) continue;
const assign = {};
peopleCols.forEach((_, i) => {
const entry = colMap[i];
if(!entry) return;
const v = (row[3+i] || '').trim();
const key = assignKey(entry.person.id, entry.role);
if(v === 'X') assign[key] = 1;
else if(v === '(X)') assign[key] = 2;
});
const lph = {};
if(lphStart !== -1){
LPHS.forEach((l, j) => {
const raw = row[lphStart + j];
if(raw !== undefined && raw !== ''){
const num = String(raw).replace(/%/g,'');
if(!isNaN(parseFloat(num)) && isFinite(num)) lph[l] = num;
}
});
}
let group = 'Aktive Projekte';
if(groupIdx >= 0 && row[groupIdx]) group = row[groupIdx].trim() || group;
else if(row.length > 3) group = row[row.length - 1].trim() || group;
let order = rIdx - projectStartIdx;
if(orderIdx >= 0 && row[orderIdx] !== undefined && row[orderIdx] !== ''){
const parsed = parseInt(row[orderIdx], 10);
if(!isNaN(parsed)) order = parsed;
}
let plan = null;
if(planIdx >= 0 && row[planIdx]){
try {
const parsed = JSON.parse(row[planIdx]);
if(parsed && typeof parsed === 'object') plan = parsed;
} catch(e) {
console.warn('Plan-Daten für Projekt konnten nicht geparst werden:', nr, name, e);
}
}
let aufwandProLP;
if(aufwandIdx >= 0 && row[aufwandIdx]){
try {
const parsed = JSON.parse(row[aufwandIdx]);
if(parsed && typeof parsed === 'object') aufwandProLP = parsed;
} catch(e) {
console.warn('LP-Aufwand für Projekt konnten nicht geparst werden:', nr, name, e);
}
}
let aktuelleLP;
if(aktiveIdx >= 0 && row[aktiveIdx]){
try {
const parsed = JSON.parse(row[aktiveIdx]);
if(Array.isArray(parsed)) aktuelleLP = parsed;
else if(typeof parsed === 'string') aktuelleLP = [parsed];
} catch(e) {
console.warn('Aktive Phasen für Projekt konnten nicht geparst werden:', nr, name, e);
}
}
projects.push({ id: uid(), nr, name, status, group, lph, assign, order, plan, aufwandProLP, aktuelleLP });
}
const attendance = {};
const attRow = rows[attendanceRowIdx];
if(attRow && attRow[0] === 'ANWESENHEIT'){
peopleCols.forEach((_, i) => {
const entry = colMap[i];
if(entry && (attRow[2+i] || '').trim() === 'X'){
attendance[entry.person.id] = true;
}
});
}
return { id: uid(), label, date, people, projects, attendance, locked };
}
+2 -2
View File
@@ -335,8 +335,8 @@ function buildGanttHtml(projects, start, end, options = {}){
function openProjectPlan(pid){
const w = currentWeek(); if(!w) return;
const p = w.projects.find(x => x.id === pid); if(!p) return;
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt).
// Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
// Projekt-IDs sind in der DB stabil, aber die Plan-Seite ist eine separate
// Route. Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
// Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der
// Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert.
const url = new URL('/' + currentDepartment + '/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin);
+7 -3
View File
@@ -114,16 +114,20 @@
const todos = Array.isArray(plan.todos) ? plan.todos : [];
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
const todoIds = todos.map(t => t.id);
const milestoneIds = milestones.map(m => m.id);
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
const todoRowIds = rows.filter(r => r.classList.contains('gantt-todo')).map(r => r.dataset.todoId).filter(Boolean);
const milestoneRowIds = rows.filter(r => r.classList.contains('gantt-milestone-row')).map(r => r.dataset.milestoneId).filter(Boolean);
// Sicherheitscheck: Anzahl muss stimmen
// Sicherheitscheck: Anzahl muss stimmen. Nicht jedes Meilenstein wird als
// eigene .gantt-milestone-row gerendert – nur die mit einer TODO-basierten
// Position (before:/after:) sind eigene Zeilen. Meilensteine ohne Position
// liegen in der Projektzeile und werden hier nicht umsortiert, deshalb nur
// die positionierten zählen.
if(todoRowIds.length !== todoIds.length) return;
if(milestoneRowIds.length !== milestoneIds.length) return;
const positionedIds = milestones.filter(m => m.position && /^(before|after):/.test(m.position)).map(m => m.id);
if(milestoneRowIds.length !== positionedIds.length) return;
// TODOs neu ordnen
const newTodos = [];
+19 -22
View File
@@ -66,20 +66,17 @@ window.PlanApp = {
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der
// anderen plan-Module.
// anderen plan-Module. Die Buttons binden ihre Handler bereits über das
// onclick-Attribut in project-plan.html; hier werden nur Lücken gefüllt,
// falls ein Button-Element fehlt.
function bindToolbarButtons(){
const addTodoBtn = document.getElementById('addTodoBtn');
if(addTodoBtn) addTodoBtn.addEventListener('click', () => addTodo());
const autoScheduleBtn = document.getElementById('autoScheduleBtn');
if(autoScheduleBtn) autoScheduleBtn.addEventListener('click', () => autoScheduleTodos());
const criticalPathBtn = document.getElementById('criticalPathBtn');
if(criticalPathBtn) criticalPathBtn.addEventListener('click', () => toggleCriticalPath());
}
// loadPlanData: lädt alle Sitzungen und die globale Personenliste der
// Abteilung über die Server-API, sortiert die Wochen chronologisch und
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage().
// Abteilung über die Server-API (JSON-Dokumente), sortiert die Wochen
// chronologisch und sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt
// Fehlermeldungen der Reihenfolge nach im Seitenkopf an und startet sonst
// renderPlanPage().
function loadPlanData(){
if(!window.fetch){
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
@@ -90,12 +87,15 @@ window.PlanApp = {
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
db = { weeks: [], currentWeek: null, globalPeople: {} };
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people);
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
parsed.forEach(s => {
const w = parseSessionCsv(s.filename, s.rows);
if(w) db.weeks.push(w);
});
db.globalPeople[app.department] = Array.isArray(data.people) ? data.people : [];
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
.map(w => {
if(!Array.isArray(w.people)) w.people = [];
if(!Array.isArray(w.projects)) w.projects = [];
if(!w.attendance) w.attendance = {};
return w;
});
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
app.week = db.weeks.find(w => w.label === app.weekLabel);
if(!app.week){
@@ -179,18 +179,15 @@ window.PlanApp = {
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
function savePlan(){
if(!app.week || !app.project) return;
const filename = filenameForWeek(app.week.label);
const csv = weekToCsv(app.week);
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
sendWeekToServer(app.week).catch(err => console.error('Speichern fehlgeschlagen:', err));
}
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als
// _people.csv auf den Server, aber nur wenn sie nicht leer ist.
// JSON-Array auf den Server, aber nur wenn sie nicht leer ist.
function saveGlobalPeople(){
const people = db.globalPeople[app.department] || [];
if(people.length > 0){
const peopleCsv = globalPeopleToCsv(people);
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
}
}
+4 -3
View File
@@ -62,6 +62,7 @@ function editProjectActiveLP(id, value){
function renderProjects(){
const w = currentWeek();
if(!w) return;
const locked = isWeekLocked(w);
let html="";
const groups = {};
w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); });
@@ -71,10 +72,10 @@ function renderProjects(){
sortGroups(Object.keys(groups)).forEach(g=>{
html += `<div class="section-title">${esc(g)}</div>`;
html += `<div class="projects-sortable" data-group="${g}" ondragover="handleDragOver(event)" ondrop="handleDrop(event)">`;
html += `<div class="projects-sortable" data-group="${g}" ${locked ? '' : 'ondragover="handleDragOver(event)" ondrop="handleDrop(event)"'}>`;
groups[g].forEach(p=>{
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>
html += `<div class="person-list-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>`}
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr">
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung">
<select onchange="editProjectField('${p.id}','group',this.value)">
+31 -17
View File
@@ -1,13 +1,15 @@
// -----------------------------
// Projektübersicht — Server-Kommunikation (CSV-API)
// Projektübersicht — Server-Kommunikation (JSON-API)
// -----------------------------
// Diese Datei kapselt die HTTP-Kommunikation mit dem Express-Server (siehe
// server.js im Projekt-Root) und wird nach js/csv.js von beiden
// server.js im Projekt-Root) und wird nach js/date-picker.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt baseUrl, blobToDataUrl,
// sendCsvToServer und deleteCsvFromServer bereit. Genutzt wird sie von
// js/store.js (syncAllToServer, Speichern/Löschen aller Wochen),
// js/weeks.js (Wochen-Löschung) und js/plan/plan-state.js (savePlan schreibt
// direkt die aktuelle Woche über sendCsvToServer).
// sendWeekToServer, sendPeopleToServer und deleteWeekFromServer bereit.
// Genutzt wird sie von js/store.js (syncAllToServer, Speichern aller Wochen
// und der Personenliste), js/weeks.js (Wochen-Löschung) und
// js/plan/plan-state.js (savePlan schreibt direkt die aktuelle Woche über
// sendWeekToServer). Die Daten werden als strukturierte JSON-Objekte an die
// SQLite-API des Servers übergeben (keine CSV-Strings mehr).
// baseUrl: dynamische Server-URL, damit die App auch von anderen Geraeten im Netzwerk erreichbar ist.
// Fallback auf localhost:3003, wenn die Seite als lokale Datei geladen wurde.
@@ -31,24 +33,36 @@ function blobToDataUrl(blob){
});
}
// sendCsvToServer: POSTet eine Speicherwoche im CSV-Format an <baseUrl>/save-csv
// Parameter: filename (String), csvContent (String). Rückgabe: Promise mit Server-Antwort.
function sendCsvToServer(filename, csvContent){
// sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit Server-Antwort.
function sendWeekToServer(week){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/save-csv',{
return fetch(baseUrl() + '/save-week',{
method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'},
body:JSON.stringify({filename,csv:csvContent,department:currentDepartment})
body:JSON.stringify({week, department:currentDepartment})
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
}
// deleteCsvFromServer: fordert das Löschen einer Speicherwoche über POST /delete-csv an
// Parameter: filename (String). Rückgabe: Promise mit Server-Antwort.
function deleteCsvFromServer(filename){
// sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
// JSON-Array an <baseUrl>/save-people. Parameter: people (Array).
// Rückgabe: Promise mit Server-Antwort.
function sendPeopleToServer(people){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/delete-csv', {
return fetch(baseUrl() + '/save-people',{
method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'},
body:JSON.stringify({people, department:currentDepartment})
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
}
// deleteWeekFromServer: fordert das Löschen einer Woche über POST /delete-week an
// Parameter: label (String, das Wochen-Label). Rückgabe: Promise mit Server-Antwort.
function deleteWeekFromServer(label){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/delete-week', {
method: 'POST',
headers: {'Content-Type':'application/json; charset=utf-8'},
body: JSON.stringify({ filename, department: currentDepartment })
body: JSON.stringify({ label, department: currentDepartment })
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
}
}
+33 -32
View File
@@ -1,18 +1,20 @@
// -----------------------------
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln
// -----------------------------
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungs-CSVs
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungen
// beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH).
// Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt
// Die Daten liegen serverseitig in einer SQLite-Datenbank und werden als
// strukturierte JSON-Objekte über die API ausgetauscht (keine CSV-Strings).
// Sie wird von `Test.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
// `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
// Jede Sitzung wird als eigene CSV-Datei gespeichert. Personen und Rollen werden
// in der Sitzungscsv selbst abgelegt.
// save: synchronisiert die aktuellen Daten zum Server.
// Jede Sitzung wird als eigenes JSON-Dokument gespeichert. Personen und Rollen
// werden in der Sitzung selbst abgelegt.
function save(){
scheduleSessionSync();
}
@@ -25,31 +27,28 @@ function scheduleSessionSync(){
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
}
// syncAllToServer: schreibt die gesamte Datenbank als CSV-Dateien zum Server.
// Zuerst wird die globale Personenliste (`_people.csv`) gespeichert, danach jede
// Sitzung einzeln über `filenameForWeek()`/`weekToCsv()` und `sendCsvToServer()`.
// syncAllToServer: schreibt die gesamte Datenbank als JSON-Dokumente zum Server.
// Zuerst wird die globale Personenliste (`save-people`) gespeichert, danach jede
// Sitzung einzeln über `sendWeekToServer()`.
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
function syncAllToServer(){
// Zuerst globale Personenliste speichern
saveGlobalPeopleToServer();
// Dann jede Sitzung speichern
db.weeks.forEach(w => {
const filename = filenameForWeek(w.label);
const csv = weekToCsv(w);
sendCsvToServer(filename, csv)
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', filename); })
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', filename, err.message || err); });
sendWeekToServer(w)
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', w.label); })
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err); });
});
}
// saveGlobalPeopleToServer: serialisiert die globale Personenliste der aktuellen
// Abteilung per `globalPeopleToCsv()` und speichert sie als `_people.csv`.
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
// Abteilung über `sendPeopleToServer()` (JSON-Array).
// Leere Personenlisten werden übersprungen (kein Schreibvorgang).
function saveGlobalPeopleToServer(){
const people = db.globalPeople[currentDepartment] || [];
if(people.length === 0) return;
const csv = globalPeopleToCsv(people);
sendCsvToServer('_people.csv', csv)
sendPeopleToServer(people)
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
}
@@ -62,7 +61,7 @@ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return;
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die
// CSV der falschen Abteilung schreibt.
// Daten der falschen Abteilung schreibt.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
currentDepartment = dept;
saveUiState();
@@ -84,11 +83,11 @@ function switchDepartment(dept){
load();
}
// load: liest alle CSV-Dateien der aktuellen Abteilung über `/load-all` vom
// Server ein, parst Personen und Sitzungen (mit `parseSessionCsv()`) und baut
// daraus `db.weeks` auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()`
// an und rendert die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder
// fehlenden Daten werden Beispieldaten über `seed()` geladen.
// load: liest alle Sitzungen und die globale Personenliste der aktuellen
// Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
// auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
// Daten werden Beispieldaten über `seed()` geladen.
function load(){
if(!window.fetch){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
@@ -106,18 +105,20 @@ function load(){
.then(data => {
if(currentDepartment !== requestedDepartment) return;
// Globale Personenliste laden
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
db.globalPeople[currentDepartment] = Array.isArray(data.people) ? data.people : [];
const parsedSessions = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
db.weeks = [];
parsedSessions.forEach(s => {
const w = parseSessionCsv(s.filename, s.rows);
if(w) db.weeks.push(w);
});
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
.map(w => {
// Felder absichern, damit alte/kaputte Dokumente nicht crashen
if(!Array.isArray(w.people)) w.people = [];
if(!Array.isArray(w.projects)) w.projects = [];
if(!w.attendance) w.attendance = {};
return w;
});
if(db.weeks.length === 0){
console.warn('Server lieferte 0 Sitzungsdateien. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob der Ordner sessions/ mitkopiert wurde.');
console.warn('Server lieferte 0 Sitzungen. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob die SQLite-Datenbank sessions/projektuebersicht.db existiert.');
seed();
}
else {
+14 -3
View File
@@ -5,7 +5,7 @@
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen,
// u. a. js/csv.js, js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
// u. a. js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
// (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck.
@@ -38,8 +38,7 @@ function sortGroups(arr){
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der
// Projektplan-Seite) und pdf.js darauf zugreifen.
// Liegt im gemeinsamen Modul, weil board/load/pdf auf sie zugreifen.
function peopleByRole(target){
const m = {}; ROLES.forEach(r=>m[r]=[]);
const people = [];
@@ -65,6 +64,18 @@ function peopleByRole(target){
return m;
}
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel,
// sodass z.B. "KW 9 / 2026" vor "KW 10 / 2026" kommt.
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
function weekSortKey(label){
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
if(m){
const kw = m[1].padStart(2,'0');
return `${m[2]}-${kw}`;
}
return String(label || '').toLowerCase();
}
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
+10 -10
View File
@@ -124,9 +124,9 @@ function unlockCurrentWeek(skipAuth = false){
}
// deleteWeek: löscht die aktuell ausgewählte Sitzung (nur wenn mehr als 1
// vorhanden) inkl. ihrer CSV-Datei auf dem Server. Parameter `skipAuth=true`
// überspringt die Passwortabfrage. Danach wird die letzte verbleibende Woche
// aktiviert und neu gerendert. Keine Rückgabe.
// vorhanden) auf dem Server. Parameter `skipAuth=true` überspringt die
// Passwortabfrage. Danach wird die letzte verbleibende Woche aktiviert und neu
// gerendert. Keine Rückgabe.
function deleteWeek(skipAuth = false){
if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; }
if(!skipAuth && !isAuthenticated){
@@ -138,13 +138,13 @@ function deleteWeek(skipAuth = false){
}
if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; }
const current = currentWeek();
const filename = current ? filenameForWeek(current.label) : null;
if(filename){
console.log('Lösche CSV-Datei auf Server:', filename);
deleteCsvFromServer(filename).then(res=>{
if(res && res.ok){ console.log('CSV-Datei gelöscht:', filename); }
else { alert('CSV-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
}).catch(err=>{ alert('CSV-Löschung fehlgeschlagen: ' + (err.message || err)); });
if(current){
const label = current.label;
console.log('Lösche Woche auf Server:', label);
deleteWeekFromServer(label).then(res=>{
if(res && res.ok){ console.log('Woche gelöscht:', label); }
else { alert('Wochen-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
}).catch(err=>{ alert('Wochen-Löschung fehlgeschlagen: ' + (err.message || err)); });
}
db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek);
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
-3279
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -6,7 +6,7 @@
in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
"/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr.
kommen als URL-Parameter (weekLabel, projNr). Sie lädt styles.css und ausschließlich die gemeinsamen
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/csv.js, js/server.js
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/server.js
und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js,
js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js,
js/plan/plan-modals.js und js/plan/plan-schedule.js. WICHTIG: store, weeks, board, people,
@@ -318,7 +318,6 @@
<script src="/js/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.js?v=5"></script>
<script src="/js/csv.js?v=5"></script>
<script src="/js/server.js?v=5"></script>
<script src="/js/gantt.js?v=5"></script>
<script src="/js/plan/plan-state.js?v=5"></script>
File diff suppressed because it is too large. Load diff
+748 -79
View File
@@ -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'){
BIN
View File
Binary file not shown.
-8
View File
@@ -1,8 +0,0 @@
WOCHE;KW 32 / 2026;2026-08-05;
PERSONEN;;;RS|Planung|Rieke Stilleke;AC|Planung|Atilla Ceylan;CJ|Planung|Carina Jost;SM|Planung|Shahab Mushref;TC|Planung|Tetiana Chevhuz;MK|Projektleitung|Maximilian Kriener;SS|Projektleitung|Sekandar Seradj;MK|Bauüberwachung|Maximilian Kriener;SS|Bauüberwachung|Sekandar Seradj;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;RS;AC;CJ;SM;TC;MK;SS;MK;SS;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;
21-4001;Feuerwehrgeräteh. Rodenkirchen;- Wettbewerbsbeitrag erstellt;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;5;
;Umbau Praxisräume;- Bestandsaufnahme geplant Test schreiben ;;;;;;;;;;100%;;;;;;;;;In Anbahnung;0;
;Denkmalschutzgutachten;- Kurzcheck erstellt;;;;;;;;;;100%;;;;;;;;;In Anbahnung;1;
;Architektur Zeichnungsvorlagen;- Vorlagen aktualisiert;;;;;;;;;;100%;;;;;;;;;Allgemein;0;
1 WOCHE;KW 32 / 2026;2026-08-05;
2 PERSONEN;;;RS|Planung|Rieke Stilleke;AC|Planung|Atilla Ceylan;CJ|Planung|Carina Jost;SM|Planung|Shahab Mushref;TC|Planung|Tetiana Chevhuz;MK|Projektleitung|Maximilian Kriener;SS|Projektleitung|Sekandar Seradj;MK|Bauüberwachung|Maximilian Kriener;SS|Bauüberwachung|Sekandar Seradj;;;;;;;;;;;
3 Pr.-Nr.;Bezeichnung;Status;RS;AC;CJ;SM;TC;MK;SS;MK;SS;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan
4 ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;
5 21-4001;Feuerwehrgeräteh. Rodenkirchen;- Wettbewerbsbeitrag erstellt;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;5;
6 ;Umbau Praxisräume;- Bestandsaufnahme geplant Test schreiben ;;;;;;;;;;100%;;;;;;;;;In Anbahnung;0;
7 ;Denkmalschutzgutachten;- Kurzcheck erstellt;;;;;;;;;;100%;;;;;;;;;In Anbahnung;1;
8 ;Architektur Zeichnungsvorlagen;- Vorlagen aktualisiert;;;;;;;;;;100%;;;;;;;;;Allgemein;0;
-8
View File
@@ -1,8 +0,0 @@
NAME;Rollen;Voller Name;E-Mail;Abwesenheiten
AC;Planung;Atilla Ceylan;a.ceylan@rauschenberg-ing.de;[]
CJ;Planung;Carina Jost;c.jost@rauschenberg-ing.de;[]
MK;Projektleitung,Bauüberwachung;Maximilian Kriener;m.kriener@rauschenberg-ing.de;[]
RS;Planung;Rieke Stilleke;r.stilleke@rauschenberg-ing.de;[]
SM;Planung;Shahab Mushref;s.mushref@rauschenberg-ing.de;[]
SS;Projektleitung,Bauüberwachung;Sekandar Seradj;s.seradj@rauschenberg-ing.de;[]
TC;Planung;Tetiana Chevhuz;t.chevhuz@rauschenberg-ing.de;
1 NAME Rollen Voller Name E-Mail Abwesenheiten
2 AC Planung Atilla Ceylan a.ceylan@rauschenberg-ing.de []
3 CJ Planung Carina Jost c.jost@rauschenberg-ing.de []
4 MK Projektleitung,Bauüberwachung Maximilian Kriener m.kriener@rauschenberg-ing.de []
5 RS Planung Rieke Stilleke r.stilleke@rauschenberg-ing.de []
6 SM Planung Shahab Mushref s.mushref@rauschenberg-ing.de []
7 SS Projektleitung,Bauüberwachung Sekandar Seradj s.seradj@rauschenberg-ing.de []
8 TC Planung Tetiana Chevhuz t.chevhuz@rauschenberg-ing.de
-9
View File
@@ -1,9 +0,0 @@
WOCHE;KW 30 / 2026;2026-07-24;1
PERSONEN;;;AP|Planung|AP;SK|Planung|SK;AK|Planung|AK;ED|Planung|ED;SE|Planung|SE;THK|Planung|THK;SP|Planung|SP;SG|Planung|SG;TW|Planung|TW;RR|Planung|RR;MK|Planung|MK;HR|Planung|HR;;;;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;AP;SK;AK;ED;SE;THK;SP;SG;TW;RR;MK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;
18-4007;Janusz-Korczak-Gesamtsch. Bottrop;LPH5 als Vorabzug raus. NW-Räume vorauss. August.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;0;;;
21-4030;NSHV Köln Immobilienprüfung in Hof;Thorsten W. klärt und übernimmt Schriftverkehr. LV liegt zur Sichtung bei T.W.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;1;;;
22-4056;Gymnasium Hemer;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;2;;;
22-4066;DSW 21 – Umbau E-Mobilität;Elt-Montagen ab KW 33.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;3;;;
24-4043;S-K ODL-Heim Sanierung Krefeld;Voraussichtlich LV-Abgabe.;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;4;;;
1 WOCHE;KW 30 / 2026;2026-07-24;1
2 PERSONEN;;;AP|Planung|AP;SK|Planung|SK;AK|Planung|AK;ED|Planung|ED;SE|Planung|SE;THK|Planung|THK;SP|Planung|SP;SG|Planung|SG;TW|Planung|TW;RR|Planung|RR;MK|Planung|MK;HR|Planung|HR;;;;;;;;;;;;;;
3 Pr.-Nr.;Bezeichnung;Status;AP;SK;AK;ED;SE;THK;SP;SG;TW;RR;MK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
4 ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;
5 18-4007;Janusz-Korczak-Gesamtsch. Bottrop;LPH5 als Vorabzug raus. NW-Räume vorauss. August.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;0;;;
6 21-4030;NSHV Köln Immobilienprüfung in Hof;Thorsten W. klärt und übernimmt Schriftverkehr. LV liegt zur Sichtung bei T.W.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;1;;;
7 22-4056;Gymnasium Hemer;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;2;;;
8 22-4066;DSW 21 – Umbau E-Mobilität;Elt-Montagen ab KW 33.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;3;;;
9 24-4043;S-K ODL-Heim Sanierung Krefeld;Voraussichtlich LV-Abgabe.;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;4;;;
-33
View File
@@ -1,33 +0,0 @@
WOCHE;KW 31 / 2026;2026-07-29;1
PERSONEN;;;AP|Planung|Alexander Plum;AK|Planung|Alexander Kenne;ED|Planung|Erik Dunin;SK|Planung|Simon Kunigk;SE|Planung|Steven Esche;WS|Planung|Winfried Sältzer;ED|Projektleitung|Erik Dunin;SP|Projektleitung|Sebastian Pöttinger;THK|Projektleitung|Thorsten Kramp;RR|Projektleitung|Robert Röllecke;MK|Projektleitung|Maximilian Kriener;TW|Projektleitung|Thorsten Werner;SG|Projektleitung|Steffen Gaida;ED|Bauüberwachung|Erik Dunin;SK|Bauüberwachung|Simon Kunigk;SP|Bauüberwachung|Sebastian Pöttinger;THK|Bauüberwachung|Thorsten Kramp;HR|Bauüberwachung|Hermann Rollecke;;;;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;AP;AK;ED;SK;SE;WS;ED;SP;THK;RR;MK;TW;SG;ED;SK;SP;THK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
ANWESENHEIT;;X;X;;X;X;X;;X;X;;;;X;;X;X;X;;;;;;;;;;;;;;;
18-4007;Janusz-Korczak-Gesamtsch. Bottrop;- KW 35 NW Raum Planung <div>- KW 44 Möbel </div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;90%;;;;;Aktive Projekte;0;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";;
21-4030;NSHV Köln Immobilienprüfung in Hof;- Thorsten W. klärt und übernimmt Schriftverkehr - LV liegt zur Sichtung bei T.W.;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;2;;;
22-4066;DSW 21, Umbau E-Mobilität;"- Elt-Montagen ab <font color=""#c53030""><b>KW 33</b></font>.";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;6;;;
24-4043;S-K ODL-Heim Sanierung Krefeld;- Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;10;;;
21-4001;Feuerwehrgeräteh. Rodenkirchen;"- KNX muß noch programmiert werden. <font color=""#c53030"">Abnahme 27.07</font> <div><font color=""#c53030""><b>- Fertigstellung Ende Juli</b></font><div><font color=""#c53030""><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font color=""#c53030""><b>- Blackbuilding Test 26.07</b></font></div></div>";;X;;;;;;;;;;;X;;;;;;;;;;100%;100%;100%;;;Aktive Projekte;1;"{""start"":"""",""end"":"""",""milestones"":[{""id"":""mslrv3i1edl08"",""name"":""SV Abnahme"",""date"":""2026-08-18""},{""id"":""mslrvh7hjau1e"",""name"":""Test"",""date"":""2026-09-05""}],""people"":[""p_ap"",""p_ak"",""p_sk"",""p_sg""],""progress"":""0"",""todos"":[{""id"":""msluufbp114g1"",""title"":""Test"",""start"":""2026-08-09"",""end"":""2026-08-13"",""done"":false},{""id"":""mslv6h76cmx8k"",""title"":""iuguzgv"",""start"":""2026-08-23"",""end"":""2026-09-06"",""done"":false},{""id"":""mslw4l3qp1v18"",""title"":""Hallo"",""start"":""2026-11-29"",""end"":""2027-04-30"",""done"":false}]}";;
22-4027;Sporthalle Soest;- SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;3;;;
22-4056;Gymnasium Hemer;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;4;;;
22-4056-1;Gymnasium Hemer (Deckensanierung);- Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff);;;;;;;;;;;;;;;;;;;;;100%;;100%;100%;;;;Aktive Projekte;5;;;
23-4042;GWK Neufelder Straße;"- Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font color=""#c53030"">- 22.07 Aufschaltung BMA und Amok</font></div><div><font color=""#c53030"">- KW 31 VOB Abnahme</font></div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;7;;;
23-4070;Umbau Bürogebäude NEW, MV10;"- neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;8;;;
23-4072;S-K OGS-Kaldenkirchen Schulanbau;"- Feininstallation beginnt, Fertigstellung voraussichtlich <font color=""#c53030"">01.08</font>";;;;;;;;;;;;;;;;;;;;;;;;;;70%;;Aktive Projekte;9;;;
24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;11;;;
24-4069;S-K Erw. GS Blankenheim Neubau;- nur backup;;;;;;;;;;;;;;;;;;;;;;;100%;100%;;;;Kleine Projekte;0;;;
24-4096;Umbau Wohngebäude Kirchhoff;"- Position HA wird von uns bestimmt. - Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font color=""#c53030"">- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font color=""#c53030"">- LV Fertig bis Ende KW 32 </font> </div>";;;;(X);;;;;;;;;;;;;;;100%;100%;100%;;95%;85%;0%;;;Aktive Projekte;12;;;
25-4003;Märkische Bank Boele;- Feininstallation<div>- Ab KW30 Einbringung Tresore</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;80%;;Aktive Projekte;13;;;
25-4028;TenBrinke, Amprion, Wietmarschen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;14;;;
25-4028-1;TenBrinke, Amprion, Meerbusch;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;15;;;
25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;16;;;
26-4018;TUI Immobilien GmbH Salzgitter;;;;;;;;;;;;;;;;;;;;50%;;;;;;;;;Aktive Projekte;17;;;
26-4023;HA Rettungsw. Nahmer Gasst. - Los 1;;;;X;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;0;;;
26-4024;HA Rettungsw. Dahl Am Obergr. - Los 2;;;;X;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;1;;;
26-4025;HA, SeePark Hengstey;- LPH2 erfolgt, weitere Arbeiten folgen.;;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;18;;;
26-4026;Stadt Hemer, Anbau OGS Oesetalschule;"<font color=""#1a1a1a"">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>";;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;19;;;
26-4046;AXA Außenbeleuchtung;- Warten auf Unterlagen (Grundlagen & Vorplanung);;;;;;;;;;;;;;;;;;;10%;;;;;;;;;Aktive Projekte;20;;;
;KFW Köln/Bonn Sicherheitskonzept;- Auftrag erhalten;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;2;;;
;Swisstal;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;3;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";;
;Plancodierung/Benennung;- Von Rieke erstellt. Info folgt ? KW31?;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;0;;;
;Ordnerstruktur;- in Projekthilfen _Ordnerstruktur abgelegt, ist in Anlehnung an Leitfaden aufgebaut;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;1;;;
26-4042;Zanderstraße 7, PV-Anlage;- 82 kWp Anlage angefragt. Preisschätzung wird angefertigt;;;;;;;;;;;;;;;;;;;100%;80%;;;;80%;;;;Kleine Projekte;1;;;
1 WOCHE KW 31 / 2026 2026-07-29 1
2 PERSONEN AP|Planung|Alexander Plum AK|Planung|Alexander Kenne ED|Planung|Erik Dunin SK|Planung|Simon Kunigk SE|Planung|Steven Esche WS|Planung|Winfried Sältzer ED|Projektleitung|Erik Dunin SP|Projektleitung|Sebastian Pöttinger THK|Projektleitung|Thorsten Kramp RR|Projektleitung|Robert Röllecke MK|Projektleitung|Maximilian Kriener TW|Projektleitung|Thorsten Werner SG|Projektleitung|Steffen Gaida ED|Bauüberwachung|Erik Dunin SK|Bauüberwachung|Simon Kunigk SP|Bauüberwachung|Sebastian Pöttinger THK|Bauüberwachung|Thorsten Kramp HR|Bauüberwachung|Hermann Rollecke
3 Pr.-Nr. Bezeichnung Status AP AK ED SK SE WS ED SP THK RR MK TW SG ED SK SP THK HR LPH 1 LPH 2 LPH 3 LPH 4 LPH 5 LPH 6 LPH 7 LPH 8 W+M Gruppe Index Plan Aufwand Aktive Phasen
4 ANWESENHEIT X X X X X X X X X X X
5 18-4007 Janusz-Korczak-Gesamtsch. Bottrop - KW 35 NW Raum Planung <div>- KW 44 Möbel </div> 100% 100% 100% 90% Aktive Projekte 0 {"start":"","end":"","milestones":[],"people":[],"progress":0}
6 21-4030 NSHV Köln Immobilienprüfung in Hof - Thorsten W. klärt und übernimmt Schriftverkehr - LV liegt zur Sichtung bei T.W. 100% 100% 100% 100% 100% Aktive Projekte 2
7 22-4066 DSW 21, Umbau E-Mobilität - Elt-Montagen ab <font color="#c53030"><b>KW 33</b></font>. 100% 100% 100% 100% 100% 100% 20% Aktive Projekte 6
8 24-4043 S-K ODL-Heim Sanierung Krefeld - Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div> 100% 100% 100% 100% Aktive Projekte 10
9 21-4001 Feuerwehrgeräteh. Rodenkirchen - KNX muß noch programmiert werden. <font color="#c53030">Abnahme 27.07</font> <div><font color="#c53030"><b>- Fertigstellung Ende Juli</b></font><div><font color="#c53030"><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font color="#c53030"><b>- Blackbuilding Test 26.07</b></font></div></div> X X 100% 100% 100% Aktive Projekte 1 {"start":"","end":"","milestones":[{"id":"mslrv3i1edl08","name":"SV Abnahme","date":"2026-08-18"},{"id":"mslrvh7hjau1e","name":"Test","date":"2026-09-05"}],"people":["p_ap","p_ak","p_sk","p_sg"],"progress":"0","todos":[{"id":"msluufbp114g1","title":"Test","start":"2026-08-09","end":"2026-08-13","done":false},{"id":"mslv6h76cmx8k","title":"iuguzgv","start":"2026-08-23","end":"2026-09-06","done":false},{"id":"mslw4l3qp1v18","title":"Hallo","start":"2026-11-29","end":"2027-04-30","done":false}]}
10 22-4027 Sporthalle Soest - SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div> 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 3
11 22-4056 Gymnasium Hemer 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 4
12 22-4056-1 Gymnasium Hemer (Deckensanierung) - Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff) 100% 100% 100% Aktive Projekte 5
13 23-4042 GWK Neufelder Straße - Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font color="#c53030">- 22.07 Aufschaltung BMA und Amok</font></div><div><font color="#c53030">- KW 31 VOB Abnahme</font></div> 100% 100% 100% 100% 100% 100% 95% Aktive Projekte 7
14 23-4070 Umbau Bürogebäude NEW, MV10 - neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div> 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 8
15 23-4072 S-K OGS-Kaldenkirchen Schulanbau - Feininstallation beginnt, Fertigstellung voraussichtlich <font color="#c53030">01.08</font> 70% Aktive Projekte 9
16 24-4049 BMA Remscheid - Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div> 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 11
17 24-4069 S-K Erw. GS Blankenheim Neubau - nur backup 100% 100% Kleine Projekte 0
18 24-4096 Umbau Wohngebäude Kirchhoff - Position HA wird von uns bestimmt. - Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font color="#c53030">- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font color="#c53030">- LV Fertig bis Ende KW 32 </font> </div> (X) 100% 100% 100% 95% 85% 0% Aktive Projekte 12
19 25-4003 Märkische Bank Boele - Feininstallation<div>- Ab KW30 Einbringung Tresore</div> 100% 100% 100% 100% 100% 100% 80% Aktive Projekte 13
20 25-4028 TenBrinke, Amprion, Wietmarschen 100% 100% 100% 95% Aktive Projekte 14
21 25-4028-1 TenBrinke, Amprion, Meerbusch 100% 100% 100% 95% Aktive Projekte 15
22 25-4028-2 TenBrinke, Amprion, Gelsenkirchen 100% 100% 100% 95% Aktive Projekte 16
23 26-4018 TUI Immobilien GmbH Salzgitter 50% Aktive Projekte 17
24 26-4023 HA Rettungsw. Nahmer Gasst. - Los 1 X In Anbahnung 0
25 26-4024 HA Rettungsw. Dahl Am Obergr. - Los 2 X In Anbahnung 1
26 26-4025 HA, SeePark Hengstey - LPH2 erfolgt, weitere Arbeiten folgen. 100% 100% Aktive Projekte 18
27 26-4026 Stadt Hemer, Anbau OGS Oesetalschule <font color="#1a1a1a">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font> 100% 100% Aktive Projekte 19
28 26-4046 AXA Außenbeleuchtung - Warten auf Unterlagen (Grundlagen & Vorplanung) 10% Aktive Projekte 20
29 KFW Köln/Bonn Sicherheitskonzept - Auftrag erhalten In Anbahnung 2
30 Swisstal In Anbahnung 3 {"start":"","end":"","milestones":[],"people":[],"progress":0}
31 Plancodierung/Benennung - Von Rieke erstellt. Info folgt ? KW31? Allgemein 0
32 Ordnerstruktur - in Projekthilfen _Ordnerstruktur abgelegt, ist in Anlehnung an Leitfaden aufgebaut Allgemein 1
33 26-4042 Zanderstraße 7, PV-Anlage - 82 kWp Anlage angefragt. Preisschätzung wird angefertigt 100% 80% 80% Kleine Projekte 1
-33
View File
@@ -1,33 +0,0 @@
WOCHE;KW 33 / 2026;2026-08-09;
PERSONEN;;;AP|Planung|Alexander Plum;AK|Planung|Alex Kenne;ED|Planung|Erik Dunin;SK|Planung|Simon Kunigk;SE|Planung|Steven Esche;WS|Planung|Winfried Sältzer;ED|Projektleitung|Erik Dunin;SP|Projektleitung|Sebastian Pöttinger;THK|Projektleitung|Thorsten Kramp;RR|Projektleitung|Robert Röllecke;MK|Projektleitung|Maximilian Kriener;TW|Projektleitung|Thorsten Werner;SG|Projektleitung|Steffen Gaida;ED|Bauüberwachung|Erik Dunin;SK|Bauüberwachung|Simon Kunigk;SP|Bauüberwachung|Sebastian Pöttinger;THK|Bauüberwachung|Thorsten Kramp;HR|Bauüberwachung|Hermann Rollecke;;;;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;AP;AK;ED;SK;SE;WS;ED;SP;THK;RR;MK;TW;SG;ED;SK;SP;THK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
18-4007;Janusz-Korczak-Gesamtsch. Bottrop;- KW 35 NW Raum Planung <div>- KW 44 Möbel </div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;90%;;;;;Aktive Projekte;0;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";"{""LPH 5"":{""Planung"":0.6},""LPH 8"":{""Planung"":0,""Projektleitung"":0.2}}";
21-4030;NSHV Köln Immobilienprüfung in Hof;- Thorsten W. klärt und übernimmt Schriftverkehr <div>- LV liegt zur Sichtung bei T.W.</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;2;;;
22-4066;DSW 21, Umbau E-Mobilität;- Elt-Montagen ab <font><b>KW 33</b></font>.;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;7;;;
24-4043;S-K ODL-Heim Sanierung Krefeld;- Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;11;;;
21-4001;Feuerwehrgeräteh. Rodenkirchen;- KNX muß noch programmiert werden. <font>Abnahme 27.07</font> <div><font><b>- Fertigstellung Ende Juli</b></font><div><font><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font><b>- Blackbuilding Test 26.07</b></font></div></div>;X;;X;;;;X;X;;;;;;X;;;;;100%;100%;100%;;100%;100%;100%;;;Aktive Projekte;1;"{""start"":"""",""end"":"""",""milestones"":[{""id"":""mslrvh7hjau1e"",""name"":""Übergabe"",""date"":""2026-09-12"",""dependsOn"":[""mslv6h76cmx8k""],""position"":""after:mslv6h76cmx8k""}],""people"":[""p_ap"",""p_ak"",""p_sk"",""p_sg""],""progress"":""0"",""todos"":[{""id"":""msluufbp114g1"",""title"":""Test"",""start"":""2026-08-10"",""end"":""2026-08-14"",""done"":false,""dependsOn"":[],""assignees"":[""p_ap"",""p_ed""],""progress"":0},{""id"":""mslv6h76cmx8k"",""title"":""Test 2"",""start"":""2026-08-24"",""end"":""2026-09-04"",""done"":false,""dependsOn"":[""msluufbp114g1""],""assignees"":[""p_ap""],""progress"":41},{""id"":""mslw4l3qp1v18"",""title"":""Test 3"",""start"":""2026-09-14"",""end"":""2026-09-22"",""done"":false,""dependsOn"":[""m_mslrvh7hjau1e""],""assignees"":[],""progress"":0},{""id"":""msoltaa1itx48"",""title"":""Gehen"",""start"":""2026-09-23"",""end"":""2026-09-25"",""done"":false,""progress"":0,""dependsOn"":[""mslw4l3qp1v18""],""assignees"":[""p_ws""]}]}";"{""LPH 1"":{""Planung"":0.8},""LPH 2"":{""Planung"":0.8},""LPH 3"":{""Planung"":0.8},""LPH 4"":{""Planung"":0.8},""LPH 5"":{""Planung"":0.8}}";"[""LPH 5""]"
22-4027;Sporthalle Soest;- SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div>;X;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;3;;;"[""LPH 5""]"
22-4056;Gymnasium Hemer;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;4;;;
22-4056-1;Gymnasium Hemer (Deckensanierung);- Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff);;;;;;;;;;;;;;;;;;;;;100%;;100%;100%;;;;Aktive Projekte;5;;;
23-4042;GWK Neufelder Straße;- Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font>- 22.07 Aufschaltung BMA und Amok</font></div><div><font>- KW 31 VOB Abnahme</font></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;8;;;
23-4070;Umbau Bürogebäude NEW, MV10;"- neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;9;;;
23-4072;S-K OGS-Kaldenkirchen Schulanbau;- Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font>;;;;;;;;;;;;;;;;;;;;;;;;;;70%;;Aktive Projekte;10;;;
24-4049;BMA Remscheid;"- Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div>";;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;12;;;
24-4069;S-K Erw. GS Blankenheim Neubau;- nur backup;;;;;;;;;;;;;;;;;;;;;;;100%;100%;;;;Kleine Projekte;0;;;
24-4096;Umbau Wohngebäude Kirchhoff;- Position HA wird von uns bestimmt. <div>- Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font>- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font>- LV Fertig bis Ende KW 32 </font> </div></div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;85%;0%;;;Aktive Projekte;13;;;
25-4003;Märkische Bank Boele;- Feininstallation<div>- Ab KW30 Einbringung Tresore</div>;;;;;;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;80%;;Aktive Projekte;14;;;
25-4028;TenBrinke, Amprion, Wietmarschen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;15;;;
25-4028-1;TenBrinke, Amprion, Meerbusch;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;16;;;
25-4028-2;TenBrinke, Amprion, Gelsenkirchen;;;;;;;;;;;;;;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;17;;;
26-4018;TUI Immobilien GmbH Salzgitter;;;;;;;;;;;;;;;;;;;;50%;;;;;;;;;Aktive Projekte;18;;;
26-4023;HA Rettungsw. Nahmer Gasst. - Los 1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;0;;;
26-4024;HA Rettungsw. Dahl Am Obergr. - Los 2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;1;;;
26-4025;HA, SeePark Hengstey;- LPH2 erfolgt, weitere Arbeiten folgen.;;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;19;;;
26-4026;Stadt Hemer, Anbau OGS Oesetalschule;"<font color=""#1a1a1a"">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font>";;;;;;;;;;;;;;;;;;;100%;100%;;;;;;;;Aktive Projekte;20;;;
26-4046;AXA Außenbeleuchtung;- Warten auf Unterlagen (Grundlagen & Vorplanung);;;;;;;;;;;;;;;;;;;10%;;;;;;;;;Aktive Projekte;21;;;
;KFW Köln/Bonn Sicherheitskonzept;- Auftrag erhalten;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;2;;;
;Swisstal;;;;;;;;;;;;;;;;;;;;;;;;;;;;;In Anbahnung;3;"{""start"":"""",""end"":"""",""milestones"":[],""people"":[],""progress"":0}";;
;Plancodierung/Benennung;- Von Rieke erstellt. Info folgt ? KW31?;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;0;;;
;Ordnerstruktur;- in Projekthilfen _Ordnerstruktur abgelegt, ist in Anlehnung an Leitfaden aufgebaut;;;;;;;;;;;;;;;;;;;;;;;;;;;;Allgemein;1;;;
26-4042;Zanderstraße 7, PV-Anlage;- 82 kWp Anlage angefragt. Preisschätzung wird angefertigt;;;;;;;;;;;;;;;;;;;100%;80%;;;;80%;;;;Kleine Projekte;1;;;
1 WOCHE KW 33 / 2026 2026-08-09
2 PERSONEN AP|Planung|Alexander Plum AK|Planung|Alex Kenne ED|Planung|Erik Dunin SK|Planung|Simon Kunigk SE|Planung|Steven Esche WS|Planung|Winfried Sältzer ED|Projektleitung|Erik Dunin SP|Projektleitung|Sebastian Pöttinger THK|Projektleitung|Thorsten Kramp RR|Projektleitung|Robert Röllecke MK|Projektleitung|Maximilian Kriener TW|Projektleitung|Thorsten Werner SG|Projektleitung|Steffen Gaida ED|Bauüberwachung|Erik Dunin SK|Bauüberwachung|Simon Kunigk SP|Bauüberwachung|Sebastian Pöttinger THK|Bauüberwachung|Thorsten Kramp HR|Bauüberwachung|Hermann Rollecke
3 Pr.-Nr. Bezeichnung Status AP AK ED SK SE WS ED SP THK RR MK TW SG ED SK SP THK HR LPH 1 LPH 2 LPH 3 LPH 4 LPH 5 LPH 6 LPH 7 LPH 8 W+M Gruppe Index Plan Aufwand Aktive Phasen
4 ANWESENHEIT
5 18-4007 Janusz-Korczak-Gesamtsch. Bottrop - KW 35 NW Raum Planung <div>- KW 44 Möbel </div> X 100% 100% 100% 90% Aktive Projekte 0 {"start":"","end":"","milestones":[],"people":[],"progress":0} {"LPH 5":{"Planung":0.6},"LPH 8":{"Planung":0,"Projektleitung":0.2}}
6 21-4030 NSHV Köln Immobilienprüfung in Hof - Thorsten W. klärt und übernimmt Schriftverkehr <div>- LV liegt zur Sichtung bei T.W.</div> X 100% 100% 100% 100% 100% Aktive Projekte 2
7 22-4066 DSW 21, Umbau E-Mobilität - Elt-Montagen ab <font><b>KW 33</b></font>. 100% 100% 100% 100% 100% 100% 20% Aktive Projekte 7
8 24-4043 S-K ODL-Heim Sanierung Krefeld - Vorraussichtlich LV-Abgabe XX.XX<div>- LPH5 mündlich freigegeben <div>- Neuer EG Grundriss mit diversen Änderungen kommt noch von Architekten</div></div> 100% 100% 100% 100% Aktive Projekte 11
9 21-4001 Feuerwehrgeräteh. Rodenkirchen - KNX muß noch programmiert werden. <font>Abnahme 27.07</font> <div><font><b>- Fertigstellung Ende Juli</b></font><div><font><b>- Abnahmen laufen bis Ende KW31</b></font></div><div><font><b>- Blackbuilding Test 26.07</b></font></div></div> X X X X X 100% 100% 100% 100% 100% 100% Aktive Projekte 1 {"start":"","end":"","milestones":[{"id":"mslrvh7hjau1e","name":"Übergabe","date":"2026-09-12","dependsOn":["mslv6h76cmx8k"],"position":"after:mslv6h76cmx8k"}],"people":["p_ap","p_ak","p_sk","p_sg"],"progress":"0","todos":[{"id":"msluufbp114g1","title":"Test","start":"2026-08-10","end":"2026-08-14","done":false,"dependsOn":[],"assignees":["p_ap","p_ed"],"progress":0},{"id":"mslv6h76cmx8k","title":"Test 2","start":"2026-08-24","end":"2026-09-04","done":false,"dependsOn":["msluufbp114g1"],"assignees":["p_ap"],"progress":41},{"id":"mslw4l3qp1v18","title":"Test 3","start":"2026-09-14","end":"2026-09-22","done":false,"dependsOn":["m_mslrvh7hjau1e"],"assignees":[],"progress":0},{"id":"msoltaa1itx48","title":"Gehen","start":"2026-09-23","end":"2026-09-25","done":false,"progress":0,"dependsOn":["mslw4l3qp1v18"],"assignees":["p_ws"]}]} {"LPH 1":{"Planung":0.8},"LPH 2":{"Planung":0.8},"LPH 3":{"Planung":0.8},"LPH 4":{"Planung":0.8},"LPH 5":{"Planung":0.8}} ["LPH 5"]
10 22-4027 Sporthalle Soest - SV und VOB Abnahme erfolgt.<div>- Nachprüfung VOB 24.07</div><div>- Keine Infos über Nachprüfung</div> X 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 3 ["LPH 5"]
11 22-4056 Gymnasium Hemer 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 4
12 22-4056-1 Gymnasium Hemer (Deckensanierung) - Beginn der Arbeiten vorraussichtlich KW 30 (KW 32 Kickoff) 100% 100% 100% Aktive Projekte 5
13 23-4042 GWK Neufelder Straße - Nachweis für E-30 Leitungen notwendig. Rechnungen wurden nicht anerkannt. <div><font>- 22.07 Aufschaltung BMA und Amok</font></div><div><font>- KW 31 VOB Abnahme</font></div> 100% 100% 100% 100% 100% 100% 95% Aktive Projekte 8
14 23-4070 Umbau Bürogebäude NEW, MV10 - neue NSHV anbieten(Warten auf Infos), <div>- 28.07 Sachverständiger Abnahme ELT&amp;SiBe </div><div>- 30.07 Sachverständigen Abnahme BMA</div> 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 9
15 23-4072 S-K OGS-Kaldenkirchen Schulanbau - Feininstallation beginnt, Fertigstellung voraussichtlich <font>01.08</font> 70% Aktive Projekte 10
16 24-4049 BMA Remscheid - Feuerwehrabnahme organisiert die Errichterfirma. <div>- SV-Abnahmen 15 &amp; 16.07: nicht abgenommen</div> 100% 100% 100% 100% 100% 100% 90% Aktive Projekte 12
17 24-4069 S-K Erw. GS Blankenheim Neubau - nur backup 100% 100% Kleine Projekte 0
18 24-4096 Umbau Wohngebäude Kirchhoff - Position HA wird von uns bestimmt. <div>- Notwendiges Leerrohr für Leuchten im Vordach ist zu klären.<div><font>- Abgabe Kostenberechnung bis Ende KW29</font></div><div><font>- LV Fertig bis Ende KW 32 </font> </div></div> 100% 100% 100% 95% 85% 0% Aktive Projekte 13
19 25-4003 Märkische Bank Boele - Feininstallation<div>- Ab KW30 Einbringung Tresore</div> 100% 100% 100% 100% 100% 100% 80% Aktive Projekte 14
20 25-4028 TenBrinke, Amprion, Wietmarschen 100% 100% 100% 95% Aktive Projekte 15
21 25-4028-1 TenBrinke, Amprion, Meerbusch 100% 100% 100% 95% Aktive Projekte 16
22 25-4028-2 TenBrinke, Amprion, Gelsenkirchen 100% 100% 100% 95% Aktive Projekte 17
23 26-4018 TUI Immobilien GmbH Salzgitter 50% Aktive Projekte 18
24 26-4023 HA Rettungsw. Nahmer Gasst. - Los 1 In Anbahnung 0
25 26-4024 HA Rettungsw. Dahl Am Obergr. - Los 2 In Anbahnung 1
26 26-4025 HA, SeePark Hengstey - LPH2 erfolgt, weitere Arbeiten folgen. 100% 100% Aktive Projekte 19
27 26-4026 Stadt Hemer, Anbau OGS Oesetalschule <font color="#1a1a1a">- Bearbeitung LPH3 in Absprache mit Achitektur S+D</font> 100% 100% Aktive Projekte 20
28 26-4046 AXA Außenbeleuchtung - Warten auf Unterlagen (Grundlagen & Vorplanung) 10% Aktive Projekte 21
29 KFW Köln/Bonn Sicherheitskonzept - Auftrag erhalten In Anbahnung 2
30 Swisstal In Anbahnung 3 {"start":"","end":"","milestones":[],"people":[],"progress":0}
31 Plancodierung/Benennung - Von Rieke erstellt. Info folgt ? KW31? Allgemein 0
32 Ordnerstruktur - in Projekthilfen _Ordnerstruktur abgelegt, ist in Anlehnung an Leitfaden aufgebaut Allgemein 1
33 26-4042 Zanderstraße 7, PV-Anlage - 82 kWp Anlage angefragt. Preisschätzung wird angefertigt 100% 80% 80% Kleine Projekte 1
-14
View File
@@ -1,14 +0,0 @@
NAME;Rollen;Voller Name;E-Mail;Abwesenheiten
AK;Planung;Alex Kenne;a.kenne@rauschenberg-ing.de;[]
AP;Planung;Alexander Plum;a.plum@rauschenberg-ing.de;[]
ED;Planung,Projektleitung,Bauüberwachung;Erik Dunin;e.dunin@rauschenberg-ing.de;[]
HR;Planung,Bauüberwachung;Hermann Rollecke;h.roellecke@rauschenberg-ing.de;[]
MK;Planung,Projektleitung;Maximilian Kriener;m.kriener@rauschenberg-ing.de;[]
RR;Planung,Projektleitung;Robert Röllecke;r.roellecke@rauschenberg-ing.de;[]
SE;Planung;Steven Esche;s.esche@rauschenberg-ing.de;[]
SG;Planung,Projektleitung;Steffen Gaida;s.gaida@rauschenberg-ing.de;[]
SK;Planung,Bauüberwachung;Simon Kunigk;s.kunigk@rauschenberg-ing.de;[]
SP;Planung,Projektleitung,Bauüberwachung;Sebastian Pöttinger;s.poettinger@rauschenberg-ing.de;[]
THK;Planung,Projektleitung,Bauüberwachung;Thorsten Kramp;t.kramp@rauschenberg-ing.de;[]
TW;Planung,Projektleitung;Thorsten Werner;t.werner@rauschenberg-ing.de;[]
WS;Planung;Winfried Sältzer;w.sältzer@rauschenberg-ing.de;[]
1 NAME Rollen Voller Name E-Mail Abwesenheiten
2 AK Planung Alex Kenne a.kenne@rauschenberg-ing.de []
3 AP Planung Alexander Plum a.plum@rauschenberg-ing.de []
4 ED Planung,Projektleitung,Bauüberwachung Erik Dunin e.dunin@rauschenberg-ing.de []
5 HR Planung,Bauüberwachung Hermann Rollecke h.roellecke@rauschenberg-ing.de []
6 MK Planung,Projektleitung Maximilian Kriener m.kriener@rauschenberg-ing.de []
7 RR Planung,Projektleitung Robert Röllecke r.roellecke@rauschenberg-ing.de []
8 SE Planung Steven Esche s.esche@rauschenberg-ing.de []
9 SG Planung,Projektleitung Steffen Gaida s.gaida@rauschenberg-ing.de []
10 SK Planung,Bauüberwachung Simon Kunigk s.kunigk@rauschenberg-ing.de []
11 SP Planung,Projektleitung,Bauüberwachung Sebastian Pöttinger s.poettinger@rauschenberg-ing.de []
12 THK Planung,Projektleitung,Bauüberwachung Thorsten Kramp t.kramp@rauschenberg-ing.de []
13 TW Planung,Projektleitung Thorsten Werner t.werner@rauschenberg-ing.de []
14 WS Planung Winfried Sältzer w.sältzer@rauschenberg-ing.de []
-9
View File
@@ -1,9 +0,0 @@
WOCHE;KW 30 / 2026;2026-07-24;1
PERSONEN;;;AP|Planung|AP;SK|Planung|SK;AK|Planung|AK;ED|Planung|ED;SE|Planung|SE;THK|Planung|THK;SP|Planung|SP;SG|Planung|SG;TW|Planung|TW;RR|Planung|RR;MK|Planung|MK;HR|Planung|HR;;;;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;AP;SK;AK;ED;SE;THK;SP;SG;TW;RR;MK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;
18-4007;Janusz-Korczak-Gesamtsch. Bottrop;LPH5 als Vorabzug raus. NW-Räume vorauss. August.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;0;;;
21-4030;NSHV Köln Immobilienprüfung in Hof;Thorsten W. klärt und übernimmt Schriftverkehr. LV liegt zur Sichtung bei T.W.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;1;;;
22-4056;Gymnasium Hemer;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;2;;;
22-4066;DSW 21 – Umbau E-Mobilität;Elt-Montagen ab KW 33.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;3;;;
24-4043;S-K ODL-Heim Sanierung Krefeld;Voraussichtlich LV-Abgabe.;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;4;;;
1 WOCHE;KW 30 / 2026;2026-07-24;1
2 PERSONEN;;;AP|Planung|AP;SK|Planung|SK;AK|Planung|AK;ED|Planung|ED;SE|Planung|SE;THK|Planung|THK;SP|Planung|SP;SG|Planung|SG;TW|Planung|TW;RR|Planung|RR;MK|Planung|MK;HR|Planung|HR;;;;;;;;;;;;;;
3 Pr.-Nr.;Bezeichnung;Status;AP;SK;AK;ED;SE;THK;SP;SG;TW;RR;MK;HR;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
4 ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;;;;;;;
5 18-4007;Janusz-Korczak-Gesamtsch. Bottrop;LPH5 als Vorabzug raus. NW-Räume vorauss. August.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;95%;;Aktive Projekte;0;;;
6 21-4030;NSHV Köln Immobilienprüfung in Hof;Thorsten W. klärt und übernimmt Schriftverkehr. LV liegt zur Sichtung bei T.W.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;;;;Aktive Projekte;1;;;
7 22-4056;Gymnasium Hemer;;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;90%;;Aktive Projekte;2;;;
8 22-4066;DSW 21 – Umbau E-Mobilität;Elt-Montagen ab KW 33.;;;;;;;;;;;;;100%;100%;100%;;100%;100%;100%;20%;;Aktive Projekte;3;;;
9 24-4043;S-K ODL-Heim Sanierung Krefeld;Voraussichtlich LV-Abgabe.;;;;;;;;;;;;;100%;100%;100%;;100%;;;;;Aktive Projekte;4;;;
-11
View File
@@ -1,11 +0,0 @@
WOCHE;KW 32 / 2026;2026-08-05;
PERSONEN;;;MZ|Planung|Markus Zimmermann;FP|Planung|Frank Pietruschka;TK|Planung|Tim Küppers;RR|Projektleitung|Robert Röllecke;AS|Bauüberwachung|Andreas Simon;SL|Bauüberwachung|Steffan Licata;;;;;;;;;;;;;;
Pr.-Nr.;Bezeichnung;Status;MZ;FP;TK;RR;AS;SL;LPH 1;LPH 2;LPH 3;LPH 4;LPH 5;LPH 6;LPH 7;LPH 8;W+M;Gruppe;Index;Plan;Aufwand;Aktive Phasen
ANWESENHEIT;;;;;;;;;;;;;;;;;;;;;
24-4101;Krankenhaus Köln, Heizungsmodernisierung;- Austausch Kesselhaus geplant;;;;;;;100%;100%;100%;;100%;100%;;90%;;Aktive Projekte;0;"{""start"":"""",""end"":"""",""todos"":[{""id"":""msmzj493z5gie"",""title"":""Abgabe"",""start"":""2026-08-10"",""end"":""2026-08-28"",""done"":false,""progress"":23,""dependsOn"":[],""assignees"":[""p_fp""]}],""milestones"":[{""id"":""msmzi2pbbsv1s"",""name"":""sxcdv"",""date"":""2026-08-15"",""dependsOn"":[],""position"":""""}],""people"":[],""progress"":0}";"{""LPH 1"":{""Planung"":1}}";"[""LPH 1""]"
24-4102;Bürokomplex Dortmund, Lüftung;- Filterkonzept in Abstimmung;;;;;;;100%;100%;100%;;95%;;;;;Aktive Projekte;1;"{""start"":"""",""end"":"""",""todos"":[],""milestones"":[{""id"":""msmzhc7wo8alc"",""name"":""Test 123"",""date"":""2026-08-10"",""dependsOn"":[],""position"":""""}],""people"":[],""progress"":0}";;
26-4105;Wohnanlage Essen, Regenwasser;- Planung Drainage;;;;;;;100%;100%;;;;;;;;Aktive Projekte;4;;;
;Badmodernisierung Privatkunde;- Angebot erstellt;;;;;;;;;;;100%;;;;;In Anbahnung;0;;;
;Wärmepumpen-Studie;- Erstgespräch durchgeführt;;;;;;;100%;;;;;;;;;In Anbahnung;1;;;
;HLS Planungscheckliste;- Vorlage aktualisiert;;;;;;;100%;;;;;;;;;Allgemein;0;;;
21-4001;Feuerwehrgeräteh. Rodenkirchen;;;;;;;;;;;;;;;;;Aktive Projekte;5;;;
1 WOCHE KW 32 / 2026 2026-08-05
2 PERSONEN MZ|Planung|Markus Zimmermann FP|Planung|Frank Pietruschka TK|Planung|Tim Küppers RR|Projektleitung|Robert Röllecke AS|Bauüberwachung|Andreas Simon SL|Bauüberwachung|Steffan Licata
3 Pr.-Nr. Bezeichnung Status MZ FP TK RR AS SL LPH 1 LPH 2 LPH 3 LPH 4 LPH 5 LPH 6 LPH 7 LPH 8 W+M Gruppe Index Plan Aufwand Aktive Phasen
4 ANWESENHEIT
5 24-4101 Krankenhaus Köln, Heizungsmodernisierung - Austausch Kesselhaus geplant 100% 100% 100% 100% 100% 90% Aktive Projekte 0 {"start":"","end":"","todos":[{"id":"msmzj493z5gie","title":"Abgabe","start":"2026-08-10","end":"2026-08-28","done":false,"progress":23,"dependsOn":[],"assignees":["p_fp"]}],"milestones":[{"id":"msmzi2pbbsv1s","name":"sxcdv","date":"2026-08-15","dependsOn":[],"position":""}],"people":[],"progress":0} {"LPH 1":{"Planung":1}} ["LPH 1"]
6 24-4102 Bürokomplex Dortmund, Lüftung - Filterkonzept in Abstimmung 100% 100% 100% 95% Aktive Projekte 1 {"start":"","end":"","todos":[],"milestones":[{"id":"msmzhc7wo8alc","name":"Test 123","date":"2026-08-10","dependsOn":[],"position":""}],"people":[],"progress":0}
7 26-4105 Wohnanlage Essen, Regenwasser - Planung Drainage 100% 100% Aktive Projekte 4
8 Badmodernisierung Privatkunde - Angebot erstellt 100% In Anbahnung 0
9 Wärmepumpen-Studie - Erstgespräch durchgeführt 100% In Anbahnung 1
10 HLS Planungscheckliste - Vorlage aktualisiert 100% Allgemein 0
11 21-4001 Feuerwehrgeräteh. Rodenkirchen Aktive Projekte 5
-7
View File
@@ -1,7 +0,0 @@
NAME;Rollen;Voller Name;E-Mail;Abwesenheiten
AS;Bauüberwachung;Andreas Simon;[];[]
FP;Planung;Frank Pietruschka;[];[]
MZ;Planung;Markus Zimmermann;[];[]
RR;Planung,Projektleitung;Robert Röllecke;[];[]
SL;Bauüberwachung;Steffan Licata;[];[]
TK;Planung;Tim Küppers;[];[]
1 NAME Rollen Voller Name E-Mail Abwesenheiten
2 AS Bauüberwachung Andreas Simon [] []
3 FP Planung Frank Pietruschka [] []
4 MZ Planung Markus Zimmermann [] []
5 RR Planung,Projektleitung Robert Röllecke [] []
6 SL Bauüberwachung Steffan Licata [] []
7 TK Planung Tim Küppers [] []