From c256848d591452f818e90d4d8b8e533ce3503fe5 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 10:04:55 +0200 Subject: [PATCH] "feat: Implement new notification system in frontend --- backend/app/app.py | 95 ++++++++ backend/app/database/myp.db-shm | Bin 32768 -> 32768 bytes backend/app/database/myp.db-wal | Bin 226632 -> 259592 bytes backend/app/static/js/notifications.js | 287 +++++++++++++++++++++++++ backend/app/templates/base.html | 38 ++++ backend/app/utils/logging_config.py | 77 +++++-- 6 files changed, 478 insertions(+), 19 deletions(-) create mode 100644 backend/app/static/js/notifications.js diff --git a/backend/app/app.py b/backend/app/app.py index 2594ca941..615c27771 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -3957,6 +3957,101 @@ def export_admin_logs(): # ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE ===== +# ===== BENACHRICHTIGUNGS-API-ENDPUNKTE ===== + +@app.route('/api/notifications', methods=['GET']) +@login_required +def get_notifications(): + """Holt alle Benachrichtigungen für den aktuellen Benutzer""" + try: + db_session = get_db_session() + + # Benachrichtigungen für den aktuellen Benutzer laden + notifications = db_session.query(Notification).filter( + Notification.user_id == current_user.id + ).order_by(Notification.created_at.desc()).limit(50).all() + + notifications_data = [notification.to_dict() for notification in notifications] + + db_session.close() + + return jsonify({ + "success": True, + "notifications": notifications_data + }) + + except Exception as e: + app_logger.error(f"Fehler beim Laden der Benachrichtigungen: {str(e)}") + return jsonify({ + "success": False, + "message": f"Fehler beim Laden der Benachrichtigungen: {str(e)}" + }), 500 + +@app.route('/api/notifications//read', methods=['POST']) +@login_required +def mark_notification_read(notification_id): + """Markiert eine Benachrichtigung als gelesen""" + try: + db_session = get_db_session() + + notification = db_session.query(Notification).filter( + Notification.id == notification_id, + Notification.user_id == current_user.id + ).first() + + if not notification: + db_session.close() + return jsonify({ + "success": False, + "message": "Benachrichtigung nicht gefunden" + }), 404 + + notification.read = True + db_session.commit() + db_session.close() + + return jsonify({ + "success": True, + "message": "Benachrichtigung als gelesen markiert" + }) + + except Exception as e: + app_logger.error(f"Fehler beim Markieren der Benachrichtigung: {str(e)}") + return jsonify({ + "success": False, + "message": f"Fehler beim Markieren: {str(e)}" + }), 500 + +@app.route('/api/notifications/mark-all-read', methods=['POST']) +@login_required +def mark_all_notifications_read(): + """Markiert alle Benachrichtigungen als gelesen""" + try: + db_session = get_db_session() + + # Alle ungelesenen Benachrichtigungen des Benutzers finden und als gelesen markieren + updated_count = db_session.query(Notification).filter( + Notification.user_id == current_user.id, + Notification.read == False + ).update({"read": True}) + + db_session.commit() + db_session.close() + + return jsonify({ + "success": True, + "message": f"{updated_count} Benachrichtigungen als gelesen markiert" + }) + + except Exception as e: + app_logger.error(f"Fehler beim Markieren aller Benachrichtigungen: {str(e)}") + return jsonify({ + "success": False, + "message": f"Fehler beim Markieren: {str(e)}" + }), 500 + +# ===== ENDE BENACHRICHTIGUNGS-API-ENDPUNKTE ===== + # ===== STARTUP UND MAIN ===== if __name__ == "__main__": diff --git a/backend/app/database/myp.db-shm b/backend/app/database/myp.db-shm index 6e8e3101693209042e46fea6debc668a01b67290..5cfb43b2ad0baffc4a1845666cf8c7894f7dbded 100644 GIT binary patch delta 175 zcmZo@U}|V!s+V}A%K!q*K+MR%Am9L`1%cQm_vrWRv%kbnrY8uNxg>w7llV7xlFHLB zq^buR1qKd4@;?%Qh%zuS*iLNxJ@EqzD-%$JX>%iErQ_y*P8`gvb`16m4nY2Ej?Lfv GxC{XqZaDD( delta 164 zcmZo@U}|V!s+V}A%K!qLK+MR%AYcom1%dd*{D&{z9#qjdnVuk6=92tndQrkehGo64 xNL3Fs3JgHz{zn2(VI~HXiH*NEPhre*oV6k5UN+1_o9j7Cf1rAXw&-{3Yv7Nlm4RZq{apjq7;$4GpXe zjI2xy^vsOR%uOt|>po=aVVZ7#h>2(V-Oo(xL{YSUo;Ss}f3brOPOVQE(G8sLu#j;k zF7qU}sVs6ld`ttUdG?G#Xbx;tVw(K%o!I0x?`ui2>fFQ|)h)pald)Pgd2a12bkiok zdpC_*mS1_XflFX*>a%A)rMKQJ2;i=`9t?|zxre;=VSC$6m=cE<+W*Zxa u7nK=!mm8KwB^#GUgr%Ejho^?-=UZkw<{PA%g}X=jCg&NaMkRtwbprsrUKiQ` delta 15 WcmeBp!++uxZ$k@X3)2>6k5T|RNd`gy diff --git a/backend/app/static/js/notifications.js b/backend/app/static/js/notifications.js new file mode 100644 index 000000000..3a4f35207 --- /dev/null +++ b/backend/app/static/js/notifications.js @@ -0,0 +1,287 @@ +/** + * Benachrichtigungssystem für die MYP 3D-Druck Platform + * Verwaltet die Anzeige und Interaktion mit Benachrichtigungen + */ + +class NotificationManager { + 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.init(); + } + + 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: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + // Benachrichtigung als gelesen markieren + const notification = this.notifications.find(n => n.id == notificationId); + if (notification) { + notification.read = true; + this.updateUI(); + } + } + } 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: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + // Alle Benachrichtigungen als gelesen markieren + this.notifications.forEach(notification => { + notification.read = true; + }); + this.updateUI(); + } + } catch (error) { + console.error('Fehler beim Markieren aller als gelesen:', error); + } + } +} + +// Initialisierung nach DOM-Load +document.addEventListener('DOMContentLoaded', () => { + new NotificationManager(); +}); \ No newline at end of file diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index 04a5684c0..2a1f4029c 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -252,6 +252,41 @@ {% if current_user.is_authenticated %} + +
+ + + + +
+