/** * 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 { useTranslations, useLocale } from 'next-intl'; import { useRouter } from 'next/navigation'; import { listNotifications, markNotificationAsRead, markAllNotificationsAsRead, deleteNotification, } from '@/lib/api'; import type { NotificationResponse } from '@/types/api'; import { X, Trash2, CheckCheck, Filter, Bell, Package, RefreshCw, XCircle, CheckCircle, Mail, Timer, FileText, Megaphone, User, Building2, } from 'lucide-react'; import { useConfirm } from '@/components/ui/use-confirm'; interface NotificationPanelProps { isOpen: boolean; onClose: () => void; } export default function NotificationPanel({ isOpen, onClose }: NotificationPanelProps) { const confirm = useConfirm(); const t = useTranslations('components.notificationPanel'); const tTime = useTranslations('components.notificationDropdown.time'); const locale = useLocale(); const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US'; const [selectedFilter, setSelectedFilter] = useState<'all' | 'unread' | 'read'>('all'); const [currentPage, setCurrentPage] = useState(1); const queryClient = useQueryClient(); const router = useRouter(); 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); const markAsReadMutation = useMutation({ mutationFn: markNotificationAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const markAllAsReadMutation = useMutation({ mutationFn: markAllNotificationsAsRead, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const deleteNotificationMutation = useMutation({ mutationFn: deleteNotification, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const handleNotificationClick = (notification: NotificationResponse) => { if (!notification.read) { markAsReadMutation.mutate(notification.id); } if (notification.actionUrl) { onClose(); router.push(notification.actionUrl); } }; const handleDelete = async (e: React.MouseEvent, notificationId: string) => { e.stopPropagation(); if (await confirm({ title: t('deleteConfirm'), destructive: true })) { 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-brand-turquoise bg-brand-turquoise/5', }; return colors[priority as keyof typeof colors] || 'border-l-4 border-neutral-300'; }; const getNotificationIconComponent = (type: string) => { const icons: Record = { booking_created: Package, booking_updated: RefreshCw, booking_cancelled: XCircle, booking_confirmed: CheckCircle, csv_booking_accepted: CheckCircle, csv_booking_rejected: XCircle, csv_booking_request_sent: Mail, rate_quote_expiring: Timer, document_uploaded: FileText, system_announcement: Megaphone, user_invited: User, organization_update: Building2, }; return icons[type.toLowerCase()] || Bell; }; 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 tTime('now'); if (diffMins < 60) return tTime('minutes', { minutes: diffMins }); if (diffHours < 24) return tTime('hours', { hours: diffHours }); if (diffDays < 7) return tTime('days', { days: diffDays }); return date.toLocaleDateString(dateLocale, { day: 'numeric', month: 'short', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, }); }; if (!isOpen) return null; return ( <> {/* Backdrop */}
{/* Panel */}
{/* Header */}

{t('title')}

{t('totalCount', { count: total })}

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

{t('loading')}

) : notifications.length === 0 ? (

{t('emptyTitle')}

{selectedFilter === 'unread' ? t('emptyUnread') : t('emptyAll')}

) : (
{notifications.map((notification: NotificationResponse) => (
handleNotificationClick(notification)} className={`p-6 hover:bg-neutral-50 transition-all cursor-pointer group ${ !notification.read ? 'bg-brand-turquoise/5/50' : '' } ${getPriorityColor(notification.priority || 'low')}`} >
{/* Icon */}
{(() => { const Icon = getNotificationIconComponent(notification.type); return ; })()}
{/* Content */}

{notification.title}

{!notification.read && ( )}

{notification.message}

{/* Metadata */}
{formatTime(notification.createdAt)} {notification.type.replace(/_/g, ' ').toUpperCase()} {notification.priority && ( {notification.priority.toUpperCase()} )}
{notification.actionUrl && ( {t('viewDetails')} )}
))}
)}
{/* Pagination */} {totalPages > 1 && (
{t('pagination.page', { current: currentPage, total: totalPages })}
)}
); }