import { useState, useRef, useEffect, useCallback } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' 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 type { TicketNotification } from '../types' const BellIcon = () => ( ) const CheckIcon = () => ( ) interface TicketNotificationBellProps { isAdmin?: boolean } export default function TicketNotificationBell({ isAdmin = false }: TicketNotificationBellProps) { const { t } = useTranslation() const navigate = useNavigate() const queryClient = useQueryClient() const { isAuthenticated } = useAuthStore() const { showToast } = useToast() const [isOpen, setIsOpen] = useState(false) const dropdownRef = useRef(null) // 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 */} setIsOpen(!isOpen)} className="relative p-2.5 rounded-xl transition-all duration-200 hover:bg-dark-800/50 text-dark-400 hover:text-dark-100" title={t('notifications.ticketNotifications', 'Ticket notifications')} > {unreadCount > 0 && ( {unreadCount > 99 ? '99+' : unreadCount} )} {/* Dropdown */} {isOpen && ( {/* Header */} {t('notifications.ticketNotifications', 'Ticket Notifications')} {unreadCount > 0 && ( markAllReadMutation.mutate()} disabled={markAllReadMutation.isPending} className="flex items-center gap-1.5 text-xs text-accent-400 hover:text-accent-300 disabled:opacity-50 transition-colors" > {t('notifications.markAllRead', 'Mark all read')} )} {/* Notifications list */} {isLoading ? ( ) : notificationsData?.items && notificationsData.items.length > 0 ? ( notificationsData.items.map((notification: TicketNotification) => ( handleNotificationClick(notification)} className={`w-full text-left px-4 py-3 border-b border-dark-800/50 last:border-b-0 hover:bg-dark-800/50 transition-all duration-200 ${ !notification.is_read ? 'bg-accent-500/5' : '' }`} > {getNotificationIcon(notification.notification_type)} {notification.message} {formatTime(notification.created_at)} {!notification.is_read && ( )} )) ) : ( {t('notifications.noNotifications', 'No notifications')} )} {/* Footer */} {notificationsData?.items && notificationsData.items.length > 0 && ( { setIsOpen(false) navigate(isAdmin ? '/admin/tickets' : '/support') }} className="w-full text-center text-sm text-accent-400 hover:text-accent-300 py-1 transition-colors" > {t('notifications.viewAll', 'View all tickets')} )} )} ) }
{notification.message}
{formatTime(notification.created_at)}
{t('notifications.noNotifications', 'No notifications')}