von single Page hin zur mehreren Seiten geändert

This commit is contained in:
erik committed 2026-08-14 08:26:46 +02:00
1 parent 8c94fc91f6
commit 31c4bd47cd
36 files changed
+206 -96

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+33 -12
View File
@@ -30,7 +30,7 @@ user-facing text.
├── .vscode/extensions.json ├── .vscode/extensions.json
├── public/ ├── public/
│ ├── Test.html # main app (served at /) │ ├── Test.html # main app (served at /)
│ ├── js/ # main app modules (classic scripts, global fns) │ ├── js/ # shared modules (classic scripts, global fns)
│ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment │ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, weekSortKey-Quelle │ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, weekSortKey-Quelle
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme │ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
@@ -39,15 +39,19 @@ user-facing text.
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl │ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed │ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung │ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung
│ │ ├── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung
│ │ ├── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten
│ │ ├── projects.js # Projekte-Tab-Rendering + Drag-and-Drop
│ │ ├── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal
│ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared │ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared
│ │ ├── showmode.js # Show-Mode (Präsentationsmodus) │ │ ├── showmode.js # Show-Mode (Präsentationsmodus)
│ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz) │ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz)
│ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer) │ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer)
│ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html) │ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html)
│ │ ├── board/ # tab-specific: Projektboard
│ │ │ └── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung
│ │ ├── people/ # tab-specific: Projektbeteiligte
│ │ │ └── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten
│ │ ├── projects/ # tab-specific: Projekte
│ │ │ └── projects.js # Projekte-Tab-Rendering + Drag-and-Drop
│ │ ├── load/ # tab-specific: Auslastung
│ │ │ └── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal
│ ├── project-plan.html # Gantt/TODO planning view │ ├── project-plan.html # Gantt/TODO planning view
│ ├── js/plan/ # planning modules (nur von project-plan.html geladen) │ ├── js/plan/ # planning modules (nur von project-plan.html geladen)
│ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts │ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts
@@ -79,6 +83,16 @@ docker compose up --build # Docker variant (compose file is at repo root)
## Server API (`server.js`) ## Server API (`server.js`)
- `GET /` → `Test.html`; `GET /health`. - `GET /` → `Test.html`; `GET /health`.
- Deep links per department + tab: `GET /<Dept>/<Tab>` → `Test.html`, e.g.
`/HLS/Auslastung` or `/ELT/Projektübersicht`. Tab slugs (`TAB_SLUGS` in
`js/globals.js`) are `Projektübersicht|Auslastung|Projekte|Projektbeteiligte`;
anything else returns 404. Exception: `GET /<Dept>/Projektplan(/<ProjName>)?`
(`PLAN_SLUG`) serves `project-plan.html`, the GANTT view, with
`weekLabel|projNr` as query params (the project name is a readable path
segment only; identification happens via weekLabel + projNr). The frontend
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with - `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with
`_people.csv` content in `people`; sessions sorted by filename. `_people.csv` content in `people`; sessions sorted by filename.
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV. - `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV.
@@ -112,8 +126,9 @@ docker compose up --build # Docker variant (compose file is at repo root)
them via `<script src>` tags; load order in the HTML defines dependencies, them via `<script src>` tags; load order in the HTML defines dependencies,
since there is no bundler: since there is no bundler:
- `Test.html` loads `globals → utils → ui → date-picker → csv → server → - `Test.html` loads `globals → utils → ui → date-picker → csv → server →
store → weeks → board → people → projects → load → gantt → showmode → store → weeks` (shared modules), then the tab-specific folders `board →
tabs-auth → pdf → init`. See the exact list in `Test.html`. 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 → csv →
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`). touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
@@ -122,8 +137,10 @@ docker compose up --build # Docker variant (compose file is at repo root)
`db`/`currentDepartment` live in `js/globals.js`. `db`/`currentDepartment` live in `js/globals.js`.
- State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only). - State: `let db = { weeks, currentWeek, globalPeople }` (in-memory only).
- Project IDs are regenerated on every CSV load, so projects are addressed - Project IDs are regenerated on every CSV load, so projects are addressed
cross-page by `department` + week `label` + `projNr` + `projName` via URL cross-page by `department` + week `label` + `projNr` (+ readable projName path
params (`openProjectPlan`, `findProjectForPlan`). segment) via URL params: `openProjectPlan()` builds
`/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…`, `findProjectForPlan`
resolves them.
- Person IDs are stable across reloads via `personIdForName(name)` → - Person IDs are stable across reloads via `personIdForName(name)` →
`p_<lowercased alnum>`. `p_<lowercased alnum>`.
- `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus - `save()` debounces 500 ms then `syncAllToServer()` writes **all** weeks plus
@@ -135,8 +152,12 @@ docker compose up --build # Docker variant (compose file is at repo root)
- `applyAutoLock()` locks every week except the chronologically last one after - `applyAutoLock()` locks every week except the chronologically last one after
load/clone/new-week; `localStorage` UI state is not restored for locked tabs. load/clone/new-week; `localStorage` UI state is not restored for locked tabs.
- `localStorage` keys: `projuebersicht_v1` (department only — tab is never - `localStorage` keys: `projuebersicht_v1` (department only — tab is never
restored), `projuebersicht-theme`, header-collapse key. Do **not** store data restored from storage; at startup the tab comes from the deep-link URL if
in `localStorage`; it is UI state only. present, otherwise `board`), `projuebersicht-theme`, header-collapse key. Do
**not** store data in `localStorage`; it is UI state only.
- Deep-link URLs (`/<Dept>/<Tab>`) are shared via the address bar:
`updateRouteUrl()` rewrites the path via `history.replaceState` on tab and
department switches; protected tabs keep the URL but open the password modal.
## Auth / security quirk ## Auth / security quirk
@@ -147,7 +168,7 @@ docker compose up --build # Docker variant (compose file is at repo root)
loaded from `cdnjs.cloudflare.com` in `Test.html`, so those features need loaded from `cdnjs.cloudflare.com` in `Test.html`, so those features need
internet access and won't work offline. `project-plan.html` loads neither. internet access and won't work offline. `project-plan.html` loads neither.
- User-provided status HTML is sanitized on render via a small whitelist - User-provided status HTML is sanitized on render via a small whitelist
(`sanitizeStatusHtml` in `js/board.js`) to mitigate XSS. (`sanitizeStatusHtml` in `js/board/board.js`) to mitigate XSS.
## Docker ## Docker
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "projektuebersicht", "name": "projektuebersicht",
"version": "1.0.0", "version": "1.0.0",
"description": "Single-page App zur Verfolgung von TGA-Projekten", "description": "App zur Verfolgung von TGA-Projekten",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js" "start": "node server.js"
BIN
View File
Binary file not shown.
+24 -22
View File
@@ -5,9 +5,11 @@
und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im
seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet
styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js, styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js,
js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, js/board.js, js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, danach die
js/people.js, js/projects.js, js/load.js, js/gantt.js, js/showmode.js, js/tabs-auth.js, tab-spezifischen Module js/board/board.js, js/people/people.js,
js/pdf.js und js/init.js; crypto-js und pdfmake kommen von einem CDN. Über openProjectPlan() 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
CDN. Über openProjectPlan()
aus js/gantt.js öffnet diese Seite die Projektplan-Ansicht project-plan.html in einem aus js/gantt.js öffnet diese Seite die Projektplan-Ansicht project-plan.html in einem
neuen Tab, die nur die gemeinsamen Module plus die js/plan/*-Module lädt. neuen Tab, die nur die gemeinsamen Module plus die js/plan/*-Module lädt.
--> -->
@@ -16,7 +18,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projektübersicht Elektro</title> <title>Projektübersicht Elektro</title>
<link rel="stylesheet" href="styles.css?v=3"> <link rel="stylesheet" href="/styles.css?v=3">
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.10/pdfmake.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.10/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.10/vfs_fonts.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.10/vfs_fonts.js"></script>
@@ -36,7 +38,7 @@
<header> <header>
<div class="header-bar"> <div class="header-bar">
<h1 id="pageTitle">Projektübersicht Elektro</h1> <h1 id="pageTitle">Projektübersicht Elektro</h1>
<img src="dein_bild.ico" alt="Logo" class="header-logo"> <img src="/dein_bild.ico" alt="Logo" class="header-logo">
</div> </div>
</header> </header>
@@ -221,22 +223,22 @@
</div> </div>
</div> </div>
<script src="js/globals.js?v=5"></script> <script src="/js/globals.js?v=5"></script>
<script src="js/utils.js?v=5"></script> <script src="/js/utils.js?v=5"></script>
<script src="js/ui.js?v=5"></script> <script src="/js/ui.js?v=5"></script>
<script src="js/date-picker.js?v=5"></script> <script src="/js/date-picker.js?v=5"></script>
<script src="js/csv.js?v=5"></script> <script src="/js/csv.js?v=5"></script>
<script src="js/server.js?v=5"></script> <script src="/js/server.js?v=5"></script>
<script src="js/store.js?v=5"></script> <script src="/js/store.js?v=5"></script>
<script src="js/weeks.js?v=5"></script> <script src="/js/weeks.js?v=5"></script>
<script src="js/board.js?v=5"></script> <script src="/js/board/board.js?v=6"></script>
<script src="js/people.js?v=5"></script> <script src="/js/people/people.js?v=6"></script>
<script src="js/projects.js?v=5"></script> <script src="/js/projects/projects.js?v=6"></script>
<script src="js/load.js?v=5"></script> <script src="/js/load/load.js?v=6"></script>
<script src="js/gantt.js?v=5"></script> <script src="/js/gantt.js?v=5"></script>
<script src="js/showmode.js?v=5"></script> <script src="/js/showmode.js?v=5"></script>
<script src="js/tabs-auth.js?v=5"></script> <script src="/js/tabs-auth.js?v=5"></script>
<script src="js/pdf.js?v=5"></script> <script src="/js/pdf.js?v=5"></script>
<script src="js/init.js?v=5"></script> <script src="/js/init.js?v=5"></script>
</body> </body>
</html> </html>
@@ -4,10 +4,10 @@
// Diese Datei rendert das Projektboard (Tab "Projektboard" in `Test.html`), // Diese Datei rendert das Projektboard (Tab "Projektboard" in `Test.html`),
// die zentrale Tabelle mit Projekten, Status-Kommentaren, Personen-Spalten // die zentrale Tabelle mit Projekten, Status-Kommentaren, Personen-Spalten
// (Anwesenheit/Zuordnung) und Leistungsphasen in Prozent. Sie wird von // (Anwesenheit/Zuordnung) und Leistungsphasen in Prozent. Sie wird von
// `Test.html` nach `js/weeks.js` (vor `js/people.js`) geladen und stellt // `Test.html` nach `js/weeks.js` (vor `js/people/people.js`) geladen und stellt
// globale Funktionen wie `renderBoard()`, `statusFocus()`, `cycleAssign()`, // globale Funktionen wie `renderBoard()`, `statusFocus()`, `cycleAssign()`,
// `editLph()` und `delProj()` bereit. Sie nutzt `currentWeek()`/ // `editLph()` und `delProj()` bereit. Sie nutzt `currentWeek()`/
// `isWeekLocked()` aus `js/weeks.js`, `lpActiveList()` aus `js/load.js`, // `isWeekLocked()` aus `js/weeks.js`, `lpActiveList()` aus `js/load/load.js`,
// `assignKey()` aus `js/utils.js` und `openProjectPlan()` aus `js/gantt.js`. // `assignKey()` aus `js/utils.js` und `openProjectPlan()` aus `js/gantt.js`.
// currentStatusEditor: aktuell fokussiertes contenteditable-Element des // currentStatusEditor: aktuell fokussiertes contenteditable-Element des
+1 -1
View File
@@ -9,7 +9,7 @@
// und stellt den Kalender-Picker (Einzeldatum "openDatePicker" und Bereich // und stellt den Kalender-Picker (Einzeldatum "openDatePicker" und Bereich
// "openRangePicker") bereit. Der Zustand des offenen Popups liegt in der // "openRangePicker") bereit. Der Zustand des offenen Popups liegt in der
// Modulvariablen rangePickerState. Genutzt wird der Picker v. a. von // Modulvariablen rangePickerState. Genutzt wird der Picker v. a. von
// js/load.js (LP-Bereiche), js/people.js (Abwesenheiten) und den // js/load/load.js (LP-Bereiche), js/people/people.js (Abwesenheiten) und den
// Plan-Modulen unter js/plan/* (z. B. GANTT-Datumsangaben in plan-modals.js). // Plan-Modulen unter js/plan/* (z. B. GANTT-Datumsangaben in plan-modals.js).
// rangePickerState: Zustand des aktuell geöffneten Picker-Popups (Optionen, // rangePickerState: Zustand des aktuell geöffneten Picker-Popups (Optionen,
+10 -9
View File
@@ -2,9 +2,9 @@
// Projektübersicht — GANTT-Diagramm (gemeinsam genutzt) // Projektübersicht — GANTT-Diagramm (gemeinsam genutzt)
// ----------------------------- // -----------------------------
// Gemeinsames Modul für das GANTT-Diagramm: Es wird von Test.html (nach // Gemeinsames Modul für das GANTT-Diagramm: Es wird von Test.html (nach
// js/load.js) UND von project-plan.html geladen – dort zusammen mit globals, // js/load/load.js) UND von project-plan.html geladen – dort zusammen mit globals,
// utils, ui, date-picker, csv, server und den js/plan/*-Modulen, aber OHNE // utils, ui, date-picker, csv, server und den js/plan/*-Modulen, aber OHNE
// js/store.js, js/weeks.js, js/board.js usw. Es darf daher auf der Plan-Seite // js/store.js, js/weeks.js, js/board/board.js usw. Es darf daher auf der Plan-Seite
// keine Hauptseiten-spezifischen Funktionen voraussetzen; Funktionen, die // keine Hauptseiten-spezifischen Funktionen voraussetzen; Funktionen, die
// currentWeek()/isWeekLocked() aus js/weeks.js brauchen (renderGantt, // currentWeek()/isWeekLocked() aus js/weeks.js brauchen (renderGantt,
// fitGanttRange, openProjectPlan), werden nur vom Hauptfenster aufgerufen. // fitGanttRange, openProjectPlan), werden nur vom Hauptfenster aufgerufen.
@@ -328,19 +328,20 @@ function buildGanttHtml(projects, start, end, options = {}){
// ---------- Projektplan-Navigation ---------- // ---------- Projektplan-Navigation ----------
// openProjectPlan: öffnet project-plan.html in einem neuen Tab. Weil // openProjectPlan: öffnet project-plan.html in einem neuen Tab. Weil
// Projekt-IDs nicht persistent sind, werden department, Wochen-Label, Proj.-Nr. // Projekt-IDs nicht persistent sind, werden Wochen-Label und Proj.-Nr. als
// und -Name als URL-Parameter übergeben. Parameter: pid = Projekt-ID der // URL-Parameter übergeben; Abteilung und Projektname stehen im Pfad nach dem
// aktuellen Woche. Kein Rückgabewert. // Schema "/<Abteilung>/Projektplan/<Projektname>". Parameter: pid =
// Projekt-ID der aktuellen Woche. Kein Rückgabewert.
function openProjectPlan(pid){ function openProjectPlan(pid){
const w = currentWeek(); if(!w) return; const w = currentWeek(); if(!w) return;
const p = w.projects.find(x => x.id === pid); if(!p) return; const p = w.projects.find(x => x.id === pid); if(!p) return;
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt). // IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt).
// Daher werden stabile Werte (Label, Projekt-Nr., Name) in der URL übergeben. // Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
const url = new URL('project-plan.html', window.location.href); // Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der
url.searchParams.set('department', currentDepartment); // Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert.
const url = new URL('/' + currentDepartment + '/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin);
url.searchParams.set('weekLabel', w.label); url.searchParams.set('weekLabel', w.label);
url.searchParams.set('projNr', p.nr || ''); url.searchParams.set('projNr', p.nr || '');
url.searchParams.set('projName', p.name || '');
window.open(url.toString(), '_blank'); window.open(url.toString(), '_blank');
} }
+16 -1
View File
@@ -5,7 +5,7 @@
// legt alle zentralen Konstanten (Leistungsphasen, Rollen, Gruppen, Abteilungen, // legt alle zentralen Konstanten (Leistungsphasen, Rollen, Gruppen, Abteilungen,
// Farben, Passwort-Hash) sowie den globalen In-Memory-Zustand (db, // Farben, Passwort-Hash) sowie den globalen In-Memory-Zustand (db,
// currentDepartment, currentTab) fest. Sie stellt keine Funktionen, sondern nur // currentDepartment, currentTab) fest. Sie stellt keine Funktionen, sondern nur
// Daten bereit, auf die alle weiteren Module (js/store.js, js/board.js, // Daten bereit, auf die alle weiteren Module (js/store.js, js/board/board.js,
// js/gantt.js, js/plan/*, ...) zugreifen. Auf dem Plan-Gantt lädt zusätzlich // js/gantt.js, js/plan/*, ...) zugreifen. Auf dem Plan-Gantt lädt zusätzlich
// js/plan/plan-state.js Zustand für die Planungsseite. // js/plan/plan-state.js Zustand für die Planungsseite.
@@ -47,3 +47,18 @@ const DEPARTMENTS = {
HLS: { label: 'HLS', icon: '' }, HLS: { label: 'HLS', icon: '' },
ARCH: { label: 'Architektur', icon: '' } ARCH: { label: 'Architektur', icon: '' }
}; };
// TAB_SLUGS / TAB_BY_SLUG: sprechende URL-Abschnitte für die Tabs, damit jede
// Abteilungs-/Tab-Kombination eine eigene Seite hat (z. B. "/HLS/Auslastung").
// Wird von js/ui.js (applyUrlRoute/updateRouteUrl) und dem Server genutzt.
const TAB_SLUGS = {
board: 'Projektübersicht',
load: 'Auslastung',
projects:'Projekte',
people: 'Projektbeteiligte'
};
const TAB_BY_SLUG = Object.fromEntries(Object.entries(TAB_SLUGS).map(([k,v]) => [v,k]));
// PLAN_SLUG: URL-Schlüssel für die Projektplan-Seite (project-plan.html), im
// gleichen Deep-Link-Schema wie die Tabs: "/<Abteilung>/Projektplan".
const PLAN_SLUG = 'Projektplan';
+12 -7
View File
@@ -8,8 +8,8 @@
// initUiState() und syncRangeDisplay() bereit und nutzt loadUiState()/ // initUiState() und syncRangeDisplay() bereit und nutzt loadUiState()/
// loadTheme()/loadHeaderState() aus js/ui.js, switchDepartment() aus // loadTheme()/loadHeaderState() aus js/ui.js, switchDepartment() aus
// js/store.js, showTab() aus js/tabs-auth.js sowie die Render-Funktionen // js/store.js, showTab() aus js/tabs-auth.js sowie die Render-Funktionen
// renderBoard()/renderLoad()/renderProjects()/renderPeople() aus js/board.js, // renderBoard()/renderLoad()/renderProjects()/renderPeople() aus js/board/board.js,
// js/load.js, js/projects.js, js/people.js. // js/load/load.js, js/projects/projects.js, js/people/people.js.
// renderAll: Convenience um UI-Elemente zu initialisieren // renderAll: Convenience um UI-Elemente zu initialisieren
function renderAll(){ function renderAll(){
@@ -24,10 +24,16 @@ function renderAll(){
// initUiState: stellt beim Start den UI-Zustand her (LocalStorage-UI-State, // initUiState: stellt beim Start den UI-Zustand her (LocalStorage-UI-State,
// Theme, Header-Status), setzt Titel und aktive Abteilungs-Schaltfläche und // Theme, Header-Status), setzt Titel und aktive Abteilungs-Schaltfläche und
// zeigt den ersten Tab an – passwortgeschützte Tabs ('projects'/'people') // zeigt den ersten Tab an. Eine Deep-Link-URL (z. B. "/HLS/Auslastung") aus
// werden beim Start nie direkt aktiviert. Kein Rückgabewert. // applyUrlRoute() übersteuert dabei Abteilung und Tab; passwortgeschützte Tabs
// öffnen das Passwort-Modal (showTab ohne skipAuth). Kein Rückgabewert.
function initUiState(){ function initUiState(){
loadUiState(); loadUiState();
const route = applyUrlRoute();
if(route && DEPARTMENTS[route.department]){
currentDepartment = route.department;
currentTab = route.tab;
}
loadTheme(); loadTheme();
loadHeaderState(); loadHeaderState();
document.querySelectorAll('.dept-tab').forEach(btn => { document.querySelectorAll('.dept-tab').forEach(btn => {
@@ -39,9 +45,8 @@ function initUiState(){
if(titleEl) titleEl.textContent = info.icon + ' Projektübersicht ' + info.label; if(titleEl) titleEl.textContent = info.icon + ' Projektübersicht ' + info.label;
const tab = ['board','load','projects','people'].includes(currentTab) ? currentTab : DEFAULT_TAB; const tab = ['board','load','projects','people'].includes(currentTab) ? currentTab : DEFAULT_TAB;
// Beim initialen Start niemals einen passwortgeschützten Tab direkt anzeigen. // Geschützte Tabs ('projects'/'people') verlangen hier ihr Passwort.
const safeTab = (tab === 'projects' || tab === 'people') ? DEFAULT_TAB : tab; showTab(tab);
showTab(safeTab, true);
} }
// syncRangeDisplay: schreibt den aus zwei Datumsfeldern (startId/endId) // syncRangeDisplay: schreibt den aus zwei Datumsfeldern (startId/endId)
+1 -1
View File
@@ -6,7 +6,7 @@
// people → projects → load → gantt → …). Es stellt renderLoad(), das LP- // people → projects → load → gantt → …). Es stellt renderLoad(), das LP-
// Aufwand-Modal und das Neues-Projekt-Modal bereit. Genutzt werden LPHS/ROLES // Aufwand-Modal und das Neues-Projekt-Modal bereit. Genutzt werden LPHS/ROLES
// aus js/globals.js, currentWeek()/save() aus js/store.js, guardWeekLocked() // aus js/globals.js, currentWeek()/save() aus js/store.js, guardWeekLocked()
// aus js/weeks.js sowie peopleByRole()/assignKey() aus js/board.js/js/utils.js. // aus js/weeks.js sowie peopleByRole()/assignKey() aus js/board/board.js/js/utils.js.
// Die Auslastungs-Logik (berechneAuslastung, lpActiveList, faktorFormat, // Die Auslastungs-Logik (berechneAuslastung, lpActiveList, faktorFormat,
// pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den // pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den
// PDF-Export wiederverwendet. // PDF-Export wiederverwendet.
+2 -2
View File
@@ -8,10 +8,10 @@
// vor js/init.js); exportPDF() hängt am Export-Button von Test.html und nutzt // vor js/init.js); exportPDF() hängt am Export-Button von Test.html und nutzt
// die per CDN eingebundene Bibliothek pdfmake (offline nicht verfügbar). Es // die per CDN eingebundene Bibliothek pdfmake (offline nicht verfügbar). Es
// liest die aktuelle Woche über currentWeek() aus js/store.js, Personen über // liest die aktuelle Woche über currentWeek() aus js/store.js, Personen über
// peopleByRole() aus js/board.js, Helfer wie assignKey()/sortGroups()/ // peopleByRole() aus js/board/board.js, Helfer wie assignKey()/sortGroups()/
// formatWeekDate() aus js/utils.js sowie die Auslastungs-Berechnung // formatWeekDate() aus js/utils.js sowie die Auslastungs-Berechnung
// (berechneAuslastung, lpActiveList, faktorFormat, pdfLoadAmpelColor, // (berechneAuslastung, lpActiveList, faktorFormat, pdfLoadAmpelColor,
// LOAD_BAR_MAX) aus js/load.js. Das Modul stellt ausschließlich exportPDF() // LOAD_BAR_MAX) aus js/load/load.js. Das Modul stellt ausschließlich exportPDF()
// bereit. // bereit.
// Farbkonstanten des PDF-Layouts: Rollen-Hintergründe, neutrale bzw. // Farbkonstanten des PDF-Layouts: Rollen-Hintergründe, neutrale bzw.
@@ -4,12 +4,12 @@
// Diese Datei rendert den Personen-Tab (Tab "Projektbeteiligte" in `Test.html`) // Diese Datei rendert den Personen-Tab (Tab "Projektbeteiligte" in `Test.html`)
// und verwaltet die globale Personenliste pro Abteilung: Anlegen, Bearbeiten, // und verwaltet die globale Personenliste pro Abteilung: Anlegen, Bearbeiten,
// Löschen, Rollen-Auswahl, Drag-and-Drop-Sortierung sowie Urlaub/Abwesenheiten. // Löschen, Rollen-Auswahl, Drag-and-Drop-Sortierung sowie Urlaub/Abwesenheiten.
// Sie wird von `Test.html` nach `js/board.js` (vor `js/projects.js`) geladen // Sie wird von `Test.html` nach `js/board/board.js` (vor `js/projects/projects.js`) geladen
// und stellt globale Funktionen wie `addPerson()`, `delPerson()`, // und stellt globale Funktionen wie `addPerson()`, `delPerson()`,
// `editPerson()`, `renderPeople()` und `addAbsence()` bereit. Genutzt wird sie // `editPerson()`, `renderPeople()` und `addAbsence()` bereit. Genutzt wird sie
// u.a. von `js/board.js` (`renderBoard`), `js/gantt.js` und // u.a. von `js/board/board.js` (`renderBoard`), `js/gantt.js` und
// `js/plan/plan-modals.js`; die Sortier-Hilfe `getDragAfterElement()` liegt in // `js/plan/plan-modals.js`; die Sortier-Hilfe `getDragAfterElement()` liegt in
// `js/projects.js`. // `js/projects/projects.js`.
// addPerson: fügt eine neue Person der globalen Liste hinzu und // addPerson: fügt eine neue Person der globalen Liste hinzu und
// synchronisiert sie in die aktuelle Sitzung. // synchronisiert sie in die aktuelle Sitzung.
+9 -4
View File
@@ -3,8 +3,9 @@
// ----------------------------- // -----------------------------
// Dieses Modul wird als erstes der plan-*-Module von public/project-plan.html // Dieses Modul wird als erstes der plan-*-Module von public/project-plan.html
// geladen und definiert window.PlanApp als gemeinsamen Zustand aller // geladen und definiert window.PlanApp als gemeinsamen Zustand aller
// Projektplan-Module. Es liest die URL-Parameter (department, weekLabel, // Projektplan-Module. Es liest die URL-Route (Abteilung + Projektname, z. B.
// projNr, projName), über die das zu bearbeitende Projekt adressiert wird. // "/HLS/Projektplan/Gymnasium%20Hemer") und die URL-Parameter (weekLabel,
// projNr), über die das zu bearbeitende Projekt adressiert wird.
// Nach DOMContentLoaded werden Theme, Toolbar-Buttons und die Sitzungsdaten // Nach DOMContentLoaded werden Theme, Toolbar-Buttons und die Sitzungsdaten
// über die Server-API geladen. Die hier definierten Funktionen (savePlan, // über die Server-API geladen. Die hier definierten Funktionen (savePlan,
// isLocked, showToast u. a.) werden über app.* den anderen plan-Modulen // isLocked, showToast u. a.) werden über app.* den anderen plan-Modulen
@@ -33,10 +34,14 @@ window.PlanApp = {
(function(){ (function(){
const app = window.PlanApp; const app = window.PlanApp;
app.department = app.params.get('department') || 'ELT'; // Abteilung und Projektname aus dem Deep-Link-Pfad
// "/<Abteilung>/Projektplan/<Projektname>" lesen. Fallback auf die früheren
// Query-Parameter ?department=/&projName= für alte project-plan.html-Links.
const routeMatch = window.location.pathname.match(/^\/([^/]+)\/(?:Projektplan|project-plan\.html)(?:\/(.+))?\/?$/);
app.department = routeMatch ? routeMatch[1] : (app.params.get('department') || 'ELT');
app.weekLabel = app.params.get('weekLabel'); app.weekLabel = app.params.get('weekLabel');
app.projNr = app.params.get('projNr') || ''; app.projNr = app.params.get('projNr') || '';
app.projName = app.params.get('projName') || ''; app.projName = routeMatch && routeMatch[2] ? decodeURIComponent(routeMatch[2]) : (app.params.get('projName') || '');
// init: Einstiegspunkt nach DOMContentLoaded. Übernimmt das Theme aus // init: Einstiegspunkt nach DOMContentLoaded. Übernimmt das Theme aus
// localStorage/System, prüft die URL-Parameter und bricht mit Fehlermeldung // localStorage/System, prüft die URL-Parameter und bricht mit Fehlermeldung
@@ -4,12 +4,12 @@
// Diese Datei rendert den Projekte-Tab (Tab "Projekte" in `Test.html`) als // Diese Datei rendert den Projekte-Tab (Tab "Projekte" in `Test.html`) als
// sortierbare Listenansicht: Projekte werden nach Gruppe ("Aktive Projekte", // sortierbare Listenansicht: Projekte werden nach Gruppe ("Aktive Projekte",
// "Kleine Projekte" usw.) gruppiert, inline bearbeitbar und per // "Kleine Projekte" usw.) gruppiert, inline bearbeitbar und per
// Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people.js` // Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people/people.js`
// (vor `js/load.js`) geladen und stellt globale Funktionen wie // (vor `js/load/load.js`) geladen und stellt globale Funktionen wie
// `renderProjects()`, `editProjectField()`, `editProjectActiveLP()` sowie die // `renderProjects()`, `editProjectField()`, `editProjectActiveLP()` sowie die
// Drag-and-Drop-Handler und die Sortier-Hilfe `getDragAfterElement()` bereit. // Drag-and-Drop-Handler und die Sortier-Hilfe `getDragAfterElement()` bereit.
// Letztere wird auch von `js/people.js` (Personen-Sortierung) genutzt; // Letztere wird auch von `js/people/people.js` (Personen-Sortierung) genutzt;
// `renderBoard()` aus `js/board.js` wird nach Änderungen aktualisiert. // `renderBoard()` aus `js/board/board.js` wird nach Änderungen aktualisiert.
// delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der // delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der
// Listen-Ansicht. Parameter `id`: Projekt-ID. Keine Rückgabe; bei gesperrter // Listen-Ansicht. Parameter `id`: Projekt-ID. Keine Rückgabe; bei gesperrter
@@ -133,7 +133,7 @@ function handleDragEnd(event){
// gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem // gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem
// Element mit der kleinsten negativen Distanz zur Mitte). Rückgabe: // Element mit der kleinsten negativen Distanz zur Mitte). Rückgabe:
// Ziel-Element oder undefined (→ ans Ende anfügen). Wird auch von // Ziel-Element oder undefined (→ ans Ende anfügen). Wird auch von
// `js/people.js` für die Personen-Sortierung verwendet. // `js/people/people.js` für die Personen-Sortierung verwendet.
function getDragAfterElement(container, y){ function getDragAfterElement(container, y){
const draggableElements = [...container.querySelectorAll('.person-list-item:not([style*="opacity"])')]; const draggableElements = [...container.querySelectorAll('.person-list-item:not([style*="opacity"])')];
return draggableElements.reduce((closest, child) => { return draggableElements.reduce((closest, child) => {
+1 -1
View File
@@ -7,7 +7,7 @@
// und Auslastung) und zeigt sie in einem Vollbild-Overlay. Genutzt werden // und Auslastung) und zeigt sie in einem Vollbild-Overlay. Genutzt werden
// showTab() aus js/tabs-auth.js, switchDepartment() aus js/store.js, // showTab() aus js/tabs-auth.js, switchDepartment() aus js/store.js,
// setHeaderCollapsed() aus js/ui.js sowie renderBoard()/renderLoad() aus // setHeaderCollapsed() aus js/ui.js sowie renderBoard()/renderLoad() aus
// js/board.js/js/load.js. Der gesamte Lauf-Zustand liegt in den // js/board/board.js/js/load/load.js. Der gesamte Lauf-Zustand liegt in den
// Modulvariablen am Dateikopf. // Modulvariablen am Dateikopf.
// showModeActive: true, solange der Show Mode läuft // showModeActive: true, solange der Show Mode läuft
+6 -5
View File
@@ -7,7 +7,7 @@
// Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt // Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`, // die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`, // `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
// `js/board.js`, `js/people.js`, `js/projects.js` und `js/load.js`; das Speichern // `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced. // ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server. // save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
@@ -62,6 +62,7 @@ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return; if(!DEPARTMENTS[dept]) return;
currentDepartment = dept; currentDepartment = dept;
saveUiState(); saveUiState();
updateRouteUrl(dept, currentTab);
// UI-Status der Abteilungs-Tabs aktualisieren // UI-Status der Abteilungs-Tabs aktualisieren
document.querySelectorAll('.dept-tab').forEach(btn => { document.querySelectorAll('.dept-tab').forEach(btn => {
@@ -118,11 +119,11 @@ function load(){
} }
console.log(`Server-Load erfolgreich: ${db.weeks.length} Sitzung(en).`); console.log(`Server-Load erfolgreich: ${db.weeks.length} Sitzung(en).`);
} }
// Geschützte Tabs dürfen nicht ohne Passwort angezeigt werden, auch wenn // Geschützte Tabs ('projects'/'people') dürfen nur mit Passwort angezeigt
// sie im UI-State gespeichert waren. // werden. showTab() öffnet dafür automatisch das Passwort-Modal – auch
if(currentTab === 'projects' || currentTab === 'people') currentTab = DEFAULT_TAB; // wenn der Tab per Deep-Link-URL (z. B. "/ELT/Projekte") gewählt wurde.
renderAll(); renderAll();
showTab(currentTab, true); showTab(currentTab);
}) })
.catch(err => { .catch(err => {
console.error('Laden vom Server fehlgeschlagen:', err); console.error('Laden vom Server fehlgeschlagen:', err);
+3 -2
View File
@@ -9,8 +9,8 @@
// ausstehende Aktion wird in requestedAction (js/globals.js) gemerkt – davon // ausstehende Aktion wird in requestedAction (js/globals.js) gemerkt – davon
// profitieren auch die Passwort-Gates in js/weeks.js (Löschen/Entsperren von // profitieren auch die Passwort-Gates in js/weeks.js (Löschen/Entsperren von
// Sitzungen). Gerendert werden beim Tab-Wechsel renderBoard()/renderLoad()/ // Sitzungen). Gerendert werden beim Tab-Wechsel renderBoard()/renderLoad()/
// renderProjects()/renderPeople() aus js/board.js, js/load.js, js/projects.js, // renderProjects()/renderPeople() aus js/board/board.js, js/load/load.js, js/projects/projects.js,
// js/people.js. // js/people/people.js.
// showTab: wechselt zwischen Ansichten; für sensible Tabs wird ein Passwort verlangt // showTab: wechselt zwischen Ansichten; für sensible Tabs wird ein Passwort verlangt
function showTab(t, skipAuth = false){ function showTab(t, skipAuth = false){
@@ -24,6 +24,7 @@ function showTab(t, skipAuth = false){
} }
currentTab = t; currentTab = t;
saveUiState(); saveUiState();
updateRouteUrl(currentDepartment, t);
['board','projects','load','people'].forEach(x=>{ ['board','projects','load','people'].forEach(x=>{
const el = document.getElementById(x); const el = document.getElementById(x);
+30
View File
@@ -33,6 +33,36 @@ function loadUiState(){
} }
} }
// ---------- Deep-Link-Routen (Abteilung + Tab als URL) ----------
// applyUrlRoute: wertet einen Deep-Link "/<Abteilung>/<Tab>" aus der URL aus,
// z. B. "/HLS/Auslastung" oder "/ELT/Projektübersicht". Gültige Routen liefern
// { department, tab }, alles andere (auch file://-Pfade) liefert null.
function applyUrlRoute(){
try {
const m = window.location.pathname.match(/^\/([^/]+)\/([^/]+)\/?$/);
if(!m) return null;
const dept = m[1];
const tab = TAB_BY_SLUG[decodeURIComponent(m[2])];
if(!DEPARTMENTS[dept] || !tab) return null;
return { department: dept, tab };
} catch (err) {
return null;
}
}
// updateRouteUrl: schreibt Abteilung + aktiven Tab als sprechende URL in die
// Adresszeile (z. B. "/HLS/Auslastung"), ohne die Seite neu zu laden. Nur über
// HTTP (nicht per file://) und nur bei bekannter Abteilung/Tab. Keine Rückgabe.
function updateRouteUrl(dept, tab){
try {
if(!window.location.protocol || window.location.protocol.indexOf('http') !== 0) return;
const slug = TAB_SLUGS[tab];
if(!slug || !DEPARTMENTS[dept]) return;
window.history.replaceState(null, '', '/' + dept + '/' + slug);
} catch (err) {}
}
// ---------- Kollabierbarer Header ---------- // ---------- Kollabierbarer Header ----------
// HEADER_COLLAPSED_KEY: LocalStorage-Key für den Einklapp-Zustand des Header-Menüs // HEADER_COLLAPSED_KEY: LocalStorage-Key für den Einklapp-Zustand des Header-Menüs
+1 -1
View File
@@ -5,7 +5,7 @@
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden // ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen // HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen, // ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen,
// u. a. js/csv.js, js/board.js, js/load.js sowie den Plan-Modulen unter js/plan/* // u. a. js/csv.js, js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
// (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js. // (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck. // departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck.
+1 -1
View File
@@ -6,7 +6,7 @@
// Sie wird von `Test.html` nach `js/store.js` geladen und stellt globale // Sie wird von `Test.html` nach `js/store.js` geladen und stellt globale
// Funktionen wie `currentWeek()`, `switchWeek()`, `newWeek()`, `cloneWeek()`, // Funktionen wie `currentWeek()`, `switchWeek()`, `newWeek()`, `cloneWeek()`,
// `applyAutoLock()`, `guardWeekLocked()` und `deleteWeek()` bereit. Genutzt wird // `applyAutoLock()`, `guardWeekLocked()` und `deleteWeek()` bereit. Genutzt wird
// sie u.a. von `js/board.js` (`renderBoard`, `isWeekLocked`), `js/store.js` // sie u.a. von `js/board/board.js` (`renderBoard`, `isWeekLocked`), `js/store.js`
// (`load`) und `js/tabs-auth.js` (Passwort-Aktionen für Entsperren/Löschen). // (`load`) und `js/tabs-auth.js` (Passwort-Aktionen für Entsperren/Löschen).
// refreshWeekSelect: aktualisiert das Dropdown `weekSelect` mit allen Sitzungen // refreshWeekSelect: aktualisiert das Dropdown `weekSelect` mit allen Sitzungen
+17 -16
View File
@@ -3,8 +3,9 @@
PROJEKTPLAN-ANSICHT: zeigt für ein einzelnes Projekt ein GANTT-Diagramm mit TODO- und PROJEKTPLAN-ANSICHT: zeigt für ein einzelnes Projekt ein GANTT-Diagramm mit TODO- und
Meilenstein-Zeilen, Abhängigkeiten, Zoom, kritischem Pfad, Auto-Schedule, Ressourcen- Meilenstein-Zeilen, Abhängigkeiten, Zoom, kritischem Pfad, Auto-Schedule, Ressourcen-
Auslastung und Abwesenheiten der Projektbeteiligten. Die Seite wird über openProjectPlan() Auslastung und Abwesenheiten der Projektbeteiligten. Die Seite wird über openProjectPlan()
in js/gantt.js in einem neuen Tab geöffnet und über URL-Parameter (Abteilung, Woche, in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
Projekt-Nr./Name) adressiert. Sie lädt styles.css und ausschließlich die gemeinsamen "/<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/csv.js, js/server.js
und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js, und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js,
js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js, js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js,
@@ -18,7 +19,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projektplan</title> <title>Projektplan</title>
<link rel="stylesheet" href="styles.css?v=3"> <link rel="stylesheet" href="/styles.css?v=3">
<style> <style>
.plan-page { min-height: 100vh; display: flex; flex-direction: column; } .plan-page { min-height: 100vh; display: flex; flex-direction: column; }
.plan-header { .plan-header {
@@ -313,18 +314,18 @@
</div> </div>
</div> </div>
<script src="js/globals.js?v=5"></script> <script src="/js/globals.js?v=5"></script>
<script src="js/utils.js?v=5"></script> <script src="/js/utils.js?v=5"></script>
<script src="js/ui.js?v=5"></script> <script src="/js/ui.js?v=5"></script>
<script src="js/date-picker.js?v=5"></script> <script src="/js/date-picker.js?v=5"></script>
<script src="js/csv.js?v=5"></script> <script src="/js/csv.js?v=5"></script>
<script src="js/server.js?v=5"></script> <script src="/js/server.js?v=5"></script>
<script src="js/gantt.js?v=5"></script> <script src="/js/gantt.js?v=5"></script>
<script src="js/plan/plan-state.js?v=5"></script> <script src="/js/plan/plan-state.js?v=5"></script>
<script src="js/plan/plan-build-gantt.js?v=5"></script> <script src="/js/plan/plan-build-gantt.js?v=5"></script>
<script src="js/plan/plan-gantt.js?v=5"></script> <script src="/js/plan/plan-gantt.js?v=5"></script>
<script src="js/plan/plan-drag.js?v=5"></script> <script src="/js/plan/plan-drag.js?v=5"></script>
<script src="js/plan/plan-modals.js?v=5"></script> <script src="/js/plan/plan-modals.js?v=5"></script>
<script src="js/plan/plan-schedule.js?v=5"></script> <script src="/js/plan/plan-schedule.js?v=5"></script>
</body> </body>
</html> </html>
+28
View File
@@ -121,6 +121,34 @@ app.get('/', (req,res)=>{
res.sendFile(path.join(__dirname, 'public', 'Test.html')); res.sendFile(path.join(__dirname, 'public', 'Test.html'));
}); });
// Gültige Tab-Slugs für Deep-Link-Routen (muss zu TAB_SLUGS in js/globals.js passen)
const VALID_TAB_SLUGS = new Set(['Projektübersicht','Auslastung','Projekte','Projektbeteiligte']);
// Slug der Projektplan-Seite (muss zu PLAN_SLUG in js/globals.js passen)
const PLAN_SLUG = 'Projektplan';
// Route: Deep-Links je Abteilung + Tab, z. B. "/HLS/Auslastung" oder
// "/ELT/Projektübersicht". Liefert wie "/" die Hauptseite; unbekannte
// Abteilungen/Tabs werden abgelehnt (404), damit keine Test.html auf beliebigen
// Pfaden ausgeliefert wird.
app.get('/:department/:tab', (req, res) => {
const { department, tab } = req.params;
if(!VALID_DEPARTMENTS.has(department)) return res.status(404).send('Nicht gefunden');
if(tab === PLAN_SLUG){
// Projektplan-Seite (GANTT), z. B. "/HLS/Projektplan?weekLabel=..."
return res.sendFile(path.join(__dirname, 'public', 'project-plan.html'));
}
if(!VALID_TAB_SLUGS.has(tab)) return res.status(404).send('Nicht gefunden');
res.sendFile(path.join(__dirname, 'public', 'Test.html'));
});
// Route: Projektplan-Seite mit Projektname im Pfad, z. B.
// "/HLS/Projektplan/Gymnasium%20Hemer?weekLabel=..." – das Pfad-Segment dient
// nur der lesbaren Adresse, die Identifikation erfolgt über weekLabel/projNr.
app.get('/:department/Projektplan/:projName', (req, res) => {
if(!VALID_DEPARTMENTS.has(req.params.department)) return res.status(404).send('Nicht gefunden');
res.sendFile(path.join(__dirname, 'public', 'project-plan.html'));
});
// Serverstart mit Fehlerbehandlung (z.B. Port bereits belegt) // 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(`CSV-Session server running on http://localhost:${PORT}`));
server.on('error', (err) => { server.on('error', (err) => {
BIN
View File
Binary file not shown.