2025-06-04 10:03:22 +02:00

521 lines
20 KiB
JavaScript

/**
* 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();
});