import { useState, useRef, useEffect, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { ticketNotificationsApi } from '../api/ticketNotifications'; import { useAuthStore } from '../store/auth'; import { useToast } from './Toast'; import { useWebSocket, WSMessage } from '../hooks/useWebSocket'; import { useHeaderHeight } from '../hooks/useHeaderHeight'; import type { TicketNotification } from '../types'; import { BellIcon, CheckIcon } from '@/components/icons'; interface TicketNotificationBellProps { isAdmin?: boolean; } export default function TicketNotificationBell({ isAdmin = false }: TicketNotificationBellProps) { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const { showToast } = useToast(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const { mobile: dropdownTop, isMobileFullscreen } = useHeaderHeight(); // Show toast for WebSocket notification const showWSNotificationToast = useCallback( (message: WSMessage) => { const isNewTicket = message.type === 'ticket.new'; const isAdminReply = message.type === 'ticket.admin_reply'; const isUserReply = message.type === 'ticket.user_reply'; const icon = isNewTicket ? ( 🎫 ) : isAdminReply ? ( 💬 ) : ( 📨 ); const ticketTitle = message.title || ''; let toastTitle: string; let toastMessage: string; if (isNewTicket) { toastTitle = t('notifications.newTicketTitle', 'New Ticket'); toastMessage = message.message || t('notifications.newTicket', 'New ticket: {{title}}', { title: ticketTitle }); } else if (isUserReply) { toastTitle = t('notifications.newUserReplyTitle', 'User Reply'); toastMessage = message.message || t('notifications.newUserReply', 'User replied in ticket: {{title}}', { title: ticketTitle, }); } else { toastTitle = t('notifications.newReplyTitle', 'New Reply'); toastMessage = message.message || t('notifications.newReply', 'New reply in ticket: {{title}}', { title: ticketTitle }); } showToast({ type: 'info', title: toastTitle, message: toastMessage, icon, onClick: () => { navigate( isAdmin ? `/admin/tickets?ticket=${message.ticket_id}` : `/support?ticket=${message.ticket_id}`, ); }, duration: 8000, }); }, [showToast, navigate, isAdmin, t], ); // Handle WebSocket message const handleWSMessage = useCallback( (message: WSMessage) => { // Check if this notification is relevant for this user type const isAdminNotification = message.type === 'ticket.new' || message.type === 'ticket.user_reply'; const isUserNotification = message.type === 'ticket.admin_reply'; if ((isAdmin && isAdminNotification) || (!isAdmin && isUserNotification)) { // Show toast showWSNotificationToast(message); // Invalidate queries to refresh count and list queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications-count'] : ['ticket-notifications-count'], }); queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications'] : ['ticket-notifications'], }); } }, [isAdmin, showWSNotificationToast, queryClient], ); // WebSocket connection useWebSocket({ onMessage: handleWSMessage, }); // Fetch unread count (with slower polling as fallback when WS disconnects) const { data: unreadData } = useQuery({ queryKey: isAdmin ? ['admin-ticket-notifications-count'] : ['ticket-notifications-count'], queryFn: isAdmin ? ticketNotificationsApi.getAdminUnreadCount : ticketNotificationsApi.getUnreadCount, enabled: isAuthenticated, refetchInterval: 60000, // Poll every 60 seconds as fallback staleTime: 30000, }); // Fetch notifications when dropdown is open const { data: notificationsData, isLoading } = useQuery({ queryKey: isAdmin ? ['admin-ticket-notifications'] : ['ticket-notifications'], queryFn: () => isAdmin ? ticketNotificationsApi.getAdminNotifications(false, 10) : ticketNotificationsApi.getNotifications(false, 10), enabled: isAuthenticated && isOpen, staleTime: 5000, }); // Mark all as read mutation const markAllReadMutation = useMutation({ mutationFn: isAdmin ? ticketNotificationsApi.markAllAdminAsRead : ticketNotificationsApi.markAllAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications'] : ['ticket-notifications'], }); queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications-count'] : ['ticket-notifications-count'], }); }, }); // Mark single as read mutation const markReadMutation = useMutation({ mutationFn: isAdmin ? ticketNotificationsApi.markAdminAsRead : ticketNotificationsApi.markAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications'] : ['ticket-notifications'], }); queryClient.invalidateQueries({ queryKey: isAdmin ? ['admin-ticket-notifications-count'] : ['ticket-notifications-count'], }); }, }); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleNotificationClick = (notification: TicketNotification) => { if (!notification.is_read) { markReadMutation.mutate(notification.id); } setIsOpen(false); navigate( isAdmin ? `/admin/tickets?ticket=${notification.ticket_id}` : `/support?ticket=${notification.ticket_id}`, ); }; const formatTime = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return t('notifications.justNow', 'Just now'); if (diffMins < 60) return t('notifications.minutesAgo', '{{count}} min ago', { count: diffMins }); if (diffHours < 24) return t('notifications.hoursAgo', '{{count}} h ago', { count: diffHours }); return t('notifications.daysAgo', '{{count}} d ago', { count: diffDays }); }; const getNotificationIcon = (type: string) => { switch (type) { case 'new_ticket': return 🎫; case 'admin_reply': return 💬; case 'user_reply': return 📨; default: return 🔔; } }; const unreadCount = unreadData?.unread_count || 0; return (
{/* Bell button */} {/* Dropdown */} {isOpen && (
{/* Header */}

{t('notifications.ticketNotifications', 'Ticket Notifications')}

{unreadCount > 0 && ( )}
{/* Notifications list */}
{isLoading ? (
) : notificationsData?.items && notificationsData.items.length > 0 ? ( notificationsData.items.map((notification: TicketNotification) => ( )) ) : (

{t('notifications.noNotifications', 'No notifications')}

)}
{/* Footer */} {notificationsData?.items && notificationsData.items.length > 0 && (
)}
)}
); }