Unnötige Datein gelöscht. DB normalisiert
This commit is contained in:
1 parent
da64472251
commit
4e083bee73
32 files changed
+970
-5541
No files matched your search
@@ -0,0 +1,7 @@
|
||||
# SQLite-Datenbank (Laufzeitartefakt in sessions/)
|
||||
sessions/*.db
|
||||
sessions/*.db-wal
|
||||
sessions/*.db-shm
|
||||
|
||||
# macOS-Artefakte
|
||||
.DS_Store
|
||||
@@ -4,39 +4,38 @@
|
||||
|
||||
Single-page app for tracking engineering projects across weekly sessions
|
||||
("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build
|
||||
step, no bundler, no tests. Data is persisted as semicolon-separated CSVs
|
||||
under `sessions/`.
|
||||
step, no bundler, no tests. Data is persisted in a SQLite database
|
||||
(`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`.
|
||||
|
||||
Two HTML entrypoints, both served statically by Express:
|
||||
- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte,
|
||||
Projektbeteiligte). Served at `/`.
|
||||
- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new
|
||||
tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules
|
||||
(`globals`, `utils`, `ui`, `date-picker`, `csv`, `server`, `gantt`) plus the
|
||||
(`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the
|
||||
plan modules under `js/plan/` — NOT the full main module set.
|
||||
|
||||
All UI strings/comments/CSV content are German. Preserve German in
|
||||
All UI strings/comments are German. Preserve German in
|
||||
user-facing text.
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
.
|
||||
├── server.js # Express server + CSV API
|
||||
├── server.js # Express server + SQLite API
|
||||
├── package.json # deps: express, cors only
|
||||
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile
|
||||
├── Docker/Dockerfile # node:20-alpine
|
||||
├── Docker/Dockerfile # node:22-alpine (nötig für node:sqlite)
|
||||
├── .dockerignore
|
||||
├── .vscode/extensions.json
|
||||
├── public/
|
||||
│ ├── Test.html # main app (served at /)
|
||||
│ ├── js/ # shared modules (classic scripts, global fns)
|
||||
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment
|
||||
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey-Quelle
|
||||
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey
|
||||
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
|
||||
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
|
||||
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek
|
||||
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
|
||||
│ │ ├── server.js # baseUrl, sendWeekToServer, sendPeopleToServer, blobToDataUrl
|
||||
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
|
||||
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
|
||||
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
|
||||
@@ -62,8 +61,8 @@ user-facing text.
|
||||
│ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link
|
||||
│ ├── styles.css
|
||||
│ └── dein_bild.ico
|
||||
└── sessions/ # data; bind-mounted into Docker container
|
||||
├── ELT/ HLS/ ARCH/ # one folder per department
|
||||
└── sessions/ # SQLite-DB; bind-mounted into Docker container
|
||||
└── projektuebersicht.db # node:sqlite database (WAL mode)
|
||||
```
|
||||
|
||||
## Running
|
||||
@@ -93,31 +92,55 @@ docker compose up --build # Docker variant (compose file is at repo root)
|
||||
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
|
||||
`js/ui.js` and all static assets in the HTML use **absolute** paths
|
||||
(`/js/...`, `/styles.css`) so they resolve on nested routes.
|
||||
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with
|
||||
`_people.csv` content in `people`; sessions sorted by filename.
|
||||
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV.
|
||||
- `POST /delete-csv {filename, department}`.
|
||||
- `GET /load-all?department=` → `{weeks:[weekObj], people:[personObj]}` with the
|
||||
global people list in `people`; weeks are full JSON week objects (id, label,
|
||||
date, locked, people, attendance, projects), sorted by label.
|
||||
- `POST /save-week {department, week}` → upserts one week (JSON object, keyed by
|
||||
department + label). Used by `sendWeekToServer()`.
|
||||
- `POST /save-people {department, people}` → replaces the department's global
|
||||
people list (JSON array).
|
||||
- `POST /delete-week {department, label}` → deletes the week; missing label
|
||||
yields 400, missing week tolerates with `deleted:false`.
|
||||
- Valid departments hard-coded as `ELT|HLS|ARCH` in `server.js`
|
||||
(`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in
|
||||
`DEPARTMENTS` in `js/globals.js`.
|
||||
- `safeName()` strictly validates filenames (path traversal protection);
|
||||
static assets get `Cache-Control: no-store`.
|
||||
- Static assets get `Cache-Control: no-store`.
|
||||
- JSON body limit is 2 MB.
|
||||
|
||||
## CSV format
|
||||
## Persistence (SQLite)
|
||||
|
||||
- Delimiter `;`, UTF-8 with BOM. Hand-editing is possible but risky; the parser
|
||||
reconstructs objects from the fixed column layout.
|
||||
- Week files `<Label>.csv` (e.g. `KW_30___2026.csv` — `filenameForWeek()`
|
||||
replaces non-word chars with `_`). Row layout is positional:
|
||||
`WOCHE` (label, date, locked flag) → `PERSONEN` (`name|role|fullName` per
|
||||
column) → header row → `ANWESENHEIT` → one row per project.
|
||||
- `_people.csv` per department: `NAME;Rollen;Voller Name;E-Mail;Abwesenheiten`
|
||||
(absences as JSON). People are global per department; new people are copied
|
||||
into the current week. Historical weeks keep their own person copies, so
|
||||
deleting a global person leaves old sessions unaffected no matter what.
|
||||
- Don't change the column layout without updating both `weekToCsv()` and
|
||||
`parseSessionCsv()` (`js/csv.js`), which must stay inverse of each other.
|
||||
- Data lives in `sessions/projektuebersicht.db` (`node:sqlite` `DatabaseSync`,
|
||||
WAL mode, `PRAGMA foreign_keys = ON`), created automatically. The schema is
|
||||
**fully relational** (18 tables) instead of one big JSON document per week:
|
||||
- `weeks` (department, label, week_id, date, locked; PK `(department,label)`)
|
||||
- `week_people` / `week_people_roles` — person copies per week (`array_order`
|
||||
keeps the list order, `role_order` the role array order)
|
||||
- `attendance` — présence per week/person
|
||||
- `projects` (`array_order` = position in the JSON array, `order_value` = the
|
||||
`p.order` field, which is only unique *within a group*, not globally!)
|
||||
- `project_lph`, `project_assign` (person_id|role keys split into columns),
|
||||
`project_aufwand` (LP effort per role), `project_aktuelle_lp`
|
||||
- `project_plan`, `plan_people`, `plan_todos` / `plan_todo_assignees` /
|
||||
`plan_todo_deps`, `plan_milestones` (`position` column!) /
|
||||
`plan_milestone_deps`
|
||||
- `people` / `person_roles` / `person_absences` — global people per department
|
||||
- The HTTP API stays JSON-shaped: `serializeWeek()` /
|
||||
`serializeGlobalPeople()` split incoming week/people objects across the
|
||||
tables inside a transaction, `loadWeeks()` / `loadGlobalPeople()`
|
||||
(re)assemble them on read. `saveWeekRelational()` deletes the week's rows
|
||||
first (`deleteWeekData()`, note `weeks` uses `label`, all child tables
|
||||
`week_label`).
|
||||
- Migration on every start (idempotent):
|
||||
`migrateLegacySchema()` converts a DB from the old single-JSON-document format
|
||||
(detected via a `data` column in `weeks`): it reads the documents, drops all
|
||||
tables, recreates the relational schema and inserts everything in one
|
||||
transaction.
|
||||
- The DB file sits in `sessions/`, so the existing Docker bind-mount
|
||||
`./sessions:/app/sessions` keeps it persistent. The path is overridable via
|
||||
the `DB_PATH` environment variable (used for tests), production default is
|
||||
`sessions/projektuebersicht.db`.
|
||||
- Project/person IDs are stable across reloads; only week-level fields are
|
||||
queryable columns.
|
||||
|
||||
## Frontend architecture
|
||||
|
||||
@@ -125,28 +148,28 @@ docker compose up --build # Docker variant (compose file is at repo root)
|
||||
`public/js/`; **all functions are globals on `window`**. Two HTML files include
|
||||
them via `<script src>` tags; load order in the HTML defines dependencies,
|
||||
since there is no bundler:
|
||||
- `Test.html` loads `globals → utils → ui → date-picker → csv → server →
|
||||
- `Test.html` loads `globals → utils → ui → date-picker → server →
|
||||
store → weeks` (shared modules), then the tab-specific folders `board →
|
||||
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
|
||||
init`. See the exact list in `Test.html`.
|
||||
- `project-plan.html` loads only `globals → utils → ui → date-picker → csv →
|
||||
- `project-plan.html` loads only `globals → utils → ui → date-picker →
|
||||
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
|
||||
touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
|
||||
- Shared state object `window.PlanApp` (`js/plan/plan-state.js`) holds all
|
||||
project-plan page state (week, project, zoom level, drag/`modal`-state),
|
||||
`db`/`currentDepartment` live in `js/globals.js`.
|
||||
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
|
||||
- Project IDs are regenerated on every CSV load, so projects are addressed
|
||||
cross-page by `department` + week `label` + `projNr` (+ readable projName path
|
||||
segment) via URL params: `openProjectPlan()` builds
|
||||
`/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`, `findProjectForPlan`
|
||||
resolves them.
|
||||
- Person IDs are stable across reloads via `personIdForName(name)` →
|
||||
`p_<lowercased alnum>`.
|
||||
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus
|
||||
`_people.csv` — not just the changed week. Exception: `js/plan/plan-state.js`
|
||||
`savePlan()` bypasses `save()` and writes only the current week's CSV directly
|
||||
via `sendCsvToServer()`.
|
||||
- Project/person IDs are stable across reloads because the server splits and
|
||||
reassembles week objects deterministically (JSON IDs are kept in the DB
|
||||
columns); projects are addressed cross-page by `department` + week `label` +
|
||||
`projNr` (+ readable projName path segment) via URL params:
|
||||
`openProjectPlan()` builds `/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`,
|
||||
`findProjectForPlan` resolves them. Person IDs follow
|
||||
`personIdForName(name)` → `p_<lowercased alnum>`.
|
||||
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks (via
|
||||
`sendWeekToServer()`) plus the people list (via `sendPeopleToServer()`) — not
|
||||
just the changed week. Exception: `js/plan/plan-state.js` `savePlan()` bypasses
|
||||
`save()` and writes only the current week directly via `sendWeekToServer()`.
|
||||
- `switchDepartment()` cancels a pending save debounce before switching, and
|
||||
`load()` discards stale `/load-all` responses after a department switch
|
||||
(frozen `requestedDepartment`), so late responses can never render or persist
|
||||
@@ -185,9 +208,10 @@ docker compose up --build # Docker variant (compose file is at repo root)
|
||||
|
||||
- `docker compose up --build` from the repo root. Compose maps `3003:3003`,
|
||||
bind-mounts `./sessions:/app/sessions`, `restart: unless-stopped`.
|
||||
- Dockerfile: `node:20-alpine`, `npm ci --production`, runs as non-root `node`,
|
||||
HEALTHCHECK on `/health`. `.dockerignore` excludes `Docker/*` (except
|
||||
`Docker/Dockerfile`), `node_modules/`, `.git`, `npm-debug.log`.
|
||||
- Dockerfile: `node:22-alpine` (nötig für node:sqlite), `npm ci --production`,
|
||||
runs as non-root `node`, HEALTHCHECK on `/health`. `.dockerignore` excludes
|
||||
`Docker/*` (except `Docker/Dockerfile`), `node_modules/`, `.git`,
|
||||
`npm-debug.log`.
|
||||
|
||||
## Conventions & verification
|
||||
|
||||
@@ -195,4 +219,4 @@ docker compose up --build # Docker variant (compose file is at repo root)
|
||||
`http://192.168.3.187:3000/erik/projektuebersicht.git`.
|
||||
- No linter, formatter, typecheck, or test suite. Manual verification only:
|
||||
start server, open `http://localhost:3003`, exercises the relevant tab,
|
||||
confirm CSV changes in `sessions/`.
|
||||
confirm SQLite changes (the DB file `sessions/projektuebersicht.db` grows).
|
||||
+4
-2
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
|
||||
|
||||
- einem Frontend in `Test.html`, `styles.css` und den JS-Modulen unter `public/js/`
|
||||
- einem einfachen Node.js-Server in `server.js`
|
||||
- einem Datenspeicher in CSV-Dateien unter `sessions/`
|
||||
- einem Datenspeicher in einer SQLite-Datenbank (`node:sqlite`) unter `sessions/projektuebersicht.db`
|
||||
- einem Docker-Setup unter `Docker/`
|
||||
|
||||
## Datei- und Komponentenübersicht
|
||||
@@ -17,9 +17,10 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
|
||||
- stellt statische Dateien (`Test.html`, die Module unter `js/`, `styles.css`, etc.) bereit
|
||||
- implementiert Endpunkte:
|
||||
- `GET /` → liefert `Test.html`
|
||||
- `GET /load-all` → lädt alle CSV-Dateien aus dem aktiven `sessions/<DEPARTMENT>/`-Ordner inkl. `_people.csv`
|
||||
- `POST /save-csv` → speichert eine CSV-Datei (Dateiname wird validiert)
|
||||
- `POST /delete-csv` → löscht eine CSV-Datei
|
||||
- `GET /load-all?department=…` → lädt alle Sitzungen plus globale Personenliste einer Abteilung
|
||||
- `POST /save-week {department, week}` → speichert/aktualisiert eine Sitzung
|
||||
- `POST /save-people {department, people}` → ersetzt die globale Personenliste einer Abteilung
|
||||
- `POST /delete-week {department, label}` → löscht eine Sitzung
|
||||
- `GET /health` → Gesundheitscheck
|
||||
- legt das `sessions`-Verzeichnis beim Start an, falls es nicht existiert
|
||||
- beendet sich mit aussagekräftiger Fehlermeldung, wenn der Port belegt ist
|
||||
@@ -33,16 +34,15 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
|
||||
|
||||
- `public/js/` (Module)
|
||||
- Die Client-Logik ist in kleine klassische Script-Dateien aufgeteilt (alle Funktionen global auf `window`):
|
||||
`globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `csv.js`, `server.js`, `store.js`, `weeks.js`,
|
||||
`globals.js`, `utils.js`, `ui.js`, `date-picker.js`, `server.js`, `store.js`, `weeks.js`,
|
||||
`board.js`, `people.js`, `projects.js`, `load.js`, `gantt.js`, `showmode.js`, `tabs-auth.js`, `pdf.js`,
|
||||
`init.js` sowie die Plan-Module unter `js/plan/` (`plan-state`, `plan-build-gantt`, `plan-gantt`,
|
||||
`plan-drag`, `plan-modals`, `plan-schedule`).
|
||||
- rendert das Projektboard, die Projekt- und Personenverwaltung sowie die Auslastungsansicht
|
||||
- lädt Daten über den Server und speichert Änderungen zurück als CSV
|
||||
- lädt Daten über den Server und speichert Änderungen als JSON-Objekte in die SQLite-Datenbank zurück
|
||||
- verwendet `currentDepartment` zur Trennung von Abteilungen (`ELT`, `HLS`, `ARCH`)
|
||||
- speichert:
|
||||
- `sessions/<DEPT>/<WEEK>.csv`
|
||||
- `sessions/<DEPT>/_people.csv` (globale Personenliste pro Abteilung)
|
||||
- alle Sitzungen und die globale Personenliste pro Abteilung in `sessions/projektuebersicht.db`
|
||||
- sortiert Sitzungen nach Kalenderwoche, nicht alphabetisch
|
||||
- sperrt historische Sitzungen automatisch (bis auf die chronologisch letzte)
|
||||
- Personen sind global pro Abteilung verwaltet; historische Sitzungen behalten ihre lokalen Personenkopien
|
||||
@@ -91,27 +91,18 @@ Dieses Projekt ist eine lokale Single-Page-Webanwendung zur Verwaltung elektrote
|
||||
## Speicherorte und Datenfluss
|
||||
|
||||
### Speicherung
|
||||
- Alle persistierten Daten werden als CSV-Dateien in `sessions/` gespeichert.
|
||||
- Innerhalb von `sessions/` existieren Abteilungsordner:
|
||||
- `sessions/ELT/`
|
||||
- `sessions/HLS/`
|
||||
- `sessions/ARCH/`
|
||||
- Jede Abteilung enthält mindestens:
|
||||
- `_people.csv` → globale Personenliste mit Rollen und vollständigem Namen
|
||||
- `KW_XX___YYYY.csv` oder vergleichbare Sitzungsdateien → Sitzungsdaten mit Projekten, Zuordnungen und Anwesenheit
|
||||
- Alle persistierten Daten werden in einer SQLite-Datenbank (`node:sqlite`, `DatabaseSync`, WAL-Modus) unter `sessions/projektuebersicht.db` gespeichert.
|
||||
- Innerhalb von `sessions/` liegen ausschließlich die SQLite-Datenbankdateien (`projektuebersicht.db` + WAL/SHM).
|
||||
|
||||
### Wo wird der Server erstellt?
|
||||
- Der Server wird in `server.js` erstellt.
|
||||
- `server.js` startet einen Express-Server auf Port `3003` oder dem Wert von `process.env.PORT`.
|
||||
- Das Frontend ruft diesen Server über `window.location.origin` an.
|
||||
|
||||
### Wo werden die Dateien gespeichert?
|
||||
- Das Frontend schreibt über den Server in das Dateisystem.
|
||||
- In `js/store.js` ruft die Anwendung beim Speichern `/save-csv` auf.
|
||||
- Der Server legt CSV-Dateien unter `sessions/<DEPARTMENT>/` ab.
|
||||
- Beispiel:
|
||||
- `sessions/ELT/KW_32___2026.csv`
|
||||
- `sessions/ELT/_people.csv`
|
||||
### Wo werden die Daten gespeichert?
|
||||
- Das Frontend schreibt über den Server in die SQLite-Datenbank.
|
||||
- In `js/store.js` ruft die Anwendung beim Speichern `/save-week` (Sitzungen) bzw. `/save-people` (globale Personen) auf.
|
||||
- Der Server schreibt die relationalen Tabellen in `sessions/projektuebersicht.db` (eine Transaktion pro Sitzung, siehe `serializeWeek()`/`loadWeeks()` in `server.js`).
|
||||
|
||||
## Betrieb
|
||||
|
||||
@@ -141,8 +132,8 @@ http://localhost:3003
|
||||
```
|
||||
|
||||
## Wichtige Hinweise
|
||||
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/`.
|
||||
- Der Server benötigt Lese-/Schreibzugriff auf `sessions/` (für die SQLite-Datenbank und das Docker-Bind-Mount).
|
||||
- Die Docker-Compose-Konfiguration mountet `./sessions` in den Container. Achte darauf, dass der relative Pfad im Kontext korrekt ist, wenn du `docker compose` aus einem anderen Verzeichnis startest.
|
||||
- `js/store.js` und `js/server.js` arbeiten mit Abteilungsparametern und speichern Daten abteilungsweise in separaten Unterordnern.
|
||||
- `js/store.js` und `js/server.js` arbeiten mit Abteilungsparametern und speichern Daten abteilungsweise getrennt.
|
||||
- Die Passwortfunktion ist nur ein einfacher Client-Mechanismus im Frontend und kein echter Sicherheitsmechanismus.
|
||||
- Globale Personen werden in `_people.csv` verwaltet. Wird eine Person global gelöscht, bleibt sie in bereits existierenden Sitzungen sichtbar.
|
||||
- Globale Personen werden pro Abteilung in der Datenbank verwaltet. Wird eine Person global gelöscht, bleibt sie in bereits existierenden Sitzungen sichtbar (Wochen-Personenkopien).
|
||||
Vendored
BIN
Binary file not shown.
+1
-2
@@ -5,7 +5,7 @@
|
||||
und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im
|
||||
seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet
|
||||
styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js,
|
||||
js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, danach die
|
||||
js/date-picker.js, js/server.js, js/store.js, js/weeks.js, danach die
|
||||
tab-spezifischen Module js/board/board.js, js/people/people.js,
|
||||
js/projects/projects.js, js/load/load.js sowie js/gantt.js, js/showmode.js,
|
||||
js/tabs-auth.js, js/pdf.js und js/init.js; crypto-js und pdfmake kommen von einem
|
||||
@@ -227,7 +227,6 @@
|
||||
<script src="/js/utils.js?v=5"></script>
|
||||
<script src="/js/ui.js?v=5"></script>
|
||||
<script src="/js/date-picker.js?v=5"></script>
|
||||
<script src="/js/csv.js?v=5"></script>
|
||||
<script src="/js/server.js?v=5"></script>
|
||||
<script src="/js/store.js?v=5"></script>
|
||||
<script src="/js/weeks.js?v=5"></script>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -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>`;
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — CSV-Serialisierung & Parsing
|
||||
// Hinweis: weekToCsv() und parseSessionCsv() sind invers zueinander.
|
||||
// Das Spaltenlayout darf nicht geändert werden, ohne beide Funktionen anzupassen.
|
||||
// -----------------------------
|
||||
// Diese Datei enthält das gesamte CSV-Handling (Serialisieren und Parsen) und
|
||||
// wird nach js/date-picker.js von beiden HTML-Einstiegspunkten geladen. Sie
|
||||
// stellt die globalen Funktionen weekToCsv/parseSessionCsv, das Personen-CSV
|
||||
// (globalPeopleToCsv/parseGlobalPeopleCsv) sowie Helfer wie weekSortKey und
|
||||
// decodeWeekLabel bereit. Hauptnutzer sind js/store.js (Laden/Speichern aller
|
||||
// Sitzungen), js/weeks.js (Wochen-Klonen) und js/plan/plan-state.js (savePlan),
|
||||
// die die CSV-Zeichenketten über js/server.js austauschen.
|
||||
|
||||
// filenameForWeek: leitet aus dem Wochenlabel einen Dateinamen ab
|
||||
// (alle Nicht-Wort-Zeichen werden durch _ ersetzt) und hängt .csv an.
|
||||
// Parameter: label (String). Rückgabe: String wie 'KW_30___2026.csv'.
|
||||
function filenameForWeek(label){
|
||||
return (label.replace(/[^\w]/g,'_'))+'.csv';
|
||||
}
|
||||
|
||||
// csvEscape: maskiert ein CSV-Feld, falls es Sonderzeichen (;, ", Zeilenumbruch) enthält
|
||||
// Parameter: v (beliebiger Wert). Rückgabe: String, bei Bedarf in Anführungszeichen.
|
||||
function csvEscape(v){
|
||||
const s = (v === null || v === undefined ? '' : String(v));
|
||||
if(/[;"\n\r]/.test(s)) return '"' + s.replace(/"/g,'""') + '"';
|
||||
return s;
|
||||
}
|
||||
|
||||
// globalPeopleToCsv: serialisiert die globalen Personen (aller Abteilungen) als CSV-Text
|
||||
// Parameter: people (Array von Person-Objekten). Rückgabe: UTF-8-BOM-CSV-String.
|
||||
function globalPeopleToCsv(people){
|
||||
const rows = [['NAME', 'Rollen', 'Voller Name', 'E-Mail', 'Abwesenheiten']];
|
||||
people.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(p => {
|
||||
const absences = Array.isArray(p.absences) ? JSON.stringify(p.absences) : '';
|
||||
rows.push([p.name, (p.role || []).join(','), p.fullName || '', p.email || '', absences]);
|
||||
});
|
||||
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
|
||||
}
|
||||
|
||||
// parseGlobalPeopleCsv: parst den _people.csv-Inhalt zurück in Person-Objekte
|
||||
// Parameter: text (String). Rückgabe: Array von Person-Objekten (mit stabiler ID).
|
||||
function parseGlobalPeopleCsv(text){
|
||||
if(!text) return [];
|
||||
const rows = parseCsvText(text);
|
||||
if(!rows.length) return [];
|
||||
const people = [];
|
||||
rows.slice(1).forEach(row => {
|
||||
const name = (row[0] || '').trim();
|
||||
if(!name) return;
|
||||
const roles = String(row[1] || 'Planung').split(',').map(x => x.trim()).filter(Boolean);
|
||||
const fullName = (row[2] || '').trim() || name;
|
||||
const email = (row[3] || '').trim();
|
||||
let absences = [];
|
||||
if(row[4]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[4]);
|
||||
if(Array.isArray(parsed)) absences = parsed;
|
||||
} catch(e) {
|
||||
// Ignoriere fehlerhafte JSON-Daten
|
||||
}
|
||||
}
|
||||
people.push({ id: personIdForName(name), name, fullName, email, role: roles, absences });
|
||||
});
|
||||
return people;
|
||||
}
|
||||
|
||||
// weekToCsv: serialisiert eine komplette Sitzung (Woche) in das feste CSV-Spaltenlayout
|
||||
// Parameter: w (Wochen-Objekt mit label, date, locked, people, attendance, projects).
|
||||
// Rückgabe: UTF-8-BOM-CSV-String. Invers zu parseSessionCsv().
|
||||
function weekToCsv(w){
|
||||
// allPeople enthält für das Board jede (Person, Rolle)-Kombination einmal.
|
||||
const grouped = peopleByRole(w);
|
||||
const allPeople = [];
|
||||
ROLES.forEach(r => grouped[r].forEach(entry => allPeople.push(entry)));
|
||||
const rows = [];
|
||||
// Metadaten-Zeile: Label, Datum und Sperrstatus der Sitzung
|
||||
rows.push(['WOCHE', w.label || '', w.date || '', w.locked ? '1' : '']);
|
||||
// PERSONEN-Zeile: Pro Spalte Name|Rolle|Voller Name, damit beim Laden
|
||||
// die Zuordnung pro Rolle eindeutig bleibt und der volle Name erhalten ist.
|
||||
// Der volle Name wird immer als dritter Teil gespeichert (auch leer),
|
||||
// damit er beim Bearbeiten nicht versehentlich verloren geht.
|
||||
const peopleMeta = allPeople.map(e => `${e.person.name}|${e.role}|${e.person.fullName || ''}`);
|
||||
rows.push(['PERSONEN', '', '', ...peopleMeta, ...LPHS.map(() => ''), '','','','','']);
|
||||
rows.push(['Pr.-Nr.','Bezeichnung','Status', ...allPeople.map(e => e.person.name), ...LPHS, 'Gruppe','Index','Plan','Aufwand','Aktive Phasen']);
|
||||
rows.push(['ANWESENHEIT','', ...allPeople.map(e => w.attendance && w.attendance[e.person.id] ? 'X' : ''), ...LPHS.map(() => ''),'','','','','']);
|
||||
w.projects.forEach(p => {
|
||||
const arr = [p.nr || '', p.name || '', p.status || ''];
|
||||
allPeople.forEach(entry => {
|
||||
const s = p.assign[assignKey(entry.person.id, entry.role)];
|
||||
arr.push(s === 1 ? 'X' : s === 2 ? '(X)' : '');
|
||||
});
|
||||
LPHS.forEach(l => {
|
||||
const v = p.lph[l];
|
||||
arr.push(v !== undefined && v !== '' ? String(v).replace(/%/g,'') + '%' : '');
|
||||
});
|
||||
arr.push(p.group || '');
|
||||
arr.push(p.order !== undefined ? String(p.order) : '');
|
||||
arr.push(p.plan ? JSON.stringify(p.plan) : '');
|
||||
arr.push(p.aufwandProLP ? JSON.stringify(p.aufwandProLP) : '');
|
||||
arr.push(p.aktuelleLP ? JSON.stringify(p.aktuelleLP) : '');
|
||||
rows.push(arr);
|
||||
});
|
||||
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
|
||||
}
|
||||
|
||||
|
||||
// parseCsvText: entfernt ein (mehrfach kodiertes) BOM und zerlegt den Text in Zeilen-Arrays
|
||||
// Parameter: text (String). Rückgabe: Array aus Zeilen, jede ein Feld-Array.
|
||||
function parseCsvText(text){
|
||||
// Manche ältere CSV-Dateien enthalten ein UTF-8-BOM gefolgt von einem
|
||||
// literalen "" (dreifach kodiert). Beides am Anfang entfernen.
|
||||
return text.replace(/^[\ufeff\u00EF\u00BB\u00BF]+/,'').split(/\r?\n/).filter(line => line.trim() !== '').map(parseCsvLine);
|
||||
}
|
||||
|
||||
// Toleranter CSV-Zeilenparser. Versteht sauber gequotetes CSV (wie es
|
||||
// csvEscape/weekToCsv erzeugen). Semikolons innerhalb eines quoted Feldes sind
|
||||
// Feldinhalt und kein Trennzeichen – so bleibt weekToCsv()/parseSessionCsv()
|
||||
// auch bei Status-Texten, die mit ";" enden, invers.
|
||||
// parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array
|
||||
// Parameter: line (String). Rückgabe: Array der Felder.
|
||||
function parseCsvLine(line){
|
||||
const result = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
let hasUnclosedQuotes = false;
|
||||
let i = 0;
|
||||
while(i < line.length){
|
||||
const c = line[i];
|
||||
const n = line[i+1];
|
||||
if(inQuotes){
|
||||
if(c === '"' && n === '"'){
|
||||
current += '"'; i += 2;
|
||||
} else if(c === '"' && (n === ';' || n === undefined)){
|
||||
result.push(current);
|
||||
current = '';
|
||||
inQuotes = false;
|
||||
i += (n === ';' ? 2 : 1);
|
||||
} else {
|
||||
current += c; i++;
|
||||
}
|
||||
} else {
|
||||
if(c === ';'){
|
||||
result.push(current);
|
||||
current = ''; i++;
|
||||
} else if(c === '"'){
|
||||
inQuotes = true; i++;
|
||||
} else {
|
||||
current += c; i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(inQuotes) hasUnclosedQuotes = true;
|
||||
result.push(current);
|
||||
if(hasUnclosedQuotes){
|
||||
console.warn('CSV-Zeile enthält nicht geschlossene Anführungszeichen:', line.slice(0,80));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// decodeWeekLabel: rekonstruiert das Wochenlabel aus einem Dateinamen
|
||||
// (ersetzt _ durch Leerzeichen und ergänzt das Layout "KW XX / YYYY").
|
||||
// Parameter: filename (String). Rückgabe: Label-String.
|
||||
function decodeWeekLabel(filename){
|
||||
const base = filename.replace(/\.csv$/i,'');
|
||||
let label = base.replace(/_+/g,' ').trim();
|
||||
if(/^KW \d+ \d+$/.test(label)){
|
||||
label = label.replace(/^KW (\d+) (\d+)$/, 'KW $1 / $2');
|
||||
}
|
||||
return label || base;
|
||||
}
|
||||
|
||||
// Erzeugt einen Sortierschlüssel aus einem Wochenlabel, sodass z.B.
|
||||
// "KW 9 / 2026" vor "KW 10 / 2026" kommt.
|
||||
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel
|
||||
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
|
||||
function weekSortKey(label){
|
||||
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
|
||||
if(m){
|
||||
const kw = m[1].padStart(2,'0');
|
||||
return `${m[2]}-${kw}`;
|
||||
}
|
||||
return String(label || '').toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
// parseSessionCsv: parst die CSV-Zeilen einer Sitzung zurück in ein Wochen-Objekt
|
||||
// Parameter: filename (String), rows (Array von Feld-Arrays aus parseCsvText).
|
||||
// Rückgabe: Wochen-Objekt {id, label, date, people, projects, attendance, locked}
|
||||
// oder null bei leerer Eingabe. Invers zu weekToCsv().
|
||||
function parseSessionCsv(filename, rows){
|
||||
if(!rows || rows.length === 0) return null;
|
||||
let label, date, headerRowIdx = 0, attendanceRowIdx = 1, projectStartIdx = 2;
|
||||
let locked = false;
|
||||
let metaPeopleRow = null;
|
||||
if(rows[0][0] === 'WOCHE'){
|
||||
label = rows[0][1] || decodeWeekLabel(filename);
|
||||
date = rows[0][2] || '';
|
||||
locked = (rows[0][3] || '').trim() === '1';
|
||||
if(rows[1] && rows[1][0] === 'PERSONEN'){
|
||||
metaPeopleRow = rows[1];
|
||||
headerRowIdx = 2; attendanceRowIdx = 3;
|
||||
} else {
|
||||
headerRowIdx = 1; attendanceRowIdx = 2;
|
||||
}
|
||||
} else {
|
||||
label = decodeWeekLabel(filename);
|
||||
date = '';
|
||||
}
|
||||
projectStartIdx = attendanceRowIdx + 1;
|
||||
|
||||
const header = rows[headerRowIdx];
|
||||
if(!header || header.length < 4) return { id: uid(), label, date, people: [], projects: [], attendance: {} };
|
||||
|
||||
const lphStart = header.indexOf('LPH 1');
|
||||
const groupIdx = header.indexOf('Gruppe');
|
||||
const orderIdx = header.indexOf('Index');
|
||||
const planIdx = header.indexOf('Plan');
|
||||
const aufwandIdx = header.indexOf('Aufwand');
|
||||
const aktiveIdx = header.indexOf('Aktive Phasen');
|
||||
const peopleCols = header.slice(3, lphStart === -1 ? header.length : lphStart);
|
||||
|
||||
const people = [];
|
||||
const colMap = [];
|
||||
const personMeta = metaPeopleRow ? metaPeopleRow.slice(3, lphStart === -1 ? metaPeopleRow.length : lphStart) : [];
|
||||
const personByName = {};
|
||||
const nameOccurrences = {};
|
||||
peopleCols.forEach((name, idx) => {
|
||||
const n = (name || '').trim();
|
||||
if(!n){ colMap.push(null); return; }
|
||||
|
||||
let roles = ['Planung'];
|
||||
let fullName = n;
|
||||
if(personMeta[idx]){
|
||||
const parts = String(personMeta[idx]).split('|');
|
||||
const rawRoles = parts[1] || '';
|
||||
const parsedRoles = rawRoles.split(',').map(x => x.trim()).filter(x => ROLES.includes(x));
|
||||
if(parsedRoles.length > 0) roles = parsedRoles;
|
||||
// Voller Name ist alles ab dem zweiten Pipe; damit sind auch Pipe-Zeichen
|
||||
// im Namen möglich. Leere Strings werden bewusst übernommen.
|
||||
if(parts.length >= 3) fullName = parts.slice(2).join('|').trim();
|
||||
} else {
|
||||
const occurrence = nameOccurrences[n] || 0;
|
||||
roles = [ROLES[Math.min(occurrence, ROLES.length - 1)]];
|
||||
nameOccurrences[n] = occurrence + 1;
|
||||
}
|
||||
|
||||
// Gleiche Person (gleiches Kürzel) wird im Board pro Rolle mehrfach
|
||||
// dargestellt, soll aber im Datenmodell nur einmal existieren.
|
||||
let person = personByName[n];
|
||||
if(!person){
|
||||
person = { id: personIdForName(n), name: n, fullName: fullName, role: [] };
|
||||
personByName[n] = person;
|
||||
people.push(person);
|
||||
}
|
||||
// Überschreibe den vollen Namen mit dem Wert aus der CSV; die CSV ist
|
||||
// hier die führende Datenquelle für den vollständigen Namen.
|
||||
person.fullName = fullName;
|
||||
// Sammle alle bekannten Rollen der Person; die aktuelle Spaltenrolle
|
||||
// wird für die Zuordnung über colMap verwendet.
|
||||
roles.forEach(r => { if(!person.role.includes(r)) person.role.push(r); });
|
||||
|
||||
colMap.push({ person, role: roles[0] });
|
||||
});
|
||||
|
||||
const projects = [];
|
||||
for(let rIdx = projectStartIdx; rIdx < rows.length; rIdx++){
|
||||
const row = rows[rIdx];
|
||||
if(row.length < 4) continue;
|
||||
const nr = row[0] || '';
|
||||
const name = row[1] || '';
|
||||
const status = row[2] || '';
|
||||
if(!nr.trim() && !name.trim()) continue;
|
||||
|
||||
const assign = {};
|
||||
peopleCols.forEach((_, i) => {
|
||||
const entry = colMap[i];
|
||||
if(!entry) return;
|
||||
const v = (row[3+i] || '').trim();
|
||||
const key = assignKey(entry.person.id, entry.role);
|
||||
if(v === 'X') assign[key] = 1;
|
||||
else if(v === '(X)') assign[key] = 2;
|
||||
});
|
||||
|
||||
const lph = {};
|
||||
if(lphStart !== -1){
|
||||
LPHS.forEach((l, j) => {
|
||||
const raw = row[lphStart + j];
|
||||
if(raw !== undefined && raw !== ''){
|
||||
const num = String(raw).replace(/%/g,'');
|
||||
if(!isNaN(parseFloat(num)) && isFinite(num)) lph[l] = num;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let group = 'Aktive Projekte';
|
||||
if(groupIdx >= 0 && row[groupIdx]) group = row[groupIdx].trim() || group;
|
||||
else if(row.length > 3) group = row[row.length - 1].trim() || group;
|
||||
|
||||
let order = rIdx - projectStartIdx;
|
||||
if(orderIdx >= 0 && row[orderIdx] !== undefined && row[orderIdx] !== ''){
|
||||
const parsed = parseInt(row[orderIdx], 10);
|
||||
if(!isNaN(parsed)) order = parsed;
|
||||
}
|
||||
|
||||
let plan = null;
|
||||
if(planIdx >= 0 && row[planIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[planIdx]);
|
||||
if(parsed && typeof parsed === 'object') plan = parsed;
|
||||
} catch(e) {
|
||||
console.warn('Plan-Daten für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
let aufwandProLP;
|
||||
if(aufwandIdx >= 0 && row[aufwandIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[aufwandIdx]);
|
||||
if(parsed && typeof parsed === 'object') aufwandProLP = parsed;
|
||||
} catch(e) {
|
||||
console.warn('LP-Aufwand für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
let aktuelleLP;
|
||||
if(aktiveIdx >= 0 && row[aktiveIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[aktiveIdx]);
|
||||
if(Array.isArray(parsed)) aktuelleLP = parsed;
|
||||
else if(typeof parsed === 'string') aktuelleLP = [parsed];
|
||||
} catch(e) {
|
||||
console.warn('Aktive Phasen für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
projects.push({ id: uid(), nr, name, status, group, lph, assign, order, plan, aufwandProLP, aktuelleLP });
|
||||
}
|
||||
|
||||
const attendance = {};
|
||||
const attRow = rows[attendanceRowIdx];
|
||||
if(attRow && attRow[0] === 'ANWESENHEIT'){
|
||||
peopleCols.forEach((_, i) => {
|
||||
const entry = colMap[i];
|
||||
if(entry && (attRow[2+i] || '').trim() === 'X'){
|
||||
attendance[entry.person.id] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { id: uid(), label, date, people, projects, attendance, locked };
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -66,20 +66,17 @@ window.PlanApp = {
|
||||
|
||||
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
|
||||
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der
|
||||
// anderen plan-Module.
|
||||
// anderen plan-Module. Die Buttons binden ihre Handler bereits über das
|
||||
// onclick-Attribut in project-plan.html; hier werden nur Lücken gefüllt,
|
||||
// falls ein Button-Element fehlt.
|
||||
function bindToolbarButtons(){
|
||||
const addTodoBtn = document.getElementById('addTodoBtn');
|
||||
if(addTodoBtn) addTodoBtn.addEventListener('click', () => addTodo());
|
||||
const autoScheduleBtn = document.getElementById('autoScheduleBtn');
|
||||
if(autoScheduleBtn) autoScheduleBtn.addEventListener('click', () => autoScheduleTodos());
|
||||
const criticalPathBtn = document.getElementById('criticalPathBtn');
|
||||
if(criticalPathBtn) criticalPathBtn.addEventListener('click', () => toggleCriticalPath());
|
||||
}
|
||||
|
||||
// loadPlanData: lädt alle Sitzungen und die globale Personenliste der
|
||||
// Abteilung über die Server-API, sortiert die Wochen chronologisch und
|
||||
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen
|
||||
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage().
|
||||
// Abteilung über die Server-API (JSON-Dokumente), sortiert die Wochen
|
||||
// chronologisch und sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt
|
||||
// Fehlermeldungen der Reihenfolge nach im Seitenkopf an und startet sonst
|
||||
// renderPlanPage().
|
||||
function loadPlanData(){
|
||||
if(!window.fetch){
|
||||
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
|
||||
@@ -90,12 +87,15 @@ window.PlanApp = {
|
||||
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
|
||||
.then(data => {
|
||||
db = { weeks: [], currentWeek: null, globalPeople: {} };
|
||||
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people);
|
||||
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
|
||||
parsed.forEach(s => {
|
||||
const w = parseSessionCsv(s.filename, s.rows);
|
||||
if(w) db.weeks.push(w);
|
||||
});
|
||||
db.globalPeople[app.department] = Array.isArray(data.people) ? data.people : [];
|
||||
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
|
||||
.filter(w => w && typeof w.label === 'string')
|
||||
.map(w => {
|
||||
if(!Array.isArray(w.people)) w.people = [];
|
||||
if(!Array.isArray(w.projects)) w.projects = [];
|
||||
if(!w.attendance) w.attendance = {};
|
||||
return w;
|
||||
});
|
||||
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
|
||||
app.week = db.weeks.find(w => w.label === app.weekLabel);
|
||||
if(!app.week){
|
||||
@@ -179,18 +179,15 @@ window.PlanApp = {
|
||||
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
|
||||
function savePlan(){
|
||||
if(!app.week || !app.project) return;
|
||||
const filename = filenameForWeek(app.week.label);
|
||||
const csv = weekToCsv(app.week);
|
||||
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
|
||||
sendWeekToServer(app.week).catch(err => console.error('Speichern fehlgeschlagen:', err));
|
||||
}
|
||||
|
||||
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als
|
||||
// _people.csv auf den Server, aber nur wenn sie nicht leer ist.
|
||||
// JSON-Array auf den Server, aber nur wenn sie nicht leer ist.
|
||||
function saveGlobalPeople(){
|
||||
const people = db.globalPeople[app.department] || [];
|
||||
if(people.length > 0){
|
||||
const peopleCsv = globalPeopleToCsv(people);
|
||||
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)">
|
||||
|
||||
+30
-16
@@ -1,13 +1,15 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Server-Kommunikation (CSV-API)
|
||||
// Projektübersicht — Server-Kommunikation (JSON-API)
|
||||
// -----------------------------
|
||||
// Diese Datei kapselt die HTTP-Kommunikation mit dem Express-Server (siehe
|
||||
// server.js im Projekt-Root) und wird nach js/csv.js von beiden
|
||||
// server.js im Projekt-Root) und wird nach js/date-picker.js von beiden
|
||||
// HTML-Einstiegspunkten geladen. Sie stellt baseUrl, blobToDataUrl,
|
||||
// sendCsvToServer und deleteCsvFromServer bereit. Genutzt wird sie von
|
||||
// js/store.js (syncAllToServer, Speichern/Löschen aller Wochen),
|
||||
// js/weeks.js (Wochen-Löschung) und js/plan/plan-state.js (savePlan schreibt
|
||||
// direkt die aktuelle Woche über sendCsvToServer).
|
||||
// sendWeekToServer, sendPeopleToServer und deleteWeekFromServer bereit.
|
||||
// Genutzt wird sie von js/store.js (syncAllToServer, Speichern aller Wochen
|
||||
// und der Personenliste), js/weeks.js (Wochen-Löschung) und
|
||||
// js/plan/plan-state.js (savePlan schreibt direkt die aktuelle Woche über
|
||||
// sendWeekToServer). Die Daten werden als strukturierte JSON-Objekte an die
|
||||
// SQLite-API des Servers übergeben (keine CSV-Strings mehr).
|
||||
|
||||
// baseUrl: dynamische Server-URL, damit die App auch von anderen Geraeten im Netzwerk erreichbar ist.
|
||||
// Fallback auf localhost:3003, wenn die Seite als lokale Datei geladen wurde.
|
||||
@@ -31,24 +33,36 @@ function blobToDataUrl(blob){
|
||||
});
|
||||
}
|
||||
|
||||
// sendCsvToServer: POSTet eine Speicherwoche im CSV-Format an <baseUrl>/save-csv
|
||||
// Parameter: filename (String), csvContent (String). Rückgabe: Promise mit Server-Antwort.
|
||||
function sendCsvToServer(filename, csvContent){
|
||||
// sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
|
||||
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit Server-Antwort.
|
||||
function sendWeekToServer(week){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/save-csv',{
|
||||
return fetch(baseUrl() + '/save-week',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json; charset=utf-8'},
|
||||
body:JSON.stringify({filename,csv:csvContent,department:currentDepartment})
|
||||
body:JSON.stringify({week, department:currentDepartment})
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
|
||||
// deleteCsvFromServer: fordert das Löschen einer Speicherwoche über POST /delete-csv an
|
||||
// Parameter: filename (String). Rückgabe: Promise mit Server-Antwort.
|
||||
function deleteCsvFromServer(filename){
|
||||
// sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
|
||||
// JSON-Array an <baseUrl>/save-people. Parameter: people (Array).
|
||||
// Rückgabe: Promise mit Server-Antwort.
|
||||
function sendPeopleToServer(people){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/delete-csv', {
|
||||
return fetch(baseUrl() + '/save-people',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json; charset=utf-8'},
|
||||
body:JSON.stringify({people, department:currentDepartment})
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
|
||||
// deleteWeekFromServer: fordert das Löschen einer Woche über POST /delete-week an
|
||||
// Parameter: label (String, das Wochen-Label). Rückgabe: Promise mit Server-Antwort.
|
||||
function deleteWeekFromServer(label){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/delete-week', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json; charset=utf-8'},
|
||||
body: JSON.stringify({ filename, department: currentDepartment })
|
||||
body: JSON.stringify({ label, department: currentDepartment })
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
+33
-32
@@ -1,18 +1,20 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln
|
||||
// -----------------------------
|
||||
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungs-CSVs
|
||||
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungen
|
||||
// beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
|
||||
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH).
|
||||
// Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt
|
||||
// Die Daten liegen serverseitig in einer SQLite-Datenbank und werden als
|
||||
// strukturierte JSON-Objekte über die API ausgetauscht (keine CSV-Strings).
|
||||
// Sie wird von `Test.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
|
||||
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
|
||||
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
|
||||
// `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
|
||||
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
|
||||
|
||||
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
|
||||
// Jede Sitzung wird als eigene CSV-Datei gespeichert. Personen und Rollen werden
|
||||
// in der Sitzungscsv selbst abgelegt.
|
||||
// save: synchronisiert die aktuellen Daten zum Server.
|
||||
// Jede Sitzung wird als eigenes JSON-Dokument gespeichert. Personen und Rollen
|
||||
// werden in der Sitzung selbst abgelegt.
|
||||
function save(){
|
||||
scheduleSessionSync();
|
||||
}
|
||||
@@ -25,31 +27,28 @@ function scheduleSessionSync(){
|
||||
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
|
||||
}
|
||||
|
||||
// syncAllToServer: schreibt die gesamte Datenbank als CSV-Dateien zum Server.
|
||||
// Zuerst wird die globale Personenliste (`_people.csv`) gespeichert, danach jede
|
||||
// Sitzung einzeln über `filenameForWeek()`/`weekToCsv()` und `sendCsvToServer()`.
|
||||
// syncAllToServer: schreibt die gesamte Datenbank als JSON-Dokumente zum Server.
|
||||
// Zuerst wird die globale Personenliste (`save-people`) gespeichert, danach jede
|
||||
// Sitzung einzeln über `sendWeekToServer()`.
|
||||
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
|
||||
function syncAllToServer(){
|
||||
// Zuerst globale Personenliste speichern
|
||||
saveGlobalPeopleToServer();
|
||||
// Dann jede Sitzung speichern
|
||||
db.weeks.forEach(w => {
|
||||
const filename = filenameForWeek(w.label);
|
||||
const csv = weekToCsv(w);
|
||||
sendCsvToServer(filename, csv)
|
||||
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', filename); })
|
||||
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', filename, err.message || err); });
|
||||
sendWeekToServer(w)
|
||||
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', w.label); })
|
||||
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err); });
|
||||
});
|
||||
}
|
||||
|
||||
// saveGlobalPeopleToServer: serialisiert die globale Personenliste der aktuellen
|
||||
// Abteilung per `globalPeopleToCsv()` und speichert sie als `_people.csv`.
|
||||
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
|
||||
// Abteilung über `sendPeopleToServer()` (JSON-Array).
|
||||
// Leere Personenlisten werden übersprungen (kein Schreibvorgang).
|
||||
function saveGlobalPeopleToServer(){
|
||||
const people = db.globalPeople[currentDepartment] || [];
|
||||
if(people.length === 0) return;
|
||||
const csv = globalPeopleToCsv(people);
|
||||
sendCsvToServer('_people.csv', csv)
|
||||
sendPeopleToServer(people)
|
||||
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
|
||||
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
|
||||
}
|
||||
@@ -62,7 +61,7 @@ function switchDepartment(dept){
|
||||
if(!DEPARTMENTS[dept]) return;
|
||||
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
|
||||
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die
|
||||
// CSV der falschen Abteilung schreibt.
|
||||
// Daten der falschen Abteilung schreibt.
|
||||
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
|
||||
currentDepartment = dept;
|
||||
saveUiState();
|
||||
@@ -84,11 +83,11 @@ function switchDepartment(dept){
|
||||
load();
|
||||
}
|
||||
|
||||
// load: liest alle CSV-Dateien der aktuellen Abteilung über `/load-all` vom
|
||||
// Server ein, parst Personen und Sitzungen (mit `parseSessionCsv()`) und baut
|
||||
// daraus `db.weeks` auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()`
|
||||
// an und rendert die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder
|
||||
// fehlenden Daten werden Beispieldaten über `seed()` geladen.
|
||||
// load: liest alle Sitzungen und die globale Personenliste der aktuellen
|
||||
// Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
|
||||
// auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
|
||||
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
|
||||
// Daten werden Beispieldaten über `seed()` geladen.
|
||||
function load(){
|
||||
if(!window.fetch){
|
||||
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
|
||||
@@ -106,18 +105,20 @@ function load(){
|
||||
.then(data => {
|
||||
if(currentDepartment !== requestedDepartment) return;
|
||||
// Globale Personenliste laden
|
||||
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
|
||||
db.globalPeople[currentDepartment] = Array.isArray(data.people) ? data.people : [];
|
||||
|
||||
const parsedSessions = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
|
||||
|
||||
db.weeks = [];
|
||||
parsedSessions.forEach(s => {
|
||||
const w = parseSessionCsv(s.filename, s.rows);
|
||||
if(w) db.weeks.push(w);
|
||||
});
|
||||
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
|
||||
.filter(w => w && typeof w.label === 'string')
|
||||
.map(w => {
|
||||
// Felder absichern, damit alte/kaputte Dokumente nicht crashen
|
||||
if(!Array.isArray(w.people)) w.people = [];
|
||||
if(!Array.isArray(w.projects)) w.projects = [];
|
||||
if(!w.attendance) w.attendance = {};
|
||||
return w;
|
||||
});
|
||||
|
||||
if(db.weeks.length === 0){
|
||||
console.warn('Server lieferte 0 Sitzungsdateien. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob der Ordner sessions/ mitkopiert wurde.');
|
||||
console.warn('Server lieferte 0 Sitzungen. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob die SQLite-Datenbank sessions/projektuebersicht.db existiert.');
|
||||
seed();
|
||||
}
|
||||
else {
|
||||
|
||||
+14
-3
@@ -5,7 +5,7 @@
|
||||
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
|
||||
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
|
||||
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen,
|
||||
// u. a. js/csv.js, js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
|
||||
// u. a. js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
|
||||
// (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
|
||||
|
||||
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck.
|
||||
@@ -38,8 +38,7 @@ function sortGroups(arr){
|
||||
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
|
||||
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
|
||||
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
|
||||
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der
|
||||
// Projektplan-Seite) und pdf.js darauf zugreifen.
|
||||
// Liegt im gemeinsamen Modul, weil board/load/pdf auf sie zugreifen.
|
||||
function peopleByRole(target){
|
||||
const m = {}; ROLES.forEach(r=>m[r]=[]);
|
||||
const people = [];
|
||||
@@ -65,6 +64,18 @@ function peopleByRole(target){
|
||||
return m;
|
||||
}
|
||||
|
||||
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel,
|
||||
// sodass z.B. "KW 9 / 2026" vor "KW 10 / 2026" kommt.
|
||||
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
|
||||
function weekSortKey(label){
|
||||
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
|
||||
if(m){
|
||||
const kw = m[1].padStart(2,'0');
|
||||
return `${m[2]}-${kw}`;
|
||||
}
|
||||
return String(label || '').toLowerCase();
|
||||
}
|
||||
|
||||
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
|
||||
// Zuordnungen auch nach einem Neuladen stabil bleiben.
|
||||
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
|
||||
|
||||
+10
-10
@@ -124,9 +124,9 @@ function unlockCurrentWeek(skipAuth = false){
|
||||
}
|
||||
|
||||
// deleteWeek: löscht die aktuell ausgewählte Sitzung (nur wenn mehr als 1
|
||||
// vorhanden) inkl. ihrer CSV-Datei auf dem Server. Parameter `skipAuth=true`
|
||||
// überspringt die Passwortabfrage. Danach wird die letzte verbleibende Woche
|
||||
// aktiviert und neu gerendert. Keine Rückgabe.
|
||||
// vorhanden) auf dem Server. Parameter `skipAuth=true` überspringt die
|
||||
// Passwortabfrage. Danach wird die letzte verbleibende Woche aktiviert und neu
|
||||
// gerendert. Keine Rückgabe.
|
||||
function deleteWeek(skipAuth = false){
|
||||
if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; }
|
||||
if(!skipAuth && !isAuthenticated){
|
||||
@@ -138,13 +138,13 @@ function deleteWeek(skipAuth = false){
|
||||
}
|
||||
if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; }
|
||||
const current = currentWeek();
|
||||
const filename = current ? filenameForWeek(current.label) : null;
|
||||
if(filename){
|
||||
console.log('Lösche CSV-Datei auf Server:', filename);
|
||||
deleteCsvFromServer(filename).then(res=>{
|
||||
if(res && res.ok){ console.log('CSV-Datei gelöscht:', filename); }
|
||||
else { alert('CSV-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
|
||||
}).catch(err=>{ alert('CSV-Löschung fehlgeschlagen: ' + (err.message || err)); });
|
||||
if(current){
|
||||
const label = current.label;
|
||||
console.log('Lösche Woche auf Server:', label);
|
||||
deleteWeekFromServer(label).then(res=>{
|
||||
if(res && res.ok){ console.log('Woche gelöscht:', label); }
|
||||
else { alert('Wochen-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
|
||||
}).catch(err=>{ alert('Wochen-Löschung fehlgeschlagen: ' + (err.message || err)); });
|
||||
}
|
||||
db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek);
|
||||
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
|
||||
|
||||
-3279
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -1,19 +1,32 @@
|
||||
/*
|
||||
* Express-Server der Projektübersicht: liefert die statischen Frontend-Dateien aus
|
||||
* public/ (Test.html unter "/") aus und stellt den Frontend-Modulen unter public/js/
|
||||
* eine CSV-API zum Laden und Speichern der Sitzungsdaten in sessions/ bereit.
|
||||
* Standardport ist 3003, überschreibbar über die Umgebungsvariable PORT.
|
||||
* Endpunkte: GET /, GET /health, GET /load-all, POST /save-csv, POST /delete-csv.
|
||||
* Die Daten werden als UTF-8-CSV mit BOM geschrieben; ungültige Abteilungen fallen
|
||||
* auf ELT zurück, Dateinamen werden über safeName() streng validiert.
|
||||
* public/ (Test.html unter "/") und stellt den Frontend-Modulen unter public/js/
|
||||
* eine JSON-API zum Laden und Speichern der Sitzungsdaten bereit. Die Daten liegen
|
||||
* in einer SQLite-Datenbank (node:sqlite, Node >= 22.13) unter sessions/projektuebersicht.db.
|
||||
*
|
||||
* Das Schema ist voll relational normalisiert: statt eines grossen JSON-Dokuments
|
||||
* pro Woche gibt es eigene Tabellen für Wochen-Metadaten, Wochen-Personen, Rollen,
|
||||
* Anwesenheit, Projekte, LPH-Werte, Projekt-Zuordnungen, Projektpläne, TODOs,
|
||||
* Meilensteine, Abhängigkeiten sowie die globalen Personen inkl. Rollen und
|
||||
* Abwesenheiten. Die HTTP-API bleibt davon unberührt: Das Frontend sendet und
|
||||
* erhält weiterhin komplette Wochen- und Personen-Objekte (JSON); der Server
|
||||
* verteilt sie beim Speichern auf die Tabellen und setzt sie beim Laden wieder
|
||||
* Zusammen (Serializer/Deserializer).
|
||||
*
|
||||
* Eine bestehende Datenbank aus dem alten Ein-Dokument-pro-Woche-Format wird
|
||||
* automatisch auf das relationale Schema umgestellt.
|
||||
*
|
||||
* Standardport ist 3003, überschreibbar über die Umgebungsvariable PORT.
|
||||
* Endpunkte: GET /, GET /health, GET /load-all, POST /save-week, POST /save-people,
|
||||
* POST /delete-week. Ungültige Abteilungen fallen auf ELT zurück.
|
||||
*/
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const app = express();
|
||||
|
||||
// Wichtige Konstanten: UTF-8-BOM für CSV-Dateien, Serverport (PORT-Env) und die gültigen Abteilungsordner
|
||||
const BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
|
||||
// Wichtige Konstanten: Serverport (PORT-Env) und die gültigen Abteilungsordner
|
||||
const PORT = process.env.PORT || 3003;
|
||||
const VALID_DEPARTMENTS = new Set(['ELT','HLS','ARCH']);
|
||||
|
||||
@@ -30,87 +43,743 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
||||
}
|
||||
}));
|
||||
|
||||
// Datenverzeichnis sessions/ bei Bedarf anlegen
|
||||
// Datenverzeichnis sessions/ bei Bedarf anlegen (bind-mounted in Docker, enthält die DB)
|
||||
const SESSIONS_DIR = path.join(__dirname, 'sessions');
|
||||
if(!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
||||
|
||||
// Erlaubt ausschliesslich sichere CSV-Dateinamen im Format <Name>.csv
|
||||
function safeName(name){
|
||||
const raw = String(name || '');
|
||||
// Pfad-Trennzeichen und Verzeichnisnavigation ablehnen
|
||||
if(/[\\/]|\.\./.test(raw)) return null;
|
||||
const base = path.basename(raw);
|
||||
if(!base || base.toLowerCase() === '.csv') return null;
|
||||
const safe = base.replace(/[^a-zA-Z0-9_\-\.]/g,'_');
|
||||
if(!/^([a-zA-Z0-9_\-]+)\.csv$/i.test(safe)) return null;
|
||||
return safe;
|
||||
// SQLite-Datenbank öffnen (wird angelegt, falls nicht vorhanden). Der Pfad ist
|
||||
// über die Umgebungsvariable DB_PATH übersteuerbar (für Tests/Backups), sonst
|
||||
// standardmäßig sessions/projektuebersicht.db.
|
||||
const DB_PATH = process.env.DB_PATH ? path.resolve(process.env.DB_PATH) : path.join(SESSIONS_DIR, 'projektuebersicht.db');
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
db.exec('PRAGMA journal_mode = WAL;');
|
||||
db.exec('PRAGMA foreign_keys = ON;');
|
||||
|
||||
// ---------- Schema (voll relational normalisiert) ----------
|
||||
|
||||
// Der serialisierte Datenbestand einer Woche besteht aus folgenden Entitäten:
|
||||
// - weeks: Metadaten (id, label, date, locked)
|
||||
// - week_people / week_people_roles: personen-Kopien pro Woche
|
||||
// - attendance: Anwesenheit pro Woche/Person
|
||||
// - projects: Projekt-Metadaten pro Woche
|
||||
// - project_lph: LPH-Fortschrittswerte pro Projekt
|
||||
// - project_assign: Zuordnungen (Haupt/Optional) pro Projekt/Person/Rolle
|
||||
// - project_plan: Plan-Kopf (start, end, progress) pro Projekt
|
||||
// - plan_people: im Plan beteiligte Personen
|
||||
// - plan_todos / plan_todo_assignees / plan_todo_deps: TODO-Listen
|
||||
// - plan_milestones / plan_milestone_deps: Meilensteine
|
||||
// - people / person_roles / person_absences: globale Personen pro Abteilung
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
|
||||
department TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
week_id TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
locked INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, label)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people_roles (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS attendance (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
present INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS projects (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
nr TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
"group" TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
order_value INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_lph (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_assign (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
state INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aufwand (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
value REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, lph, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aktuelle_lp (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_plan (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '',
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_people (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todos (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '',
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_assignees (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_deps (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL,
|
||||
depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestones (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
position TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestone_deps (
|
||||
department TEXT NOT NULL,
|
||||
week_label TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL,
|
||||
depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS people (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_roles (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_absences (
|
||||
department TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL,
|
||||
absence TEXT NOT NULL,
|
||||
PRIMARY KEY (department, person_id, absence)
|
||||
)`);
|
||||
|
||||
// Erlaubt ausschliesslich die gültigen Abteilungen (ELT|HLS|ARCH, sonst ELT)
|
||||
function departmentKey(department){
|
||||
return (typeof department === 'string' && VALID_DEPARTMENTS.has(department)) ? department : 'ELT';
|
||||
}
|
||||
|
||||
// Liefert den Zielordner für eine Abteilung (nur ELT|HLS|ARCH, sonst ELT) und legt ihn bei Bedarf an
|
||||
function departmentDir(department){
|
||||
const dept = (typeof department === 'string' && VALID_DEPARTMENTS.has(department)) ? department : 'ELT';
|
||||
const dir = path.join(SESSIONS_DIR, dept);
|
||||
if(!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
// ---------- Migration: altes Ein-Dokument-Format -> relationales Schema ----------
|
||||
|
||||
// hasLegacyWeeksTable: erkennt das alte Schema an der Daten-Spalte in `weeks`.
|
||||
function hasLegacyWeeksTable(){
|
||||
const cols = db.prepare("SELECT name FROM pragma_table_info('weeks') WHERE name = 'data'").all();
|
||||
return cols.length > 0;
|
||||
}
|
||||
|
||||
// Route: Speichert eine CSV-Datei (mit UTF-8-BOM) für eine Abteilung im sessions/-Ordner
|
||||
app.post('/save-csv', (req, res) => {
|
||||
const { filename, csv, department } = req.body || {};
|
||||
if(!filename || typeof csv !== 'string') return res.status(400).json({ ok: false, error: 'Missing filename or csv' });
|
||||
const safe = safeName(filename);
|
||||
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' });
|
||||
const dir = departmentDir(department);
|
||||
const out = path.join(dir, safe);
|
||||
const data = Buffer.concat([BOM, Buffer.from(csv, 'utf8')]);
|
||||
fs.writeFile(out, data, (err) => {
|
||||
if(err){ console.error('Write error', err); return res.status(500).json({ ok:false, error: String(err) }); }
|
||||
console.log('Saved CSV:', out);
|
||||
return res.json({ ok: true, path: out });
|
||||
});
|
||||
});
|
||||
// serializeWeek: zerlegt ein Wochen-Objekt (JSON vom Frontend) in die relationalen
|
||||
// Tabellen. Wird in einer Transaktion vom Aufrufer verwendet (deleteWeekData +
|
||||
// serializeWeek). Ersetzt alle Zeilen der Woche.
|
||||
function serializeWeek(department, week){
|
||||
const weekLabel = String(week.label || '');
|
||||
const upsertWeek = db.prepare(`INSERT INTO weeks (department, label, week_id, date, locked) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(department, label) DO UPDATE SET week_id = excluded.week_id, date = excluded.date, locked = excluded.locked`);
|
||||
upsertWeek.run(department, weekLabel, String(week.id || ''), String(week.date || ''), week.locked ? 1 : 0);
|
||||
|
||||
// Route: Löscht eine CSV-Datei einer Abteilung aus dem sessions/-Ordner (fehlende Datei wird toleriert)
|
||||
app.post('/delete-csv', (req, res) => {
|
||||
const { filename, department } = req.body || {};
|
||||
if(!filename) return res.status(400).json({ ok: false, error: 'Missing filename' });
|
||||
const safe = safeName(filename);
|
||||
if(!safe) return res.status(400).json({ ok: false, error: 'Invalid filename' });
|
||||
const dir = departmentDir(department);
|
||||
const out = path.join(dir, safe);
|
||||
fs.unlink(out, (err) => {
|
||||
if(err){
|
||||
if(err.code === 'ENOENT') {
|
||||
console.log('CSV-Datei nicht gefunden:', out);
|
||||
return res.json({ ok: true, missing: true });
|
||||
}
|
||||
console.error('Delete error', err);
|
||||
return res.status(500).json({ ok:false, error: String(err) });
|
||||
}
|
||||
console.log('Deleted CSV:', out);
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
const insWeekPerson = db.prepare(`INSERT INTO week_people (department, week_label, person_id, name, full_name, array_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insWeekPersonRole = db.prepare(`INSERT OR IGNORE INTO week_people_roles (department, week_label, person_id, role, role_order)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insAttendance = db.prepare(`INSERT OR REPLACE INTO attendance (department, week_label, person_id, present)
|
||||
VALUES (?, ?, ?, ?)`);
|
||||
|
||||
// Route: Lädt alle Sitzungs-CSV-Dateien einer Abteilung plus _people.csv (nach Dateiname sortiert)
|
||||
app.get('/load-all', (req, res) => {
|
||||
const dir = departmentDir(req.query.department);
|
||||
fs.readdir(dir, (err, files) => {
|
||||
if(err){ console.error('Read error', err); return res.status(500).json({ ok:false, error: String(err) }); }
|
||||
const csvFiles = files.filter(f => f.toLowerCase().endsWith('.csv'));
|
||||
const result = { ok: true, sessions: [], people: null };
|
||||
csvFiles.forEach(filename => {
|
||||
if(filename.toLowerCase() === '_people.csv'){
|
||||
result.people = fs.readFileSync(path.join(dir, filename), 'utf8');
|
||||
return;
|
||||
}
|
||||
const content = fs.readFileSync(path.join(dir, filename), 'utf8');
|
||||
result.sessions.push({ filename, content });
|
||||
(Array.isArray(week.people) ? week.people : []).forEach((p, idx) => {
|
||||
insWeekPerson.run(department, weekLabel, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insWeekPersonRole.run(department, weekLabel, String(p.id || ''), String(r || ''), ridx));
|
||||
});
|
||||
const att = week.attendance || {};
|
||||
Object.keys(att).forEach(pid => {
|
||||
insAttendance.run(department, weekLabel, pid, att[pid] ? 1 : 0);
|
||||
});
|
||||
|
||||
const insProject = db.prepare(`INSERT INTO projects (department, week_label, project_id, nr, name, status, "group", array_order, order_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insLph = db.prepare(`INSERT OR REPLACE INTO project_lph (department, week_label, project_id, lph, value)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insAssign = db.prepare(`INSERT OR REPLACE INTO project_assign (department, week_label, project_id, person_id, role, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insAufwand = db.prepare(`INSERT OR REPLACE INTO project_aufwand (department, week_label, project_id, lph, role, value)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insAktuelleLp = db.prepare(`INSERT OR IGNORE INTO project_aktuelle_lp (department, week_label, project_id, lph)
|
||||
VALUES (?, ?, ?, ?)`);
|
||||
const insPlan = db.prepare(`INSERT OR REPLACE INTO project_plan (department, week_label, project_id, start, end, progress)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insPlanPerson = db.prepare(`INSERT OR IGNORE INTO plan_people (department, week_label, project_id, person_id, array_order)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insTodo = db.prepare(`INSERT INTO plan_todos (department, week_label, project_id, todo_id, title, start, end, done, progress, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insTodoAssignee = db.prepare(`INSERT OR IGNORE INTO plan_todo_assignees (department, week_label, project_id, todo_id, person_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (department, week_label, project_id, todo_id, depends_on_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
const insMilestone = db.prepare(`INSERT INTO plan_milestones (department, week_label, project_id, milestone_id, name, date, position, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
|
||||
(Array.isArray(week.projects) ? week.projects : []).forEach((p, idx) => {
|
||||
const projId = String(p.id || '');
|
||||
insProject.run(department, weekLabel, projId, String(p.nr || ''), String(p.name || ''),
|
||||
String(p.status || ''), String(p.group || ''), idx, typeof p.order === 'number' ? p.order : idx);
|
||||
const lph = p.lph || {};
|
||||
Object.keys(lph).forEach(l => insLph.run(department, weekLabel, projId, String(l || ''), String(lph[l] === undefined ? '' : lph[l])));
|
||||
const assign = p.assign || {};
|
||||
Object.keys(assign).forEach(key => {
|
||||
// Key-Format: "personId|Rolle"
|
||||
const sep = key.indexOf('|');
|
||||
const pid = sep >= 0 ? key.slice(0, sep) : key;
|
||||
const role = sep >= 0 ? key.slice(sep + 1) : '';
|
||||
insAssign.run(department, weekLabel, projId, pid, role, Number(assign[key]) || 0);
|
||||
});
|
||||
// Sortiere Sitzungen nach Dateiname, damit die Reihenfolge stabil ist
|
||||
result.sessions.sort((a,b) => a.filename.localeCompare(b.filename));
|
||||
res.json(result);
|
||||
const aufwand = p.aufwandProLP || {};
|
||||
Object.keys(aufwand).forEach(lph => {
|
||||
const roles = aufwand[lph] || {};
|
||||
Object.keys(roles).forEach(role => {
|
||||
const v = Number(roles[role]);
|
||||
if(!isNaN(v)) insAufwand.run(department, weekLabel, projId, String(lph || ''), String(role || ''), v);
|
||||
});
|
||||
});
|
||||
(Array.isArray(p.aktuelleLP) ? p.aktuelleLP : []).forEach(lph => {
|
||||
insAktuelleLp.run(department, weekLabel, projId, String(lph || ''));
|
||||
});
|
||||
const plan = p.plan || null;
|
||||
if(plan){
|
||||
insPlan.run(department, weekLabel, projId, String(plan.start || ''), String(plan.end || ''), Number(plan.progress) || 0);
|
||||
(Array.isArray(plan.people) ? plan.people : []).forEach((pid, pidx) => insPlanPerson.run(department, weekLabel, projId, String(pid || ''), pidx));
|
||||
(Array.isArray(plan.todos) ? plan.todos : []).forEach((t, tidx) => {
|
||||
insTodo.run(department, weekLabel, projId, String(t.id || ''), String(t.title || ''),
|
||||
String(t.start || ''), String(t.end || ''), t.done ? 1 : 0, Number(t.progress) || 0, tidx);
|
||||
(Array.isArray(t.assignees) ? t.assignees : []).forEach(pid => insTodoAssignee.run(department, weekLabel, projId, String(t.id || ''), String(pid || '')));
|
||||
(Array.isArray(t.dependsOn) ? t.dependsOn : []).forEach(did => insTodoDep.run(department, weekLabel, projId, String(t.id || ''), String(did || '')));
|
||||
});
|
||||
(Array.isArray(plan.milestones) ? plan.milestones : []).forEach((m, midx) => {
|
||||
insMilestone.run(department, weekLabel, projId, String(m.id || ''), String(m.name || ''), String(m.date || ''), String(m.position || ''), midx);
|
||||
(Array.isArray(m.dependsOn) ? m.dependsOn : []).forEach(did => insMilestoneDep.run(department, weekLabel, projId, String(m.id || ''), String(did || '')));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleteWeekData: entfernt alle relationalen Zeilen einer Woche (Abteilung + Label)
|
||||
function deleteWeekData(department, weekLabel){
|
||||
const tables = ['week_people_roles','week_people','attendance','project_lph','project_assign',
|
||||
'project_aufwand','project_aktuelle_lp',
|
||||
'project_plan','plan_people','plan_todos','plan_todo_assignees','plan_todo_deps',
|
||||
'plan_milestones','plan_milestone_deps','projects'];
|
||||
tables.forEach(t => {
|
||||
db.prepare(`DELETE FROM ${t} WHERE department = ? AND week_label = ?`).run(department, weekLabel);
|
||||
});
|
||||
// Die Wochen-Tabelle selbst nutzt `label` statt `week_label`
|
||||
db.prepare('DELETE FROM weeks WHERE department = ? AND label = ?').run(department, weekLabel);
|
||||
}
|
||||
|
||||
// saveWeekRelational: speichert eine Woche vollständig relational (Transaktion)
|
||||
function saveWeekRelational(department, week){
|
||||
const weekLabel = String(week.label || '');
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
deleteWeekData(department, weekLabel);
|
||||
serializeWeek(department, week);
|
||||
db.exec('COMMIT');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// serializeGlobalPeople: zerlegt die globale Personenliste (JSON-Array) in die
|
||||
// Tabellen people/person_roles/person_absences. Ersetzt die gesamte Abteilungsliste.
|
||||
function serializeGlobalPeople(department, people){
|
||||
const del = db.prepare(`DELETE FROM people WHERE department = ?`);
|
||||
const delRoles = db.prepare(`DELETE FROM person_roles WHERE department = ?`);
|
||||
const delAbsences = db.prepare(`DELETE FROM person_absences WHERE department = ?`);
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
del.run(department); delRoles.run(department); delAbsences.run(department);
|
||||
const insPerson = db.prepare(`INSERT INTO people (department, person_id, name, full_name, email, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insRole = db.prepare(`INSERT OR IGNORE INTO person_roles (department, person_id, role, role_order) VALUES (?, ?, ?, ?)`);
|
||||
const insAbsence = db.prepare(`INSERT OR IGNORE INTO person_absences (department, person_id, absence) VALUES (?, ?, ?)`);
|
||||
people.forEach((p, idx) => {
|
||||
insPerson.run(department, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), String(p.email || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insRole.run(department, String(p.id || ''), String(r || ''), ridx));
|
||||
(Array.isArray(p.absences) ? p.absences : []).forEach(a => insAbsence.run(department, String(p.id || ''), String(a || '')));
|
||||
});
|
||||
db.exec('COMMIT');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// loadGlobalPeople: setzt die globale Personenliste einer Abteilung aus den
|
||||
// Tabellen people/person_roles/person_absences wieder zusammen. Rückgabe: Array.
|
||||
function loadGlobalPeople(department){
|
||||
const people = db.prepare('SELECT * FROM people WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const roles = db.prepare('SELECT * FROM person_roles WHERE department = ? ORDER BY role_order').all(department);
|
||||
const absences = db.prepare('SELECT * FROM person_absences WHERE department = ?').all(department);
|
||||
const rolesByPerson = {};
|
||||
const absByPerson = {};
|
||||
roles.forEach(r => { (rolesByPerson[r.person_id] = rolesByPerson[r.person_id] || []).push(r.role); });
|
||||
absences.forEach(a => { (absByPerson[a.person_id] = absByPerson[a.person_id] || []).push(a.absence); });
|
||||
return people.map(p => ({
|
||||
id: p.person_id,
|
||||
name: p.name,
|
||||
fullName: p.full_name,
|
||||
email: p.email,
|
||||
role: rolesByPerson[p.person_id] || [],
|
||||
absences: absByPerson[p.person_id] || [],
|
||||
}));
|
||||
}
|
||||
|
||||
// loadWeeks: setzt alle Wochen einer Abteilung aus den relationalen Tabellen
|
||||
// wieder zu kompletten Wochen-Objekten zusammen (Format wie vom Frontend erwartet).
|
||||
// Rückgabe: Array von Wochen-Objekten, sortiert nach Label.
|
||||
function loadWeeks(department){
|
||||
const weeks = db.prepare('SELECT * FROM weeks WHERE department = ? ORDER BY label').all(department);
|
||||
const weekPeople = db.prepare('SELECT * FROM week_people WHERE department = ? ORDER BY array_order').all(department);
|
||||
const weekRoles = db.prepare('SELECT * FROM week_people_roles WHERE department = ? ORDER BY role_order').all(department);
|
||||
const attendance = db.prepare('SELECT * FROM attendance WHERE department = ?').all(department);
|
||||
const projects = db.prepare('SELECT * FROM projects WHERE department = ? ORDER BY array_order').all(department);
|
||||
const lph = db.prepare('SELECT * FROM project_lph WHERE department = ?').all(department);
|
||||
const assign = db.prepare('SELECT * FROM project_assign WHERE department = ?').all(department);
|
||||
const aufwand = db.prepare('SELECT * FROM project_aufwand WHERE department = ?').all(department);
|
||||
const aktuelleLp = db.prepare('SELECT * FROM project_aktuelle_lp WHERE department = ?').all(department);
|
||||
const plans = db.prepare('SELECT * FROM project_plan WHERE department = ?').all(department);
|
||||
const planPeople = db.prepare('SELECT * FROM plan_people WHERE department = ? ORDER BY array_order').all(department);
|
||||
const todos = db.prepare('SELECT * FROM plan_todos WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const todoAssignees = db.prepare('SELECT * FROM plan_todo_assignees WHERE department = ?').all(department);
|
||||
const todoDeps = db.prepare('SELECT * FROM plan_todo_deps WHERE department = ?').all(department);
|
||||
const milestones = db.prepare('SELECT * FROM plan_milestones WHERE department = ? ORDER BY sort_order').all(department);
|
||||
const milestoneDeps = db.prepare('SELECT * FROM plan_milestone_deps WHERE department = ?').all(department);
|
||||
|
||||
// Index-Karten für schnellen Zugriff
|
||||
const weekKey = (label) => department + '|' + label;
|
||||
const projKey = (label, pid) => department + '|' + label + '|' + pid;
|
||||
const todoKey = (label, pid, tid) => department + '|' + label + '|' + pid + '|' + tid;
|
||||
const msKey = (label, pid, mid) => department + '|' + label + '|' + pid + '|' + mid;
|
||||
|
||||
const weekPeopleMap = {};
|
||||
const weekRolesMap = {};
|
||||
const attendanceMap = {};
|
||||
weekPeople.forEach(r => {
|
||||
const k = weekKey(r.week_label);
|
||||
(weekPeopleMap[k] = weekPeopleMap[k] || [])[r.array_order] = { id: r.person_id, name: r.name, fullName: r.full_name };
|
||||
});
|
||||
weekRoles.forEach(r => {
|
||||
const k = weekKey(r.week_label);
|
||||
const arr = (weekRolesMap[k] = weekRolesMap[k] || {});
|
||||
(arr[r.person_id] = arr[r.person_id] || []).push(r.role);
|
||||
});
|
||||
attendance.forEach(r => {
|
||||
attendanceMap[weekKey(r.week_label) + '|' + r.person_id] = !!r.present;
|
||||
});
|
||||
|
||||
const projectsMap = {};
|
||||
projects.forEach(r => {
|
||||
projectsMap[projKey(r.week_label, r.project_id)] = {
|
||||
id: r.project_id, nr: r.nr, name: r.name, status: r.status, group: r.group,
|
||||
lph: {}, assign: {}, order: r.order_value, plan: null,
|
||||
_arrayOrder: r.array_order,
|
||||
};
|
||||
});
|
||||
lph.forEach(r => { const p = projectsMap[projKey(r.week_label, r.project_id)]; if(p) p.lph[r.lph] = r.value; });
|
||||
assign.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p) p.assign[r.person_id + (r.role ? '|' + r.role : '')] = r.state;
|
||||
});
|
||||
aufwand.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p){ p.aufwandProLP = p.aufwandProLP || {}; (p.aufwandProLP[r.lph] = p.aufwandProLP[r.lph] || {})[r.role] = r.value; }
|
||||
});
|
||||
aktuelleLp.forEach(r => {
|
||||
const p = projectsMap[projKey(r.week_label, r.project_id)];
|
||||
if(p){ p.aktuelleLP = p.aktuelleLP || []; if(!p.aktuelleLP.includes(r.lph)) p.aktuelleLP.push(r.lph); }
|
||||
});
|
||||
|
||||
const planMap = {};
|
||||
plans.forEach(r => { planMap[projKey(r.week_label, r.project_id)] = { start: r.start, end: r.end, progress: r.progress, people: [], todos: [], milestones: [] }; });
|
||||
planPeople.forEach(r => {
|
||||
const plan = planMap[projKey(r.week_label, r.project_id)];
|
||||
if(plan) plan.people.push(r.person_id);
|
||||
});
|
||||
|
||||
const todoMap = {};
|
||||
const todoAssigneeMap = {};
|
||||
const todoDepMap = {};
|
||||
todos.forEach(r => {
|
||||
const k = todoKey(r.week_label, r.project_id, r.todo_id);
|
||||
todoMap[k] = { id: r.todo_id, title: r.title, start: r.start, end: r.end, done: !!r.done, dependsOn: [], assignees: [], progress: r.progress };
|
||||
});
|
||||
todoAssignees.forEach(r => {
|
||||
const t = todoMap[todoKey(r.week_label, r.project_id, r.todo_id)];
|
||||
if(t) t.assignees.push(r.person_id);
|
||||
});
|
||||
todoDeps.forEach(r => {
|
||||
const t = todoMap[todoKey(r.week_label, r.project_id, r.todo_id)];
|
||||
if(t) t.dependsOn.push(r.depends_on_id);
|
||||
});
|
||||
|
||||
const msMap = {};
|
||||
const msDepMap = {};
|
||||
milestones.forEach(r => {
|
||||
const k = msKey(r.week_label, r.project_id, r.milestone_id);
|
||||
msMap[k] = { id: r.milestone_id, name: r.name, date: r.date, position: r.position, dependsOn: [] };
|
||||
});
|
||||
milestoneDeps.forEach(r => {
|
||||
const m = msMap[msKey(r.week_label, r.project_id, r.milestone_id)];
|
||||
if(m) m.dependsOn.push(r.depends_on_id);
|
||||
});
|
||||
|
||||
// TODO- und Meilenstein-Objekte in die Projekt-Pläne einsortieren
|
||||
Object.keys(todoMap).forEach(k => {
|
||||
const parts = k.split('|');
|
||||
const plan = planMap[department + '|' + parts[1] + '|' + parts[2]];
|
||||
if(plan) plan.todos.push(todoMap[k]);
|
||||
});
|
||||
Object.keys(msMap).forEach(k => {
|
||||
const parts = k.split('|');
|
||||
const plan = planMap[department + '|' + parts[1] + '|' + parts[2]];
|
||||
if(plan) plan.milestones.push(msMap[k]);
|
||||
});
|
||||
|
||||
// Wochen-Objekte zusammensetzen
|
||||
return weeks.map(w => {
|
||||
const wkey = weekKey(w.label);
|
||||
const people = (weekPeopleMap[wkey] || []).filter(Boolean).map(p => {
|
||||
const roles = (weekRolesMap[wkey] || {})[p.id] || [];
|
||||
const full = Object.assign({}, p);
|
||||
if(roles.length) full.role = roles;
|
||||
return full;
|
||||
});
|
||||
const att = {};
|
||||
(weekPeopleMap[wkey] || []).filter(Boolean).forEach(p => {
|
||||
const present = attendanceMap[wkey + '|' + p.id];
|
||||
if(present !== undefined) att[p.id] = present;
|
||||
});
|
||||
const weekProjects = Object.keys(projectsMap)
|
||||
.filter(k => k.startsWith(department + '|' + w.label + '|'))
|
||||
.map(k => {
|
||||
const p = projectsMap[k];
|
||||
const plan = planMap[k];
|
||||
if(plan) p.plan = plan;
|
||||
return p;
|
||||
})
|
||||
.sort((a,b) => a._arrayOrder - b._arrayOrder)
|
||||
.map(({ _arrayOrder, ...rest }) => rest);
|
||||
return {
|
||||
id: w.week_id, label: w.label, date: w.date,
|
||||
people, projects: weekProjects, attendance: att, locked: !!w.locked,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Migration: altes Ein-Dokument-Format -> relationales Schema ----------
|
||||
|
||||
// migrateLegacySchema: überführt eine bestehende Datenbank aus dem alten Format
|
||||
// (eine `weeks`-Tabelle mit JSON-`data`-Spalte sowie `people` mit JSON-`data`)
|
||||
// in das relationale Schema. Wird einmalig beim Start ausgeführt.
|
||||
function migrateLegacySchema(){
|
||||
if(!hasLegacyWeeksTable()) return;
|
||||
console.log('Migriere altes Wochen-Dokument-Format in relationales Schema …');
|
||||
|
||||
// Alte Wochen-Dokumente lesen
|
||||
const legacyWeeks = db.prepare('SELECT department, label, data FROM weeks').all();
|
||||
// Alte globale Personen-Dokumente lesen (eine JSON-Array-Zeile pro Abteilung)
|
||||
const legacyPeopleRows = [];
|
||||
const hasLegacyPeople = db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='people' AND sql LIKE '%data%'").get().c > 0;
|
||||
if(hasLegacyPeople){
|
||||
legacyPeopleRows.push(...db.prepare('SELECT department, data FROM people').all());
|
||||
}
|
||||
const tx = db.exec('BEGIN');
|
||||
try {
|
||||
// Alte Tabellen entfernen (Reihenfolge: erst Kind-Tabellen, dann Stamm)
|
||||
db.exec(`DROP TABLE IF EXISTS week_people_roles; DROP TABLE IF EXISTS week_people;
|
||||
DROP TABLE IF EXISTS attendance; DROP TABLE IF EXISTS project_lph;
|
||||
DROP TABLE IF EXISTS project_assign; DROP TABLE IF EXISTS project_aufwand;
|
||||
DROP TABLE IF EXISTS project_aktuelle_lp; DROP TABLE IF EXISTS project_plan;
|
||||
DROP TABLE IF EXISTS plan_people; DROP TABLE IF EXISTS plan_todos;
|
||||
DROP TABLE IF EXISTS plan_todo_assignees; DROP TABLE IF EXISTS plan_todo_deps;
|
||||
DROP TABLE IF EXISTS plan_milestones; DROP TABLE IF EXISTS plan_milestone_deps;
|
||||
DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS weeks;
|
||||
DROP TABLE IF EXISTS people; DROP TABLE IF EXISTS person_roles; DROP TABLE IF EXISTS person_absences;`);
|
||||
|
||||
// Neue Tabellen anlegen (Schema-DDL oben ausführen)
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
|
||||
department TEXT NOT NULL, label TEXT NOT NULL, week_id TEXT NOT NULL DEFAULT '',
|
||||
date TEXT NOT NULL DEFAULT '', locked INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, label)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '', full_name TEXT NOT NULL DEFAULT '',
|
||||
array_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS week_people_roles (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL, role_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS attendance (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
present INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS projects (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
nr TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT '',
|
||||
"group" TEXT NOT NULL DEFAULT '', array_order INTEGER NOT NULL DEFAULT 0,
|
||||
order_value INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_lph (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, value TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_assign (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT '', state INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aufwand (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, role TEXT NOT NULL DEFAULT '', value REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, lph, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_aktuelle_lp (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
lph TEXT NOT NULL, PRIMARY KEY (department, week_label, project_id, lph)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS project_plan (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
start TEXT NOT NULL DEFAULT '', end TEXT NOT NULL DEFAULT '', progress INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_people (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
person_id TEXT NOT NULL, array_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todos (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', start TEXT NOT NULL DEFAULT '',
|
||||
end TEXT NOT NULL DEFAULT '', done INTEGER NOT NULL DEFAULT 0, progress INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, week_label, project_id, todo_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_assignees (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, person_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_todo_deps (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
todo_id TEXT NOT NULL, depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, todo_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestones (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', date TEXT NOT NULL DEFAULT '',
|
||||
position TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS plan_milestone_deps (
|
||||
department TEXT NOT NULL, week_label TEXT NOT NULL, project_id TEXT NOT NULL,
|
||||
milestone_id TEXT NOT NULL, depends_on_id TEXT NOT NULL,
|
||||
PRIMARY KEY (department, week_label, project_id, milestone_id, depends_on_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS people (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, name TEXT NOT NULL DEFAULT '',
|
||||
full_name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, person_id)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_roles (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, role TEXT NOT NULL,
|
||||
role_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (department, person_id, role)
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS person_absences (
|
||||
department TEXT NOT NULL, person_id TEXT NOT NULL, absence TEXT NOT NULL,
|
||||
PRIMARY KEY (department, person_id, absence)
|
||||
)`);
|
||||
|
||||
// Alte Dokumente einlesen und relational speichern. Die Tabellen wurden
|
||||
// gerade neu angelegt und sind leer; `serializeWeek` genügt (kein eigener
|
||||
// Transaktions-Wrapper, da die äußere BEGIN/COMMIT der Migration aktiv ist).
|
||||
legacyWeeks.forEach(row => {
|
||||
let week;
|
||||
try { week = JSON.parse(row.data); } catch(e){ console.error('Kaputte Woche bei Migration:', row.label, e.message); return; }
|
||||
if(!week || !week.label) return;
|
||||
serializeWeek(row.department, week);
|
||||
});
|
||||
// Globale Personen aus dem alten Dokument-Format migrieren. serializeGlobalPeople
|
||||
// startet selbst eine Transaktion, daher werden die Zeilen hier direkt in die
|
||||
// neuen Tabellen eingefügt (serializeGlobalPeople ist in Transaktionen nicht
|
||||
// aufrufbar, weil BEGIN innerhalb BEGIN nicht erlaubt ist).
|
||||
legacyPeopleRows.forEach(row => {
|
||||
let people;
|
||||
try { people = JSON.parse(row.data); } catch(e){ console.error('Kaputte Personen-Liste bei Migration:', row.department, e.message); return; }
|
||||
if(!Array.isArray(people)) return;
|
||||
const insPerson = db.prepare(`INSERT INTO people (department, person_id, name, full_name, email, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
const insRole = db.prepare(`INSERT OR IGNORE INTO person_roles (department, person_id, role, role_order) VALUES (?, ?, ?, ?)`);
|
||||
const insAbsence = db.prepare(`INSERT OR IGNORE INTO person_absences (department, person_id, absence) VALUES (?, ?, ?)`);
|
||||
people.forEach((p, idx) => {
|
||||
insPerson.run(row.department, String(p.id || ''), String(p.name || ''), String(p.fullName || ''), String(p.email || ''), idx);
|
||||
(Array.isArray(p.role) ? p.role : []).forEach((r, ridx) => insRole.run(row.department, String(p.id || ''), String(r || ''), ridx));
|
||||
(Array.isArray(p.absences) ? p.absences : []).forEach(a => insAbsence.run(row.department, String(p.id || ''), String(a || '')));
|
||||
});
|
||||
});
|
||||
db.exec('COMMIT');
|
||||
console.log('Migration abgeschlossen (' + legacyWeeks.length + ' Wochen).');
|
||||
} catch(err){
|
||||
db.exec('ROLLBACK');
|
||||
console.error('Migration fehlgeschlagen:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Migration beim Serverstart ausführen
|
||||
migrateLegacySchema();
|
||||
|
||||
// Route: Speichert eine komplette Woche (als JSON-Objekt) für eine Abteilung
|
||||
app.post('/save-week', (req, res) => {
|
||||
const { department, week } = req.body || {};
|
||||
if(!week || typeof week.label !== 'string' || !week.label) return res.status(400).json({ ok: false, error: 'Missing week or label' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
saveWeekRelational(dept, week);
|
||||
console.log('Saved week:', dept + ' / ' + week.label);
|
||||
return res.json({ ok: true });
|
||||
} catch(err){
|
||||
console.error('Save week error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Speichert die globale Personenliste einer Abteilung (als JSON-Array)
|
||||
app.post('/save-people', (req, res) => {
|
||||
const { department, people } = req.body || {};
|
||||
if(!Array.isArray(people)) return res.status(400).json({ ok: false, error: 'Missing people array' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
serializeGlobalPeople(dept, people);
|
||||
console.log('Saved people:', dept, '(' + people.length + ' Personen)');
|
||||
return res.json({ ok: true });
|
||||
} catch(err){
|
||||
console.error('Save people error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Löscht eine Woche einer Abteilung (fehlende Woche wird toleriert)
|
||||
app.post('/delete-week', (req, res) => {
|
||||
const { department, label } = req.body || {};
|
||||
if(!label) return res.status(400).json({ ok: false, error: 'Missing label' });
|
||||
const dept = departmentKey(department);
|
||||
try {
|
||||
const row = db.prepare('SELECT 1 FROM weeks WHERE department = ? AND label = ?').get(dept, String(label));
|
||||
const existed = !!row;
|
||||
deleteWeekData(dept, String(label));
|
||||
console.log('Deleted week:', dept + ' / ' + label, existed ? '' : '(nicht vorhanden)');
|
||||
return res.json({ ok: true, deleted: existed });
|
||||
} catch(err){
|
||||
console.error('Delete week error', err);
|
||||
return res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Lädt alle Wochen plus globale Personenliste einer Abteilung.
|
||||
// Liefert strukturierte JSON-Objekte (aus den relationalen Tabellen
|
||||
// zusammengesetzt); nach Label sortiert.
|
||||
app.get('/load-all', (req, res) => {
|
||||
try {
|
||||
const dept = departmentKey(req.query.department);
|
||||
const weeks = loadWeeks(dept);
|
||||
const people = loadGlobalPeople(dept);
|
||||
res.json({ ok: true, weeks, people });
|
||||
} catch(err){
|
||||
console.error('Load error', err);
|
||||
res.status(500).json({ ok: false, error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Healthcheck-Endpunkt für Docker/Überwachung
|
||||
@@ -150,7 +819,7 @@ app.get('/:department/Projektplan/:projName', (req, res) => {
|
||||
});
|
||||
|
||||
// Serverstart mit Fehlerbehandlung (z.B. Port bereits belegt)
|
||||
const server = app.listen(PORT, ()=> console.log(`CSV-Session server running on http://localhost:${PORT}`));
|
||||
const server = app.listen(PORT, ()=> console.log(`Projektübersicht server running on http://localhost:${PORT}`));
|
||||
server.on('error', (err) => {
|
||||
console.error('Server konnte nicht starten:', err.message);
|
||||
if(err.code === 'EADDRINUSE'){
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -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,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,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,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&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 & 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,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&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 & 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,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,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,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,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;[];[]
|
||||
|
Reference in new issue
Block a user