/** * MYP Platform Offline-First Application * Version: 2.0.0 * Description: Offline-fähige PWA für die MYP Plattform */ class MYPApp { constructor() { this.isOffline = !navigator.onLine; this.registerTime = new Date().toISOString(); this.darkMode = document.documentElement.classList.contains('dark'); this.setupOfflineDetection(); this.setupServiceWorker(); this.setupLocalStorage(); this.setupUI(); this.setupThemeListeners(); // Debug-Info für die Entwicklung console.log(`MYP App initialisiert um ${this.registerTime}`); console.log(`Initiale Netzwerkverbindung: ${navigator.onLine ? 'Online' : 'Offline'}`); console.log(`Aktueller Modus: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`); } /** * Überwacht die Netzwerkverbindung */ setupOfflineDetection() { window.addEventListener('online', () => { this.isOffline = false; document.body.classList.remove('offline-mode'); console.log('Netzwerkverbindung wiederhergestellt!'); // Bei Wiederverbindung: Synchronisiere Daten this.syncOfflineData(); // Event an andere Komponenten senden window.dispatchEvent(new CustomEvent('networkStatusChange', { detail: { isOffline: false } })); }); window.addEventListener('offline', () => { this.isOffline = true; document.body.classList.add('offline-mode'); console.log('Netzwerkverbindung verloren!'); // Event an andere Komponenten senden window.dispatchEvent(new CustomEvent('networkStatusChange', { detail: { isOffline: true } })); }); // Klasse setzen, wenn initial offline if (this.isOffline) { document.body.classList.add('offline-mode'); } } /** * Registriert den Service Worker für Offline-Funktionalität */ setupServiceWorker() { if ('serviceWorker' in navigator) { const swPath = '/sw.js'; navigator.serviceWorker.register(swPath, { scope: '/' }).then(registration => { console.log('Service Worker erfolgreich registriert mit Scope:', registration.scope); // Status des Service Workers überprüfen if (registration.installing) { console.log('Service Worker wird installiert'); } else if (registration.waiting) { console.log('Service Worker wartet auf Aktivierung'); } else if (registration.active) { console.log('Service Worker ist aktiv'); } // Auf Updates überwachen registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { console.log(`Service Worker Status: ${newWorker.state}`); // Bei Aktivierung Cache aktualisieren if (newWorker.state === 'activated') { this.fetchAndCacheAppData(); } }); }); // Prüfen auf Update beim App-Start registration.update(); }).catch(error => { console.error('Service Worker Registrierung fehlgeschlagen:', error); // Fallback auf lokalen Cache wenn Service Worker fehlschlägt this.setupLocalCache(); }); // Kontrolltransfer abfangen navigator.serviceWorker.addEventListener('controllerchange', () => { console.log('Service Worker Controller hat gewechselt'); }); } else { console.warn('Service Worker werden von diesem Browser nicht unterstützt'); // Fallback auf lokalen Cache wenn Service Worker nicht unterstützt werden this.setupLocalCache(); } } /** * Lokaler Cache-Fallback wenn Service Worker nicht verfügbar */ setupLocalCache() { console.log('Verwende lokalen Cache als Fallback'); // Wichtige App-Daten sofort laden und cachen this.fetchAndCacheAppData(); // Timer für regelmäßige Aktualisierung des Cache setInterval(() => { if (navigator.onLine) { this.fetchAndCacheAppData(); } }, 15 * 60 * 1000); // Alle 15 Minuten } /** * Laden und Cachen wichtiger API-Endpunkte */ fetchAndCacheAppData() { if (!navigator.onLine) return; const endpoints = [ '/api/printers', '/api/jobs', '/api/schedule', '/api/status' ]; for (const endpoint of endpoints) { fetch(endpoint) .then(response => response.json()) .then(data => { localStorage.setItem(`cache_${endpoint}`, JSON.stringify({ timestamp: new Date().getTime(), data: data })); console.log(`Daten für ${endpoint} gecached`); }) .catch(error => { console.error(`Fehler beim Cachen von ${endpoint}:`, error); }); } } /** * Lokaler Speicher für Offline-Daten */ setupLocalStorage() { // Speicher für Offline-Bearbeitung einrichten if (!localStorage.getItem('offlineChanges')) { localStorage.setItem('offlineChanges', JSON.stringify([])); } // Speicher für App-Konfiguration einrichten if (!localStorage.getItem('appConfig')) { const defaultConfig = { theme: 'system', notifications: true, dataSync: true, lastSync: null }; localStorage.setItem('appConfig', JSON.stringify(defaultConfig)); } // Aufräumen alter Cache-Einträge (älter als 7 Tage) this.cleanupOldCache(7); } /** * Entfernt alte Cache-Einträge * @param {number} daysOld - Alter in Tagen */ cleanupOldCache(daysOld) { const now = new Date().getTime(); const maxAge = daysOld * 24 * 60 * 60 * 1000; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); // Nur Cache-Einträge prüfen if (key.startsWith('cache_')) { try { const item = JSON.parse(localStorage.getItem(key)); if (item && item.timestamp && (now - item.timestamp > maxAge)) { localStorage.removeItem(key); console.log(`Alter Cache-Eintrag entfernt: ${key}`); } } catch (e) { console.error(`Fehler beim Verarbeiten von Cache-Eintrag ${key}:`, e); } } } } /** * Synchronisiert offline getätigte Änderungen */ syncOfflineData() { if (!navigator.onLine) return; const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]'); if (offlineChanges.length === 0) { console.log('Keine Offline-Änderungen zu synchronisieren'); return; } console.log(`${offlineChanges.length} Offline-Änderungen werden synchronisiert...`); // Status-Indikator für Synchronisierung anzeigen document.body.classList.add('syncing'); const syncPromises = offlineChanges.map(change => { return fetch(change.url, { method: change.method, headers: { 'Content-Type': 'application/json', 'X-Offline-Change': 'true' }, body: JSON.stringify(change.data) }) .then(response => { if (!response.ok) { throw new Error(`Fehler ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { console.log(`Änderung erfolgreich synchronisiert: ${change.url}`); return { success: true, change }; }) .catch(error => { console.error(`Synchronisierung fehlgeschlagen für ${change.url}:`, error); return { success: false, change, error }; }); }); Promise.all(syncPromises) .then(results => { // Fehlgeschlagene Änderungen behalten const failedChanges = results .filter(result => !result.success) .map(result => result.change); localStorage.setItem('offlineChanges', JSON.stringify(failedChanges)); // App-Konfiguration aktualisieren const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}'); appConfig.lastSync = new Date().toISOString(); localStorage.setItem('appConfig', JSON.stringify(appConfig)); // Status-Indikator entfernen document.body.classList.remove('syncing'); // Event-Benachrichtigung const syncEvent = new CustomEvent('offlineDataSynced', { detail: { total: offlineChanges.length, succeeded: offlineChanges.length - failedChanges.length, failed: failedChanges.length } }); window.dispatchEvent(syncEvent); console.log(`Synchronisierung abgeschlossen: ${offlineChanges.length - failedChanges.length} erfolgreich, ${failedChanges.length} fehlgeschlagen`); }); } /** * Fügt Offline-Änderung zum Synchronisierungsstapel hinzu * @param {string} url - API-Endpunkt * @param {string} method - HTTP-Methode (POST, PUT, DELETE) * @param {object} data - Zu sendende Daten */ addOfflineChange(url, method, data) { const offlineChanges = JSON.parse(localStorage.getItem('offlineChanges') || '[]'); offlineChanges.push({ id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5), url, method, data, timestamp: new Date().toISOString() }); localStorage.setItem('offlineChanges', JSON.stringify(offlineChanges)); console.log(`Offline-Änderung gespeichert: ${method} ${url}`); } /** * Theme-Wechsel überwachen und reagieren */ setupThemeListeners() { // Auf Dark Mode Änderungen reagieren window.addEventListener('darkModeChanged', (e) => { this.darkMode = e.detail.isDark; this.updateAppTheme(); // App-Konfiguration aktualisieren const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}'); appConfig.theme = this.darkMode ? 'dark' : 'light'; localStorage.setItem('appConfig', JSON.stringify(appConfig)); console.log(`Theme geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`); }); // Bei Systemthemen-Änderung prüfen, ob wir automatisch wechseln sollen window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { const appConfig = JSON.parse(localStorage.getItem('appConfig') || '{}'); // Nur reagieren, wenn der Nutzer "system" als Theme gewählt hat if (appConfig.theme === 'system') { this.darkMode = e.matches; this.updateAppTheme(); console.log(`Systemthema geändert: ${this.darkMode ? 'Dark Mode' : 'Light Mode'}`); } }); } /** * Theme im UI aktualisieren */ updateAppTheme() { // Theme-color Meta-Tag aktualisieren für Mobile Browser const metaThemeColor = document.getElementById('metaThemeColor') || document.querySelector('meta[name="theme-color"]'); if (metaThemeColor) { metaThemeColor.setAttribute('content', this.darkMode ? '#0f172a' : '#ffffff'); } // Dynamische UI-Elemente anpassen this.updateUIForTheme(); } /** * UI-Elemente für das aktuelle Theme anpassen */ updateUIForTheme() { // Offline-Banner und Sync-Indikator anpassen const offlineBanner = document.getElementById('offline-banner'); const syncIndicator = document.getElementById('sync-indicator'); if (offlineBanner) { if (this.darkMode) { offlineBanner.classList.add('dark-theme'); offlineBanner.classList.remove('light-theme'); } else { offlineBanner.classList.add('light-theme'); offlineBanner.classList.remove('dark-theme'); } } if (syncIndicator) { if (this.darkMode) { syncIndicator.classList.add('dark-theme'); syncIndicator.classList.remove('light-theme'); } else { syncIndicator.classList.add('light-theme'); syncIndicator.classList.remove('dark-theme'); } } // Andere dynamische UI-Elemente hier anpassen } /** * UI-Komponenten initialisieren */ setupUI() { // Offline-Modus Banner einfügen wenn nicht vorhanden if (!document.getElementById('offline-banner')) { const banner = document.createElement('div'); banner.id = 'offline-banner'; banner.className = `hidden fixed top-0 left-0 right-0 bg-amber-500 dark:bg-amber-600 text-white text-center py-2 px-4 z-50 ${this.darkMode ? 'dark-theme' : 'light-theme'}`; banner.textContent = 'Sie sind offline. Einige Funktionen sind eingeschränkt.'; document.body.prepend(banner); // Anzeigen wenn offline if (this.isOffline) { banner.classList.remove('hidden'); } // Event-Listener für Online/Offline-Status window.addEventListener('online', () => banner.classList.add('hidden')); window.addEventListener('offline', () => banner.classList.remove('hidden')); } // Synchronisierungs-Indikator einfügen wenn nicht vorhanden if (!document.getElementById('sync-indicator')) { const indicator = document.createElement('div'); indicator.id = 'sync-indicator'; indicator.className = `hidden fixed bottom-4 right-4 bg-indigo-600 dark:bg-indigo-700 text-white text-sm rounded-full py-1 px-3 z-50 flex items-center ${this.darkMode ? 'dark-theme' : 'light-theme'}`; const spinnerSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); spinnerSvg.setAttribute('class', 'animate-spin -ml-1 mr-2 h-4 w-4 text-white'); spinnerSvg.setAttribute('fill', 'none'); spinnerSvg.setAttribute('viewBox', '0 0 24 24'); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('class', 'opacity-25'); circle.setAttribute('cx', '12'); circle.setAttribute('cy', '12'); circle.setAttribute('r', '10'); circle.setAttribute('stroke', 'currentColor'); circle.setAttribute('stroke-width', '4'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('class', 'opacity-75'); path.setAttribute('fill', 'currentColor'); path.setAttribute('d', 'M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'); spinnerSvg.appendChild(circle); spinnerSvg.appendChild(path); const text = document.createElement('span'); text.textContent = 'Synchronisiere...'; indicator.appendChild(spinnerSvg); indicator.appendChild(text); document.body.appendChild(indicator); // Anzeigen wenn synchronisiert wird const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (document.body.classList.contains('syncing')) { indicator.classList.remove('hidden'); } else { indicator.classList.add('hidden'); } } }); }); observer.observe(document.body, { attributes: true }); } // Mobile Optimierungen this.setupMobileOptimizations(); } /** * Mobile Optimierungen */ setupMobileOptimizations() { // Service Worker-Status auf mobilen Geräten überwachen und UI entsprechend anpassen if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { // Verbessert Touch-Handhabung auf Mobilgeräten this.setupTouchFeedback(); // Viewport-Höhe für mobile Browser anpassen (Adressleisten-Problem) this.fixMobileViewportHeight(); // Scroll-Wiederherstellung deaktivieren für eine bessere UX auf Mobilgeräten if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } } } /** * Touch-Feedback für mobile Geräte */ setupTouchFeedback() { // Aktive Klasse für Touch-Feedback hinzufügen document.addEventListener('touchstart', function(e) { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.closest('button') || e.target.closest('a')) { const element = e.target.tagName === 'BUTTON' || e.target.tagName === 'A' ? e.target : (e.target.closest('button') || e.target.closest('a')); element.classList.add('touch-active'); } }, { passive: true }); document.addEventListener('touchend', function() { const activeElements = document.querySelectorAll('.touch-active'); activeElements.forEach(el => el.classList.remove('touch-active')); }, { passive: true }); } /** * Viewport-Höhe für mobile Browser fixieren */ fixMobileViewportHeight() { // Mobile Viewport-Höhe berechnen und als CSS-Variable setzen const setViewportHeight = () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); }; // Initial setzen und bei Größenänderung aktualisieren setViewportHeight(); window.addEventListener('resize', setViewportHeight); // Auch bei Orientierungsänderung aktualisieren window.addEventListener('orientationchange', () => { setTimeout(setViewportHeight, 100); }); } } // App initialisieren wenn DOM geladen ist document.addEventListener('DOMContentLoaded', () => { window.myp = new MYPApp(); });