Projektarbeit-MYP/backend/static/js/notifications.js
2025-05-31 22:40:29 +02:00

315 lines
12 KiB
JavaScript

/**
* 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 = [];
// CSRF-Token aus Meta-Tag holen
this.csrfToken = this.getCSRFToken();
this.init();
}
/**
* 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 = `
<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: 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);
}
}
}
// Initialisierung nach DOM-Load
document.addEventListener('DOMContentLoaded', () => {
new NotificationManager();
});