// Copyright (c) 2026 PowerOn AG // All rights reserved. /** * 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 { FloatingPortal } from '../UiComponents/FloatingPortal'; import styles from './NotificationBell.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; const typeIcons: Record = { invitation: , system: , workflow: , mention: , }; interface NotificationBellProps { className?: string; } export const NotificationBell: React.FC = ({ className }) => { const { t } = useLanguage(); const bellButtonRef = useRef(null); const formatRelativeTime = useCallback((timestamp: number): string => { if (!Number.isFinite(timestamp) || timestamp <= 0) { return ''; } const now = Date.now() / 1000; const diff = now - timestamp; if (diff < 60) return t('Gerade eben'); if (diff < 3600) return t('vor {minutes} Min.', { minutes: String(Math.floor(diff / 60)) }); if (diff < 86400) return t('vor {hours} Std.', { hours: String(Math.floor(diff / 3600)) }); if (diff < 604800) return t('vor {days} Tagen', { days: String(Math.floor(diff / 86400)) }); const date = new Date(timestamp * 1000); if (Number.isNaN(date.getTime())) { return ''; } return date.toLocaleDateString('de-DE'); }, [t]); 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); useEffect(() => { startPolling(30000); return () => stopPolling(); }, [startPolling, stopPolling]); useEffect(() => { if (isOpen) { fetchNotifications({ limit: 10 }); } }, [isOpen, fetchNotifications]); 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); if (actionId === 'accept' && notification.referenceType === 'Invitation') { window.dispatchEvent(new CustomEvent('features-changed')); } setTimeout(() => { setActionSuccess(null); fetchNotifications({ limit: 10 }); }, 2000); } }, [executeAction, fetchNotifications]); const handleDismiss = useCallback(async ( notification: UserNotification, event: React.MouseEvent, ) => { event.stopPropagation(); await dismissNotification(notification.id); }, [dismissNotification]); const handleNotificationClick = useCallback(async (notification: UserNotification) => { if (notification.status === 'unread') { await markAsRead(notification.id); } }, [markAsRead]); const visibleNotifications = notifications.filter(n => n.status !== 'dismissed'); return (
setIsOpen(false)} placement="auto" align="end" >

{t('Benachrichtigungen')}

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

{t('Keine Benachrichtigungen')}

)} {visibleNotifications.map(notification => (
handleNotificationClick(notification)} > {actionSuccess === notification.id && (
{notification.actionResult || t('Erfolgreich')}
)}
{typeIcons[notification.type] || }
{notification.title}
{notification.message}
{formatRelativeTime(notification.createdAt)}
{notification.actions && notification.status !== 'actioned' && (
{notification.actions.map(action => ( ))}
)} {notification.actionTaken && (
{notification.actionResult}
)}
{notification.status !== 'actioned' && ( )}
))}
); }; export default NotificationBell;