import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Badge, Box, Button, CircularProgress, Divider, IconButton, List, ListItem, ListItemButton, ListItemText, Popover, Tooltip, Typography, } from '@mui/material'; import { Notifications as BellIcon, NotificationsNone as BellEmptyIcon, Circle as CircleIcon, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import { notificationsApi } from '../../services/notifications'; import type { Notification, NotificationSchwere } from '../../types/notification.types'; const POLL_INTERVAL_MS = 60_000; // 60 seconds function schwerebColor(schwere: NotificationSchwere): 'error' | 'warning' | 'info' { if (schwere === 'fehler') return 'error'; if (schwere === 'warnung') return 'warning'; return 'info'; } function formatRelative(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const minutes = Math.floor(diff / 60_000); if (minutes < 2) return 'Gerade eben'; if (minutes < 60) return `vor ${minutes} Min.`; const hours = Math.floor(minutes / 60); if (hours < 24) return `vor ${hours} Std.`; const days = Math.floor(hours / 24); return `vor ${days} Tag${days !== 1 ? 'en' : ''}`; } const NotificationBell: React.FC = () => { const navigate = useNavigate(); const [unreadCount, setUnreadCount] = useState(0); const [notifications, setNotifications] = useState([]); const [loadingList, setLoadingList] = useState(false); const [anchorEl, setAnchorEl] = useState(null); const pollTimerRef = useRef | null>(null); const fetchUnreadCount = useCallback(async () => { try { const count = await notificationsApi.getUnreadCount(); setUnreadCount(count); } catch { // non-critical } }, []); // Poll unread count every 60 seconds useEffect(() => { fetchUnreadCount(); pollTimerRef.current = setInterval(fetchUnreadCount, POLL_INTERVAL_MS); return () => { if (pollTimerRef.current) clearInterval(pollTimerRef.current); }; }, [fetchUnreadCount]); const handleOpen = async (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); setLoadingList(true); try { const data = await notificationsApi.getNotifications(); setNotifications(data); // Refresh count after loading full list const count = await notificationsApi.getUnreadCount(); setUnreadCount(count); } catch { // non-critical } finally { setLoadingList(false); } }; const handleClose = () => { setAnchorEl(null); }; const handleClickNotification = async (n: Notification) => { if (!n.gelesen) { try { await notificationsApi.markRead(n.id); setNotifications((prev) => prev.map((item) => item.id === n.id ? { ...item, gelesen: true } : item) ); setUnreadCount((c) => Math.max(0, c - 1)); } catch { // non-critical } } handleClose(); if (n.link) { if (n.link.startsWith('http://') || n.link.startsWith('https://')) { window.open(n.link, '_blank'); } else { navigate(n.link); } } }; const handleMarkAllRead = async () => { try { await notificationsApi.markAllRead(); setNotifications((prev) => prev.map((n) => ({ ...n, gelesen: true }))); setUnreadCount(0); } catch { // non-critical } }; const open = Boolean(anchorEl); const hasUnread = unreadCount > 0; return ( <> {hasUnread ? : } {/* Header */} Benachrichtigungen {unreadCount > 0 && ( )} {/* Body */} {loadingList ? ( ) : notifications.length === 0 ? ( Keine Benachrichtigungen ) : ( {notifications.map((n, idx) => ( {idx > 0 && } handleClickNotification(n)} sx={{ py: 1.5, px: 2, bgcolor: n.gelesen ? 'transparent' : 'action.hover', alignItems: 'flex-start', gap: 1, }} > {n.titel} } secondary={ {n.nachricht} {formatRelative(n.erstellt_am)} } disableTypography /> ))} )} ); }; export default NotificationBell;