/** * Modernes Benachrichtigungssystem für die MYP 3D-Druck Platform * Verwaltet einheitliche Glassmorphism-Benachrichtigungen aller Art */ class ModernNotificationManager { constructor() { this.notificationToggle = document.getElementById('notificationToggle'); this.notificationDropdown = document.getElementById('notificationDropdown'); this.notificationBadge = document.getElementById('notificationBadge'); this.notificationList = document.getElementById('notificationList'); this.markAllReadBtn = document.getElementById('markAllRead'); this.isOpen = false; this.notifications = []; this.activeToasts = new Map(); this.toastCounter = 0; // CSRF-Token aus Meta-Tag holen this.csrfToken = this.getCSRFToken(); this.init(); this.setupGlobalNotificationSystem(); } /** * Holt das CSRF-Token aus dem Meta-Tag * @returns {string} Das CSRF-Token */ getCSRFToken() { const metaTag = document.querySelector('meta[name="csrf-token"]'); return metaTag ? metaTag.getAttribute('content') : ''; } /** * Erstellt die Standard-Headers für API-Anfragen mit CSRF-Token * @returns {Object} Headers-Objekt */ getAPIHeaders() { const headers = { 'Content-Type': 'application/json', }; if (this.csrfToken) { headers['X-CSRFToken'] = this.csrfToken; } return headers; } init() { if (!this.notificationToggle) return; // Event Listeners this.notificationToggle.addEventListener('click', (e) => { e.stopPropagation(); this.toggleDropdown(); }); if (this.markAllReadBtn) { this.markAllReadBtn.addEventListener('click', () => { this.markAllAsRead(); }); } // Dropdown schließen bei Klick außerhalb document.addEventListener('click', (e) => { if (!this.notificationDropdown.contains(e.target) && !this.notificationToggle.contains(e.target)) { this.closeDropdown(); } }); // Benachrichtigungen laden this.loadNotifications(); // Regelmäßige Updates setInterval(() => { this.loadNotifications(); }, 30000); // Alle 30 Sekunden } toggleDropdown() { if (this.isOpen) { this.closeDropdown(); } else { this.openDropdown(); } } openDropdown() { this.notificationDropdown.classList.remove('hidden'); this.notificationToggle.setAttribute('aria-expanded', 'true'); this.isOpen = true; // Animation this.notificationDropdown.style.opacity = '0'; this.notificationDropdown.style.transform = 'translateY(-10px)'; requestAnimationFrame(() => { this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease'; this.notificationDropdown.style.opacity = '1'; this.notificationDropdown.style.transform = 'translateY(0)'; }); } closeDropdown() { this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease'; this.notificationDropdown.style.opacity = '0'; this.notificationDropdown.style.transform = 'translateY(-10px)'; setTimeout(() => { this.notificationDropdown.classList.add('hidden'); this.notificationToggle.setAttribute('aria-expanded', 'false'); this.isOpen = false; }, 200); } async loadNotifications() { try { const response = await fetch('/api/notifications'); if (response.ok) { const data = await response.json(); this.notifications = data.notifications || []; this.updateUI(); } } catch (error) { console.error('Fehler beim Laden der Benachrichtigungen:', error); } } updateUI() { this.updateBadge(); this.updateNotificationList(); } updateBadge() { const unreadCount = this.notifications.filter(n => !n.read).length; if (unreadCount > 0) { this.notificationBadge.textContent = unreadCount > 99 ? '99+' : unreadCount.toString(); this.notificationBadge.classList.remove('hidden'); } else { this.notificationBadge.classList.add('hidden'); } } updateNotificationList() { if (this.notifications.length === 0) { this.notificationList.innerHTML = `
Keine neuen Benachrichtigungen
`; return; } const notificationHTML = this.notifications.map(notification => { const isUnread = !notification.read; const timeAgo = this.formatTimeAgo(new Date(notification.created_at)); return `
${this.getNotificationIcon(notification.type)}

${this.getNotificationTitle(notification.type)}

${isUnread ? '
' : ''}

${this.getNotificationMessage(notification)}

${timeAgo}

`; }).join(''); this.notificationList.innerHTML = notificationHTML; // Event Listeners für Benachrichtigungen this.notificationList.querySelectorAll('.notification-item').forEach(item => { item.addEventListener('click', (e) => { const notificationId = item.dataset.notificationId; this.markAsRead(notificationId); }); }); } getNotificationIcon(type) { const icons = { 'guest_request': `
`, 'job_completed': `
`, 'system': `
` }; return icons[type] || icons['system']; } getNotificationTitle(type) { const titles = { 'guest_request': 'Neue Gastanfrage', 'job_completed': 'Druckauftrag abgeschlossen', 'system': 'System-Benachrichtigung' }; return titles[type] || 'Benachrichtigung'; } getNotificationMessage(notification) { try { const payload = JSON.parse(notification.payload || '{}'); switch (notification.type) { case 'guest_request': return `${payload.guest_name || 'Ein Gast'} hat eine neue Druckanfrage gestellt.`; case 'job_completed': return `Der Druckauftrag "${payload.job_name || 'Unbekannt'}" wurde erfolgreich abgeschlossen.`; default: return payload.message || 'Neue Benachrichtigung erhalten.'; } } catch (error) { return 'Neue Benachrichtigung erhalten.'; } } formatTimeAgo(date) { const now = new Date(); const diffInSeconds = Math.floor((now - date) / 1000); if (diffInSeconds < 60) { return 'Gerade eben'; } else if (diffInSeconds < 3600) { const minutes = Math.floor(diffInSeconds / 60); return `vor ${minutes} Minute${minutes !== 1 ? 'n' : ''}`; } else if (diffInSeconds < 86400) { const hours = Math.floor(diffInSeconds / 3600); return `vor ${hours} Stunde${hours !== 1 ? 'n' : ''}`; } else { const days = Math.floor(diffInSeconds / 86400); return `vor ${days} Tag${days !== 1 ? 'en' : ''}`; } } async markAsRead(notificationId) { try { const response = await fetch(`/api/notifications/${notificationId}/read`, { method: 'POST', headers: this.getAPIHeaders() }); if (response.ok) { // Benachrichtigung als gelesen markieren const notification = this.notifications.find(n => n.id == notificationId); if (notification) { notification.read = true; this.updateUI(); } } else { console.error('Fehler beim Markieren als gelesen:', response.status, response.statusText); } } catch (error) { console.error('Fehler beim Markieren als gelesen:', error); } } async markAllAsRead() { try { const response = await fetch('/api/notifications/mark-all-read', { method: 'POST', headers: this.getAPIHeaders() }); if (response.ok) { // Alle Benachrichtigungen als gelesen markieren this.notifications.forEach(notification => { notification.read = true; }); this.updateUI(); } else { console.error('Fehler beim Markieren aller als gelesen:', response.status, response.statusText); } } catch (error) { console.error('Fehler beim Markieren aller als gelesen:', error); } } setupGlobalNotificationSystem() { // Globale Funktionen für einheitliche Benachrichtigungen window.showFlashMessage = this.showGlassToast.bind(this); window.showToast = this.showGlassToast.bind(this); window.showNotification = this.showGlassToast.bind(this); window.showSuccessMessage = (message) => this.showGlassToast(message, 'success'); window.showErrorMessage = (message) => this.showGlassToast(message, 'error'); window.showWarningMessage = (message) => this.showGlassToast(message, 'warning'); window.showInfoMessage = (message) => this.showGlassToast(message, 'info'); } /** * Zeigt eine moderne Glassmorphism-Toast-Benachrichtigung */ showGlassToast(message, type = 'info', duration = 5000, options = {}) { // DND-Check if (window.dndManager && window.dndManager.isEnabled) { window.dndManager.suppressNotification(message, type); return; } const toastId = `toast-${++this.toastCounter}`; // Toast-Element erstellen const toast = document.createElement('div'); toast.id = toastId; toast.className = `glass-toast notification notification-${type} show`; // Icon basierend auf Typ const iconMap = { success: ` `, error: ` `, warning: ` `, info: ` ` }; toast.innerHTML = `
${iconMap[type] || iconMap.info}
${options.title ? `
${options.title}
` : ''}
${message}
`; // Container für Toasts erstellen falls nicht vorhanden let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'notifications-container'; document.body.appendChild(container); } // Position berechnen basierend auf existierenden Toasts const existingToasts = container.children.length; toast.style.top = `${1 + existingToasts * 5}rem`; container.appendChild(toast); this.activeToasts.set(toastId, toast); // Hover-Effekt pausiert Auto-Close let isPaused = false; let timeoutId; const startTimer = () => { if (!isPaused && duration > 0) { timeoutId = setTimeout(() => { this.closeToast(toastId); }, duration); } }; toast.addEventListener('mouseenter', () => { isPaused = true; clearTimeout(timeoutId); toast.style.transform = 'translateY(-2px) scale(1.02)'; }); toast.addEventListener('mouseleave', () => { isPaused = false; toast.style.transform = 'translateY(0) scale(1)'; startTimer(); }); // Initial timer starten setTimeout(startTimer, 100); // Sound-Effekt (optional) if (options.playSound !== false) { this.playNotificationSound(type); } return toastId; } /** * Schließt eine spezifische Toast-Benachrichtigung */ closeToast(toastId) { const toast = this.activeToasts.get(toastId); if (!toast) return; toast.classList.add('hiding'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } this.activeToasts.delete(toastId); this.repositionToasts(); }, 400); } /** * Repositioniert alle aktiven Toasts */ repositionToasts() { let index = 0; this.activeToasts.forEach((toast) => { if (toast.parentNode) { toast.style.top = `${1 + index * 5}rem`; index++; } }); } /** * Spielt einen Benachrichtigungston ab */ playNotificationSound(type) { try { // Nur wenn AudioContext verfügbar ist if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); // Verschiedene Töne für verschiedene Typen const frequencies = { success: [523.25, 659.25, 783.99], // C-E-G Akkord error: [440, 415.3], // A-Ab warning: [493.88, 523.25], // B-C info: [523.25] // C }; const freq = frequencies[type] || frequencies.info; freq.forEach((f, i) => { setTimeout(() => { const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination); osc.frequency.setValueAtTime(f, audioContext.currentTime); gain.gain.setValueAtTime(0.1, audioContext.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); osc.start(audioContext.currentTime); osc.stop(audioContext.currentTime + 0.1); }, i * 100); }); } } catch (error) { // Silent fail - Audio nicht verfügbar } } /** * Zeigt Browser-Benachrichtigung (falls berechtigt) */ showBrowserNotification(title, message, options = {}) { if ('Notification' in window) { if (Notification.permission === 'granted') { const notification = new Notification(title, { body: message, icon: options.icon || '/static/icons/static/icons/notification-icon.png', badge: options.badge || '/static/icons/static/icons/badge-icon.png', tag: options.tag || 'myp-notification', requireInteraction: options.requireInteraction || false, ...options }); notification.onclick = options.onClick || (() => { window.focus(); notification.close(); }); return notification; } else if (Notification.permission === 'default') { Notification.requestPermission().then(permission => { if (permission === 'granted') { this.showBrowserNotification(title, message, options); } }); } } return null; } /** * Zeigt eine Alert-Benachrichtigung mit Glassmorphism */ showAlert(message, type = 'info', options = {}) { const alertId = `alert-${Date.now()}`; const alert = document.createElement('div'); alert.id = alertId; alert.className = `alert alert-${type}`; alert.innerHTML = `
${this.getIconForType(type)}
${options.title ? `

${options.title}

` : ''}

${message}

${options.dismissible !== false ? ` ` : ''}
`; // Alert in Container einfügen const container = options.container || document.querySelector('.flash-messages') || document.body; container.appendChild(alert); // Auto-dismiss if (options.autoDismiss !== false) { setTimeout(() => { if (alert.parentNode) { alert.style.opacity = '0'; alert.style.transform = 'translateY(-20px)'; setTimeout(() => alert.remove(), 300); } }, options.duration || 7000); } return alertId; } getIconForType(type) { const icons = { success: ` `, error: ` `, warning: ` `, info: ` ` }; return icons[type] || icons.info; } } // Legacy NotificationManager für Rückwärtskompatibilität class NotificationManager extends ModernNotificationManager { constructor() { super(); console.warn('NotificationManager ist deprecated. Verwenden Sie ModernNotificationManager.'); } } // Globale Instanz erstellen const modernNotificationManager = new ModernNotificationManager(); // Für Rückwärtskompatibilität if (typeof window !== 'undefined') { window.notificationManager = modernNotificationManager; window.modernNotificationManager = modernNotificationManager; } // CSS für glassmorphe Toasts hinzufügen const toastStyles = ` `; // Styles zur Seite hinzufügen if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) { const styleElement = document.createElement('div'); styleElement.id = 'toast-styles'; styleElement.innerHTML = toastStyles; document.head.appendChild(styleElement); }