"feat: Implement new notification system in frontend
This commit is contained in:
287
backend/app/static/js/notifications.js
Normal file
287
backend/app/static/js/notifications.js
Normal file
@@ -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 = `
|
||||
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
|
||||
Keine neuen Benachrichtigungen
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationHTML = this.notifications.map(notification => {
|
||||
const isUnread = !notification.read;
|
||||
const timeAgo = this.formatTimeAgo(new Date(notification.created_at));
|
||||
|
||||
return `
|
||||
<div class="notification-item p-4 border-b border-slate-200 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}"
|
||||
data-notification-id="${notification.id}">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
${this.getNotificationIcon(notification.type)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-slate-900 dark:text-white">
|
||||
${this.getNotificationTitle(notification.type)}
|
||||
</p>
|
||||
${isUnread ? '<div class="w-2 h-2 bg-blue-500 rounded-full"></div>' : ''}
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||
${this.getNotificationMessage(notification)}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-500 mt-2">
|
||||
${timeAgo}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).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': `
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
'job_completed': `
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
'system': `
|
||||
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-900 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
Reference in New Issue
Block a user