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
+960 -5531

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 Single-page app for tracking engineering projects across weekly sessions
("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build ("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build
step, no bundler, no tests. Data is persisted as semicolon-separated CSVs step, no bundler, no tests. Data is persisted in a SQLite database
under `sessions/`. (`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`.
Two HTML entrypoints, both served statically by Express: Two HTML entrypoints, both served statically by Express:
- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte, - `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte,
Projektbeteiligte). Served at `/`. Projektbeteiligte). Served at `/`.
- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new - `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 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. 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. user-facing text.
## File structure ## File structure
``` ```
. .
├── server.js # Express server + CSV API ├── server.js # Express server + SQLite API
├── package.json # deps: express, cors only ├── package.json # deps: express, cors only
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile ├── 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 ├── .dockerignore
├── .vscode/extensions.json ├── .vscode/extensions.json
├── public/ ├── public/
│ ├── Test.html # main app (served at /) │ ├── Test.html # main app (served at /)
│ ├── js/ # shared modules (classic scripts, global fns) │ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment │ │ ├── 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 │ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays │ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek │ │ ├── server.js # baseUrl, sendWeekToServer, sendPeopleToServer, blobToDataUrl
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed │ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung │ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared │ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
@@ -62,8 +61,8 @@ user-facing text.
│ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link │ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link
│ ├── styles.css │ ├── styles.css
│ └── dein_bild.ico │ └── dein_bild.ico
└── sessions/ # data; bind-mounted into Docker container └── sessions/ # SQLite-DB; bind-mounted into Docker container
├── ELT/ HLS/ ARCH/ # one folder per department └── projektuebersicht.db # node:sqlite database (WAL mode)
``` ```
## Running ## 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 reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths `js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes. (`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with - `GET /load-all?department=` → `{weeks:[weekObj], people:[personObj]}` with the
`_people.csv` content in `people`; sessions sorted by filename. global people list in `people`; weeks are full JSON week objects (id, label,
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV. date, locked, people, attendance, projects), sorted by label.
- `POST /delete-csv {filename, department}`. - `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 hard-coded as `ELT|HLS|ARCH` in `server.js`
(`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in (`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in
`DEPARTMENTS` in `js/globals.js`. `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. - JSON body limit is 2 MB.
## CSV format ## Persistence (SQLite)
- Delimiter `;`, UTF-8 with BOM. Hand-editing is possible but risky; the parser - Data lives in `sessions/projektuebersicht.db` (`node:sqlite` `DatabaseSync`,
reconstructs objects from the fixed column layout. WAL mode, `PRAGMA foreign_keys = ON`), created automatically. The schema is
- Week files `<Label>.csv` (e.g. `KW_30___2026.csv` — `filenameForWeek()` **fully relational** (18 tables) instead of one big JSON document per week:
replaces non-word chars with `_`). Row layout is positional: - `weeks` (department, label, week_id, date, locked; PK `(department,label)`)
`WOCHE` (label, date, locked flag) → `PERSONEN` (`name|role|fullName` per - `week_people` / `week_people_roles` — person copies per week (`array_order`
column) → header row → `ANWESENHEIT` → one row per project. keeps the list order, `role_order` the role array order)
- `_people.csv` per department: `NAME;Rollen;Voller Name;E-Mail;Abwesenheiten` - `attendance` — présence per week/person
(absences as JSON). People are global per department; new people are copied - `projects` (`array_order` = position in the JSON array, `order_value` = the
into the current week. Historical weeks keep their own person copies, so `p.order` field, which is only unique *within a group*, not globally!)
deleting a global person leaves old sessions unaffected no matter what. - `project_lph`, `project_assign` (person_id|role keys split into columns),
- Don't change the column layout without updating both `weekToCsv()` and `project_aufwand` (LP effort per role), `project_aktuelle_lp`
`parseSessionCsv()` (`js/csv.js`), which must stay inverse of each other. - `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 ## 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 `public/js/`; **all functions are globals on `window`**. Two HTML files include
them via `<script src>` tags; load order in the HTML defines dependencies, them via `<script src>` tags; load order in the HTML defines dependencies,
since there is no bundler: 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 → store → weeks` (shared modules), then the tab-specific folders `board →
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf → people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
init`. See the exact list in `Test.html`. 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 server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`). touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
- Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all - Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all
project-plan page state (week, project, zoom level, drag/`modal`-state), project-plan page state (week, project, zoom level, drag/`modal`-state),
`db`/`currentDepartment` live in `js/globals.js`. `db`/`currentDepartment` live in `js/globals.js`.
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only). - State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
- Project IDs are regenerated on every CSV load, so projects are addressed - Project/person IDs are stable across reloads because the server splits and
cross-page by `department` + week `label` + `projNr` (+ readable projName path reassembles week objects deterministically (JSON IDs are kept in the DB
segment) via URL params: `openProjectPlan()` builds columns); projects are addressed cross-page by `department` + week `label` +
`/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`, `findProjectForPlan` `projNr` (+ readable projName path segment) via URL params:
resolves them. `openProjectPlan()` builds `/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`,
- Person IDs are stable across reloads via `personIdForName(name)` → `findProjectForPlan` resolves them. Person IDs follow
`p_<lowercased alnum>`. `personIdForName(name)` → `p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus - `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks (via
`_people.csv` — not just the changed week. Exception: `js/plan/plan-state.js` `sendWeekToServer()`) plus the people list (via `sendPeopleToServer()`) — not
`savePlan()` bypasses `save()` and writes only the current week's CSV directly just the changed week. Exception: `js/plan/plan-state.js` `savePlan()` bypasses
via `sendCsvToServer()`. `save()` and writes only the current week directly via `sendWeekToServer()`.
- `switchDepartment()` cancels a pending save debounce before switching, and - `switchDepartment()` cancels a pending save debounce before switching, and
`load()` discards stale `/load-all` responses after a department switch `load()` discards stale `/load-all` responses after a department switch
(frozen `requestedDepartment`), so late responses can never render or persist (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`, - `docker compose up --build` from the repo root. Compose maps `3003:3003`,
bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`. bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`.
- Dockerfile: `node:20-alpine`, `npm ci --production`, runs as non-root `node`, - Dockerfile: `node:22-alpine` (nötig für node:sqlite), `npm ci --production`,
HEALTHCHECK on `/health`. `.dockerignore` excludes `Docker/*` (except runs as non-root `node`, HEALTHCHECK on `/health`. `.dockerignore` excludes
`Docker/Dockerfile`), `node_modules/`, `.git`, `npm-debug.log`. `Docker/*` (except `Docker/Dockerfile`), `node_modules/`, `.git`,
`npm-debug.log`.
## Conventions & verification ## 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`. `http://192.168.3.187:3000/erik/projektuebersicht.git`.
- No linter, formatter, typecheck, or test suite. Manual verification only: - No linter, formatter, typecheck, or test suite. Manual verification only:
start server, open `http://localhost:3003`, exercises the relevant tab, 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 # 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 WORKDIR /app
@@ -11,7 +13,7 @@ RUN npm ci --production
COPY server.js ./ COPY server.js ./
COPY public ./public 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 RUN mkdir -p /app/sessions && chown -R node:node /app
USER node 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 Frontend in `Test.html`, `styles.css` und den JS-Modulen unter `public/js/`
- einem einfachen Node.js-Server in `server.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/` - einem Docker-Setup unter `Docker/`
## Datei- und Komponentenübersicht ## 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 - stellt statische Dateien (`Test.html`, die Module unter `js/`, `styles.css`, etc.) bereit
- implementiert Endpunkte: - implementiert Endpunkte:
- `GET /` → liefert `Test.html` - `GET /` → liefert `Test.html`
- `GET /load-all` → lädt alle CSV-Dateien aus dem aktiven `sessions/<DEPARTMENT>/`-Ordner inkl. `_people.csv` - `GET /load-all?department=…` → lädt alle Sitzungen plus globale Personenliste einer Abteilung
- `POST /save-csv` → speichert eine CSV-Datei (Dateiname wird validiert) - `POST /save-week {department, week}` → speichert/aktualisiert eine Sitzung
- `POST /delete-csv` → löscht eine CSV-Datei - `POST /save-people {department, people}` → ersetzt die globale Personenliste einer Abteilung
- `POST /delete-week {department, label}` → löscht eine Sitzung
- `GET /health` → Gesundheitscheck - `GET /health` → Gesundheitscheck
- legt das `sessions`-Verzeichnis beim Start an, falls es nicht existiert - legt das `sessions`-Verzeichnis beim Start an, falls es nicht existiert
- beendet sich mit aussagekräftiger Fehlermeldung, wenn der Port belegt ist - 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) - `public/js/` (Module)
- Die Client-Logik ist in kleine klassische Script-Dateien aufgeteilt (alle Funktionen global auf `window`): - 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`, `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`, `init.js` sowie die Plan-Module unter `js/plan/` (`plan-state`, `plan-build-gantt`, `plan-gantt`,
`plan-drag`, `plan-modals`, `plan-schedule`). `plan-drag`, `plan-modals`, `plan-schedule`).
- rendert das Projektboard, die Projekt- und Personenverwaltung sowie die Auslastungsansicht - 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`) - verwendet `currentDepartment` zur Trennung von Abteilungen (`ELT`, `HLS`, `ARCH`)
- speichert: - speichert:
- `sessions/<DEPT>/<WEEK>.csv` - alle Sitzungen und die globale Personenliste pro Abteilung in `sessions/projektuebersicht.db`
- `sessions/<DEPT>/_people.csv` (globale Personenliste pro Abteilung)
- sortiert Sitzungen nach Kalenderwoche, nicht alphabetisch - sortiert Sitzungen nach Kalenderwoche, nicht alphabetisch
- sperrt historische Sitzungen automatisch (bis auf die chronologisch letzte) - sperrt historische Sitzungen automatisch (bis auf die chronologisch letzte)
- Personen sind global pro Abteilung verwaltet; historische Sitzungen behalten ihre lokalen Personenkopien - 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 ## Speicherorte und Datenfluss
### Speicherung ### Speicherung
- Alle persistierten Daten werden als CSV-Dateien in `sessions/` gespeichert. - Alle persistierten Daten werden in einer SQLite-Datenbank (`node:sqlite`, `DatabaseSync`, WAL-Modus) unter `sessions/projektuebersicht.db` gespeichert.
- Innerhalb von `sessions/` existieren Abteilungsordner: - Innerhalb von `sessions/` liegen ausschließlich die SQLite-Datenbankdateien (`projektuebersicht.db` + WAL/SHM).
- `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
### Wo wird der Server erstellt? ### Wo wird der Server erstellt?
- Der Server wird in `server.js` erstellt. - Der Server wird in `server.js` erstellt.
- `server.js` startet einen Express-Server auf Port `3003` oder dem Wert von `process.env.PORT`. - `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. - Das Frontend ruft diesen Server über `window.location.origin` an.
### Wo werden die Dateien gespeichert? ### Wo werden die Daten gespeichert?
- Das Frontend schreibt über den Server in das Dateisystem. - Das Frontend schreibt über den Server in die SQLite-Datenbank.
- In `js/store.js` ruft die Anwendung beim Speichern `/save-csv` auf. - In `js/store.js` ruft die Anwendung beim Speichern `/save-week` (Sitzungen) bzw. `/save-people` (globale Personen) auf.
- Der Server legt CSV-Dateien unter `sessions/<DEPARTMENT>/` ab. - Der Server schreibt die relationalen Tabellen in `sessions/projektuebersicht.db` (eine Transaktion pro Sitzung, siehe `serializeWeek()`/`loadWeeks()` in `server.js`).
- Beispiel:
- `sessions/ELT/KW_32___2026.csv`
- `sessions/ELT/_people.csv`
## Betrieb ## Betrieb
@@ -141,8 +132,8 @@ http://localhost:3003
``` ```
## Wichtige Hinweise ## 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. - 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. - 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 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 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, 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, 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/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 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/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script> <script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.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/server.js?v=5"></script>
<script src="/js/store.js?v=5"></script> <script src="/js/store.js?v=5"></script>
<script src="/js/weeks.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 // 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 // roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role` // (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role`
@@ -85,7 +85,7 @@ function renderBoard(){
let h2 = `<tr>`; let h2 = `<tr>`;
ROLES.forEach(r=>{ ROLES.forEach(r=>{
if(grouped[r].length===0) h2 += `<th class="rot ${roleClass(r)}">–</th>`; 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>`); LPHS.forEach(l=> h2 += `<th class="rot">${l}</th>`);
h2 += `</tr>`; 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){ function openProjectPlan(pid){
const w = currentWeek(); if(!w) return; const w = currentWeek(); if(!w) return;
const p = w.projects.find(x => x.id === pid); if(!p) return; const p = w.projects.find(x => x.id === pid); if(!p) return;
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt). // Projekt-IDs sind in der DB stabil, aber die Plan-Seite ist eine separate
// Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben. // Route. Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
// Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der // Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der
// Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert. // Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert.
const url = new URL('/' + currentDepartment + '/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin); 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 todos = Array.isArray(plan.todos) ? plan.todos : [];
const milestones = Array.isArray(plan.milestones) ? plan.milestones : []; const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
const todoIds = todos.map(t => t.id); const todoIds = todos.map(t => t.id);
const milestoneIds = milestones.map(m => m.id);
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge // Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)]; 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 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); 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(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 // TODOs neu ordnen
const newTodos = []; const newTodos = [];
+18 -21
View File
@@ -66,20 +66,17 @@ window.PlanApp = {
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen, // bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der // 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(){ 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 // loadPlanData: lädt alle Sitzungen und die globale Personenliste der
// Abteilung über die Server-API, sortiert die Wochen chronologisch und // Abteilung über die Server-API (JSON-Dokumente), sortiert die Wochen
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen // chronologisch und sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage(). // Fehlermeldungen der Reihenfolge nach im Seitenkopf an und startet sonst
// renderPlanPage().
function loadPlanData(){ function loadPlanData(){
if(!window.fetch){ if(!window.fetch){
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.'; document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
@@ -90,11 +87,14 @@ window.PlanApp = {
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); }) .then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => { .then(data => {
db = { weeks: [], currentWeek: null, globalPeople: {} }; db = { weeks: [], currentWeek: null, globalPeople: {} };
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people); db.globalPeople[app.department] = Array.isArray(data.people) ? data.people : [];
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) })); db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
parsed.forEach(s => { .filter(w => w && typeof w.label === 'string')
const w = parseSessionCsv(s.filename, s.rows); .map(w => {
if(w) db.weeks.push(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))); db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
app.week = db.weeks.find(w => w.label === app.weekLabel); app.week = db.weeks.find(w => w.label === app.weekLabel);
@@ -179,18 +179,15 @@ window.PlanApp = {
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben // Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
function savePlan(){ function savePlan(){
if(!app.week || !app.project) return; if(!app.week || !app.project) return;
const filename = filenameForWeek(app.week.label); sendWeekToServer(app.week).catch(err => console.error('Speichern fehlgeschlagen:', err));
const csv = weekToCsv(app.week);
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
} }
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als // 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(){ function saveGlobalPeople(){
const people = db.globalPeople[app.department] || []; const people = db.globalPeople[app.department] || [];
if(people.length > 0){ if(people.length > 0){
const peopleCsv = globalPeopleToCsv(people); sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
} }
} }
+4 -3
View File
@@ -62,6 +62,7 @@ function editProjectActiveLP(id, value){
function renderProjects(){ function renderProjects(){
const w = currentWeek(); const w = currentWeek();
if(!w) return; if(!w) return;
const locked = isWeekLocked(w);
let html=""; let html="";
const groups = {}; const groups = {};
w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); }); w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); });
@@ -71,10 +72,10 @@ function renderProjects(){
sortGroups(Object.keys(groups)).forEach(g=>{ sortGroups(Object.keys(groups)).forEach(g=>{
html += `<div class="section-title">${esc(g)}</div>`; 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=>{ groups[g].forEach(p=>{
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"> html += `<div class="person-list-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span> ${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.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"> <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)"> <select onchange="editProjectField('${p.id}','group',this.value)">
+30 -16
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 // 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, // HTML-Einstiegspunkten geladen. Sie stellt baseUrl, blobToDataUrl,
// sendCsvToServer und deleteCsvFromServer bereit. Genutzt wird sie von // sendWeekToServer, sendPeopleToServer und deleteWeekFromServer bereit.
// js/store.js (syncAllToServer, Speichern/Löschen aller Wochen), // Genutzt wird sie von js/store.js (syncAllToServer, Speichern aller Wochen
// js/weeks.js (Wochen-Löschung) und js/plan/plan-state.js (savePlan schreibt // und der Personenliste), js/weeks.js (Wochen-Löschung) und
// direkt die aktuelle Woche über sendCsvToServer). // 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. // 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. // 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 // sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
// Parameter: filename (String), csvContent (String). Rückgabe: Promise mit Server-Antwort. // <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit Server-Antwort.
function sendCsvToServer(filename, csvContent){ function sendWeekToServer(week){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar')); if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/save-csv',{ return fetch(baseUrl() + '/save-week',{
method:'POST', method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'}, 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(); }); }).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 // sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
// Parameter: filename (String). Rückgabe: Promise mit Server-Antwort. // JSON-Array an <baseUrl>/save-people. Parameter: people (Array).
function deleteCsvFromServer(filename){ // Rückgabe: Promise mit Server-Antwort.
function sendPeopleToServer(people){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar')); if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/delete-csv', { return fetch(baseUrl() + '/save-people',{
method:'POST', method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'}, headers:{'Content-Type':'application/json; charset=utf-8'},
body: JSON.stringify({ filename, department: currentDepartment }) 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({ label, department: currentDepartment })
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); }); }).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
} }
+32 -31
View File
@@ -1,18 +1,20 @@
// ----------------------------- // -----------------------------
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln // 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 // beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH). // 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()`, // die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`, // `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 // `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. // ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server. // save: synchronisiert die aktuellen Daten zum Server.
// Jede Sitzung wird als eigene CSV-Datei gespeichert. Personen und Rollen werden // Jede Sitzung wird als eigenes JSON-Dokument gespeichert. Personen und Rollen
// in der Sitzungscsv selbst abgelegt. // werden in der Sitzung selbst abgelegt.
function save(){ function save(){
scheduleSessionSync(); scheduleSessionSync();
} }
@@ -25,31 +27,28 @@ function scheduleSessionSync(){
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500); saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
} }
// syncAllToServer: schreibt die gesamte Datenbank als CSV-Dateien zum Server. // syncAllToServer: schreibt die gesamte Datenbank als JSON-Dokumente zum Server.
// Zuerst wird die globale Personenliste (`_people.csv`) gespeichert, danach jede // Zuerst wird die globale Personenliste (`save-people`) gespeichert, danach jede
// Sitzung einzeln über `filenameForWeek()`/`weekToCsv()` und `sendCsvToServer()`. // Sitzung einzeln über `sendWeekToServer()`.
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert. // Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
function syncAllToServer(){ function syncAllToServer(){
// Zuerst globale Personenliste speichern // Zuerst globale Personenliste speichern
saveGlobalPeopleToServer(); saveGlobalPeopleToServer();
// Dann jede Sitzung speichern // Dann jede Sitzung speichern
db.weeks.forEach(w => { db.weeks.forEach(w => {
const filename = filenameForWeek(w.label); sendWeekToServer(w)
const csv = weekToCsv(w); .then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', w.label); })
sendCsvToServer(filename, csv) .catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err); });
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', filename); })
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', filename, err.message || err); });
}); });
} }
// saveGlobalPeopleToServer: serialisiert die globale Personenliste der aktuellen // saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
// Abteilung per `globalPeopleToCsv()` und speichert sie als `_people.csv`. // Abteilung über `sendPeopleToServer()` (JSON-Array).
// Leere Personenlisten werden übersprungen (kein Schreibvorgang). // Leere Personenlisten werden übersprungen (kein Schreibvorgang).
function saveGlobalPeopleToServer(){ function saveGlobalPeopleToServer(){
const people = db.globalPeople[currentDepartment] || []; const people = db.globalPeople[currentDepartment] || [];
if(people.length === 0) return; if(people.length === 0) return;
const csv = globalPeopleToCsv(people); sendPeopleToServer(people)
sendCsvToServer('_people.csv', csv)
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); }) .then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); }); .catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
} }
@@ -62,7 +61,7 @@ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return; if(!DEPARTMENTS[dept]) return;
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen // Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die // 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; } if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
currentDepartment = dept; currentDepartment = dept;
saveUiState(); saveUiState();
@@ -84,11 +83,11 @@ function switchDepartment(dept){
load(); load();
} }
// load: liest alle CSV-Dateien der aktuellen Abteilung über `/load-all` vom // load: liest alle Sitzungen und die globale Personenliste der aktuellen
// Server ein, parst Personen und Sitzungen (mit `parseSessionCsv()`) und baut // Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
// daraus `db.weeks` auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` // auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
// an und rendert die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder // die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
// fehlenden Daten werden Beispieldaten über `seed()` geladen. // Daten werden Beispieldaten über `seed()` geladen.
function load(){ function load(){
if(!window.fetch){ if(!window.fetch){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.'); alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
@@ -106,18 +105,20 @@ function load(){
.then(data => { .then(data => {
if(currentDepartment !== requestedDepartment) return; if(currentDepartment !== requestedDepartment) return;
// Globale Personenliste laden // 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 = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
db.weeks = []; .map(w => {
parsedSessions.forEach(s => { // Felder absichern, damit alte/kaputte Dokumente nicht crashen
const w = parseSessionCsv(s.filename, s.rows); if(!Array.isArray(w.people)) w.people = [];
if(w) db.weeks.push(w); if(!Array.isArray(w.projects)) w.projects = [];
if(!w.attendance) w.attendance = {};
return w;
}); });
if(db.weeks.length === 0){ 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(); seed();
} }
else { else {
+14 -3
View File
@@ -5,7 +5,7 @@
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden // ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen // HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen, // 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. // (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck. // 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. // peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle // Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung. // Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der // Liegt im gemeinsamen Modul, weil board/load/pdf auf sie zugreifen.
// Projektplan-Seite) und pdf.js darauf zugreifen.
function peopleByRole(target){ function peopleByRole(target){
const m = {}; ROLES.forEach(r=>m[r]=[]); const m = {}; ROLES.forEach(r=>m[r]=[]);
const people = []; const people = [];
@@ -65,6 +64,18 @@ function peopleByRole(target){
return m; 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 // Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben. // Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'. // 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 // deleteWeek: löscht die aktuell ausgewählte Sitzung (nur wenn mehr als 1
// vorhanden) inkl. ihrer CSV-Datei auf dem Server. Parameter `skipAuth=true` // vorhanden) auf dem Server. Parameter `skipAuth=true` überspringt die
// überspringt die Passwortabfrage. Danach wird die letzte verbleibende Woche // Passwortabfrage. Danach wird die letzte verbleibende Woche aktiviert und neu
// aktiviert und neu gerendert. Keine Rückgabe. // gerendert. Keine Rückgabe.
function deleteWeek(skipAuth = false){ function deleteWeek(skipAuth = false){
if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; } if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; }
if(!skipAuth && !isAuthenticated){ if(!skipAuth && !isAuthenticated){
@@ -138,13 +138,13 @@ function deleteWeek(skipAuth = false){
} }
if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; } if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; }
const current = currentWeek(); const current = currentWeek();
const filename = current ? filenameForWeek(current.label) : null; if(current){
if(filename){ const label = current.label;
console.log('Lösche CSV-Datei auf Server:', filename); console.log('Lösche Woche auf Server:', label);
deleteCsvFromServer(filename).then(res=>{ deleteWeekFromServer(label).then(res=>{
if(res && res.ok){ console.log('CSV-Datei gelöscht:', filename); } if(res && res.ok){ console.log('Woche gelöscht:', label); }
else { alert('CSV-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); } else { alert('Wochen-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
}).catch(err=>{ alert('CSV-Löschung fehlgeschlagen: ' + (err.message || err)); }); }).catch(err=>{ alert('Wochen-Löschung fehlgeschlagen: ' + (err.message || err)); });
} }
db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek); db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek);
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label))); 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 in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
"/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr. "/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr.
kommen als URL-Parameter (weekLabel, projNr). Sie lädt styles.css und ausschließlich die gemeinsamen 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, 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-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, 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/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script> <script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.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/server.js?v=5"></script>
<script src="/js/gantt.js?v=5"></script> <script src="/js/gantt.js?v=5"></script>
<script src="/js/plan/plan-state.js?v=5"></script> <script src="/js/plan/plan-state.js?v=5"></script>
File diff suppressed because it is too large. Load diff
+740 -71
View File
@@ -1,19 +1,32 @@
/* /*
* Express-Server der Projektübersicht: liefert die statischen Frontend-Dateien aus * Express-Server der Projektübersicht: liefert die statischen Frontend-Dateien aus
* public/ (Test.html unter "/") aus und stellt den Frontend-Modulen unter public/js/ * public/ (Test.html unter "/") und stellt den Frontend-Modulen unter public/js/
* eine CSV-API zum Laden und Speichern der Sitzungsdaten in sessions/ bereit. * 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. * Standardport ist 3003, überschreibbar über die Umgebungsvariable PORT.
* Endpunkte: GET /, GET /health, GET /load-all, POST /save-csv, POST /delete-csv. * Endpunkte: GET /, GET /health, GET /load-all, POST /save-week, POST /save-people,
* Die Daten werden als UTF-8-CSV mit BOM geschrieben; ungültige Abteilungen fallen * POST /delete-week. Ungültige Abteilungen fallen auf ELT zurück.
* auf ELT zurück, Dateinamen werden über safeName() streng validiert.
*/ */
const express = require('express'); const express = require('express');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { DatabaseSync } = require('node:sqlite');
const app = express(); const app = express();
// Wichtige Konstanten: UTF-8-BOM für CSV-Dateien, Serverport (PORT-Env) und die gültigen Abteilungsordner // Wichtige Konstanten: Serverport (PORT-Env) und die gültigen Abteilungsordner
const BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
const PORT = process.env.PORT || 3003; const PORT = process.env.PORT || 3003;
const VALID_DEPARTMENTS = new Set(['ELT','HLS','ARCH']); 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'); const SESSIONS_DIR = path.join(__dirname, 'sessions');
if(!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true }); if(!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
// Erlaubt ausschliesslich sichere CSV-Dateinamen im Format <Name>.csv // SQLite-Datenbank öffnen (wird angelegt, falls nicht vorhanden). Der Pfad ist
function safeName(name){ // über die Umgebungsvariable DB_PATH übersteuerbar (für Tests/Backups), sonst
const raw = String(name || ''); // standardmäßig sessions/projektuebersicht.db.
// Pfad-Trennzeichen und Verzeichnisnavigation ablehnen const DB_PATH = process.env.DB_PATH ? path.resolve(process.env.DB_PATH) : path.join(SESSIONS_DIR, 'projektuebersicht.db');
if(/[\\/]|\.\./.test(raw)) return null; const db = new DatabaseSync(DB_PATH);
const base = path.basename(raw); db.exec('PRAGMA journal_mode = WAL;');
if(!base || base.toLowerCase() === '.csv') return null; db.exec('PRAGMA foreign_keys = ON;');
const safe = base.replace(/[^a-zA-Z0-9_\-\.]/g,'_');
if(!/^([a-zA-Z0-9_\-]+)\.csv$/i.test(safe)) return null; // ---------- Schema (voll relational normalisiert) ----------
return safe;
// 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 // ---------- Migration: altes Ein-Dokument-Format -> relationales Schema ----------
function departmentDir(department){
const dept = (typeof department === 'string' && VALID_DEPARTMENTS.has(department)) ? department : 'ELT'; // hasLegacyWeeksTable: erkennt das alte Schema an der Daten-Spalte in `weeks`.
const dir = path.join(SESSIONS_DIR, dept); function hasLegacyWeeksTable(){
if(!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const cols = db.prepare("SELECT name FROM pragma_table_info('weeks') WHERE name = 'data'").all();
return dir; return cols.length > 0;
} }
// Route: Speichert eine CSV-Datei (mit UTF-8-BOM) für eine Abteilung im sessions/-Ordner // serializeWeek: zerlegt ein Wochen-Objekt (JSON vom Frontend) in die relationalen
app.post('/save-csv', (req, res) => { // Tabellen. Wird in einer Transaktion vom Aufrufer verwendet (deleteWeekData +
const { filename, csv, department } = req.body || {}; // serializeWeek). Ersetzt alle Zeilen der Woche.
if(!filename || typeof csv !== 'string') return res.status(400).json({ ok: false, error: 'Missing filename or csv' }); function serializeWeek(department, week){
const safe = safeName(filename); const weekLabel = String(week.label || '');
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' }); const upsertWeek = db.prepare(`INSERT INTO weeks (department, label, week_id, date, locked) VALUES (?, ?, ?, ?, ?)
const dir = departmentDir(department); ON CONFLICT(department, label) DO UPDATE SET week_id = excluded.week_id, date = excluded.date, locked = excluded.locked`);
const out = path.join(dir, safe); upsertWeek.run(department, weekLabel, String(week.id || ''), String(week.date || ''), week.locked ? 1 : 0);
const data = Buffer.concat([BOM, Buffer.from(csv, 'utf8')]);
fs.writeFile(out, data, (err) => { const insWeekPerson = db.prepare(`INSERT INTO week_people (department, week_label, person_id, name, full_name, array_order)
if(err){ console.error('Write error', err); return res.status(500).json({ ok:false, error: String(err) }); } VALUES (?, ?, ?, ?, ?, ?)`);
console.log('Saved CSV:', out); const insWeekPersonRole = db.prepare(`INSERT OR IGNORE INTO week_people_roles (department, week_label, person_id, role, role_order)
return res.json({ ok: true, path: out }); VALUES (?, ?, ?, ?, ?)`);
const insAttendance = db.prepare(`INSERT OR REPLACE INTO attendance (department, week_label, person_id, present)
VALUES (?, ?, ?, ?)`);
(Array.isArray(week.people) ? week.people : []).forEach((p, idx) => {
insWeekPerson.run(department, weekLabel, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), idx);
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insWeekPersonRole.run(department, weekLabel, String(p.id || ''), String(r || ''), ridx));
}); });
const att = week.attendance || {};
Object.keys(att).forEach(pid => {
insAttendance.run(department, weekLabel, pid, att[pid] ? 1 : 0);
}); });
// Route: Löscht eine CSV-Datei einer Abteilung aus dem sessions/-Ordner (fehlende Datei wird toleriert) const insProject = db.prepare(`INSERT INTO projects (department, week_label, project_id, nr, name, status, "group", array_order, order_value)
app.post('/delete-csv', (req, res) => { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const { filename, department } = req.body || {}; const insLph = db.prepare(`INSERT OR REPLACE INTO project_lph (department, week_label, project_id, lph, value)
if(!filename) return res.status(400).json({ ok: false, error: 'Missing filename' }); VALUES (?, ?, ?, ?, ?)`);
const safe = safeName(filename); const insAssign = db.prepare(`INSERT OR REPLACE INTO project_assign (department, week_label, project_id, person_id, role, state)
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' }); VALUES (?, ?, ?, ?, ?, ?)`);
const dir = departmentDir(department); const insAufwand = db.prepare(`INSERT OR REPLACE INTO project_aufwand (department, week_label, project_id, lph, role, value)
const out = path.join(dir, safe); VALUES (?, ?, ?, ?, ?, ?)`);
fs.unlink(out, (err) => { const insAktuelleLp = db.prepare(`INSERT OR IGNORE INTO project_aktuelle_lp (department, week_label, project_id, lph)
if(err){ VALUES (?, ?, ?, ?)`);
if(err.code === 'ENOENT') { const insPlan = db.prepare(`INSERT OR REPLACE INTO project_plan (department, week_label, project_id, start, end, progress)
console.log('CSV-Datei nicht gefunden:', out); VALUES (?, ?, ?, ?, ?, ?)`);
return res.json({ ok: true, missing: true }); const insPlanPerson = db.prepare(`INSERT OR IGNORE INTO plan_people (department, week_label, project_id, person_id, array_order)
VALUES (?, ?, ?, ?, ?)`);
const insTodo = db.prepare(`INSERT INTO plan_todos (department, week_label, project_id, todo_id, title, start, end, done, progress, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insTodoAssignee = db.prepare(`INSERT OR IGNORE INTO plan_todo_assignees (department, week_label, project_id, todo_id, person_id)
VALUES (?, ?, ?, ?, ?)`);
const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (department, week_label, project_id, todo_id, depends_on_id)
VALUES (?, ?, ?, ?, ?)`);
const insMilestone = db.prepare(`INSERT INTO plan_milestones (department, week_label, project_id, milestone_id, name, date, position, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (department, week_label, project_id, milestone_id, depends_on_id)
VALUES (?, ?, ?, ?, ?)`);
(Array.isArray(week.projects) ? week.projects : []).forEach((p, idx) => {
const projId = String(p.id || '');
insProject.run(department, weekLabel, projId, String(p.nr || ''), String(p.name || ''),
String(p.status || ''), String(p.group || ''), idx, typeof p.order === 'number' ? p.order : idx);
const lph = p.lph || {};
Object.keys(lph).forEach(l => insLph.run(department, weekLabel, projId, String(l || ''), String(lph[l] === undefined ? '' : lph[l])));
const assign = p.assign || {};
Object.keys(assign).forEach(key => {
// Key-Format: "personId|Rolle"
const sep = key.indexOf('|');
const pid = sep >= 0 ? key.slice(0, sep) : key;
const role = sep >= 0 ? key.slice(sep + 1) : '';
insAssign.run(department, weekLabel, projId, pid, role, Number(assign[key]) || 0);
});
const aufwand = p.aufwandProLP || {};
Object.keys(aufwand).forEach(lph => {
const roles = aufwand[lph] || {};
Object.keys(roles).forEach(role => {
const v = Number(roles[role]);
if(!isNaN(v)) insAufwand.run(department, weekLabel, projId, String(lph || ''), String(role || ''), v);
});
});
(Array.isArray(p.aktuelleLP) ? p.aktuelleLP : []).forEach(lph => {
insAktuelleLp.run(department, weekLabel, projId, String(lph || ''));
});
const plan = p.plan || null;
if(plan){
insPlan.run(department, weekLabel, projId, String(plan.start || ''), String(plan.end || ''), Number(plan.progress) || 0);
(Array.isArray(plan.people) ? plan.people : []).forEach((pid, pidx) => insPlanPerson.run(department, weekLabel, projId, String(pid || ''), pidx));
(Array.isArray(plan.todos) ? plan.todos : []).forEach((t, tidx) => {
insTodo.run(department, weekLabel, projId, String(t.id || ''), String(t.title || ''),
String(t.start || ''), String(t.end || ''), t.done ? 1 : 0, Number(t.progress) || 0, tidx);
(Array.isArray(t.assignees) ? t.assignees : []).forEach(pid => insTodoAssignee.run(department, weekLabel, projId, String(t.id || ''), String(pid || '')));
(Array.isArray(t.dependsOn) ? t.dependsOn : []).forEach(did => insTodoDep.run(department, weekLabel, projId, String(t.id || ''), String(did || '')));
});
(Array.isArray(plan.milestones) ? plan.milestones : []).forEach((m, midx) => {
insMilestone.run(department, weekLabel, projId, String(m.id || ''), String(m.name || ''), String(m.date || ''), String(m.position || ''), midx);
(Array.isArray(m.dependsOn) ? m.dependsOn : []).forEach(did => insMilestoneDep.run(department, weekLabel, projId, String(m.id || ''), String(did || '')));
});
} }
console.error('Delete error', err); });
}
// 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) }); return res.status(500).json({ ok: false, error: String(err) });
} }
console.log('Deleted CSV:', out);
return res.json({ ok: true });
});
}); });
// Route: Lädt alle Sitzungs-CSV-Dateien einer Abteilung plus _people.csv (nach Dateiname sortiert) // Route: Speichert die globale Personenliste einer Abteilung (als JSON-Array)
app.get('/load-all', (req, res) => { app.post('/save-people', (req, res) => {
const dir = departmentDir(req.query.department); const { department, people } = req.body || {};
fs.readdir(dir, (err, files) => { if(!Array.isArray(people)) return res.status(400).json({ ok: false, error: 'Missing people array' });
if(err){ console.error('Read error', err); return res.status(500).json({ ok:false, error: String(err) }); } const dept = departmentKey(department);
const csvFiles = files.filter(f => f.toLowerCase().endsWith('.csv')); try {
const result = { ok: true, sessions: [], people: null }; serializeGlobalPeople(dept, people);
csvFiles.forEach(filename => { console.log('Saved people:', dept, '(' + people.length + ' Personen)');
if(filename.toLowerCase() === '_people.csv'){ return res.json({ ok: true });
result.people = fs.readFileSync(path.join(dir, filename), 'utf8'); } catch(err){
return; console.error('Save people error', err);
return res.status(500).json({ ok: false, error: String(err) });
} }
const content = fs.readFileSync(path.join(dir, filename), 'utf8');
result.sessions.push({ filename, content });
}); });
// Sortiere Sitzungen nach Dateiname, damit die Reihenfolge stabil ist
result.sessions.sort((a,b) => a.filename.localeCompare(b.filename)); // Route: Löscht eine Woche einer Abteilung (fehlende Woche wird toleriert)
res.json(result); 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 // 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) // 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) => { server.on('error', (err) => {
console.error('Server konnte nicht starten:', err.message); console.error('Server konnte nicht starten:', err.message);
if(err.code === 'EADDRINUSE'){ 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 [] []