521 lines
20 KiB
JavaScript
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();
|
|
}); |