"use client"; import React, { createContext, useContext, useState, useEffect, useRef, ReactNode } from "react"; import { clientFetch } from "@/lib/api"; import { toast } from "sonner"; import { getCookie } from "@/lib/api"; import { cacheUtils } from '@/lib/api-client'; import { Package } from "lucide-react"; interface Order { _id: string; orderId: string; status: string; totalPrice: number; orderDate: string; underpaid?: boolean; underpaymentAmount?: number; } interface UnreadCounts { totalUnread: number; chatCounts: Record; } interface NotificationContextType { // Chat notifications unreadCounts: UnreadCounts; chatMetadata: Record; // Order notifications newOrders: Order[]; clearOrderNotifications: () => void; // Shared state totalNotifications: number; loading: boolean; } const NotificationContext = createContext(undefined); const STORAGE_KEYS = { SEEN_ORDER_IDS: 'ember-notifications-seen-orders', NEW_ORDERS: 'ember-notifications-new-orders', LAST_CHAT_CHECK: 'ember-notifications-last-chat-check' }; interface NotificationProviderProps { children: ReactNode; } export function NotificationProvider({ children }: NotificationProviderProps) { // 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 audioRef = useRef(null); const vendorIdRef = useRef(null); // Total notifications count const totalNotifications = unreadCounts.totalUnread + newOrders.length; // Initialize localStorage and audio useEffect(() => { // Load seen order IDs from localStorage const savedSeenOrders = localStorage.getItem(STORAGE_KEYS.SEEN_ORDER_IDS); if (savedSeenOrders) { try { const orderIds = JSON.parse(savedSeenOrders); seenOrderIds.current = new Set(orderIds); } catch (error) { console.error('Error loading seen order IDs from localStorage:', error); } } // Load new orders from localStorage const savedNewOrders = localStorage.getItem(STORAGE_KEYS.NEW_ORDERS); if (savedNewOrders) { try { const orders = JSON.parse(savedNewOrders); setNewOrders(orders); } catch (error) { console.error('Error loading new orders from localStorage:', error); } } // Initialize audio audioRef.current = new Audio('/hohoho.mp3'); audioRef.current.addEventListener('error', () => { audioRef.current = null; }); return () => { if (audioRef.current) { audioRef.current = null; } }; }, []); // Save seen order IDs to localStorage whenever it changes const updateSeenOrderIds = (orderIds: Set) => { seenOrderIds.current = orderIds; localStorage.setItem(STORAGE_KEYS.SEEN_ORDER_IDS, JSON.stringify(Array.from(orderIds))); }; // Save new orders to localStorage whenever it changes useEffect(() => { localStorage.setItem(STORAGE_KEYS.NEW_ORDERS, JSON.stringify(newOrders)); }, [newOrders]); // Get vendor ID from JWT token const getVendorIdFromToken = () => { if (vendorIdRef.current) { return vendorIdRef.current; } const authToken = getCookie("Authorization") || ""; if (!authToken) { throw new Error("No auth token found"); } const tokenParts = authToken.split("."); if (tokenParts.length !== 3) { throw new Error("Invalid token format"); } const payload = JSON.parse(atob(tokenParts[1])); const vendorId = payload.id; if (!vendorId) { throw new Error("Vendor ID not found in token"); } vendorIdRef.current = vendorId; return vendorId; }; // 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, but not on admin pages if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard") || window.location.pathname.includes("/dashboard/admin")) 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(); const orderData = await clientFetch(`/orders?status=paid&limit=10&orderDate[gte]=${timestamp}`); const orders: Order[] = orderData.orders || []; // Filter out orders that are still showing as underpaid (cache issue) const validPaidOrders = orders.filter(order => { // Only include orders that are actually fully paid (not underpaid) return order.status === 'paid' && (!order.underpaid || order.underpaymentAmount === 0); }); // If this is the first fetch, just store the orders without notifications if (isInitialOrdersFetch.current) { const orderIds = new Set([...seenOrderIds.current, ...validPaidOrders.map(order => order._id)]); updateSeenOrderIds(orderIds); isInitialOrdersFetch.current = false; return; } // Check for new paid orders that haven't been seen before const latestNewOrders = validPaidOrders.filter(order => !seenOrderIds.current.has(order._id)); // Show notifications for new orders if (latestNewOrders.length > 0) { // Update the seen orders set const updatedSeenOrders = new Set([...seenOrderIds.current, ...latestNewOrders.map(order => order._id)]); updateSeenOrderIds(updatedSeenOrders); // 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)); // Invalidate order cache to ensure all components refresh cacheUtils.invalidateOrderData(); } } 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, but not on admin pages if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard") || window.location.pathname.includes("/dashboard/admin")) return; const fetchUnreadCounts = async () => { try { // Get vendor ID from token const vendorId = getVendorIdFromToken(); // Use clientFetch which will properly route through Next.js API rewrites const response = await clientFetch(`/chats/vendor/${vendorId}/unread`); // Check if there are new notifications and play sound if needed if (!loading && response.totalUnread > previousUnreadTotal) { playNotificationSound(); } // Update chat state - note that clientFetch already parses the JSON response setUnreadCounts(response); setPreviousUnreadTotal(response.totalUnread); if (response.totalUnread > 0) { const chatIds = Object.keys(response.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 clientFetch(`/chats/${chatId}?markAsRead=false`); metadata[chatId] = { buyerId: chatResponse.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]); // Clear notification handlers const clearOrderNotifications = () => { setNewOrders([]); localStorage.removeItem(STORAGE_KEYS.NEW_ORDERS); }; const contextValue: NotificationContextType = { unreadCounts, chatMetadata, newOrders, clearOrderNotifications, totalNotifications, loading, }; return ( {children} ); } export function useNotifications() { const context = useContext(NotificationContext); if (context === undefined) { throw new Error('useNotifications must be used within a NotificationProvider'); } return context; }