// ----------------------------- // Projektübersicht — UI-Zustand (LocalStorage, Theme, Header) // ----------------------------- // Diese Datei verwaltet den reinen UI-Zustand: gespeicherte Abteilung in // LocalStorage, den kollabierbaren Header sowie das Light/Dark-Theme. Sie wird // nach js/globals.js von beiden HTML-Einstiegspunkten geladen (Test.html und // project-plan.html). Die bereitgestellten Funktionen werden von js/init.js // beim Start sowie von js/tabs-auth.js (Tab-Wechsel, showTab) und js/store.js // (Abteilungswechsel, switchDepartment) aufgerufen; im Theme-Umschalter der // Seite werden toggleTheme/applyTheme direkt eingebunden. // saveUiState: speichert Abteilung und aktiven Tab in den LocalStorage // Parameter: keine. Rückgabe: keine. function saveUiState(){ try { const state = { department: currentDepartment, tab: currentTab }; window.localStorage.setItem(STORAGE, JSON.stringify(state)); } catch (err) { console.warn('UI-State kann nicht gespeichert werden:', err); } } // loadUiState: stellt die zuletzt gewählte Abteilung aus dem LocalStorage wieder her // Parameter: keine. Rückgabe: keine (setzt currentDepartment aus js/globals.js). function loadUiState(){ try { const state = JSON.parse(window.localStorage.getItem(STORAGE) || '{}'); if(state.department && DEPARTMENTS[state.department]) currentDepartment = state.department; // Tab wird nicht aus dem localStorage wiederhergestellt, damit nach dem // Neuladen immer das Projektboard angezeigt wird. } catch (err) { console.warn('UI-State kann nicht geladen werden:', err); } } // ---------- Deep-Link-Routen (Abteilung + Tab als URL) ---------- // applyUrlRoute: wertet einen Deep-Link "//" 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 const HEADER_COLLAPSED_KEY = 'projuebersicht-header-collapsed'; // loadHeaderState: liest den gespeicherten Header-Zustand aus dem LocalStorage // Parameter: keine. Rückgabe: keine (wendet den Zustand per setHeaderCollapsed an). function loadHeaderState(){ let collapsed = true; // Default: Menü ist beim ersten Start geschlossen try { const stored = window.localStorage.getItem(HEADER_COLLAPSED_KEY); if(stored !== null){ collapsed = stored === 'true'; } } catch (err) {} setHeaderCollapsed(collapsed, false); } // saveHeaderState: speichert den Einklapp-Zustand des Headers im LocalStorage // Parameter: collapsed (Boolean). Rückgabe: keine. function saveHeaderState(collapsed){ try { window.localStorage.setItem(HEADER_COLLAPSED_KEY, collapsed ? 'true' : 'false'); } catch (err) {} } // setHeaderCollapsed: klappt das Header-Menü auf/zu und aktualisiert Icon und aria-Attribute // Parameter: collapsed (Boolean), persist (Boolean, ob in LocalStorage gespeichert wird). Rückgabe: keine. function setHeaderCollapsed(collapsed, persist){ const menu = document.getElementById('headerMenu'); const btn = document.querySelector('.header-bar .header-toggle'); const icon = document.querySelector('.header-bar .header-toggle-icon'); if(!menu) return; if(collapsed){ menu.classList.remove('open'); menu.setAttribute('aria-hidden', 'true'); if(icon) icon.textContent = '☰'; if(btn) btn.setAttribute('aria-expanded', 'false'); } else { menu.classList.add('open'); menu.setAttribute('aria-hidden', 'false'); if(icon) icon.textContent = '✕'; if(btn) btn.setAttribute('aria-expanded', 'true'); } if(persist) saveHeaderState(collapsed); } // toggleHeader: klappt das Header-Menü zwischen offen und geschlossen um // Parameter: keine. Rückgabe: keine (persistiert den neuen Zustand). function toggleHeader(){ const menu = document.getElementById('headerMenu'); const isOpen = menu && menu.classList.contains('open'); setHeaderCollapsed(isOpen, true); } // ---------- Theme (Light/Dark) ---------- // applyThemeIcon: setzt das Sonne-/Mond-Icon passend zum effektiven Theme // Parameter: theme (String 'dark'/'light'). Rückgabe: keine. function applyThemeIcon(theme){ const icon = document.getElementById('themeIcon'); if(!icon) return; icon.textContent = theme === 'dark' ? '☀️' : '🌙'; } // loadTheme: liest das gespeicherte Theme aus dem LocalStorage und wendet es an // Parameter: keine. Rückgabe: keine. function loadTheme(){ let theme = 'auto'; try { theme = window.localStorage.getItem('projuebersicht-theme') || 'auto'; } catch (err) { console.warn('Theme kann nicht geladen werden:', err); } applyTheme(theme, false); } // saveTheme: speichert das gewählte Theme im LocalStorage // Parameter: theme (String 'auto'/'light'/'dark'). Rückgabe: keine. function saveTheme(theme){ try { window.localStorage.setItem('projuebersicht-theme', theme); } catch (err) { console.warn('Theme kann nicht gespeichert werden:', err); } } // effectiveTheme: ermittelt das tatsächlich verwendete Theme ('light'/'dark') // Parameter: theme (String 'auto'/'light'/'dark'). Rückgabe: 'dark' oder 'light' // (bei 'auto' anhand der Systemeinstellung). function effectiveTheme(theme){ if(theme === 'dark') return 'dark'; if(theme === 'light') return 'light'; return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } // applyTheme: setzt das data-theme-Attribut am html-Element und aktualisiert das Icon // Parameter: theme (String), persist (Boolean, ob in LocalStorage gespeichert wird). Rückgabe: keine. function applyTheme(theme, persist){ const html = document.documentElement; const eff = effectiveTheme(theme); if(theme === 'dark') html.setAttribute('data-theme', 'dark'); else if(theme === 'light') html.setAttribute('data-theme', 'light'); else html.removeAttribute('data-theme'); applyThemeIcon(eff); if(persist) saveTheme(theme); } // toggleTheme: wechselt zwischen hellem und dunklem Theme und speichert die Auswahl // Parameter: keine. Rückgabe: keine. function toggleTheme(){ const html = document.documentElement; const currentAttr = html.getAttribute('data-theme'); const currentTheme = currentAttr || 'auto'; const currentEffective = effectiveTheme(currentTheme); const next = currentEffective === 'dark' ? 'light' : 'dark'; applyTheme(next, true); } // Beim Wechsel der System-Farbschema-Einstellung wird das Theme im // "Auto"-Modus sofort neu angewendet (kein Neuladen nötig). if(window.matchMedia){ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(){ let theme = 'auto'; try { theme = window.localStorage.getItem('projuebersicht-theme') || 'auto'; } catch (err) {} applyTheme(theme, false); }); }