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
+207 -97

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
├── public/
│ ├── 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
│ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, weekSortKey-Quelle
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
@@ -39,15 +39,19 @@ user-facing text.
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, blobToDataUrl
│ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed
│ │ ├── 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
│ │ ├── showmode.js # Show-Mode (Präsentationsmodus)
│ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz)
│ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer)
│ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html)
│ │ ├── board/ # tab-specific: Projektboard
│ │ │ └── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung
│ │ ├── people/ # tab-specific: Projektbeteiligte
│ │ │ └── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten
│ │ ├── projects/ # tab-specific: Projekte
│ │ │ └── projects.js # Projekte-Tab-Rendering + Drag-and-Drop
│ │ ├── load/ # tab-specific: Auslastung
│ │ │ └── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal
│ ├── project-plan.html # Gantt/TODO planning view
│ ├── js/plan/ # planning modules (nur von project-plan.html geladen)
│ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts
@@ -79,6 +83,16 @@ docker compose up --build # Docker variant (compose file is at repo root)
## Server API (`server.js`)
- `GET /` → `Test.html`; `GET /health`.
- Deep links per department + tab: `GET /<Dept>/<Tab>` → `Test.html`, e.g.
`/HLS/Auslastung` or `/ELT/Projektübersicht`. Tab slugs (`TAB_SLUGS` in
`js/globals.js`) are `Projektübersicht|Auslastung|Projekte|Projektbeteiligte`;
anything else returns 404. Exception: `GET /<Dept>/Projektplan(/<ProjName>)?`
(`PLAN_SLUG`) serves `project-plan.html`, the GANTT view, with
`weekLabel|projNr` as query params (the project name is a readable path
segment only; identification happens via weekLabel + projNr). The frontend
reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in
`js/ui.js` and all static assets in the HTML use **absolute** paths
(`/js/...`, `/styles.css`) so they resolve on nested routes.
- `GET /load-all?department=` → `{sessions:[{filename,content}], people}` with
`_people.csv` content in `people`; sessions sorted by filename.
- `POST /save-csv {filename, csv, department}` → writes UTF-8 BOM CSV.
@@ -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,
since there is no bundler:
- `Test.html` loads `globals → utils → ui → date-picker → csv → server →
store → weeks → board → people → projects → load → gantt → showmode →
tabs-auth → pdf → init`. See the exact list in `Test.html`.
store → weeks` (shared modules), then the tab-specific folders `board →
people → projects → load`, finally `gantt → showmode → tabs-auth → pdf →
init`. See the exact list in `Test.html`.
- `project-plan.html` loads only `globals → utils → ui → date-picker → csv →
server → gantt` plus the `js/plan/*` modules. It must NOT load modules that
touch main-only page elements (`store`, `weeks`, `board`, …, `init`).
@@ -122,8 +137,10 @@ docker compose up --build # Docker variant (compose file is at repo root)
`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` + `projName` via URL
params (`openProjectPlan`, `findProjectForPlan`).
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
@@ -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
load/clone/new-week; `localStorage` UI state is not restored for locked tabs.
- `localStorage` keys: `projuebersicht_v1` (department only — tab is never
restored), `projuebersicht-theme`, header-collapse key. Do **not** store data
in `localStorage`; it is UI state only.
restored from storage; at startup the tab comes from the deep-link URL if
present, otherwise `board`), `projuebersicht-theme`, header-collapse key. Do
**not** store data in `localStorage`; it is UI state only.
- Deep-link URLs (`/<Dept>/<Tab>`) are shared via the address bar:
`updateRouteUrl()` rewrites the path via `history.replaceState` on tab and
department switches; protected tabs keep the URL but open the password modal.
## Auth / security quirk
@@ -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
internet access and won't work offline. `project-plan.html` loads neither.
- User-provided status HTML is sanitized on render via a small whitelist
(`sanitizeStatusHtml` in `js/board.js`) to mitigate XSS.
(`sanitizeStatusHtml` in `js/board/board.js`) to mitigate XSS.
## 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",
"version": "1.0.0",
"description": "Single-page App zur Verfolgung von TGA-Projekten",
"description": "App zur Verfolgung von TGA-Projekten",
"main": "server.js",
"scripts": {
"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
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, js/board.js,
js/people.js, js/projects.js, js/load.js, 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()
js/date-picker.js, js/csv.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
CDN. Über openProjectPlan()
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.
-->
@@ -16,7 +18,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/pdfmake/0.2.10/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.10/vfs_fonts.js"></script>
@@ -36,7 +38,7 @@
<header>
<div class="header-bar">
<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>
</header>
@@ -221,22 +223,22 @@
</div>
</div>
<script src="js/globals.js?v=5"></script>
<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>
<script src="js/board.js?v=5"></script>
<script src="js/people.js?v=5"></script>
<script src="js/projects.js?v=5"></script>
<script src="js/load.js?v=5"></script>
<script src="js/gantt.js?v=5"></script>
<script src="js/showmode.js?v=5"></script>
<script src="js/tabs-auth.js?v=5"></script>
<script src="js/pdf.js?v=5"></script>
<script src="js/init.js?v=5"></script>
<script src="/js/globals.js?v=5"></script>
<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>
<script src="/js/board/board.js?v=6"></script>
<script src="/js/people/people.js?v=6"></script>
<script src="/js/projects/projects.js?v=6"></script>
<script src="/js/load/load.js?v=6"></script>
<script src="/js/gantt.js?v=5"></script>
<script src="/js/showmode.js?v=5"></script>
<script src="/js/tabs-auth.js?v=5"></script>
<script src="/js/pdf.js?v=5"></script>
<script src="/js/init.js?v=5"></script>
</body>
</html>
@@ -4,10 +4,10 @@
// Diese Datei rendert das Projektboard (Tab "Projektboard" in `Test.html`),
// die zentrale Tabelle mit Projekten, Status-Kommentaren, Personen-Spalten
// (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()`,
// `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`.
// currentStatusEditor: aktuell fokussiertes contenteditable-Element des
+1 -1
View File
@@ -9,7 +9,7 @@
// und stellt den Kalender-Picker (Einzeldatum "openDatePicker" und Bereich
// "openRangePicker") bereit. Der Zustand des offenen Popups liegt in der
// 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).
// rangePickerState: Zustand des aktuell geöffneten Picker-Popups (Optionen,
+10 -9
View File
@@ -2,9 +2,9 @@
// Projektübersicht — GANTT-Diagramm (gemeinsam genutzt)
// -----------------------------
// 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
// 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
// currentWeek()/isWeekLocked() aus js/weeks.js brauchen (renderGantt,
// fitGanttRange, openProjectPlan), werden nur vom Hauptfenster aufgerufen.
@@ -328,19 +328,20 @@ function buildGanttHtml(projects, start, end, options = {}){
// ---------- Projektplan-Navigation ----------
// openProjectPlan: öffnet project-plan.html in einem neuen Tab. Weil
// Projekt-IDs nicht persistent sind, werden department, Wochen-Label, Proj.-Nr.
// und -Name als URL-Parameter übergeben. Parameter: pid = Projekt-ID der
// aktuellen Woche. Kein Rückgabewert.
// Projekt-IDs nicht persistent sind, werden Wochen-Label und Proj.-Nr. als
// URL-Parameter übergeben; Abteilung und Projektname stehen im Pfad nach dem
// Schema "/<Abteilung>/Projektplan/<Projektname>". Parameter: pid =
// Projekt-ID der aktuellen Woche. Kein Rückgabewert.
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., Name) in der URL übergeben.
const url = new URL('project-plan.html', window.location.href);
url.searchParams.set('department', currentDepartment);
// 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);
url.searchParams.set('weekLabel', w.label);
url.searchParams.set('projNr', p.nr || '');
url.searchParams.set('projName', p.name || '');
window.open(url.toString(), '_blank');
}
+17 -2
View File
@@ -5,7 +5,7 @@
// legt alle zentralen Konstanten (Leistungsphasen, Rollen, Gruppen, Abteilungen,
// Farben, Passwort-Hash) sowie den globalen In-Memory-Zustand (db,
// 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/plan/plan-state.js Zustand für die Planungsseite.
@@ -46,4 +46,19 @@ const DEPARTMENTS = {
ELT: { label: 'Elektro', icon: '' },
HLS: { label: 'HLS', 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()/
// loadTheme()/loadHeaderState() aus js/ui.js, switchDepartment() aus
// js/store.js, showTab() aus js/tabs-auth.js sowie die Render-Funktionen
// renderBoard()/renderLoad()/renderProjects()/renderPeople() aus js/board.js,
// js/load.js, js/projects.js, js/people.js.
// renderBoard()/renderLoad()/renderProjects()/renderPeople() aus js/board/board.js,
// js/load/load.js, js/projects/projects.js, js/people/people.js.
// renderAll: Convenience um UI-Elemente zu initialisieren
function renderAll(){
@@ -24,10 +24,16 @@ function renderAll(){
// initUiState: stellt beim Start den UI-Zustand her (LocalStorage-UI-State,
// Theme, Header-Status), setzt Titel und aktive Abteilungs-Schaltfläche und
// zeigt den ersten Tab an – passwortgeschützte Tabs ('projects'/'people')
// werden beim Start nie direkt aktiviert. Kein Rückgabewert.
// zeigt den ersten Tab an. Eine Deep-Link-URL (z. B. "/HLS/Auslastung") aus
// applyUrlRoute() übersteuert dabei Abteilung und Tab; passwortgeschützte Tabs
// öffnen das Passwort-Modal (showTab ohne skipAuth). Kein Rückgabewert.
function initUiState(){
loadUiState();
const route = applyUrlRoute();
if(route && DEPARTMENTS[route.department]){
currentDepartment = route.department;
currentTab = route.tab;
}
loadTheme();
loadHeaderState();
document.querySelectorAll('.dept-tab').forEach(btn => {
@@ -39,9 +45,8 @@ function initUiState(){
if(titleEl) titleEl.textContent = info.icon + ' Projektübersicht ' + info.label;
const tab = ['board','load','projects','people'].includes(currentTab) ? currentTab : DEFAULT_TAB;
// Beim initialen Start niemals einen passwortgeschützten Tab direkt anzeigen.
const safeTab = (tab === 'projects' || tab === 'people') ? DEFAULT_TAB : tab;
showTab(safeTab, true);
// Geschützte Tabs ('projects'/'people') verlangen hier ihr Passwort.
showTab(tab);
}
// 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-
// 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/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,
// pdfLoadAmpelColor, LOAD_BAR_MAX) wird zusätzlich von js/pdf.js für den
// 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
// die per CDN eingebundene Bibliothek pdfmake (offline nicht verfügbar). Es
// 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
// (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.
// Farbkonstanten des PDF-Layouts: Rollen-Hintergründe, neutrale bzw.
@@ -4,12 +4,12 @@
// Diese Datei rendert den Personen-Tab (Tab "Projektbeteiligte" in `Test.html`)
// und verwaltet die globale Personenliste pro Abteilung: Anlegen, Bearbeiten,
// 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()`,
// `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/projects.js`.
// `js/projects/projects.js`.
// addPerson: fügt eine neue Person der globalen Liste hinzu und
// 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
// geladen und definiert window.PlanApp als gemeinsamen Zustand aller
// Projektplan-Module. Es liest die URL-Parameter (department, weekLabel,
// projNr, projName), über die das zu bearbeitende Projekt adressiert wird.
// Projektplan-Module. Es liest die URL-Route (Abteilung + Projektname, z. B.
// "/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
// über die Server-API geladen. Die hier definierten Funktionen (savePlan,
// isLocked, showToast u. a.) werden über app.* den anderen plan-Modulen
@@ -33,10 +34,14 @@ window.PlanApp = {
(function(){
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.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
// 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
// sortierbare Listenansicht: Projekte werden nach Gruppe ("Aktive Projekte",
// "Kleine Projekte" usw.) gruppiert, inline bearbeitbar und per
// Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people.js`
// (vor `js/load.js`) geladen und stellt globale Funktionen wie
// Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people/people.js`
// (vor `js/load/load.js`) geladen und stellt globale Funktionen wie
// `renderProjects()`, `editProjectField()`, `editProjectActiveLP()` sowie die
// Drag-and-Drop-Handler und die Sortier-Hilfe `getDragAfterElement()` bereit.
// Letztere wird auch von `js/people.js` (Personen-Sortierung) genutzt;
// `renderBoard()` aus `js/board.js` wird nach Änderungen aktualisiert.
// Letztere wird auch von `js/people/people.js` (Personen-Sortierung) genutzt;
// `renderBoard()` aus `js/board/board.js` wird nach Änderungen aktualisiert.
// delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der
// 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
// Element mit der kleinsten negativen Distanz zur Mitte). Rückgabe:
// 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){
const draggableElements = [...container.querySelectorAll('.person-list-item:not([style*="opacity"])')];
return draggableElements.reduce((closest, child) => {
+1 -1
View File
@@ -7,7 +7,7 @@
// und Auslastung) und zeigt sie in einem Vollbild-Overlay. Genutzt werden
// showTab() aus js/tabs-auth.js, switchDepartment() aus js/store.js,
// 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.
// 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
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
// `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.
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
@@ -62,6 +62,7 @@ function switchDepartment(dept){
if(!DEPARTMENTS[dept]) return;
currentDepartment = dept;
saveUiState();
updateRouteUrl(dept, currentTab);
// UI-Status der Abteilungs-Tabs aktualisieren
document.querySelectorAll('.dept-tab').forEach(btn => {
@@ -118,11 +119,11 @@ function load(){
}
console.log(`Server-Load erfolgreich: ${db.weeks.length} Sitzung(en).`);
}
// Geschützte Tabs dürfen nicht ohne Passwort angezeigt werden, auch wenn
// sie im UI-State gespeichert waren.
if(currentTab === 'projects' || currentTab === 'people') currentTab = DEFAULT_TAB;
// Geschützte Tabs ('projects'/'people') dürfen nur mit Passwort angezeigt
// werden. showTab() öffnet dafür automatisch das Passwort-Modal – auch
// wenn der Tab per Deep-Link-URL (z. B. "/ELT/Projekte") gewählt wurde.
renderAll();
showTab(currentTab, true);
showTab(currentTab);
})
.catch(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
// profitieren auch die Passwort-Gates in js/weeks.js (Löschen/Entsperren von
// Sitzungen). Gerendert werden beim Tab-Wechsel renderBoard()/renderLoad()/
// renderProjects()/renderPeople() aus js/board.js, js/load.js, js/projects.js,
// js/people.js.
// renderProjects()/renderPeople() aus js/board/board.js, js/load/load.js, js/projects/projects.js,
// js/people/people.js.
// showTab: wechselt zwischen Ansichten; für sensible Tabs wird ein Passwort verlangt
function showTab(t, skipAuth = false){
@@ -24,6 +24,7 @@ function showTab(t, skipAuth = false){
}
currentTab = t;
saveUiState();
updateRouteUrl(currentDepartment, t);
['board','projects','load','people'].forEach(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 ----------
// 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
// 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.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.
// 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
// Funktionen wie `currentWeek()`, `switchWeek()`, `newWeek()`, `cloneWeek()`,
// `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).
// 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
Meilenstein-Zeilen, Abhängigkeiten, Zoom, kritischem Pfad, Auto-Schedule, Ressourcen-
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,
Projekt-Nr./Name) adressiert. Sie lädt styles.css und ausschließlich die gemeinsamen
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
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,
@@ -18,7 +19,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projektplan</title>
<link rel="stylesheet" href="styles.css?v=3">
<link rel="stylesheet" href="/styles.css?v=3">
<style>
.plan-page { min-height: 100vh; display: flex; flex-direction: column; }
.plan-header {
@@ -313,18 +314,18 @@
</div>
</div>
<script src="js/globals.js?v=5"></script>
<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>
<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-drag.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/globals.js?v=5"></script>
<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>
<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-drag.js?v=5"></script>
<script src="/js/plan/plan-modals.js?v=5"></script>
<script src="/js/plan/plan-schedule.js?v=5"></script>
</body>
</html>
+28
View File
@@ -121,6 +121,34 @@ app.get('/', (req,res)=>{
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)
const server = app.listen(PORT, ()=> console.log(`CSV-Session server running on http://localhost:${PORT}`));
server.on('error', (err) => {
BIN
View File
Binary file not shown.