"use client"; import React, { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { BellRing, Package, MessageCircle } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { clientFetch } from "@/lib/client-utils"; import { toast } from "sonner"; import { getCookie } from "@/lib/client-utils"; import axios from "axios"; interface Order { _id: string; orderId: string; status: string; totalPrice: number; orderDate: string; } interface ChatMessage { chatId: string; buyerId: string; messageCount: number; } interface UnreadCounts { totalUnread: number; chatCounts: Record; } export default function UnifiedNotifications() { const router = useRouter(); // Chat notification state const [unreadCounts, setUnreadCounts] = useState({ totalUnread: 0, chatCounts: {} }); const [previousUnreadTotal, setPreviousUnreadTotal] = useState(0); const [chatMetadata, setChatMetadata] = useState>({}); // Order notification state const [newOrders, setNewOrders] = useState([]); const seenOrderIds = useRef>(new Set()); const isInitialOrdersFetch = useRef(true); // Shared state const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState("all"); const audioRef = useRef(null); // Total notifications count const totalNotifications = unreadCounts.totalUnread + newOrders.length; // Initialize audio useEffect(() => { audioRef.current = new Audio('/notification.mp3'); audioRef.current.addEventListener('error', () => { audioRef.current = null; }); return () => { if (audioRef.current) { audioRef.current = null; } }; }, []); // Function to play notification sound const playNotificationSound = () => { if (audioRef.current) { audioRef.current.currentTime = 0; audioRef.current.play().catch(err => { console.log('Error playing sound:', err); // Fallback beep if audio file fails try { const context = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = context.createOscillator(); oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(800, context.currentTime); oscillator.connect(context.destination); oscillator.start(); oscillator.stop(context.currentTime + 0.2); } catch (e) { console.error('Could not play fallback audio', e); } }); } }; // Check for new paid orders useEffect(() => { // Only run this on dashboard pages if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return; const checkForNewOrders = async () => { try { // Get orders from the last 24 hours with a more efficient query const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const timestamp = yesterday.toISOString(); // Include timestamp filter to reduce load const orderData = await clientFetch(`/orders?status=paid&limit=10&after=${timestamp}`); const orders: Order[] = orderData.orders || []; // If this is the first fetch, just store the orders without notifications if (isInitialOrdersFetch.current) { orders.forEach(order => seenOrderIds.current.add(order._id)); isInitialOrdersFetch.current = false; return; } // Check for new paid orders that haven't been seen before const latestNewOrders = orders.filter(order => !seenOrderIds.current.has(order._id)); // Show notifications for new orders if (latestNewOrders.length > 0) { // Update the seen orders set latestNewOrders.forEach(order => seenOrderIds.current.add(order._id)); // Show a toast notification for each new order latestNewOrders.forEach(order => { toast.success(

New Paid Order!

Order #{order.orderId}

£{order.totalPrice.toFixed(2)}

, { duration: 8000, icon: , action: { label: "View", onClick: () => window.open(`/dashboard/orders/${order._id}`, "_blank") } } ); }); // Play notification sound playNotificationSound(); // Update the state with new orders for the dropdown setNewOrders(prev => [...latestNewOrders, ...prev].slice(0, 10)); } } catch (error) { console.error("Error checking for new orders:", error); } }; // Check for new orders every minute const orderInterval = setInterval(checkForNewOrders, 60000); // Initial check for orders checkForNewOrders(); return () => { clearInterval(orderInterval); }; }, []); // Fetch unread chat counts useEffect(() => { // Only run this on dashboard pages if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return; const fetchUnreadCounts = async () => { try { const authToken = getCookie("Authorization"); if (!authToken) return; const authAxios = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, headers: { Authorization: `Bearer ${authToken}` } }); // Get vendor info from profile endpoint const vendorResponse = await authAxios.get('/auth/me'); // Access correct property - the vendor ID is in vendor._id const vendorId = vendorResponse.data.vendor?._id; if (!vendorId) { console.error("Vendor ID not found in profile response:", vendorResponse.data); return; } const response = await authAxios.get(`/chats/vendor/${vendorId}/unread`); // Check if there are new notifications and play sound if needed if (!loading && response.data.totalUnread > previousUnreadTotal) { playNotificationSound(); } // Update chat state setUnreadCounts(response.data); setPreviousUnreadTotal(response.data.totalUnread); if (response.data.totalUnread > 0) { const chatIds = Object.keys(response.data.chatCounts); if (chatIds.length > 0) { // Create a simplified metadata object with just needed info const metadata: Record = {}; // Fetch each chat to get buyer IDs await Promise.all( chatIds.map(async (chatId) => { try { // Use markAsRead=false to ensure we don't mark messages as read const chatResponse = await authAxios.get(`/chats/${chatId}?markAsRead=false`); metadata[chatId] = { buyerId: chatResponse.data.buyerId, }; } catch (error) { console.error(`Error fetching chat ${chatId}:`, error); } }) ); setChatMetadata(metadata); } } setLoading(false); } catch (error) { console.error("Error fetching unread counts:", error); setLoading(false); } }; // Initial fetch fetchUnreadCounts(); // Set polling interval (every 10 seconds for more responsive chat notifications) const chatInterval = setInterval(fetchUnreadCounts, 10000); return () => clearInterval(chatInterval); }, [loading, previousUnreadTotal]); // Navigation handlers const handleChatClick = (chatId: string) => { router.push(`/dashboard/chats/${chatId}`); }; const handleOrderClick = (orderId: string) => { router.push(`/dashboard/orders/${orderId}`); }; // Clear notification handlers const clearOrderNotifications = () => { setNewOrders([]); }; // Format the price as currency const formatPrice = (price: number) => { return `£${price.toFixed(2)}`; }; return (
All {totalNotifications > 0 && ( {totalNotifications} )} Messages {unreadCounts.totalUnread > 0 && ( {unreadCounts.totalUnread} )} Orders {newOrders.length > 0 && ( {newOrders.length} )} {totalNotifications === 0 ? (

No new notifications

) : (
{/* Messages Section */} {unreadCounts.totalUnread > 0 && ( <>
Unread Messages
{Object.entries(unreadCounts.chatCounts).slice(0, 3).map(([chatId, count]) => ( handleChatClick(chatId)} >

Customer {chatMetadata[chatId]?.buyerId.slice(-4) || 'Unknown'}

{count} new {count === 1 ? 'message' : 'messages'}

{count}
))} {Object.keys(unreadCounts.chatCounts).length > 3 && (
+ {Object.keys(unreadCounts.chatCounts).length - 3} more unread chats
)} )} {/* Orders Section */} {newOrders.length > 0 && ( <>
New Paid Orders
{newOrders.slice(0, 3).map((order) => ( handleOrderClick(order._id)} >

Order #{order.orderId}

{formatPrice(order.totalPrice)}

Paid
))} {newOrders.length > 3 && (
+ {newOrders.length - 3} more new orders
)} )}
)}
{unreadCounts.totalUnread === 0 ? (

No unread messages

) : ( <>
{Object.entries(unreadCounts.chatCounts).map(([chatId, count]) => ( handleChatClick(chatId)} >

Customer {chatMetadata[chatId]?.buyerId.slice(-4) || 'Unknown'}

{count} new {count === 1 ? 'message' : 'messages'}

{count}
))}
)}
{newOrders.length === 0 ? (

No new paid orders

) : ( <>
New Paid Orders
{newOrders.map((order) => ( handleOrderClick(order._id)} >

Order #{order.orderId}

{formatPrice(order.totalPrice)}

Paid
))}
)}
); }