-
Time period:
+
Time Period
Revenue and Orders tabs use time filtering. Products and Customers show all-time data.
@@ -227,7 +288,7 @@ export default function AnalyticsDashboard({ initialData }: AnalyticsDashboardPr
}>
-
+
diff --git a/components/analytics/RevenueChart.tsx b/components/analytics/RevenueChart.tsx
index bb82f36..de268d4 100644
--- a/components/analytics/RevenueChart.tsx
+++ b/components/analytics/RevenueChart.tsx
@@ -12,6 +12,7 @@ import { ChartSkeleton } from './SkeletonLoaders';
interface RevenueChartProps {
timeRange: string;
+ hideNumbers?: boolean;
}
interface ChartDataPoint {
@@ -21,7 +22,7 @@ interface ChartDataPoint {
formattedDate: string;
}
-export default function RevenueChart({ timeRange }: RevenueChartProps) {
+export default function RevenueChart({ timeRange, hideNumbers = false }: RevenueChartProps) {
const [data, setData] = useState
([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
@@ -75,6 +76,19 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
const totalOrders = safeData.reduce((sum, item) => sum + (item.orders || 0), 0);
const averageRevenue = safeData.length > 0 ? totalRevenue / safeData.length : 0;
+ // Function to mask sensitive numbers
+ const maskValue = (value: string): string => {
+ if (!hideNumbers) return value;
+
+ // For currency values (£X.XX), show £***
+ if (value.includes('£')) {
+ return '£***';
+ }
+
+ // For regular numbers, replace with asterisks
+ return '***';
+ };
+
// Custom tooltip for the chart
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
@@ -83,10 +97,10 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
{data.formattedDate}
- Revenue: {formatGBP(data.revenue)}
+ Revenue: {hideNumbers ? '£***' : formatGBP(data.revenue)}
- Orders: {data.orders}
+ Orders: {hideNumbers ? '***' : data.orders}
);
@@ -173,7 +187,7 @@ export default function RevenueChart({ timeRange }: RevenueChartProps) {
/>
`£${(value / 1000).toFixed(0)}k`}
+ tickFormatter={(value) => hideNumbers ? '***' : `£${(value / 1000).toFixed(0)}k`}
/>
} />
- {formatGBP(totalRevenue)}
+ {maskValue(formatGBP(totalRevenue))}
Total Revenue
- {totalOrders}
+ {maskValue(totalOrders.toString())}
Total Orders
- {formatGBP(averageRevenue)}
+ {maskValue(formatGBP(averageRevenue))}
Avg Daily Revenue
diff --git a/components/notifications/UnifiedNotifications.tsx b/components/notifications/UnifiedNotifications.tsx
index d4eb1c1..1a8cfe0 100644
--- a/components/notifications/UnifiedNotifications.tsx
+++ b/components/notifications/UnifiedNotifications.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useEffect, useRef } from "react";
+import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -13,264 +13,21 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { clientFetch } from "@/lib/api";
-import { toast } from "sonner";
-import { getCookie } from "@/lib/api";
-import axios from "axios";
-import { cacheUtils } from '@/lib/api-client';
-
-interface Order {
- _id: string;
- orderId: string;
- status: string;
- totalPrice: number;
- orderDate: string;
- underpaid?: boolean;
- underpaymentAmount?: number;
-}
-
-interface ChatMessage {
- chatId: string;
- buyerId: string;
- messageCount: number;
-}
-
-interface UnreadCounts {
- totalUnread: number;
- chatCounts: Record;
-}
+import { useNotifications } from "@/lib/notification-context";
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);
- const vendorIdRef = 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;
- }
- };
- }, []);
-
- // 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
- 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();
-
- 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) {
- validPaidOrders.forEach(order => seenOrderIds.current.add(order._id));
- 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
- 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));
-
- // 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
- if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) 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]);
+ // Get notification state from context
+ const {
+ unreadCounts,
+ chatMetadata,
+ newOrders,
+ clearOrderNotifications,
+ totalNotifications,
+ loading,
+ } = useNotifications();
// Navigation handlers
const handleChatClick = (chatId: string) => {
@@ -281,11 +38,6 @@ export default function UnifiedNotifications() {
router.push(`/dashboard/orders/${orderId}`);
};
- // Clear notification handlers
- const clearOrderNotifications = () => {
- setNewOrders([]);
- };
-
// Format the price as currency
const formatPrice = (price: number) => {
return `£${price.toFixed(2)}`;
diff --git a/lib/notification-context.tsx b/lib/notification-context.tsx
new file mode 100644
index 0000000..ca43034
--- /dev/null
+++ b/lib/notification-context.tsx
@@ -0,0 +1,343 @@
+"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('/notification.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
+ 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();
+
+ 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
+ if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) 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;
+}
\ No newline at end of file
diff --git a/public/git-info.json b/public/git-info.json
index 8627050..0a44146 100644
--- a/public/git-info.json
+++ b/public/git-info.json
@@ -1,4 +1,4 @@
{
- "commitHash": "308a816",
- "buildTime": "2025-05-23T06:40:22.513Z"
+ "commitHash": "2452a3c",
+ "buildTime": "2025-07-28T20:50:17.059Z"
}
\ No newline at end of file