/** * TASKMATE - Notification Manager * ================================ * Frontend-Verwaltung für das Benachrichtigungssystem */ import api from './api.js'; import store from './store.js'; class NotificationManager { constructor() { this.notifications = []; this.unreadCount = 0; this.isDropdownOpen = false; this.initialized = false; } /** * Initialisierung */ async init() { if (this.initialized) return; this.bindElements(); this.bindEvents(); await this.loadNotifications(); this.initialized = true; } /** * DOM-Elemente binden */ bindElements() { this.bellBtn = document.getElementById('notification-btn'); this.badge = document.getElementById('notification-badge'); this.dropdown = document.getElementById('notification-dropdown'); this.list = document.getElementById('notification-list'); this.emptyState = document.getElementById('notification-empty'); this.markAllBtn = document.getElementById('btn-mark-all-read'); this.bellContainer = document.getElementById('notification-bell'); } /** * Event-Listener binden */ bindEvents() { // Toggle Dropdown this.bellBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.toggleDropdown(); }); // Klick außerhalb schließt Dropdown document.addEventListener('click', (e) => { if (!this.bellContainer?.contains(e.target)) { this.closeDropdown(); } }); // Alle als gelesen markieren this.markAllBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.markAllAsRead(); }); // Klicks in der Liste this.list?.addEventListener('click', (e) => { const deleteBtn = e.target.closest('.notification-delete'); if (deleteBtn) { e.stopPropagation(); const id = parseInt(deleteBtn.dataset.delete); this.deleteNotification(id); return; } const item = e.target.closest('.notification-item'); if (item) { this.handleItemClick(item); } }); // WebSocket Events window.addEventListener('notification:new', (e) => { this.addNotification(e.detail.notification); }); window.addEventListener('notification:count', (e) => { this.updateBadge(e.detail.count); }); window.addEventListener('notification:deleted', (e) => { this.removeNotification(e.detail.notificationId); }); } /** * Benachrichtigungen vom Server laden */ async loadNotifications() { try { const data = await api.getNotifications(); this.notifications = data.notifications || []; this.unreadCount = data.unreadCount || 0; this.render(); } catch (error) { console.error('Fehler beim Laden der Benachrichtigungen:', error); } } /** * Alles rendern */ render() { this.updateBadge(this.unreadCount); this.renderList(); } /** * Badge aktualisieren */ updateBadge(count) { this.unreadCount = count; // Sicherstellen, dass badge-Element existiert if (!this.badge) { this.badge = document.getElementById('notification-badge'); } if (!this.badge) return; // Wenn immer noch nicht gefunden, abbrechen if (count > 0) { this.badge.textContent = count > 99 ? '99+' : count; this.badge.classList.remove('hidden'); this.bellContainer?.classList.add('has-notifications'); } else { this.badge.classList.add('hidden'); this.bellContainer?.classList.remove('has-notifications'); } } /** * Liste rendern */ renderList() { if (!this.notifications || this.notifications.length === 0) { this.list?.classList.add('hidden'); this.emptyState?.classList.remove('hidden'); return; } this.list?.classList.remove('hidden'); this.emptyState?.classList.add('hidden'); this.list.innerHTML = this.notifications.map(n => this.renderItem(n)).join(''); } /** * Einzelnes Item rendern */ renderItem(notification) { const timeAgo = this.formatTimeAgo(notification.createdAt); const iconClass = this.getIconClass(notification.type); const icon = this.getIcon(notification.type); return `
${icon}
${this.escapeHtml(notification.title)}
${notification.message ? `
${this.escapeHtml(notification.message)}
` : ''}
${timeAgo}
${!notification.isPersistent ? `
` : ''}
`; } /** * Icon-Klasse basierend auf Typ */ getIconClass(type) { if (type.startsWith('task:assigned') || type.startsWith('task:unassigned') || type.startsWith('task:due')) { return 'task'; } if (type.startsWith('task:completed')) { return 'completed'; } if (type.startsWith('task:priority')) { return 'priority'; } if (type.startsWith('comment:mention')) { return 'mention'; } if (type.startsWith('comment:')) { return 'comment'; } if (type.startsWith('approval:')) { return 'approval'; } if (type.startsWith('knowledge:')) { return 'knowledge'; } return 'task'; } /** * Icon SVG basierend auf Typ */ getIcon(type) { if (type.startsWith('task:assigned')) { return ''; } if (type.startsWith('task:unassigned')) { return ''; } if (type.startsWith('task:completed')) { return ''; } if (type.startsWith('task:due')) { return ''; } if (type.startsWith('task:priority')) { return ''; } if (type.startsWith('comment:mention')) { return ''; } if (type.startsWith('comment:')) { return ''; } if (type.startsWith('approval:pending')) { return ''; } if (type.startsWith('approval:granted')) { return ''; } if (type.startsWith('approval:rejected')) { return ''; } if (type.startsWith('knowledge:')) { return ''; } // Default return ''; } /** * Zeit-Formatierung */ formatTimeAgo(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Gerade eben'; if (diffMins < 60) return `Vor ${diffMins} Minute${diffMins === 1 ? '' : 'n'}`; if (diffHours < 24) return `Vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`; if (diffDays < 7) return `Vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`; return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } /** * HTML escapen */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Dropdown öffnen/schließen */ toggleDropdown() { if (this.isDropdownOpen) { this.closeDropdown(); } else { this.openDropdown(); } } openDropdown() { this.dropdown?.classList.remove('hidden'); this.isDropdownOpen = true; // Ungelesene als gelesen markieren wenn Dropdown geöffnet this.markVisibleAsRead(); } closeDropdown() { this.dropdown?.classList.add('hidden'); this.isDropdownOpen = false; } /** * Sichtbare Benachrichtigungen als gelesen markieren */ async markVisibleAsRead() { const unreadItems = this.notifications.filter(n => !n.isRead && !n.isPersistent); for (const notification of unreadItems) { try { await api.markNotificationRead(notification.id); notification.isRead = true; } catch (error) { console.error('Fehler beim Markieren als gelesen:', error); } } this.renderList(); } /** * Alle als gelesen markieren */ async markAllAsRead() { try { await api.markAllNotificationsRead(); this.notifications.forEach(n => { if (!n.isPersistent) n.isRead = true; }); this.unreadCount = this.notifications.filter(n => !n.isRead).length; this.updateBadge(this.unreadCount); this.renderList(); } catch (error) { console.error('Fehler beim Markieren aller als gelesen:', error); } } /** * Benachrichtigung löschen */ async deleteNotification(id) { try { const result = await api.deleteNotification(id); this.notifications = this.notifications.filter(n => n.id !== id); this.unreadCount = result.unreadCount; this.updateBadge(this.unreadCount); this.renderList(); } catch (error) { console.error('Fehler beim Löschen der Benachrichtigung:', error); } } /** * Neue Benachrichtigung hinzufügen */ addNotification(notification) { // Am Anfang der Liste hinzufügen this.notifications.unshift(notification); this.unreadCount++; this.updateBadge(this.unreadCount); this.renderList(); // Toast anzeigen wenn Dropdown geschlossen if (!this.isDropdownOpen) { this.showToast(notification.title, notification.message); } } /** * Benachrichtigung entfernen (WebSocket) */ removeNotification(id) { this.notifications = this.notifications.filter(n => n.id !== id); this.renderList(); } /** * Toast-Benachrichtigung anzeigen */ showToast(title, message) { // Einfache Toast-Implementation const toast = document.createElement('div'); toast.className = 'notification-toast'; toast.innerHTML = ` ${this.escapeHtml(title)} ${message ? `

${this.escapeHtml(message)}

` : ''} `; toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; padding: 16px 20px; background: var(--bg-card, #fff); border: 1px solid var(--border-default, #e2e8f0); border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.15); z-index: 9999; max-width: 320px; animation: slideIn 0.3s ease; `; // Animation const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(style); document.body.appendChild(toast); // Nach 4 Sekunden entfernen setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => { toast.remove(); style.remove(); }, 300); }, 4000); } /** * Item-Klick behandeln */ handleItemClick(item) { const taskId = item.dataset.taskId; const proposalId = item.dataset.proposalId; const entryId = item.dataset.entryId; const categoryId = item.dataset.categoryId; if (taskId) { // Zur Aufgabe navigieren this.closeDropdown(); window.dispatchEvent(new CustomEvent('notification:open-task', { detail: { taskId: parseInt(taskId) } })); } else if (proposalId) { // Zum Genehmigung-Tab wechseln this.closeDropdown(); window.dispatchEvent(new CustomEvent('notification:open-proposal', { detail: { proposalId: parseInt(proposalId) } })); } else if (entryId && categoryId) { // Zum Wissenseintrag navigieren this.closeDropdown(); window.dispatchEvent(new CustomEvent('notification:open-knowledge', { detail: { entryId: parseInt(entryId), categoryId: parseInt(categoryId) }})); } } /** * Reset */ reset() { this.notifications = []; this.unreadCount = 0; this.isDropdownOpen = false; this.closeDropdown(); // Elements neu binden falls nötig if (!this.badge || !this.bellContainer) { this.bindElements(); } this.render(); } } // Singleton-Instanz const notificationManager = new NotificationManager(); export { notificationManager }; export default notificationManager;