Files
ember-market-frontend/components/notifications/OrderNotifications.tsx
g fe01f31538
Some checks failed
Build Frontend / build (push) Failing after 7s
Refactor UI imports and update component paths
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
2026-01-13 05:02:13 +00:00

239 lines
7.8 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import { clientFetch } from "@/lib/api";
import { toast } from "sonner";
import { Package, Bell } from "lucide-react";
import { useRouter } from "next/navigation";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/common/dropdown-menu";
interface Order {
_id: string;
orderId: string;
status: string;
totalPrice: number;
orderDate: string;
items?: Array<{ name: string; quantity: number }>;
customerName?: string;
}
export default function OrderNotifications() {
const router = useRouter();
const [newOrders, setNewOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const seenOrderIds = useRef<Set<string>>(new Set());
const isInitialFetch = useRef(true);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
audioRef.current = new Audio('/notification.mp3');
// Fallback if notification.mp3 doesn't exist
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;
const playPromise = audioRef.current.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
console.log('Error playing sound:', err);
// Fallback to simple 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);
}
});
}
}
};
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();
// Use orderDate parameter instead of 'after' to avoid backend casting errors
// The error logs show that the 'after' parameter is being interpreted as 'orderId' incorrectly
const orderData = await clientFetch(`/orders?status=paid&limit=10&orderDate[gte]=${timestamp}`);
const orders: Order[] = orderData.orders || [];
// If this is the first fetch, just store the orders without notifications
if (isInitialFetch.current) {
orders.forEach(order => seenOrderIds.current.add(order._id));
isInitialFetch.current = false;
setLoading(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 notification for each new order
latestNewOrders.forEach(order => {
toast.success(
<div className="flex flex-col">
<p className="font-semibold">New Paid Order!</p>
<p className="text-sm">Order #{order.orderId}</p>
<p className="text-sm font-semibold">£{order.totalPrice.toFixed(2)}</p>
</div>,
{
duration: 8000,
icon: <Package className="h-5 w-5" />,
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));
}
setLoading(false);
} catch (error) {
console.error("Error checking for new orders:", error);
setLoading(false);
}
};
// Check immediately on component mount (with a small delay)
const initialTimeout = setTimeout(() => {
checkForNewOrders();
}, 2000);
// Set up polling interval (every 60 seconds)
const interval = setInterval(checkForNewOrders, 60000);
// Clean up
return () => {
clearTimeout(initialTimeout);
clearInterval(interval);
};
}, []);
const handleOrderClick = (orderId: string) => {
router.push(`/dashboard/orders/${orderId}`);
};
const clearNotifications = () => {
setNewOrders([]);
};
// Format the price as currency
const formatPrice = (price: number) => {
return `£${price.toFixed(2)}`;
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative" disabled={loading}>
<Package className="h-5 w-5" />
{newOrders.length > 0 && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 px-1.5 py-0.5 text-xs"
>
{newOrders.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72">
<div className="p-2 border-b flex justify-between items-center">
<h3 className="font-medium">New Paid Orders</h3>
{newOrders.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearNotifications}
className="h-7 text-xs"
>
Clear
</Button>
)}
</div>
{newOrders.length === 0 ? (
<div className="p-4 flex items-center justify-center">
<p className="text-sm text-muted-foreground">No new paid orders</p>
</div>
) : (
<>
<div className="max-h-80 overflow-y-auto">
{newOrders.map((order) => (
<DropdownMenuItem
key={order._id}
className="p-3 cursor-pointer"
onClick={() => handleOrderClick(order._id)}
>
<div className="flex items-center justify-between w-full">
<div>
<p className="font-medium">Order #{order.orderId}</p>
<p className="text-sm text-muted-foreground">
{formatPrice(order.totalPrice)}
</p>
</div>
<Badge className="bg-green-500 hover:bg-green-600">Paid</Badge>
</div>
</DropdownMenuItem>
))}
</div>
<div className="p-2 border-t">
<Button
variant="outline"
className="w-full"
onClick={() => router.push('/dashboard/orders')}
>
View All Orders
</Button>
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}