5 Commits
Author SHA1 Message Date
erik 18d04e2c4a Zwischen Commit 2026-10-02 07:47:57 +02:00
erik 4fc5acfebb Claude change 2026-09-14 18:10:49 +02:00
erik 1c176ea621 DB Import und Export sowie Backup und kleinere Fehlerbeseitigung 2026-08-18 09:16:59 +02:00
erik 91cc898ec7 Legende Projektkürzel im dark mode jetzt auch dunkel 2026-08-14 15:15:22 +02:00
erik 4e083bee73 Unnötige Datein gelöscht. DB normalisiert 2026-08-14 15:03:24 +02:00
49 changed files with 2440 additions and 5729 deletions

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
-198
View File
@@ -1,198 +0,0 @@
# AGENTS.md
## Overview
Single-page app for tracking engineering projects across weekly sessions
("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build
step, no bundler, no tests. Data is persisted as semicolon-separated CSVs
under `sessions/`.
Two HTML entrypoints, both served statically by Express:
- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte,
Projektbeteiligte). Served at `/`.
- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new
tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules
(`globals`, `utils`, `ui`, `date-picker`, `csv`, `server`, `gantt`) plus the
plan modules under `js/plan/` — NOT the full main module set.
All UI strings/comments/CSV content are German. Preserve German in
user-facing text.
## File structure
```
.
├── server.js # Express server + CSV API
├── package.json # deps: express, cors only
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile
├── Docker/Dockerfile # node:20-alpine
├── .dockerignore
├── .vscode/extensions.json
├── public/
│ ├── Test.html # main app (served at /)
│ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey-Quelle
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
│ │ ├── showmode.js # Show-Mode (Präsentationsmodus)
│ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz)
│ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer)
│ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html)
│ │ ├── board/ # tab-specific: Projektboard
│ │ │ └── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung
│ │ ├── people/ # tab-specific: Projektbeteiligte
│ │ │ └── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten
│ │ ├── projects/ # tab-specific: Projekte
│ │ │ └── projects.js # Projekte-Tab-Rendering + Drag-and-Drop
│ │ ├── load/ # tab-specific: Auslastung
│ │ │ └── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal
│ ├── project-plan.html # Gantt/TODO planning view
│ ├── js/plan/ # planning modules (nur von project-plan.html geladen)
│ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts
│ │ ├── plan-build-gantt.js # buildGanttHtml-Erweiterung (TODO- und Meilensteinzeilen)
│ │ ├── plan-gantt.js # renderPlanGantt, Abhängigkeitslinien, Zoom, Ressourcen-Load
│ │ ├── plan-drag.js # GANTT-Balken-Drag/Resize + TODO-/Meilenstein-Zeilen-Drag
│ │ ├── plan-modals.js # TODO-, Meilenstein- und Abwesenheits-Modals
│ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link
│ ├── styles.css
│ └── dein_bild.ico
└── sessions/ # data; bind-mounted into Docker container
├── ELT/ HLS/ ARCH/ # one folder per department
```
## Running
```bash
npm install
npm start # http://localhost:3003, or PORT=3004 if occupied
docker compose up --build # Docker variant (compose file is at repo root)
```
- PORT env var overrides default 3003 in `server.js`.
- Frontend must be served over HTTP: `baseUrl()` in `js/server.js` uses
`window.location.origin` or falls back to `http://localhost:3003` when opened
via `file://` (then only seed/demo data loads).
- Check `/health` returns `{ok:true}` after starting.
## Server API (`server.js`)
- `GET /` → `Test.html`; `GET /health`.
- Deep links per department + tab: `GET /<Dept>/<Tab>` → `Test.html`, e.g.
`/HLS/Auslastung` or `/ELT/Projektübersicht`. Tab slugs (`TAB_SLUGS` in
`js/globals.js`) are `Projektübersicht|Auslastung|Projekte|Projektbeteiligte`;
anything else returns 404. Exception: `GET /<Dept>/Projektplan(/<ProjName>)?`
(`PLAN_SLUG`) serves `project-plan.html`, the GANTT view, with
`weekLabel|projNr` as query params (the project name is a readable path
segment only; identification happens via weekLabel + projNr). The frontend
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with
`_people.csv` content in `people`; sessions sorted by filename.
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV.
- `POST /delete-csv {filename, department}`.
- Valid departments hard-coded as `ELT|HLS|ARCH` in `server.js`
(`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in
`DEPARTMENTS` in `js/globals.js`.
- `safeName()` strictly validates filenames (path traversal protection);
static assets get `Cache-Control: no-store`.
- JSON body limit is 2 MB.
## CSV format
- Delimiter `;`, UTF-8 with BOM. Hand-editing is possible but risky; the parser
reconstructs objects from the fixed column layout.
- Week files `<Label>.csv` (e.g. `KW_30___2026.csv` — `filenameForWeek()`
replaces non-word chars with `_`). Row layout is positional:
`WOCHE` (label, date, locked flag) → `PERSONEN` (`name|role|fullName` per
column) → header row → `ANWESENHEIT` → one row per project.
- `_people.csv` per department: `NAME;Rollen;Voller Name;E-Mail;Abwesenheiten`
(absences as JSON). People are global per department; new people are copied
into the current week. Historical weeks keep their own person copies, so
deleting a global person leaves old sessions unaffected no matter what.
- Don't change the column layout without updating both `weekToCsv()` and
`parseSessionCsv()` (`js/csv.js`), which must stay inverse of each other.
## Frontend architecture
- The frontend is split into small classic non-module `<script>` files under
`public/js/`; **all functions are globals on `window`**. Two HTML files include
them via `<script src>` tags; load order in the HTML defines dependencies,
since there is no bundler:
- `Test.html` loads `globals → utils → ui → date-picker → csv → server →
store → weeks` (shared modules), then the tab-specific folders `board →
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
init`. See the exact list in `Test.html`.
- `project-plan.html` loads only `globals → utils → ui → date-picker → csv →
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
- Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all
project-plan page state (week, project, zoom level, drag/`modal`-state),
`db`/`currentDepartment` live in `js/globals.js`.
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
- Project IDs are regenerated on every CSV load, so projects are addressed
cross-page by `department` + week `label` + `projNr` (+ readable projName path
segment) via URL params: `openProjectPlan()` builds
`/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`, `findProjectForPlan`
resolves them.
- Person IDs are stable across reloads via `personIdForName(name)` →
`p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus
`_people.csv` — not just the changed week. Exception: `js/plan/plan-state.js`
`savePlan()` bypasses `save()` and writes only the current week's CSV directly
via `sendCsvToServer()`.
- `switchDepartment()` cancels a pending save debounce before switching, and
`load()` discards stale `/load-all` responses after a department switch
(frozen `requestedDepartment`), so late responses can never render or persist
data under the wrong department.
- Person drag-and-drop in the people tab: drag handle (`.drag-handle`) binds
`ondragstart`/`ondragend`, the `.people-sortable` container binds
`ondragover`/`ondrop`; insertion is computed by `getDragAfterElement(container,
y, selector)` (`js/projects/projects.js`, used with `.person-card` here and
`.person-list-item` in the projects tab). `reorderPeople()` updates both
`db.globalPeople` and the current week's person copy, then `save()`. No drag
handles are rendered on locked weeks.
- Weeks are sorted by calendar week extracted from the label (`KW XX / YYYY`)
via `weekSortKey()`, not alphabetically/by filename.
- `applyAutoLock()` locks every week except the chronologically last one after
load/clone/new-week; `localStorage` UI state is not restored for locked tabs.
- `localStorage` keys: `projuebersicht_v1` (department only — tab is never
restored from storage; at startup the tab comes from the deep-link URL if
present, otherwise `board`), `projuebersicht-theme`, header-collapse key. Do
**not** store data in `localStorage`; it is UI state only.
- Deep-link URLs (`/<Dept>/<Tab>`) are shared via the address bar:
`updateRouteUrl()` rewrites the path via `history.replaceState` on tab and
department switches; protected tabs keep the URL but open the password modal.
## Auth / security quirk
- The `projects` and `people` tabs, week deletion, and week unlock are gated by
a password modal comparing `CryptoJS.SHA256(input)` to
`PROTECTED_PASSWORD_HASH` (`js/globals.js`, modal in `js/tabs-auth.js`).
Client-side only — not real security. CryptoJS and pdfmake (PDF export) are
loaded from `cdnjs.cloudflare.com` in `Test.html`, so those features need
internet access and won't work offline. `project-plan.html` loads neither.
- User-provided status HTML is sanitized on render via a small whitelist
(`sanitizeStatusHtml` in `js/board/board.js`) to mitigate XSS.
## Docker
- `docker compose up --build` from the repo root. Compose maps `3003:3003`,
bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`.
- Dockerfile: `node:20-alpine`, `npm ci --production`, runs as non-root `node`,
HEALTHCHECK on `/health`. `.dockerignore` excludes `Docker/*` (except
`Docker/Dockerfile`), `node_modules/`, `.git`, `npm-debug.log`.
## Conventions & verification
- Git repo; commits are German (e.g. "PDF Export von A4 auf A3 …"). Remote:
`http://192.168.3.187:3000/erik/projektuebersicht.git`.
- No linter, formatter, typecheck, or test suite. Manual verification only:
start server, open `http://localhost:3003`, exercises the relevant tab,
confirm CSV changes in `sessions/`.
+275
View File
@@ -0,0 +1,275 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
Single-page app for tracking engineering projects across weekly sessions
("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build
step, no bundler, no tests. Data is persisted in a SQLite database
(`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`.
Two HTML entrypoints, both served statically by Express:
- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte,
Projektbeteiligte). Served at `/`.
- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new
tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules
(`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the
plan modules under `js/plan/` — NOT the full main module set.
All UI strings/comments are German. Preserve German in
user-facing text.
## File structure
```
.
├── server.js # Express server + SQLite API
├── package.json # deps: express, cors only
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile
├── Docker/Dockerfile # node:22-alpine (nötig für node:sqlite)
├── .dockerignore
├── public/
│ ├── Test.html # main app (served at /)
│ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── server.js # baseUrl, sendWeekToServer, sendPeopleToServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
│ │ ├── showmode.js # Show-Mode (Präsentationsmodus)
│ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz)
│ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer)
│ │ ├── csv.js # exportCSV (lädt ZIP von GET /export-csv)
│ │ ├── backup.js # downloadBackup (lädt SQLite-Snapshot von GET /backup)
│ │ ├── import.js # importDb (Backup-Wiederherstellung über POST /import, passwortgeschützt)
│ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html)
│ │ ├── board/ # tab-specific: Projektboard
│ │ │ └── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung
│ │ ├── people/ # tab-specific: Projektbeteiligte
│ │ │ └── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten
│ │ ├── projects/ # tab-specific: Projekte
│ │ │ └── projects.js # Projekte-Tab-Rendering + Drag-and-Drop
│ │ ├── load/ # tab-specific: Auslastung
│ │ │ └── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal
│ ├── project-plan.html # Gantt/TODO planning view
│ ├── js/plan/ # planning modules (nur von project-plan.html geladen)
│ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts
│ │ ├── plan-build-gantt.js # buildGanttHtml-Erweiterung (TODO- und Meilensteinzeilen)
│ │ ├── plan-gantt.js # renderPlanGantt, Abhängigkeitslinien, Zoom, Ressourcen-Load
│ │ ├── plan-drag.js # GANTT-Balken-Drag/Resize + TODO-/Meilenstein-Zeilen-Drag
│ │ ├── plan-modals.js # TODO-, Meilenstein- und Abwesenheits-Modals
│ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link
│ ├── styles.css
│ └── dein_bild.ico
└── sessions/ # SQLite-DB; bind-mounted into Docker container
└── projektuebersicht.db # node:sqlite database (WAL mode)
```
## Running
```bash
npm install
npm start # http://localhost:3003, or PORT=3004 if occupied
docker compose up --build # Docker variant (compose file is at repo root)
```
- PORT env var overrides default 3003 in `server.js`.
- Frontend must be served over HTTP: `baseUrl()` in `js/server.js` uses
`window.location.origin` or falls back to `http://localhost:3003` when opened
via `file://` (then only seed/demo data loads).
- Check `/health` returns `{ok:true}` after starting.
## Server API (`server.js`)
- `GET /` → `Test.html`; `GET /health`.
- Deep links per department + tab: `GET /<Dept>/<Tab>` → `Test.html`, e.g.
`/HLS/Auslastung` or `/ELT/Projektübersicht`. Tab slugs (`TAB_SLUGS` in
`js/globals.js`) are `Projektübersicht|Auslastung|Projekte|Projektbeteiligte`;
anything else returns 404. Exception: `GET /<Dept>/Projektplan(/<ProjName>)?`
(`PLAN_SLUG`) serves `project-plan.html`, the GANTT view, with
`weekLabel|projNr` as query params (the project name is a readable path
segment only; identification happens via weekLabel + projNr). The frontend
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{weeks:[weekObj], people:[personObj]}` with the
global people list in `people`; weeks are full JSON week objects (id, label,
date, locked, updatedAt, people, attendance, projects), sorted by label.
- `POST /save-week {department, week, baseUpdatedAt}` → upserts one week (JSON
object, keyed by department + label). Used by `sendWeekToServer()`. The client
sends its local revision in `baseUpdatedAt` (the `updatedAt` value from
`load-all`); the server compares it to `weeks.updated_at` before writing and
answers `409 {ok:false, conflict:true, error, updatedAt}` if someone else
saved the week in the meantime. A save with a **non-empty** base for a week
the server no longer knows is also rejected with 409 ("wurde gelöscht") so a
deleted week can't silently resurrect; only new weeks (empty/missing base)
pass. On success it returns `{ok:true, updatedAt}` (the new revision), which
the client stores as the new sync base. Clients WITHOUT `baseUpdatedAt` (old
versions) always pass. The check runs synchronously in-process before the
write transaction (no race between check and write).
- `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`.
- `GET /export-csv?department=` → ZIP with one readable CSV per logical entity
(wochen, personen, sitzungsteilnehmer, projekte, zuordnungen,
leistungsphasen, aktuelle-leistungsphasen, aufwand-je-leistungsphase,
projektplaene, todos, meilensteine) for that department. CSV: UTF-8 with BOM,
CRLF, `;` separator by default (overridable via `?sep=`; German Excel opens
it directly). ZIP is built in pure Node (`zlib.deflateRawSync`, manual CRC32,
no external library). Used by `exportCSV()` in `js/csv.js`.
- `GET /backup` → consistent SQLite snapshot (`VACUUM INTO`, WAL-safe) as a
`.db` file download (temp file in `os.tmpdir()`, deleted after send). Used by
`downloadBackup()` in `js/backup.js`. Additionally, on server startup the
server writes a timestamped copy to `sessions/backups/` (also via
`VACUUM INTO`) and keeps only the `MAX_BACKUPS` newest ones (default 10).
- `POST /import` → restores the whole database from a `.db` backup file sent as
a raw binary body (`express.raw`, limit 100 MB). Validates the schema
(`pragma_table_info` on every table, exact column match vs. live DB), then
swaps all data via `ATTACH DATABASE …` + DELETE/INSERT-SELECT inside one
transaction (rollback on error). Immediately BEFORE the restore the server
saves a fresh auto-backup of the current state via `runAutoBackup()` (so a
restore can be undone), then performs the swap. Used by `importDb()` in
`js/import.js`, which is password-gated (`requestedAction` type `'importDb'`
in `js/tabs-auth.js`) like week deletion.
- Valid departments hard-coded as `ELT|HLS|ARCH` in `server.js`
(`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in
`DEPARTMENTS` in `js/globals.js`.
- Static assets get `Cache-Control: no-store`.
- JSON body limit is 2 MB.
## Persistence (SQLite)
- Data lives in `sessions/projektuebersicht.db` (`node:sqlite` `DatabaseSync`,
WAL mode, `PRAGMA foreign_keys = ON`, `PRAGMA busy_timeout = 5000` to avoid
`SQLITE_BUSY` under concurrent writers), created automatically. The schema is
**fully relational** (19 tables) instead of one big JSON document per week:
- `weeks` (department, label, week_id, date, locked, updated_at; PK
`(department,label)`) — `updated_at` is the revision timestamp for the
single-user conflict handling (see `/save-week` above)
- `week_people` / `week_people_roles` — person copies per week (`array_order`
keeps the list order, `role_order` the role array order)
- `attendance` — présence per week/person
- `projects` (`array_order` = position in the JSON array, `order_value` = the
`p.order` field, which is only unique *within a group*, not globally!)
- `project_lph`, `project_assign` (person_id|role keys split into columns),
`project_aufwand` (LP effort per role), `project_aktuelle_lp`
- `project_plan`, `plan_people`, `plan_todos` / `plan_todo_assignees` /
`plan_todo_deps`, `plan_milestones` (`position` column!) /
`plan_milestone_deps`
- `people` / `person_roles` / `person_absences` — global people per department
- The HTTP API stays JSON-shaped: `serializeWeek()` /
`serializeGlobalPeople()` split incoming week/people objects across the
tables inside a transaction, `loadWeeks()` / `loadGlobalPeople()`
(re)assemble them on read. `saveWeekRelational()` deletes the week's rows
first (`deleteWeekData()`, note `weeks` uses `label`, all child tables
`week_label`) and returns the new `updated_at` revision written by
`serializeWeek()`.
- 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. Afterwards `migrateWeeksUpdatedAt()` adds the `updated_at`
column to `weeks` (idempotent, `ALTER TABLE … ADD COLUMN`).
Note: legacy rows keep `updated_at = ''`, so the first client save with an
empty `baseUpdatedAt` matches and only then a real revision is written.
- 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
- The frontend is split into small classic non-module `<script>` files under
`public/js/`; **all functions are globals on `window`**. Two HTML files include
them via `<script src>` tags; load order in the HTML defines dependencies,
since there is no bundler:
- `Test.html` loads `globals → utils → ui → date-picker → server →
store → weeks` (shared modules), then the tab-specific folders `board →
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
init`. See the exact list in `Test.html`.
- `project-plan.html` loads only `globals → utils → ui → date-picker →
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
- Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all
project-plan page state (week, project, zoom level, drag/`modal`-state),
`db`/`currentDepartment` live in `js/globals.js`.
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
- Project/person IDs are stable across reloads because the server splits and
reassembles week objects deterministically (JSON IDs are kept in the DB
columns); projects are addressed cross-page by `department` + week `label` +
`projNr` (+ readable projName path segment) via URL params:
`openProjectPlan()` builds `/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`,
`findProjectForPlan` resolves them. Person IDs follow
`personIdForName(name)` → `p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **only dirty weeks**
(`dirtyWeeks` – track `markWeekDirty()`) via `sendWeekToServer()`, plus the
people list `markPeopleDirty()` via `sendPeopleToServer()` if marked dirty.
An in-flight guard (`syncInFlight`/`syncQueued`) prevents overlapping syncs
from double-POSTing the same week with the same revision (which would look
like a spurious 409). On success the server-provided `updatedAt` revision is
adopted as the new sync base; weeks whose save failed (network errors) stay
dirty and are retried. A `409` conflict response drops the local changes for
that week and `handleWeekConflict()` reloads **only that week** from the
server (`reloadWeekFromServer()`), leaving all other dirty weeks and the
people list untouched. Exception: `js/plan/plan-state.js` `savePlan()`
bypasses `save()` and writes only the current week directly via
`sendWeekToServer()` (with the same in-flight/trailing-save guard), adopting
the returned `updatedAt` and showing a toast on conflict. `switchWeek()` (in
`js/weeks.js`) does NOT trigger a save – only actual edits do.
- `switchDepartment()` cancels a pending save debounce before switching, and
`load()` discards stale `/load-all` responses after a department switch
(frozen `requestedDepartment`), so late responses can never render or persist
data under the wrong department.
- Person drag-and-drop in the people tab: drag handle (`.drag-handle`) binds
`ondragstart`/`ondragend`, the `.people-sortable` container binds
`ondragover`/`ondrop`; insertion is computed by `getDragAfterElement(container,
y, selector)` (`js/projects/projects.js`, used with `.person-card` here and
`.person-list-item` in the projects tab). `reorderPeople()` updates both
`db.globalPeople` and the current week's person copy, then `save()`. No drag
handles are rendered on locked weeks.
- Weeks are sorted by calendar week extracted from the label (`KW XX / YYYY`)
via `weekSortKey()`, not alphabetically/by filename.
- `applyAutoLock()` locks every week except the chronologically last one after
load/clone/new-week; `localStorage` UI state is not restored for locked tabs.
- `localStorage` keys: `projuebersicht_v1` (department only — tab is never
restored from storage; at startup the tab comes from the deep-link URL if
present, otherwise `board`), `projuebersicht-theme`, header-collapse key. Do
**not** store data in `localStorage`; it is UI state only.
- Deep-link URLs (`/<Dept>/<Tab>`) are shared via the address bar:
`updateRouteUrl()` rewrites the path via `history.replaceState` on tab and
department switches; protected tabs keep the URL but open the password modal.
## Auth / security quirk
- The `projects` and `people` tabs, week deletion, and week unlock are gated by
a password modal comparing `CryptoJS.SHA256(input)` to
`PROTECTED_PASSWORD_HASH` (`js/globals.js`, modal in `js/tabs-auth.js`).
Client-side only — not real security. CryptoJS and pdfmake (PDF export) are
loaded from `cdnjs.cloudflare.com` in `Test.html`, so those features need
internet access and won't work offline. `project-plan.html` loads neither.
- User-provided status HTML is sanitized on render via a small whitelist
(`sanitizeStatusHtml` in `js/board/board.js`) to mitigate XSS.
## Docker
- `docker compose up --build` from the repo root. Compose maps `3003:3003`,
bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`.
- Dockerfile: `node:22-alpine` (nötig für node:sqlite), `npm ci --production`,
runs as non-root `node`, HEALTHCHECK on `/health`. `.dockerignore` excludes
`Docker/*` (except `Docker/Dockerfile`), `node_modules/`, `.git`,
`npm-debug.log`.
## Conventions & verification
- Git repo; commits are German (e.g. "PDF Export von A4 auf A3 …"). Remote:
`http://192.168.3.187:3000/erik/projektuebersicht.git`.
- No linter, formatter, typecheck, or test suite. Manual verification only:
start server, open `http://localhost:3003`, exercises the relevant tab,
confirm SQLite changes (the DB file `sessions/projektuebersicht.db` grows).
+4 -2
View File
@@ -1,5 +1,7 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine
# node:22-alpine, weil node:sqlite (SQLite-Persistenz in server.js) erst ab
# Node 22.13 (ohne Flag) stabil verfügbar ist.
FROM node:22-alpine
WORKDIR /app
@@ -11,7 +13,7 @@ RUN npm ci --production
COPY server.js ./
COPY public ./public
# Sessions-Ordner fuer die CSV-Daten sicherstellen
# Sessions-Ordner fuer die SQLite-Datenbank sicherstellen
RUN mkdir -p /app/sessions && chown -R node:node /app
USER node
+50 -53
View File
@@ -1,12 +1,12 @@
# Projektübersicht — README
## Übersicht
Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrotechnischer Projekte und Sitzungsübersichten. Die Anwendung besteht aus:
Dieses Projekt ist eine lokale Weboberfläche zur Verwaltung von TGA-Projekten (Elektro, HLS, Architektur) über wöchentliche Sitzungen. Die Anwendung besteht aus:
- einem Frontend in `Test.html`, `styles.css` und den JS-Modulen unter `public/js/`
- einem einfachen Node.js-Server in `server.js`
- einem Datenspeicher in CSV-Dateien unter `sessions/`
- einem Docker-Setup unter `Docker/`
- einem Frontend aus zwei HTML-Einstiegen (`Test.html` für die Hauptansicht mit Tabs, `project-plan.html` für die Projektplanung/GANTT-Ansicht) plus `styles.css` und den JS-Modulen unter `public/js/`
- einem einfachen Node.js-/Express-Server in `server.js`
- einem Datenspeicher in einer SQLite-Datenbank (`node:sqlite`, benötigt Node ≥ 22.13) unter `sessions/projektuebersicht.db`
- einem Docker-Setup (`Docker/Dockerfile` + `docker-compose.yml` im Projektstamm)
## Datei- und Komponentenübersicht
@@ -14,14 +14,19 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- `server.js`
- Node.js/Express-Server
- dient als API für das Frontend
- stellt statische Dateien (`Test.html`, die Module unter `js/`, `styles.css`, etc.) bereit
- stellt statische Dateien (`Test.html`, `project-plan.html`, die Module unter `js/`, `styles.css`, etc.) bereit; statische Assets werden mit `Cache-Control: no-store` ausgeliefert
- implementiert Endpunkte:
- `GET /` → liefert `Test.html`
- `GET /load-all` → lädt alle CSV-Dateien aus dem aktiven `sessions/<DEPARTMENT>/`-Ordner inkl. `_people.csv`
- `POST /save-csv` → speichert eine CSV-Datei (Dateiname wird validiert)
- `POST /delete-csv` → löscht eine CSV-Datei
- `GET /health` → Gesundheitscheck
- `GET /load-all?department=…` → lädt alle Sitzungen plus globale Personenliste einer Abteilung
- `POST /save-week {department, week, baseUpdatedAt}` → speichert/aktualisiert eine Sitzung; mit `baseUpdatedAt` (die vom Client gehaltene Revision = `updatedAt` aus `load-all`) erkennt der Server veraltete Speicherungen und antwortet dann mit HTTP 409 `{ok:false, conflict:true, …}`; auch gelöschte Sitzungen werden mit nicht-leerer Basis nicht wiederbelebt (409). Erfolgreiche Saves antworten `{ok:true, updatedAt}` (neue Revision)
- `POST /save-people {department, people}` → ersetzt die globale Personenliste einer Abteilung
- `POST /delete-week {department, label}` → löscht eine Sitzung
- `GET /health` → Gesundheitscheck (`{ok:true}`)
- Deep Links je Abteilung + Tab, z. B. `GET /HLS/Auslastung` oder `GET /ELT/Projektübersicht` → liefert `Test.html`; gültige Tab-Slugs sind `Projektübersicht | Auslastung | Projekte | Projektbeteiligte`, alles andere ergibt 404
- `GET /<Dept>/Projektplan(/<ProjName>)?` → liefert `project-plan.html` (GANTT-Ansicht); die Identifikation erfolgt über die Query-Parameter `weekLabel` und `projNr`, der Projektname ist nur ein lesbares Pfad-Segment
- validiert Abteilungen gegen `ELT | HLS | ARCH` (unbekannte fallen stillschweigend auf `ELT` zurück)
- legt das `sessions`-Verzeichnis beim Start an, falls es nicht existiert
- JSON-Body-Limit von 2 MB
- beendet sich mit aussagekräftiger Fehlermeldung, wenn der Port belegt ist
- `package.json`
@@ -32,54 +37,54 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- wird für reproduzierbare Docker-Builds verwendet (`npm ci`)
- `public/js/` (Module)
- Die Client-Logik ist in kleine klassische Script-Dateien aufgeteilt (alle Funktionen global auf `window`):
`globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `csv.js`, `server.js`, `store.js`, `weeks.js`,
`board.js`, `people.js`, `projects.js`, `load.js`, `gantt.js`, `showmode.js`, `tabs-auth.js`, `pdf.js`,
`init.js` sowie die Plan-Module unter `js/plan/` (`plan-state`, `plan-build-gantt`, `plan-gantt`,
`plan-drag`, `plan-modals`, `plan-schedule`).
- Die Client-Logik ist in kleine klassische Script-Dateien aufgeteilt (alle Funktionen global auf `window`, kein Bundler):
- gemeinsame Helfer: `globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `server.js`, `store.js`, `weeks.js`, `gantt.js`, `showmode.js`, `tabs-auth.js`, `pdf.js`, `init.js`
- tab-spezifisch in eigenen Ordnern: `board/board.js` (Projektboard), `people/people.js` (Projektbeteiligte), `projects/projects.js` (Projekte), `load/load.js` (Auslastung)
- Planungs-Module unter `js/plan/` (`plan-state`, `plan-build-gantt`, `plan-gantt`, `plan-drag`, `plan-modals`, `plan-schedule`)
- rendert das Projektboard, die Projekt- und Personenverwaltung sowie die Auslastungsansicht
- lädt Daten über den Server und speichert Änderungen zurück als CSV
- lädt Daten über den Server und speichert Änderungen als JSON-Objekte in die SQLite-Datenbank zurück
- verwendet `currentDepartment` zur Trennung von Abteilungen (`ELT`, `HLS`, `ARCH`)
- speichert:
- `sessions/<DEPT>/<WEEK>.csv`
- `sessions/<DEPT>/_people.csv` (globale Personenliste pro Abteilung)
- alle Sitzungen und die globale Personenliste pro Abteilung in `sessions/projektuebersicht.db`
- sortiert Sitzungen nach Kalenderwoche, nicht alphabetisch
- sperrt historische Sitzungen automatisch (bis auf die chronologisch letzte)
- Personen sind global pro Abteilung verwaltet; historische Sitzungen behalten ihre lokalen Personenkopien
- speichert keine Daten in `localStorage` (dort liegt nur UI-Zustand wie Abteilung, Theme und Header-Collapse)
- `Test.html`
- die Benutzeroberfläche der Anwendung
- die Haupt-Benutzeroberfläche der Anwendung (Tabs: Projektboard, Auslastung, Projekte, Projektbeteiligte)
- lädt `styles.css` und die JS-Module aus `js/` in fester Reihenfolge (siehe `AGENTS.md`)
- enthält die Abteilungsauswahl, Sitzungsnavigation, Tab-Navigation und das Passwort-Modal
- enthält die Abteilungsauswahl, Sitzungsnavigation, Tab-Navigation, das Passwort-Modal sowie PDF-Export
- lädt CryptoJS (Passwortprüfung) und pdfmake (PDF-Export) von `cdnjs.cloudflare.com` — diese Funktionen benötigen daher Internetzugang
- `project-plan.html`
- die Planungsansicht (GANTT/TODOs/Meilensteine) pro Projekt
- öffnet sich in einem neuen Tab über `openProjectPlan()` (`js/gantt.js`)
- lädt nur die gemeinsamen Helfermodule plus `js/plan/*`
- lädt nur die gemeinsamen Helfermodule plus `js/plan/*` und window-übergreifend abgestimmte Pfade
- `styles.css`
- Layout und Styling der Oberfläche
### Docker-bezogene Dateien
- `Docker/Dockerfile`
- Basis-Image: `node:20-alpine`
- Basis-Image: `node:22-alpine` (nötig, da `node:sqlite` erst ab Node 22.13 ohne Flag stabil verfügbar ist)
- kopiert `package.json` und `package-lock.json`, installiert Produktionsabhängigkeiten mit `npm ci`
- kopiert den Rest des Projekts in `/app`
- kopiert `server.js` und `public/` nach `/app`
- legt sicherheitshalber `/app/sessions` an
- läuft als nicht-root-User `node`
- enthält einen `HEALTHCHECK` auf `/health`
- startet `node server.js`
- `docker-compose.yml`
- `docker-compose.yml` (im Projektstamm)
- definiert den Service `projektuebersicht`
- baut das Image aus dem Projektstamm mit `Docker/Dockerfile`
- baut das Image mit Build-Kontext `.` und `Docker/Dockerfile`
- mappt Port `3003:3003`
- bindet das Host-Verzeichnis `./sessions` in den Container nach `/app/sessions`
- setzt `restart: unless-stopped`
- definiert einen `healthcheck`
- `.dockerignore`
- schließt unnötige Dateien aus dem Docker-Build aus: `Docker/`, `node_modules/`, `npm-debug.log`, `.git`, `.gitignore`
- schließt unnötige Dateien aus dem Docker-Build aus: `node_modules/`, `npm-debug.log`, `.git`, `.gitignore`, `Docker/*` (außer `Docker/Dockerfile`)
### Sonstige Dateien
- `dein_bild.ico`
@@ -88,34 +93,25 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
- `AGENTS.md`
- Projektnotizen / Dokumentation, aber nicht Teil des Anwendungslaufs
## Speicherorte und Datenfluss
## Persistenz und Datenfluss
### Speicherung
- Alle persistierten Daten werden als CSV-Dateien in `sessions/` gespeichert.
- Innerhalb von `sessions/` existieren Abteilungsordner:
- `sessions/ELT/`
- `sessions/HLS/`
- `sessions/ARCH/`
- Jede Abteilung enthält mindestens:
- `_people.csv` → globale Personenliste mit Rollen und vollständigem Namen
- `KW_XX___YYYY.csv` oder vergleichbare Sitzungsdateien → Sitzungsdaten mit Projekten, Zuordnungen und Anwesenheit
- Alle persistierten Daten werden in einer SQLite-Datenbank (`node:sqlite`, `DatabaseSync`, WAL-Modus, `PRAGMA foreign_keys = ON`) unter `sessions/projektuebersicht.db` gespeichert.
- Das Schema ist vollständig relational (19 Tabellen): `weeks`, `week_people` (+ `week_people_roles`), `attendance`, `projects`, `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`), `people` (+ `person_roles`, `person_absences`). Projekt-/Personen-IDs bleiben dadurch über Neustarts stabil.
- Der Datenbankpfad ist über die Umgebungsvariable `DB_PATH` übersteuerbar (für Tests/Backups), Standard ist `sessions/projektuebersicht.db`.
### Wo wird der Server erstellt?
- Der Server wird in `server.js` erstellt.
- `server.js` startet einen Express-Server auf Port `3003` oder dem Wert von `process.env.PORT`.
- Das Frontend ruft diesen Server über `window.location.origin` an.
### Server-API
- Der Server wird in `server.js` erstellt und startet auf Port `3003` oder dem Wert von `process.env.PORT`.
- Das Frontend ruft den Server über `window.location.origin` an (Fallback `http://localhost:3003` bei `file://`-Öffnung).
### Wo werden die Dateien gespeichert?
- Das Frontend schreibt über den Server in das Dateisystem.
- In `js/store.js` ruft die Anwendung beim Speichern `/save-csv` auf.
- Der Server legt CSV-Dateien unter `sessions/<DEPARTMENT>/` ab.
- Beispiel:
- `sessions/ELT/KW_32___2026.csv`
- `sessions/ELT/_people.csv`
### Wo werden die Daten gespeichert?
- Das Frontend schreibt über den Server in die SQLite-Datenbank.
- In `js/store.js` speichert die Anwendung nur **geänderte** Sitzungen (Dirty-Tracking über `markWeekDirty()`/`markPeopleDirty()`) via `/save-week` (Sitzungen) bzw. `/save-people` (globale Personen); ein In-Flight-Guard verhindert Doppel-Speicherungen. Jede Sitzung führt einen Revisions-Zeitstempel (`updated_at`, übertragen als `updatedAt`); speichert ein Client mit veralteter Revision (`baseUpdatedAt`), antwortet der Server mit HTTP 409 und der Client lädt nur diese eine Sitzung neu (andere ungespeicherte Änderungen bleiben erhalten).
- Der Server zerlegt die JSON-Objekte in die relationalen Tabellen (`serializeWeek()`/`loadWeeks()`) in einer Transaktion pro Sitzung. Beim Start migriert `migrateLegacySchema()` idempotent ein altes Single-JSON-Dokument-Schema in das relationale Schema; anschließend ergänzt `migrateWeeksUpdatedAt()` die `updated_at`-Spalte der `weeks`-Tabelle (idempotent). `PRAGMA busy_timeout = 5000` verhindert Fehler bei parallelen Schreibzugriffen.
## Betrieb
### Lokal mit Node.js
### Lokal mit Node.js (Node ≥ 22.13)
```bash
npm install
npm start
@@ -131,7 +127,7 @@ PORT=3004 npm start
Im Projektstamm ausführen:
```bash
docker compose -f Docker/docker-compose.yml up --build
docker compose up --build
```
Danach ist die Anwendung erreichbar unter:
@@ -141,8 +137,9 @@ http://localhost:3003
```
## Wichtige Hinweise
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/`.
- 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.
- 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.
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/` (für die SQLite-Datenbank und das Docker-Bind-Mount).
- Die Docker-Compose-Konfiguration liegt im Projektstamm und 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 getrennt.
- Die Passwortfunktion (Tabs „Projekte“/„Projektbeteiligte“, Sitzungslöschung, Entsperren) ist nur ein einfacher Client-Mechanismus im Frontend (SHA256-Vergleich) und kein echter Sicherheitsmechanismus.
- Globale Personen werden pro Abteilung in der Datenbank verwaltet. Wird eine Person global gelöscht, bleibt sie in bereits existierenden Sitzungen sichtbar (Wochen-Personenkopien).
- CryptoJS und pdfmake werden von `cdnjs.cloudflare.com` geladen — PDF-Export und Passwortprüfung funktionieren ohne Internet nicht.
+291
View File
@@ -0,0 +1,291 @@
# Agent
<!--introduced_in=v3.2.0-->
<!--type=module-->
<!-- source_link=lib/dispatcher/agent.js -->
> Stability: 2 - Stable
An `Agent` dispatches requests against multiple different origins, creating and
reusing a per-origin [`Pool`][] (or [`Client`][]) on demand. It is the default
dispatcher used by [`request`][], [`stream`][], and [`fetch`][] when no explicit
dispatcher is supplied.
Requests are not guaranteed to be dispatched in their order of invocation.
```mjs
import { Agent, setGlobalDispatcher } from 'undici'
const agent = new Agent({ connections: 10 })
setGlobalDispatcher(agent)
```
## Class: `Agent`
<!-- YAML
added: v3.2.0
-->
* Extends: {Dispatcher}
`Agent` keeps an internal map of origins to dispatchers. The first request to a
given origin lazily creates a dispatcher through the configured `factory`, and
subsequent requests to the same origin reuse it. Idle dispatchers are closed
automatically once they have no open connections and are no longer busy.
### `new Agent([options])`
<!-- YAML
added: v3.2.0
changes:
- version: v7.16.0
pr-url: https://github.com/nodejs/undici/pull/4365
description: Added the `maxOrigins` option to cap the number of origins.
-->
* `options` {AgentOptions} (optional) Extends {PoolOptions}.
* `factory` {Function} Builds the dispatcher used for a given origin. When the
resolved options contain `connections: 1`, the default factory creates a
{Client}; otherwise it creates a {Pool}. **Default:** `(origin, opts) => new Pool(origin, opts)`.
* `origin` {URL|string} The origin to create a dispatcher for.
* `opts` {Object} The resolved options for the new dispatcher.
* Returns: {Dispatcher}
* `maxOrigins` {number} Limits the total number of distinct origins that may
receive requests at the same time. A {MaxOriginsReachedError} is thrown when
a request targets a new origin once the limit is reached. When set to
`Infinity`, no limit is enforced. Must be a number greater than `0`.
**Default:** `Infinity`.
`Agent` inherits all {PoolOptions} (and therefore all {ClientOptions}). Each
origin gets a separate {Pool}, with `connections` acting as the maximum number
of clients that pool may create.
> [!NOTE]
> For an h2-capable HTTPS origin, the per-origin pool waits for the first TLS
> connection to finish ALPN negotiation. If the server selects h2, concurrent
> requests share that session up to `maxConcurrentStreams`. If it selects
> HTTP/1.1, normal connection fan-out resumes up to `connections`. See
> {PoolOptions} and {ClientOptions} for inherited options such as `allowH2`
> (default `true`) and `maxConcurrentStreams` (default `100`).
### `agent.closed`
<!-- YAML
added: v3.2.0
-->
* Type: {boolean}
`true` after [`agent.close()`][] has been called.
### `agent.destroyed`
<!-- YAML
added: v3.2.0
-->
* Type: {boolean}
`true` after [`agent.destroy()`][] has been called, or after [`agent.close()`][]
has been called and the shutdown has completed.
### `agent.stats`
<!-- YAML
added: v7.9.0
-->
* Type: {Record<string, ClientStats|PoolStats>}
Aggregate statistics for the agent, keyed by origin. Each value is the
[`ClientStats`][] or [`PoolStats`][] object reported by the dispatcher currently
serving that origin. Origins whose dispatcher does not expose `stats` are
omitted.
```mjs
import { Agent } from 'undici'
const agent = new Agent()
await agent.request({ origin: 'http://localhost:3000', path: '/', method: 'GET' })
console.log(agent.stats)
// { 'http://localhost:3000': PoolStats { ... } }
```
### `agent.dispatch(options, handler)`
<!-- YAML
added: v3.2.0
-->
* `options` {AgentDispatchOptions} Extends {DispatchOptions}.
* `origin` {string|URL} The origin to dispatch the request against. Required.
* `handler` {DispatchHandler}
* Returns: {boolean} `false` if the dispatcher is busy and the caller should
wait for the [`'drain'`][] event before dispatching again.
Routes the request to the dispatcher for `options.origin`, creating one through
the `factory` if needed. Throws an {InvalidArgumentError} when `options.origin`
is not a non-empty string or {URL}, and a {MaxOriginsReachedError} when a new
origin would exceed `maxOrigins`. Implements [`Dispatcher.dispatch()`][].
### `agent.close([callback])`
<!-- YAML
added: v3.2.0
-->
* `callback` {Function} (optional) Invoked once every per-origin dispatcher has
closed. When omitted, a {Promise} is returned.
* Returns: {Promise|void}
Gracefully closes the agent and every dispatcher it owns, waiting for in-flight
requests to complete. Implements [`Dispatcher.close()`][].
### `agent.destroy([error[, callback]])`
<!-- YAML
added: v3.2.0
-->
* `error` {Error} (optional) The error to abort pending requests with.
* `callback` {Function} (optional) Invoked once every per-origin dispatcher has
been destroyed. When omitted, a {Promise} is returned.
* Returns: {Promise|void}
Forcefully destroys the agent and every dispatcher it owns, aborting all pending
requests. Implements [`Dispatcher.destroy()`][].
### `agent.connect(options[, callback])`
<!-- YAML
added: v3.2.0
-->
See [`Dispatcher.connect()`][]. The request is routed to the dispatcher for
`options.origin`.
### `agent.pipeline(options, handler)`
<!-- YAML
added: v3.2.0
-->
See [`Dispatcher.pipeline()`][]. The request is routed to the dispatcher for
`options.origin`.
### `agent.request(options[, callback])`
<!-- YAML
added: v3.2.0
-->
See [`Dispatcher.request()`][]. The request is routed to the dispatcher for
`options.origin`.
```mjs
import { Agent } from 'undici'
const agent = new Agent()
const { statusCode, body } = await agent.request({
origin: 'http://localhost:3000',
path: '/',
method: 'GET',
})
console.log('response received', statusCode)
console.log('data', await body.text())
```
### `agent.stream(options, factory[, callback])`
<!-- YAML
added: v3.2.0
-->
See [`Dispatcher.stream()`][]. The request is routed to the dispatcher for
`options.origin`.
### `agent.upgrade(options[, callback])`
<!-- YAML
added: v3.2.0
-->
See [`Dispatcher.upgrade()`][]. The request is routed to the dispatcher for
`options.origin`.
### Event: `'connect'`
<!-- YAML
added: v6.6.0
-->
* `origin` {URL}
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
`Agent` followed by the per-origin dispatcher chain.
Emitted when a socket has been created for an origin and is ready to receive
requests. Forwarded from the underlying dispatcher's [`'connect'`][] event.
### Event: `'disconnect'`
<!-- YAML
added: v6.6.0
-->
* `origin` {URL}
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
`Agent`.
* `error` {Error}
Emitted when a socket for an origin has disconnected. Forwarded from the
underlying dispatcher's [`'disconnect'`][] event.
### Event: `'connectionError'`
<!-- YAML
added: v6.6.0
-->
* `origin` {URL}
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
`Agent`.
* `error` {Error}
Emitted when a connection attempt for an origin fails. Forwarded from the
underlying dispatcher's [`'connectionError'`][] event.
### Event: `'drain'`
<!-- YAML
added: v6.6.0
-->
* `origin` {URL}
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
`Agent`.
Emitted when an origin's dispatcher is no longer busy and can accept more
requests. Forwarded from the underlying dispatcher's [`'drain'`][] event.
[`'connect'`]: Dispatcher.md#event-connect
[`'connectionError'`]: Dispatcher.md#event-connectionerror
[`'disconnect'`]: Dispatcher.md#event-disconnect
[`'drain'`]: Dispatcher.md#event-drain
[`Client`]: Client.md#class-client
[`ClientStats`]: ClientStats.md
[`Dispatcher.close()`]: Dispatcher.md#dispatcherclosecallback-promise
[`Dispatcher.connect()`]: Dispatcher.md#dispatcherconnectoptions-callback
[`Dispatcher.destroy()`]: Dispatcher.md#dispatcherdestroyerror-callback-promise
[`Dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
[`Dispatcher.pipeline()`]: Dispatcher.md#dispatcherpipelineoptions-handler
[`Dispatcher.request()`]: Dispatcher.md#dispatcherrequestoptions-callback
[`Dispatcher.stream()`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
[`Dispatcher.upgrade()`]: Dispatcher.md#dispatcherupgradeoptions-callback
[`Pool`]: Pool.md#class-pool
[`PoolStats`]: PoolStats.md
[`agent.close()`]: #agentclosecallback
[`agent.destroy()`]: #agentdestroyerror-callback
[`fetch`]: Fetch.md
[`request`]: Dispatcher.md#dispatcherrequestoptions-callback
[`stream`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
BIN
View File
Binary file not shown.
+14 -3
View File
@@ -5,7 +5,7 @@
und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im
seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet
styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js,
js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, danach die
js/date-picker.js, js/server.js, js/store.js, js/weeks.js, danach die
tab-spezifischen Module js/board/board.js, js/people/people.js,
js/projects/projects.js, js/load/load.js sowie js/gantt.js, js/showmode.js,
js/tabs-auth.js, js/pdf.js und js/init.js; crypto-js und pdfmake kommen von einem
@@ -77,6 +77,15 @@
<button class="secondary" onclick="exportPDF()">PDF Export</button>
<button class="secondary" onclick="startShowMode()">Show Mode</button>
</div>
<hr class="menu-divider">
<div class="menu-section">
<div class="menu-label">Backup</div>
<button class="secondary" onclick="exportCSV()">CSV Export</button>
<button class="secondary" onclick="downloadBackup()">DB Backup</button>
<button class="secondary" onclick="importDb()">DB Import</button>
</div>
</div>
</div>
@@ -108,7 +117,7 @@
<div class="table-wrap">
<table id="boardTable"></table>
</div>
<div id="boardLegend" style="margin-top:16px; padding:12px; background:#f7fafc; border-radius:6px; font-size:12px;"></div>
<div id="boardLegend"></div>
</div>
<!-- LOAD -->
@@ -227,7 +236,6 @@
<script src="/js/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.js?v=5"></script>
<script src="/js/csv.js?v=5"></script>
<script src="/js/server.js?v=5"></script>
<script src="/js/store.js?v=5"></script>
<script src="/js/weeks.js?v=5"></script>
@@ -239,6 +247,9 @@
<script src="/js/showmode.js?v=5"></script>
<script src="/js/tabs-auth.js?v=5"></script>
<script src="/js/pdf.js?v=5"></script>
<script src="/js/csv.js?v=1"></script>
<script src="/js/backup.js?v=1"></script>
<script src="/js/import.js?v=1"></script>
<script src="/js/init.js?v=5"></script>
</body>
</html>
BIN
View File
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
// -----------------------------
// Projektübersicht — Datenbank-Backup (Download)
// -----------------------------
// Stellt downloadBackup() bereit: lädt einen konsistenten Snapshot der
// SQLite-Datenbank über GET /backup als .db-Datei herunter (VACUUM INTO,
// WAL-sicher). Hängt im Seitenmenü von Test.html am Button "DB Backup" und
// nutzt baseUrl() aus js/server.js. Ergänzt das automatische Backup, das der
// Server beim Start in sessions/backups/ ablegt.
// downloadBackup: fordert den DB-Snapshot an und startet den Download.
// Kein Parameter, keine Rückgabe; Fehler werden über ein Alert gemeldet.
async function downloadBackup(){
try {
const res = await fetch(baseUrl() + '/backup');
if(!res.ok){
let msg = 'Server HTTP ' + res.status;
try {
const json = await res.json();
if(json && json.error) msg = json.error;
} catch(e){ /* Antwort war kein JSON */ }
throw new Error(msg);
}
const blob = await res.blob();
const filename = 'projektuebersicht_backup_'
+ new Date().toISOString().slice(0, 10) + '.db';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch(err){
console.error('DB-Backup fehlgeschlagen:', err);
alert('DB-Backup fehlgeschlagen: ' + (err.message || err));
}
}
+3 -3
View File
@@ -53,7 +53,7 @@ function formatStatus(command, value){
}
// peopleByRole liegt jetzt im gemeinsamen Modul js/utils.js (benötigt auch
// csv.js für die Projektplan-Seite, die board.js nicht lädt).
// die Projektplan-Seite, die board.js nicht lädt).
// roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role`
@@ -85,7 +85,7 @@ function renderBoard(){
let h2 = `<tr>`;
ROLES.forEach(r=>{
if(grouped[r].length===0) h2 += `<th class="rot ${roleClass(r)}">–</th>`;
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${entry.person.name}</th>`);
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${esc(entry.person.name)}</th>`);
});
LPHS.forEach(l=> h2 += `<th class="rot">${l}</th>`);
h2 += `</tr>`;
@@ -146,7 +146,7 @@ function renderBoard(){
const v = (p.lph[l]!==undefined && p.lph[l]!="") ? p.lph[l] : "";
const aktiv = l === aktLp;
const inputAttr = locked ? 'readonly tabindex="-1"' : `onchange="editLph('${p.id}','${l}',this.value)"`;
body += `<td class="lph-cell ${locked ? 'locked-cell' : ''}${aktiv ? ' lp-aktiv' : ''}"${aktiv ? ' title="Aktive Leistungsphase"' : ''}><input type="text" value="${v}" ${inputAttr} placeholder="-">`;
body += `<td class="lph-cell ${locked ? 'locked-cell' : ''}${aktiv ? ' lp-aktiv' : ''}"${aktiv ? ' title="Aktive Leistungsphase"' : ''}><input type="text" value="${esc(v)}" ${inputAttr} placeholder="-">`;
if(v!=""){ const pct = Math.min(100, parseFloat(v)||0);
body += `<div class="bar"><div style="width:${pct}%;background:${getProgressColor(pct)}"></div></div>`; }
body += `</td>`;
+33 -342
View File
@@ -1,351 +1,42 @@
// -----------------------------
// Projektübersicht — CSV-Serialisierung & Parsing
// Hinweis: weekToCsv() und parseSessionCsv() sind invers zueinander.
// Das Spaltenlayout darf nicht geändert werden, ohne beide Funktionen anzupassen.
// Projektübersicht — CSV/ZIP-Export
// -----------------------------
// 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.
// Stellt exportCSV() bereit: lädt den ZIP mit den übersichtlichen CSV-Dateien
// der aktuellen Abteilung über GET /export-csv und stößt den Download an.
// Hängt im Seitenmenü von Test.html am Button "CSV Export" und nutzt baseUrl()
// aus js/server.js sowie currentDepartment aus js/globals.js. Der ZIP enthält
// pro logischer Entität eine CSV (Wochen, Personen, Projekte, …), erzeugt vom
// Server (siehe server.js im Projekt-Root).
// 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]){
// exportCSV: fordert den CSV-Export der aktuellen Abteilung an und lädt ihn
// als ZIP-Datei herunter. Kein Parameter, keine Rückgabe; Fehler werden über
// ein Alert gemeldet.
async function exportCSV(){
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]){
const dept = currentDepartment || 'ELT';
const res = await fetch(baseUrl() + '/export-csv?department=' + encodeURIComponent(dept));
if(!res.ok){
let msg = 'Server HTTP ' + res.status;
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);
}
const json = await res.json();
if(json && json.error) msg = json.error;
} catch(e){ /* Antwort war kein JSON */ }
throw new Error(msg);
}
const blob = await res.blob();
const filename = 'Projektuebersicht_' + dept + '_'
+ new Date().toISOString().slice(0, 10) + '.zip';
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);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch(err){
console.error('CSV-Export fehlgeschlagen:', err);
alert('CSV-Export fehlgeschlagen: ' + (err.message || err));
}
}
let aktuelleLP;
if(aktiveIdx >= 0 && row[aktiveIdx]){
try {
const parsed = JSON.parse(row[aktiveIdx]);
if(Array.isArray(parsed)) aktuelleLP = parsed;
else if(typeof parsed === 'string') aktuelleLP = [parsed];
} catch(e) {
console.warn('Aktive Phasen für Projekt konnten nicht geparst werden:', nr, name, e);
}
}
projects.push({ id: uid(), nr, name, status, group, lph, assign, order, plan, aufwandProLP, aktuelleLP });
}
const attendance = {};
const attRow = rows[attendanceRowIdx];
if(attRow && attRow[0] === 'ANWESENHEIT'){
peopleCols.forEach((_, i) => {
const entry = colMap[i];
if(entry && (attRow[2+i] || '').trim() === 'X'){
attendance[entry.person.id] = true;
}
});
}
return { id: uid(), label, date, people, projects, attendance, locked };
}
+2 -2
View File
@@ -335,8 +335,8 @@ function buildGanttHtml(projects, start, end, options = {}){
function openProjectPlan(pid){
const w = currentWeek(); if(!w) return;
const p = w.projects.find(x => x.id === pid); if(!p) return;
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt).
// Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
// Projekt-IDs sind in der DB stabil, aber die Plan-Seite ist eine separate
// Route. Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
// Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der
// Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert.
const url = new URL('/' + currentDepartment + '/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin);
+67
View File
@@ -0,0 +1,67 @@
// -----------------------------
// Projektübersicht — Datenbank-Import (Backup-Wiederherstellung)
// -----------------------------
// Stellt importDb() bereit: wählt über einen Datei-Dialog eine mit "DB Backup"
// erzeugte .db-Datei aus, lädt sie als rohen Binär-Body an POST /import und
// lädt nach erfolgreicher Wiederherstellung die Seite neu. Da der Import den
// kompletten Datenbestand ersetzt, ist er passwortgeschützt (gleiche Logik wie
// Wochen-Löschung über requestedAction/isAuthenticated, siehe js/tabs-auth.js
// und js/weeks.js) und wird zusätzlich im Browser per Bestätigungsdialog
// abgesichert.
// importDb: öffnet bei Bedarf das Passwort-Modal (requestedAction 'importDb')
// und startet danach den Datei-Dialog für das Backup. Parameter `skipAuth=true`
// überspringt die Passwortabfrage (wird von confirmPassword() genutzt).
// Keine Rückgabe.
function importDb(skipAuth = false){
if(!skipAuth && !isAuthenticated){
requestedAction = { type: 'importDb' };
document.getElementById('passwordModal').style.display = 'flex';
document.getElementById('passwordInput').value = '';
document.getElementById('passwordInput').focus();
return;
}
if(!confirm('Datenbank-Backup wirklich wiederherstellen?\n\nAlle aktuellen Daten werden ersetzt.')){
requestedAction = null;
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = '.db,application/octet-stream,application/x-sqlite3';
input.onchange = () => {
const file = input.files && input.files[0];
if(!file){
requestedAction = null;
return;
}
uploadBackup(file);
};
input.click();
requestedAction = null;
}
// uploadBackup: lädt die gewählte .db-Datei als rohen Binär-Body an
// POST /import und informiert über das Ergebnis. Bei Erfolg wird die Seite neu
// geladen, damit das Frontend den wiederhergestellten Datenbestand zeigt.
async function uploadBackup(file){
try {
const res = await fetch(baseUrl() + '/import', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: file
});
if(!res.ok){
let msg = 'Server HTTP ' + res.status;
try {
const json = await res.json();
if(json && json.error) msg = json.error;
} catch(e){ /* Antwort war kein JSON */ }
throw new Error(msg);
}
alert('Datenbank-Backup erfolgreich wiederhergestellt.\nDie Seite wird neu geladen.');
window.location.reload();
} catch(err){
console.error('DB-Import fehlgeschlagen:', err);
alert('DB-Import fehlgeschlagen: ' + (err.message || err));
}
}
+8 -8
View File
@@ -45,7 +45,7 @@ function addPerson(){
document.getElementById('newPersonFullName').value='';
document.getElementById('newPersonEmail').value='';
document.querySelectorAll('input[name="newPersonRoles"]').forEach(el=>el.checked=false);
save(); renderPeople(); renderBoard();
save({people:true}); renderPeople(); renderBoard();
}
// delPerson: entfernt eine Person aus der globalen Liste. Bereits in
@@ -76,7 +76,7 @@ function delPerson(id){
w.people = (w.people || []).filter(p=>p.id!==id);
}
save(); renderPeople(); renderBoard();
save({people:true}); renderPeople(); renderBoard();
}
// editPerson: ändert Name/Vollname/Rolle sowohl global als auch in der
@@ -132,7 +132,7 @@ function editPerson(id,field,val){
delete w.attendance[id];
}
}
save(); renderPeople(); renderBoard();
save({people:true}); renderPeople(); renderBoard();
return;
}
@@ -146,7 +146,7 @@ function editPerson(id,field,val){
const localPerson = (w.people || []).find(p => p.id === id);
if(localPerson) localPerson[field] = val;
}
save(); renderPeople(); renderBoard();
save({people:true}); renderPeople(); renderBoard();
}
// togglePersonRole: fügt eine Rolle (`role`) zur Person `id` hinzu bzw. entfernt
@@ -172,7 +172,7 @@ function togglePersonRole(id, role, checked){
if(globalPerson) updateRoles(globalPerson);
if(localPerson) updateRoles(localPerson);
save(); renderPeople(); renderBoard();
save({people:true}); renderPeople(); renderBoard();
}
// renderPeople: baut die HTML-Liste der Personen für den Personen-Tab auf
@@ -324,7 +324,7 @@ function reorderPeople(){
newPeople.forEach(p => { if(byId[p.id]) newWeekPeople.push(byId[p.id]); });
if(newWeekPeople.length === w.people.length) w.people = newWeekPeople;
}
save();
save({people:true});
}
}
@@ -348,7 +348,7 @@ function addAbsence(personId){
if(!Array.isArray(person.absences)) person.absences = [];
person.absences.push({ id: uid(), start, end, type, note });
person.absences.sort((a,b) => parseDateISO(a.start) - parseDateISO(b.start));
save();
save({week:false, people:true});
renderPeople();
if(currentTab === 'gantt') renderGantt();
}
@@ -361,7 +361,7 @@ function deleteAbsence(personId, absenceId){
const person = people.find(p => p.id === personId);
if(!person || !person.absences) return;
person.absences = person.absences.filter(a => a.id !== absenceId);
save();
save({week:false, people:true});
renderPeople();
if(currentTab === 'gantt') renderGantt();
}
+7 -3
View File
@@ -114,16 +114,20 @@
const todos = Array.isArray(plan.todos) ? plan.todos : [];
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
const todoIds = todos.map(t => t.id);
const milestoneIds = milestones.map(m => m.id);
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
const todoRowIds = rows.filter(r => r.classList.contains('gantt-todo')).map(r => r.dataset.todoId).filter(Boolean);
const milestoneRowIds = rows.filter(r => r.classList.contains('gantt-milestone-row')).map(r => r.dataset.milestoneId).filter(Boolean);
// Sicherheitscheck: Anzahl muss stimmen
// Sicherheitscheck: Anzahl muss stimmen. Nicht jedes Meilenstein wird als
// eigene .gantt-milestone-row gerendert – nur die mit einer TODO-basierten
// Position (before:/after:) sind eigene Zeilen. Meilensteine ohne Position
// liegen in der Projektzeile und werden hier nicht umsortiert, deshalb nur
// die positionierten zählen.
if(todoRowIds.length !== todoIds.length) return;
if(milestoneRowIds.length !== milestoneIds.length) return;
const positionedIds = milestones.filter(m => m.position && /^(before|after):/.test(m.position)).map(m => m.id);
if(milestoneRowIds.length !== positionedIds.length) return;
// TODOs neu ordnen
const newTodos = [];
+45 -24
View File
@@ -66,20 +66,17 @@ window.PlanApp = {
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der
// anderen plan-Module.
// anderen plan-Module. Die Buttons binden ihre Handler bereits über das
// onclick-Attribut in project-plan.html; hier werden nur Lücken gefüllt,
// falls ein Button-Element fehlt.
function bindToolbarButtons(){
const addTodoBtn = document.getElementById('addTodoBtn');
if(addTodoBtn) addTodoBtn.addEventListener('click', () => addTodo());
const autoScheduleBtn = document.getElementById('autoScheduleBtn');
if(autoScheduleBtn) autoScheduleBtn.addEventListener('click', () => autoScheduleTodos());
const criticalPathBtn = document.getElementById('criticalPathBtn');
if(criticalPathBtn) criticalPathBtn.addEventListener('click', () => toggleCriticalPath());
}
// loadPlanData: lädt alle Sitzungen und die globale Personenliste der
// Abteilung über die Server-API, sortiert die Wochen chronologisch und
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage().
// Abteilung über die Server-API (JSON-Dokumente), sortiert die Wochen
// chronologisch und sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt
// Fehlermeldungen der Reihenfolge nach im Seitenkopf an und startet sonst
// renderPlanPage().
function loadPlanData(){
if(!window.fetch){
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
@@ -90,11 +87,14 @@ window.PlanApp = {
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
db = { weeks: [], currentWeek: null, globalPeople: {} };
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people);
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
parsed.forEach(s => {
const w = parseSessionCsv(s.filename, s.rows);
if(w) db.weeks.push(w);
db.globalPeople[app.department] = Array.isArray(data.people) ? data.people : [];
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
.map(w => {
if(!Array.isArray(w.people)) w.people = [];
if(!Array.isArray(w.projects)) w.projects = [];
if(!w.attendance) w.attendance = {};
return w;
});
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
app.week = db.weeks.find(w => w.label === app.weekLabel);
@@ -176,22 +176,43 @@ window.PlanApp = {
}).join('');
}
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben.
// In-Flight-Schutz: läuft bereits ein Save, wird kein zweiter POST mit
// identischer Revisions-Basis abgeschickt (der würde fälschlich als 409-
// Konflikt gemeldet). Stattdessen wird ein Folgesave eingereiht, der nach
// Abschluss des laufenden Saves mit der dann frischen Revision nachzieht.
let savePlanRunning = false;
let savePlanQueued = false;
function savePlan(){
if(!app.week || !app.project) return;
const filename = filenameForWeek(app.week.label);
const csv = weekToCsv(app.week);
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
if(savePlanRunning){ savePlanQueued = true; return; }
savePlanRunning = true;
sendWeekToServer(app.week)
.then(res => {
// Lokale Revisions-Basis aktualisieren, damit spätere eigene Saves
// nicht fälschlich als Konflikt gemeldet werden.
if(res && res.updatedAt) app.week.updatedAt = res.updatedAt;
})
.catch(err => {
if(err && err.conflict){
showToast('Konflikt: Die Sitzung wurde zwischenzeitlich geändert. Bitte Seite neu laden.');
console.error('Speichern fehlgeschlagen (Konflikt):', err.message || err);
} else {
console.error('Speichern fehlgeschlagen:', err);
}
})
.finally(() => {
savePlanRunning = false;
if(savePlanQueued){ savePlanQueued = false; savePlan(); }
});
}
// 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. Auch eine leere Liste wird gespeichert, damit
// das Löschen der letzten Person erhalten bleibt.
function saveGlobalPeople(){
const people = db.globalPeople[app.department] || [];
if(people.length > 0){
const peopleCsv = globalPeopleToCsv(people);
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
}
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
}
// Person in der globalen Liste finden (dort werden Abwesenheiten gepflegt)
+4 -3
View File
@@ -62,6 +62,7 @@ function editProjectActiveLP(id, value){
function renderProjects(){
const w = currentWeek();
if(!w) return;
const locked = isWeekLocked(w);
let html="";
const groups = {};
w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); });
@@ -71,10 +72,10 @@ function renderProjects(){
sortGroups(Object.keys(groups)).forEach(g=>{
html += `<div class="section-title">${esc(g)}</div>`;
html += `<div class="projects-sortable" data-group="${g}" ondragover="handleDragOver(event)" ondrop="handleDrop(event)">`;
html += `<div class="projects-sortable" data-group="${g}" ${locked ? '' : 'ondragover="handleDragOver(event)" ondrop="handleDrop(event)"'}>`;
groups[g].forEach(p=>{
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>
html += `<div class="person-list-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>`}
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr">
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung">
<select onchange="editProjectField('${p.id}','group',this.value)">
+50 -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
// server.js im Projekt-Root) und wird nach js/csv.js von beiden
// server.js im Projekt-Root) und wird nach js/date-picker.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt baseUrl, blobToDataUrl,
// sendCsvToServer und deleteCsvFromServer bereit. Genutzt wird sie von
// js/store.js (syncAllToServer, Speichern/Löschen aller Wochen),
// js/weeks.js (Wochen-Löschung) und js/plan/plan-state.js (savePlan schreibt
// direkt die aktuelle Woche über sendCsvToServer).
// sendWeekToServer, sendPeopleToServer und deleteWeekFromServer bereit.
// Genutzt wird sie von js/store.js (syncAllToServer, Speichern aller Wochen
// und der Personenliste), js/weeks.js (Wochen-Löschung) und
// js/plan/plan-state.js (savePlan schreibt direkt die aktuelle Woche über
// sendWeekToServer). Die Daten werden als strukturierte JSON-Objekte an die
// SQLite-API des Servers übergeben (keine CSV-Strings mehr).
// baseUrl: dynamische Server-URL, damit die App auch von anderen Geraeten im Netzwerk erreichbar ist.
// Fallback auf localhost:3003, wenn die Seite als lokale Datei geladen wurde.
@@ -31,24 +33,56 @@ function blobToDataUrl(blob){
});
}
// sendCsvToServer: POSTet eine Speicherwoche im CSV-Format an <baseUrl>/save-csv
// Parameter: filename (String), csvContent (String). Rückgabe: Promise mit Server-Antwort.
function sendCsvToServer(filename, csvContent){
// sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit
// Server-Antwort. `baseUpdatedAt` übermittelt die Revision, auf der die
// lokalen Daten beruhen; meldet der Server 409 (conflict), wird ein Error
// mit `err.conflict = true` und `err.updatedAt` geworfen, damit der Aufrufer
// den Konflikt anzeigen kann.
function sendWeekToServer(week){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/save-csv',{
return fetch(baseUrl() + '/save-week',{
method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'},
body:JSON.stringify({filename,csv:csvContent,department:currentDepartment})
body:JSON.stringify({
week,
department:currentDepartment,
baseUpdatedAt:(week && week.updatedAt) ? week.updatedAt : undefined
})
}).then(r=>{
return r.json().then(json => {
if(r.ok && json) return json;
if(json && json.conflict){
const err = new Error(json.error || 'Konflikt: Sitzung wurde zwischenzeitlich geändert');
err.conflict = true;
err.label = (week && week.label) || '';
err.updatedAt = json.updatedAt;
throw err;
}
throw new Error('Server HTTP '+r.status);
});
});
}
// sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
// JSON-Array an <baseUrl>/save-people. Parameter: people (Array).
// Rückgabe: Promise mit Server-Antwort.
function sendPeopleToServer(people){
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
return fetch(baseUrl() + '/save-people',{
method:'POST',
headers:{'Content-Type':'application/json; charset=utf-8'},
body:JSON.stringify({people, department:currentDepartment})
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
}
// deleteCsvFromServer: fordert das Löschen einer Speicherwoche über POST /delete-csv an
// Parameter: filename (String). Rückgabe: Promise mit Server-Antwort.
function deleteCsvFromServer(filename){
// 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-csv', {
return fetch(baseUrl() + '/delete-week', {
method: 'POST',
headers: {'Content-Type':'application/json; charset=utf-8'},
body: JSON.stringify({ filename, department: currentDepartment })
body: JSON.stringify({ label, department: currentDepartment })
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
}
+172 -41
View File
@@ -1,22 +1,44 @@
// -----------------------------
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln
// -----------------------------
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungs-CSVs
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungen
// beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH).
// Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt
// Die Daten liegen serverseitig in einer SQLite-Datenbank und werden als
// strukturierte JSON-Objekte über die API ausgetauscht (keine CSV-Strings).
// Sie wird von `Test.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
// `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
// Jede Sitzung wird als eigene CSV-Datei gespeichert. Personen und Rollen werden
// in der Sitzungscsv selbst abgelegt.
function save(){
// save: plant einen (debounced) Sync. Standardmäßig wird die aktuell gewählte
// Woche als geändert markiert, da sich die meisten Bearbeitungen auf die
// aktuelle Sitzung beziehen. Optionen:
// { week:false } – aktuelle Woche NICHT markieren (z. B. reine Personen-Aktionen)
// { people:true } – zusätzlich die globale Personenliste als geändert markieren
function save(opts = {}){
if(opts.week !== false){
const w = currentWeek();
if(w) dirtyWeeks.add(w.label);
}
if(opts.people) dirtyPeople = true;
scheduleSessionSync();
}
// Dirty-Tracking: merkt sich, welche Wochen bzw. ob die globale Personenliste
// seit dem letzten Sync lokal geändert wurden. Gespeichert wird nur das, was
// sich wirklich geändert hat (statt wie früher immer alle Wochen).
let dirtyWeeks = new Set();
let dirtyPeople = false;
// In-Flight-Schutz für syncAllToServer: läuft bereits ein Sync, wird kein
// zweiter gestartet (sonst senden zwei Syncs dieselbe Woche mit gleicher
// Revisions-Basis und der zweite bekäme einen scheinbaren 409-Konflikt).
// Stattdessen wird ein Nachlauf-Sync eingereiht und nach Abschluss ausgeführt.
let syncInFlight = false;
let syncQueued = false;
// scheduleSessionSync: plant den Speichervorgang für alle Wochen und Personen
// über einen 500-ms-Timer (Debounce). Bei erneutem Aufruf wird ein laufender
// Timer zurückgesetzt, damit aufeinanderfolgende Änderungen in einem Sync enden.
@@ -25,33 +47,133 @@ function scheduleSessionSync(){
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
}
// syncAllToServer: schreibt die gesamte Datenbank als CSV-Dateien zum Server.
// Zuerst wird die globale Personenliste (`_people.csv`) gespeichert, danach jede
// Sitzung einzeln über `filenameForWeek()`/`weekToCsv()` und `sendCsvToServer()`.
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
// syncAllToServer: schreibt nur die seit dem letzten Sync geänderten Wochen
// plus – falls markiert – die globale Personenliste zum Server. Konflikte
// (409) auf einer Woche werden über handleWeekConflict() gemeldet. Fehlerhaft
// gespeicherte Wochen bleiben dirty, damit der nächste Sync sie erneut
// versucht; nur erfolgreich gespeicherte werden aus dem Dirty-Satz entfernt.
function syncAllToServer(){
// Zuerst globale Personenliste speichern
saveGlobalPeopleToServer();
// Dann jede Sitzung speichern
db.weeks.forEach(w => {
const filename = filenameForWeek(w.label);
const csv = weekToCsv(w);
sendCsvToServer(filename, csv)
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', filename); })
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', filename, err.message || err); });
if(syncInFlight){ syncQueued = true; return; }
syncInFlight = true;
// Zuerst globale Personenliste speichern, falls geändert
const jobs = [];
if(dirtyPeople) jobs.push(saveGlobalPeopleToServer());
const weekLabels = Array.from(dirtyWeeks);
weekLabels.forEach(label => {
const w = db.weeks.find(x => x.label === label);
if(!w){ dirtyWeeks.delete(label); return; }
jobs.push(sendWeekToServer(w)
.then(res => {
if(res && res.ok){
console.log('Sitzung gespeichert:', w.label);
// Lokale Revisions-Basis aktualisieren, damit spätere eigene Saves
// nicht fälschlich als Konflikt gemeldet werden.
if(res.updatedAt) w.updatedAt = res.updatedAt;
dirtyWeeks.delete(label);
} else {
console.error('Sitzung konnte nicht gespeichert werden:', w.label, res && res.error ? res.error : 'unbekannter Fehler');
}
})
.catch(err => {
if(err && err.conflict){
dirtyWeeks.delete(label);
handleWeekConflict(w, err);
} else {
console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err);
// Label bleibt dirty -> der nächste Sync versucht es erneut.
}
}));
});
Promise.all(jobs).then(() => {
syncInFlight = false;
if(syncQueued){ syncQueued = false; syncAllToServer(); }
});
}
// saveGlobalPeopleToServer: serialisiert die globale Personenliste der aktuellen
// Abteilung per `globalPeopleToCsv()` und speichert sie als `_people.csv`.
// Leere Personenlisten werden übersprungen (kein Schreibvorgang).
// markWeekDirty: markiert eine bestimmte Woche (label) als geändert – für
// Aktionen, die Wochen AUFßER der aktuellen betreffen (z. B. Auto-Lock).
function markWeekDirty(label){
if(typeof label === 'string' && label) dirtyWeeks.add(label);
}
// markPeopleDirty: markiert die globale Personenliste als geändert.
function markPeopleDirty(){
dirtyPeople = true;
}
// handleWeekConflict: wird aufgerufen, wenn der Server eine zwischenzeitlich
// geänderte Woche meldet (409). Nur diese eine Woche wird vom Server neu
// geladen; andere lokal geänderte Wochen (dirtyWeeks) und die Personenliste
// bleiben unangetastet und gehen nicht verloren.
function handleWeekConflict(week, err){
const label = (week && week.label) || (err && err.label) || '';
alert('Achtung: ' + label + ' wurde zwischenzeitlich von einer anderen Person '
+ 'geändert. Ihre Änderungen an dieser Sitzung wurden nicht gespeichert; '
+ 'die aktuellen Daten dieser Sitzung werden geladen.');
if(label) reloadWeekFromServer(label);
else load();
}
// reloadWeekFromServer: lädt die aktuelle Abteilung neu und ersetzt NUR die
// übergebene Woche (bzw. entfernt sie, falls sie am Server gelöscht wurde).
// Der Dirty-Zustand der konfliktbehafteten Woche wurde bereits von
// syncAllToServer entfernt; alle anderen Dirty-Änderungen bleiben erhalten.
function reloadWeekFromServer(label){
const requestedDepartment = currentDepartment;
fetch(baseUrl() + '/load-all' + departmentParam())
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
if(currentDepartment !== requestedDepartment) return;
const fresh = (Array.isArray(data.weeks) ? data.weeks : [])
.find(w => w && w.label === label);
const idx = db.weeks.findIndex(w => w.label === label);
if(idx >= 0){
if(fresh){
// Felder absichern (wie in load()), damit keine kaputten Daten crashen
if(!Array.isArray(fresh.people)) fresh.people = [];
if(!Array.isArray(fresh.projects)) fresh.projects = [];
if(!fresh.attendance) fresh.attendance = {};
db.weeks[idx] = fresh;
} else {
db.weeks.splice(idx, 1);
}
// Wurde die aktuelle Woche entfernt, auf die letzte verbleibende wechseln
if(!db.weeks.some(w => w.id === db.currentWeek)){
db.currentWeek = db.weeks.length ? db.weeks[db.weeks.length - 1].id : null;
}
}
renderAll();
showTab(currentTab);
})
.catch(err => {
console.error('Neu laden der Sitzung fehlgeschlagen:', err);
load();
});
}
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
// Abteilung über `sendPeopleToServer()` (JSON-Array). Auch eine leere Liste
// wird gespeichert, damit das Löschen der letzten Person erhalten bleibt. Bei
// Erfolg wird der Dirty-Status für Personen zurückgesetzt, bei Fehler bleibt
// er erhalten (Retry beim nächsten Sync). Rückgabe: Promise.
function saveGlobalPeopleToServer(){
const people = db.globalPeople[currentDepartment] || [];
if(people.length === 0) return;
const csv = globalPeopleToCsv(people);
sendCsvToServer('_people.csv', csv)
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
return sendPeopleToServer(people)
.then(res => {
if(res && res.ok){
dirtyPeople = false;
console.log('Globale Personenliste gespeichert');
} else {
console.error('Globale Personenliste konnte nicht gespeichert werden:', res && res.error ? res.error : 'unbekannter Fehler');
}
})
.catch(err => {
console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err);
// dirtyPeople bleibt true -> der nächste Sync versucht es erneut.
});
}
// switchDepartment: wechselt die aktive Abteilung (z.B. "ELT") und lädt deren
@@ -62,8 +184,10 @@ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return;
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die
// CSV der falschen Abteilung schreibt.
// Daten der falschen Abteilung schreibt.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
dirtyWeeks.clear();
dirtyPeople = false;
currentDepartment = dept;
saveUiState();
updateRouteUrl(dept, currentTab);
@@ -84,12 +208,17 @@ function switchDepartment(dept){
load();
}
// load: liest alle CSV-Dateien der aktuellen Abteilung über `/load-all` vom
// Server ein, parst Personen und Sitzungen (mit `parseSessionCsv()`) und baut
// daraus `db.weeks` auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()`
// an und rendert die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder
// fehlenden Daten werden Beispieldaten über `seed()` geladen.
// load: liest alle Sitzungen und die globale Personenliste der aktuellen
// Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
// auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
// Daten werden Beispieldaten über `seed()` geladen.
function load(){
// Nach einem (Neu-)Load sind die lokalen Daten serverseitig aktuell:
// laufenden Debounce abbrechen und Dirty-Zustand verwerfen.
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
dirtyWeeks.clear();
dirtyPeople = false;
if(!window.fetch){
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
seed(); return;
@@ -106,18 +235,20 @@ function load(){
.then(data => {
if(currentDepartment !== requestedDepartment) return;
// Globale Personenliste laden
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
db.globalPeople[currentDepartment] = Array.isArray(data.people) ? data.people : [];
const parsedSessions = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
db.weeks = [];
parsedSessions.forEach(s => {
const w = parseSessionCsv(s.filename, s.rows);
if(w) db.weeks.push(w);
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
.filter(w => w && typeof w.label === 'string')
.map(w => {
// Felder absichern, damit alte/kaputte Dokumente nicht crashen
if(!Array.isArray(w.people)) w.people = [];
if(!Array.isArray(w.projects)) w.projects = [];
if(!w.attendance) w.attendance = {};
return w;
});
if(db.weeks.length === 0){
console.warn('Server lieferte 0 Sitzungsdateien. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob der Ordner sessions/ mitkopiert wurde.');
console.warn('Server lieferte 0 Sitzungen. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob die SQLite-Datenbank sessions/projektuebersicht.db existiert.');
seed();
}
else {
+1
View File
@@ -59,6 +59,7 @@ function confirmPassword(){
if(action?.type==='tab') showTab(action.tab, true);
else if(action?.type==='deleteWeek') deleteWeek(true);
else if(action?.type==='unlockWeek') unlockCurrentWeek(true);
else if(action?.type==='importDb') importDb(true);
}, 100);
document.getElementById('passwordModal').style.display='none';
} else {
+14 -3
View File
@@ -5,7 +5,7 @@
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen,
// u. a. js/csv.js, js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
// u. a. js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
// (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck.
@@ -38,8 +38,7 @@ function sortGroups(arr){
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der
// Projektplan-Seite) und pdf.js darauf zugreifen.
// Liegt im gemeinsamen Modul, weil board/load/pdf auf sie zugreifen.
function peopleByRole(target){
const m = {}; ROLES.forEach(r=>m[r]=[]);
const people = [];
@@ -65,6 +64,18 @@ function peopleByRole(target){
return m;
}
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel,
// sodass z.B. "KW 9 / 2026" vor "KW 10 / 2026" kommt.
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
function weekSortKey(label){
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
if(m){
const kw = m[1].padStart(2,'0');
return `${m[2]}-${kw}`;
}
return String(label || '').toLowerCase();
}
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
// Zuordnungen auch nach einem Neuladen stabil bleiben.
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
+19 -13
View File
@@ -31,9 +31,11 @@ function updateWeekDate(){
// Kein Parameter; Rückgabe: das gefundene Woche-Objekt oder undefined.
function currentWeek(){ return db.weeks.find(w=>w.id===db.currentWeek); }
// switchWeek: setzt die aktuell gewählte Wochen-ID (`id`), speichert und
// rendert die Ansicht neu. Keine Rückgabe.
function switchWeek(id){ db.currentWeek = id; save(); renderAll(); }
// switchWeek: setzt die aktuell gewählte Wochen-ID (`id`) und rendert die
// Ansicht neu. Es werden keine Daten geändert, daher wird nicht gespeichert
// (eine save() würde die Woche nur als dirty markieren und unnötig senden).
// Keine Rückgabe.
function switchWeek(id){ db.currentWeek = id; renderAll(); }
// newWeek: erzeugt über zwei prompt-Dialoge eine neue leere Sitzung (Bezeichnung
// und Datum). Die neue Woche übernimmt die globale Personenliste der Abteilung,
@@ -82,7 +84,11 @@ function applyAutoLock(){
const sorted = [...db.weeks].sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
const latestId = sorted[sorted.length - 1].id;
db.weeks.forEach(w => {
const wasLocked = !!w.locked;
w.locked = (w.id !== latestId);
// Nur tatsächlich geänderte Lock-Zustände übertragen (beim bloßen Laden
// sind die Werte identisch und es wird nichts gesendet).
if(wasLocked !== w.locked) markWeekDirty(w.label);
});
}
@@ -124,9 +130,9 @@ function unlockCurrentWeek(skipAuth = false){
}
// deleteWeek: löscht die aktuell ausgewählte Sitzung (nur wenn mehr als 1
// vorhanden) inkl. ihrer CSV-Datei auf dem Server. Parameter `skipAuth=true`
// überspringt die Passwortabfrage. Danach wird die letzte verbleibende Woche
// aktiviert und neu gerendert. Keine Rückgabe.
// vorhanden) auf dem Server. Parameter `skipAuth=true` überspringt die
// Passwortabfrage. Danach wird die letzte verbleibende Woche aktiviert und neu
// gerendert. Keine Rückgabe.
function deleteWeek(skipAuth = false){
if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; }
if(!skipAuth && !isAuthenticated){
@@ -138,13 +144,13 @@ function deleteWeek(skipAuth = false){
}
if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; }
const current = currentWeek();
const filename = current ? filenameForWeek(current.label) : null;
if(filename){
console.log('Lösche CSV-Datei auf Server:', filename);
deleteCsvFromServer(filename).then(res=>{
if(res && res.ok){ console.log('CSV-Datei gelöscht:', filename); }
else { alert('CSV-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
}).catch(err=>{ alert('CSV-Löschung fehlgeschlagen: ' + (err.message || err)); });
if(current){
const label = current.label;
console.log('Lösche Woche auf Server:', label);
deleteWeekFromServer(label).then(res=>{
if(res && res.ok){ console.log('Woche gelöscht:', label); }
else { alert('Wochen-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
}).catch(err=>{ alert('Wochen-Löschung fehlgeschlagen: ' + (err.message || err)); });
}
db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek);
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
-3279
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -6,7 +6,7 @@
in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
"/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr.
kommen als URL-Parameter (weekLabel, projNr). Sie lädt styles.css und ausschließlich die gemeinsamen
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/csv.js, js/server.js
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/server.js
und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js,
js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js,
js/plan/plan-modals.js und js/plan/plan-schedule.js. WICHTIG: store, weeks, board, people,
@@ -318,7 +318,6 @@
<script src="/js/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.js?v=5"></script>
<script src="/js/csv.js?v=5"></script>
<script src="/js/server.js?v=5"></script>
<script src="/js/gantt.js?v=5"></script>
<script src="/js/plan/plan-state.js?v=5"></script>
File diff suppressed because it is too large. Load diff
+4
View File
@@ -1161,8 +1161,12 @@ th.role-bauüberwachung { color: var(--text); }
.legend-item.locked-legend-item { opacity: .7; }
#boardLegend {
margin-top: 16px;
padding: 12px;
background: var(--surface);
border: 1px solid var(--line-soft);
border-radius: 6px;
font-size: 12px;
}
.swatch {
+1330 -74
View File
File diff suppressed because it is too large. Load diff
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 [] []