/** * Notification Panel Component * * Sidebar panel that displays all notifications with detailed view */ 'use client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { listNotifications, markNotificationAsRead, markAllNotificationsAsRead, deleteNotification } from '@/lib/api'; import type { NotificationResponse } from '@/types/api'; import { X, Trash2, CheckCheck, Filter } from 'lucide-react'; interface NotificationPanelProps { isOpen: boolean; onClose: () => void; } export default function NotificationPanel({ isOpen, onClose }: NotificationPanelProps) { const [selectedFilter, setSelectedFilter] = useState<'all' | 'unread' | 'read'>('all'); const [currentPage, setCurrentPage] = useState(1); const queryClient = useQueryClient(); const router = useRouter(); // Fetch notifications with pagination const { data, isLoading } = useQuery({ queryKey: ['notifications', 'panel', selectedFilter, currentPage], queryFn: () => listNotifications({ page: currentPage, limit: 20, isRead: selectedFilter === 'all' ? undefined : selectedFilter === 'read', }), enabled: isOpen, }); const notifications = data?.notifications || []; const total = data?.total || 0; const totalPages = Math.ceil(total / 20); // Mark single notification as read const markAsReadMutation = useMutation({ mutationFn: markNotificationAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); // Mark all as read const markAllAsReadMutation = useMutation({ mutationFn: markAllNotificationsAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); // Delete notification const deleteNotificationMutation = useMutation({ mutationFn: deleteNotification, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const handleNotificationClick = (notification: NotificationResponse) => { if (!notification.read) { markAsReadMutation.mutate(notification.id); } // Navigate to actionUrl if available if (notification.actionUrl) { onClose(); router.push(notification.actionUrl); } }; const handleDelete = (e: React.MouseEvent, notificationId: string) => { e.stopPropagation(); if (confirm('Are you sure you want to delete this notification?')) { deleteNotificationMutation.mutate(notificationId); } }; const getPriorityColor = (priority: string) => { const colors = { urgent: 'border-l-4 border-red-500 bg-red-50', high: 'border-l-4 border-orange-500 bg-orange-50', medium: 'border-l-4 border-yellow-500 bg-yellow-50', low: 'border-l-4 border-blue-500 bg-blue-50', }; return colors[priority as keyof typeof colors] || 'border-l-4 border-gray-300'; }; const getNotificationIcon = (type: string) => { const icons: Record = { booking_created: '📦', booking_updated: '🔄', booking_cancelled: '❌', booking_confirmed: '✅', csv_booking_accepted: '✅', csv_booking_rejected: '❌', csv_booking_request_sent: '📧', rate_quote_expiring: '⏰', document_uploaded: '📄', system_announcement: '📢', user_invited: '👤', organization_update: '🏢', }; return icons[type.toLowerCase()] || '🔔'; }; const formatTime = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, }); }; if (!isOpen) return null; return ( <> {/* Backdrop */}
{/* Panel */}
{/* Header */}

Notifications

{total} notification{total !== 1 ? 's' : ''} total

{/* Filter Bar */}
{(['all', 'unread', 'read'] as const).map((filter) => ( ))}
{selectedFilter === 'unread' && notifications.length > 0 && ( )}
{/* Notifications List */}
{isLoading ? (

Loading notifications...

) : notifications.length === 0 ? (
🔔

No notifications

{selectedFilter === 'unread' ? "You're all caught up!" : 'No notifications to display'}

) : (
{notifications.map((notification: NotificationResponse) => (
handleNotificationClick(notification)} className={`p-6 hover:bg-gray-50 transition-all cursor-pointer group ${ !notification.read ? 'bg-blue-50/50' : '' } ${getPriorityColor(notification.priority || 'low')}`} >
{/* Icon */}
{getNotificationIcon(notification.type)}
{/* Content */}

{notification.title}

{!notification.read && ( )}

{notification.message}

{/* Metadata */}
{formatTime(notification.createdAt)} {notification.type.replace(/_/g, ' ').toUpperCase()} {notification.priority && ( {notification.priority.toUpperCase()} )}
{notification.actionUrl && ( View details → )}
))}
)}
{/* Pagination */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
); }