/** * NotificationBell Component * * Displays a bell icon with unread count badge. * Clicking opens a dropdown with recent notifications. */ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { FaBell, FaCheck, FaTimes, FaEnvelope, FaCog, FaExclamationTriangle, FaCheckCircle } from 'react-icons/fa'; import { useNotifications, UserNotification } from '../../hooks/useNotifications'; import styles from './NotificationBell.module.css'; // Icon mapping for notification types const typeIcons: Record = { invitation: , system: , workflow: , mention: }; // Format timestamp to relative time (Unix seconds) function formatRelativeTime(timestamp: number): string { if (!Number.isFinite(timestamp) || timestamp <= 0) { return ''; } const now = Date.now() / 1000; const diff = now - timestamp; if (diff < 60) return 'Gerade eben'; if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`; if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`; if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`; const date = new Date(timestamp * 1000); if (Number.isNaN(date.getTime())) { return ''; } return date.toLocaleDateString('de-DE'); } interface NotificationBellProps { className?: string; } export const NotificationBell: React.FC = ({ className }) => { const { notifications, unreadCount, loading, error, fetchNotifications, markAsRead, markAllAsRead, executeAction, dismissNotification, startPolling, stopPolling } = useNotifications(); const [isOpen, setIsOpen] = useState(false); const [actionLoading, setActionLoading] = useState(null); const [actionSuccess, setActionSuccess] = useState(null); const dropdownRef = useRef(null); // Start polling on mount useEffect(() => { startPolling(30000); // Poll every 30 seconds return () => stopPolling(); }, [startPolling, stopPolling]); // Fetch notifications when dropdown opens useEffect(() => { if (isOpen) { fetchNotifications({ limit: 10 }); } }, [isOpen, fetchNotifications]); // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isOpen]); // Handle action button click const handleAction = useCallback(async ( notification: UserNotification, actionId: string, event: React.MouseEvent ) => { event.stopPropagation(); setActionLoading(`${notification.id}-${actionId}`); const result = await executeAction(notification.id, actionId); setActionLoading(null); if (result) { setActionSuccess(notification.id); // Reload sidebar when accepting an invitation (grants new mandate/feature access) if (actionId === 'accept' && notification.referenceType === 'Invitation') { window.dispatchEvent(new CustomEvent('features-changed')); } // Clear success state after animation setTimeout(() => { setActionSuccess(null); fetchNotifications({ limit: 10 }); }, 2000); } }, [executeAction, fetchNotifications]); // Handle dismiss const handleDismiss = useCallback(async ( notification: UserNotification, event: React.MouseEvent ) => { event.stopPropagation(); await dismissNotification(notification.id); }, [dismissNotification]); // Handle notification click (mark as read) const handleNotificationClick = useCallback(async (notification: UserNotification) => { if (notification.status === 'unread') { await markAsRead(notification.id); } }, [markAsRead]); // Filter out dismissed notifications const visibleNotifications = notifications.filter(n => n.status !== 'dismissed'); return (
{/* Bell Button */} {/* Dropdown */} {isOpen && (
{/* Header */}

Benachrichtigungen

{visibleNotifications.some(n => n.status === 'unread') && ( )}
{/* Content */}
{loading && visibleNotifications.length === 0 && (
Lade...
)} {error && (
{error}
)} {!loading && !error && visibleNotifications.length === 0 && (

Keine Benachrichtigungen

)} {visibleNotifications.map(notification => (
handleNotificationClick(notification)} > {/* Success overlay */} {actionSuccess === notification.id && (
{notification.actionResult || 'Erfolgreich'}
)} {/* Icon */}
{typeIcons[notification.type] || }
{/* Content */}
{notification.title}
{notification.message}
{formatRelativeTime(notification.createdAt)}
{/* Actions */} {notification.actions && notification.status !== 'actioned' && (
{notification.actions.map(action => ( ))}
)} {/* Action result */} {notification.actionTaken && (
{notification.actionResult}
)}
{/* Dismiss button */} {notification.status !== 'actioned' && ( )}
))}
)}
); }; export default NotificationBell;