import { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { getUnreadCount, getNotifications, markAsRead, type Notification } from '../services/notificationService'; import { useWebSocket } from '../hooks/useWebSocket'; import { wsLogger } from '../utils/logger'; const NotificationBell = () => { const [unreadCount, setUnreadCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const { subscribe, unsubscribe, isConnected } = useWebSocket(); // WebSocket обработчик событий // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleWebSocketEvent = useCallback((event: any) => { if (event.type === 'notification:new') { // Добавляем новое уведомление в начало списка setNotifications((prev) => [event.notification, ...prev.slice(0, 4)]); setUnreadCount((prev) => prev + 1); wsLogger.log('Получено новое уведомление:', event.notification); } else if (event.type === 'notification:read') { // Помечаем уведомление как прочитанное setNotifications((prev) => prev.map((n) => (n.id === event.notificationId ? { ...n, isRead: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); wsLogger.log('Уведомление помечено прочитанным:', event.notificationId); } else if (event.type === 'notification:delete') { // Удаляем уведомление из списка // Если оно было непрочитанным - уменьшаем счётчик setNotifications((prev) => { const notification = prev.find((n) => n.id === event.notificationId); if (notification && !notification.isRead) { setUnreadCount((count) => Math.max(0, count - 1)); } return prev.filter((n) => n.id !== event.notificationId); }); wsLogger.log('Уведомление удалено:', event.notificationId); } }, []); // Подписка на WebSocket при монтировании useEffect(() => { if (isConnected) { subscribe('notifications', handleWebSocketEvent); wsLogger.log('Подписались на уведомления'); } return () => { if (isConnected) { unsubscribe('notifications', handleWebSocketEvent); wsLogger.log('Отписались от уведомлений'); } }; }, [isConnected, subscribe, unsubscribe, handleWebSocketEvent]); // Загрузка количества непрочитанных при монтировании useEffect(() => { loadUnreadCount(); }, []); // Загрузка последних уведомлений при открытии дропдауна useEffect(() => { if (isOpen) { loadNotifications(); } }, [isOpen]); const loadUnreadCount = async () => { try { const count = await getUnreadCount(); setUnreadCount(count); } catch (error) { console.error('Ошибка загрузки количества уведомлений:', error); } }; const loadNotifications = async () => { setLoading(true); try { const response = await getNotifications({ page: 1, limit: 5 }); // Проверяем, что response имеет правильную структуру if (response && Array.isArray(response.notifications)) { setNotifications(response.notifications); } else { console.error('Неверный формат ответа от сервера:', response); setNotifications([]); } } catch (error) { console.error('Ошибка загрузки уведомлений:', error); setNotifications([]); } finally { setLoading(false); } }; const handleNotificationClick = async (notification: Notification) => { if (!notification.isRead) { try { await markAsRead(notification.id); setUnreadCount((prev) => Math.max(0, prev - 1)); setNotifications((prev) => prev.map((n) => (n.id === notification.id ? { ...n, isRead: true } : n)) ); } catch (error) { console.error('Ошибка пометки уведомления прочитанным:', error); } } setIsOpen(false); }; const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (diffInSeconds < 60) return 'только что'; if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} мин назад`; if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} ч назад`; if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} д назад`; return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); }; return (
{/* Иконка колокольчика */} {/* Дропдаун */} {isOpen && ( <> {/* Оверлей для закрытия при клике вне */}
setIsOpen(false)} />
{/* Заголовок */}

Уведомления

setIsOpen(false)} className="text-sm text-ospab-primary hover:underline" > Все
{/* Список уведомлений */}
{loading ? (
) : notifications.length === 0 ? (

Нет уведомлений

) : ( notifications.map((notification) => ( handleNotificationClick(notification)} className={`block px-4 py-3 hover:bg-gray-50 transition-colors border-l-4 ${ notification.isRead ? 'border-transparent' : 'border-ospab-primary bg-blue-50' }`} >
{/* Цветовой индикатор вместо иконки */}
{/* Содержимое */}

{notification.title}

{notification.message}

{formatDate(notification.createdAt)}

{/* Индикатор непрочитанного */} {!notification.isRead && (
)}
)) )}
{/* Футер с кнопкой */} {notifications.length > 0 && (
setIsOpen(false)} className="block w-full text-center py-2 text-sm text-ospab-primary hover:bg-gray-50 rounded-md transition-colors" > Показать все уведомления
)}
)}
); }; export default NotificationBell;