Files
ember-market-frontend/components/dashboard/BuyerOrderInfo.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

304 lines
10 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Package, ShoppingBag, Info, ExternalLink, Loader2, AlertTriangle } from "lucide-react";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/common/tooltip";
import { getCookie } from "@/lib/api";
import axios from "axios";
import { useRouter } from "next/navigation";
import { cacheUtils } from "@/lib/api/api-client";
interface Order {
_id: string;
orderId: number;
status: string;
totalPrice: number;
orderDate: string;
products: Array<{
productId: string;
quantity: number;
pricePerUnit: number;
totalItemPrice: number;
}>;
underpaid?: boolean;
underpaymentAmount?: number;
lastBalanceReceived?: number;
cryptoTotal?: number;
}
interface BuyerOrderInfoProps {
buyerId: string;
chatId: string;
}
export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [orders, setOrders] = useState<Order[]>([]);
const [hasOrders, setHasOrders] = useState<boolean | null>(null);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const lastFetchedRef = useRef<number>(0);
const isFetchingRef = useRef<boolean>(false);
const tooltipDelayRef = useRef<NodeJS.Timeout | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
// Add order refresh subscription
useEffect(() => {
const unsubscribe = cacheUtils.onOrderRefresh(() => {
console.log("Order refresh triggered in BuyerOrderInfo");
setRefreshTrigger(prev => prev + 1);
});
return unsubscribe;
}, []);
// Fetch data without unnecessary dependencies to reduce render cycles
const fetchBuyerOrders = useCallback(async (force = false) => {
// Prevent multiple simultaneous fetches
if (isFetchingRef.current) return;
// Don't fetch if we already know there are no orders
if (hasOrders === false && !force) return;
// Don't fetch if we already have orders and data was fetched less than 10 seconds ago
const now = Date.now();
if (!force && !refreshTrigger && orders.length > 0 && now - lastFetchedRef.current < 10000) return;
// Only continue if we have a chatId
if (!chatId) return;
isFetchingRef.current = true;
setLoading(true);
try {
const authToken = getCookie("Authorization");
if (!authToken) {
console.error("No auth token found for buyer orders");
setHasOrders(false);
return;
}
const authAxios = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
Authorization: `Bearer ${authToken}`
}
});
// Use the new endpoint that works with sub-users
const response = await authAxios.get(`/chats/${chatId}/orders?limit=10`); // Limit to fewer orders for faster response
if (response.data && response.data.orders) {
setOrders(response.data.orders);
setHasOrders(response.data.orders.length > 0);
} else {
setHasOrders(false);
}
lastFetchedRef.current = Date.now();
} catch (error: any) {
console.error("Error fetching buyer orders:", error);
if (error.response?.status === 404) {
console.log("No orders found for this buyer");
setOrders([]);
setHasOrders(false);
} else {
console.error("API error:", error.response?.data || error.message);
setHasOrders(null);
}
} finally {
setLoading(false);
isFetchingRef.current = false;
}
}, [chatId, refreshTrigger]); // Add refreshTrigger as dependency
// Start fetching immediately when component mounts
useEffect(() => {
if (chatId) {
// Immediately attempt to fetch in the background
fetchBuyerOrders();
}
return () => {
// Clean up any pending timeouts
if (tooltipDelayRef.current) {
clearTimeout(tooltipDelayRef.current);
}
};
}, [chatId, fetchBuyerOrders]);
const handleViewOrder = (orderId: string) => {
router.push(`/dashboard/orders/${orderId}`);
};
// Handle hover with immediate tooltip opening
const handleButtonMouseEnter = () => {
// Start fetching data, but don't wait for it to complete
if (!isFetchingRef.current) {
queueMicrotask(() => {
fetchBuyerOrders();
});
}
};
// Handle tooltip state change
const handleTooltipOpenChange = (open: boolean) => {
setIsTooltipOpen(open);
if (open && !isFetchingRef.current) {
queueMicrotask(() => {
fetchBuyerOrders();
});
}
};
// Format the price as currency
const formatPrice = (price: number) => {
return `£${price.toFixed(2)}`;
};
// Helper function to check if order is underpaid (improved)
const isOrderUnderpaid = (order: Order) => {
return order.underpaid === true &&
order.underpaymentAmount &&
order.underpaymentAmount > 0 &&
order.status !== "paid" &&
order.status !== "completed" &&
order.status !== "shipped" &&
order.status !== "cancelled";
};
// Helper function to get underpaid percentage
const getUnderpaidPercentage = (order: Order) => {
if (!isOrderUnderpaid(order)) return 0;
const received = order.lastBalanceReceived || 0;
const required = order.cryptoTotal || 0;
return required > 0 ? ((received / required) * 100) : 0;
};
// If we know there are no orders, don't show the component at all
if (hasOrders === false) {
return null;
}
// Precompute product count for button display (only if we have orders)
const productCount = orders.length > 0
? orders.reduce((total, order) => {
return total + order.products.reduce((sum, product) => sum + product.quantity, 0);
}, 0)
: 0;
return (
<TooltipProvider>
<Tooltip onOpenChange={handleTooltipOpenChange}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-2 flex items-center gap-1.5"
onMouseEnter={handleButtonMouseEnter}
>
<ShoppingBag className="h-4 w-4" />
<span className="text-xs font-medium">
{orders.length > 0 ? `${orders.length} Orders` : "Orders"}
</span>
{productCount > 0 && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 ml-1">
{productCount} items
</Badge>
)}
{orders.some(order => isOrderUnderpaid(order)) && (
<AlertTriangle className="h-3 w-3 text-red-500" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="left" className="p-0 max-w-xs">
{loading ? (
<div className="p-3 flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Loading orders...</span>
</div>
) : orders.length === 0 ? (
<div className="p-3">
<span className="text-sm text-muted-foreground">No orders found</span>
</div>
) : (
<>
<div className="p-3 border-b">
<h4 className="font-medium text-sm">Customer Orders</h4>
<p className="text-xs text-muted-foreground">
{orders.length} {orders.length === 1 ? 'order' : 'orders'} Total: {formatPrice(
orders.reduce((sum, order) => sum + order.totalPrice, 0)
)}
</p>
</div>
<div className="max-h-64 overflow-y-auto divide-y divide-border">
{orders.map((order) => (
<div
key={order._id}
className="px-3 py-2 hover:bg-accent/50 cursor-pointer"
onClick={() => handleViewOrder(order._id)}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<Package className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Order #{order.orderId}</span>
{isOrderUnderpaid(order) && (
<AlertTriangle className="h-3 w-3 text-red-500" />
)}
</div>
<div className="flex items-center gap-1">
<Badge variant={
order.status === "paid" ? "paid" :
order.status === "unpaid" ? "unpaid" :
order.status === "shipped" ? "shipped" :
order.status === "completed" ? "completed" :
"secondary"
} className="text-[10px] h-5 px-1.5">
{order.status.toUpperCase()}
</Badge>
{isOrderUnderpaid(order) && (
<Badge variant="destructive" className="text-[10px] h-5 px-1.5">
{getUnderpaidPercentage(order).toFixed(0)}% PAID
</Badge>
)}
</div>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground pl-5">
<div className="flex gap-1">
<span>{order.products.length} {order.products.length === 1 ? 'product' : 'products'}</span>
<span>·</span>
<span>{formatPrice(order.totalPrice)}</span>
{isOrderUnderpaid(order) && (
<>
<span>·</span>
<span className="text-red-500">Underpaid</span>
</>
)}
</div>
<span className="text-[10px]">{new Date(order.orderDate).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}