"use client"; import { fetchData } from '@/lib/api'; import { clientFetch } from '@/lib/api'; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy, Loader2, RefreshCw } from "lucide-react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import Layout from "@/components/layout/layout"; import { cacheUtils } from '@/lib/api-client'; interface Order { orderId: string; status: string; pgpAddress: string; shippingMethod: { type: string; price: number }; txid: Array; products: Array<{ _id: string; productId: string; quantity: number; pricePerUnit: number; totalItemPrice: number; }>; totalPrice: number; orderDate: Date; paidAt?: Date; trackingNumber?: string; telegramUsername?: string; telegramBuyerId?: string; review?: { text: string; date: string; stars: number; _id: string; }; underpaid?: boolean; underpaymentAmount?: number; lastBalanceReceived?: number; cryptoTotal?: number; paymentAddress?: string; // Promotion fields promotion?: string; promotionCode?: string; discountAmount?: number; subtotalBeforeDiscount?: number; } interface CustomerInsights { totalOrders: number; completedOrders: number; cancelledOrders: number; paidOrders: number; totalSpent: number; averageOrderValue: number; completionRate: number; paymentSuccessRate: number; cancellationRate: number; firstOrder: string | null; lastOrder: string | null; customerSince: number; // days since first order } interface OrderResponse { order: Order; customerInsights: CustomerInsights; } interface OrderInList extends Order { _id: string; } interface OrdersResponse { orders: OrderInList[]; page: number; totalPages: number; totalOrders: number; } type ProductNames = Record; type NavigationInfo = { nextOrderId: string | null; prevOrderId: string | null; totalOrders: number; currentOrderNumber: number; totalPages: number; currentPage: number; }; const getStatusStyle = (status: string) => { switch (status) { case 'acknowledged': return 'bg-purple-500/10 text-purple-500 border-purple-500/20'; case 'paid': return 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20'; case 'shipped': return 'bg-blue-500/10 text-blue-500 border-blue-500/20'; case 'completed': return 'bg-green-500/10 text-green-500 border-green-500/20'; case 'cancelled': return 'bg-red-500/10 text-red-500 border-red-500/20'; case 'unpaid': return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20'; case 'confirming': return 'bg-orange-500/10 text-orange-500 border-orange-500/20'; default: return 'bg-gray-500/10 text-gray-500 border-gray-500/20'; } }; export default function OrderDetailsPage() { const [order, setOrder] = useState(null); const [customerInsights, setCustomerInsights] = useState(null); const [trackingNumber, setTrackingNumber] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [productNames, setProductNames] = useState>({}); const [isPaid, setIsPaid] = useState(false); const [isSending, setIsSending] = useState(false); const [isMarkingShipped, setIsMarkingShipped] = useState(false); const [isDiscarding, setIsDiscarding] = useState(false); const [nextOrderId, setNextOrderId] = useState(null); const [prevOrderId, setPrevOrderId] = useState(null); const [totalOrders, setTotalOrders] = useState(0); const [currentOrderNumber, setCurrentOrderNumber] = useState(0); const [totalPages, setTotalPages] = useState(1); const [currentPage, setCurrentPage] = useState(1); const [isAcknowledging, setIsAcknowledging] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [refreshTrigger, setRefreshTrigger] = useState(0); // shipping dialog removed; use inline tracking input instead const router = useRouter(); const params = useParams(); const orderId = params?.id; const fetchProductNames = async ( productIds: string[], authToken: string ): Promise> => { const productNamesMap: Record = {}; // Process each product ID independently const fetchPromises = productIds.map(async (id) => { try { const product = await fetchData(`${process.env.NEXT_PUBLIC_API_URL}/products/${id}`, { method: "GET", headers: { Authorization: `Bearer ${authToken}` }, }); productNamesMap[id] = product?.name || "Unknown Product (Deleted)"; } catch (err) { console.error(`Failed to fetch product ${id}:`, err); productNamesMap[id] = "Unknown Product (Deleted)"; } }); // Wait for all fetch operations to complete (successful or failed) await Promise.all(fetchPromises); return productNamesMap; }; const handleMarkAsPaid = async () => { try { // Add a loading state to give feedback const loadingToast = toast.loading("Marking order as paid..."); // Log the request for debugging console.log(`Sending request to /orders/${orderId}/status with clientFetch`); console.log("Request payload:", { status: "paid" }); // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", body: JSON.stringify({ status: "paid" }), }); // Log the response console.log("API response:", response); toast.dismiss(loadingToast); if (response && response.message === "Order status updated successfully") { // Update both states setIsPaid(true); setOrder((prevOrder) => (prevOrder ? { ...prevOrder, status: "paid", // Clear underpayment flags when marking as paid underpaid: false, underpaymentAmount: 0 } : null)); // Invalidate order cache to ensure fresh data everywhere cacheUtils.invalidateOrderData(orderId as string); toast.success("Order marked as paid successfully"); // Refresh order data to get latest status setTimeout(() => { setRefreshTrigger(prev => prev + 1); }, 500); } else { // Handle unexpected response format console.error("Unexpected response format:", response); throw new Error( response.error || response.message || "Failed to mark order as paid" ); } } catch (error: any) { console.error("Failed to mark order as paid:", error); // More detailed error handling let errorMessage = "Failed to mark order as paid"; if (error.message) { errorMessage += `: ${error.message}`; } if (error.response) { try { const errorData = await error.response.json(); console.error("Error response data:", errorData); if (errorData.message) { errorMessage = errorData.message; } } catch (e) { console.error("Could not parse error response:", e); } } toast.error(errorMessage); } }; const handleMarkAsShipped = async () => { try { setIsMarkingShipped(true); // First mark as shipped (clientFetch handles API URL and auth token) const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", body: JSON.stringify({ status: "shipped" }), }); if (response && response.message === "Order status updated successfully") { setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "shipped" } : null); toast.success("Order marked as shipped successfully!"); // If a tracking number is present in the inline box, add it after marking as shipped if (trackingNumber && trackingNumber.trim()) { await handleAddTrackingNumber(trackingNumber.trim()); setTrackingNumber(""); } } else { throw new Error(response.error || "Failed to mark order as shipped"); } } catch (error: any) { console.error("Failed to mark order as shipped:", error); toast.error(error.message || "Failed to mark order as shipped"); } finally { setIsMarkingShipped(false); } }; const handleAddTrackingNumber = async (trackingNumber: string) => { try { const authToken = document.cookie.split("Authorization=")[1]; const response = await fetchData( `${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}/tracking`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ trackingNumber }), } ); if (response.error) throw new Error(response.error); // Update the local state setOrder(prevOrder => prevOrder ? { ...prevOrder, trackingNumber: trackingNumber } : null); toast.success("Tracking number added successfully!"); } catch (err: any) { console.error("Failed to add tracking number:", err); toast.error(err.message || "Failed to add tracking number"); } }; // shipping dialog removed const handleMarkAsAcknowledged = async () => { try { setIsAcknowledging(true); // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", body: JSON.stringify({ status: "acknowledged" }), }); if (response && response.message === "Order status updated successfully") { setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "acknowledged" } : null); toast.success("Order marked as acknowledged!"); } else { throw new Error(response.error || "Failed to mark order as acknowledged"); } } catch (error: any) { console.error("Failed to mark order as acknowledged:", error); toast.error(error.message || "Failed to mark order as acknowledged"); } finally { setIsAcknowledging(false); } }; const handleDiscardOrder = async () => { try { setIsDiscarding(true); const authToken = document.cookie.split("Authorization=")[1]; const response = await fetchData( `${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}`, { method: "DELETE", headers: { Authorization: `Bearer ${authToken}`, }, } ); if (response && response.message === "Order deleted successfully") { toast.success("Order deleted successfully!"); router.push('/dashboard/orders'); } else { throw new Error(response.error || "Failed to delete order"); } } catch (error: any) { console.error("Failed to delete order:", error); toast.error(error.message || "Failed to delete order"); } finally { setIsDiscarding(false); } }; const handleCancelOrder = async () => { try { setIsCancelling(true); // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", body: JSON.stringify({ status: "cancelled" }), }); if (response && response.message === "Order status updated successfully") { setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "cancelled" } : null); toast.success("Order cancelled successfully"); } else { throw new Error(response.error || "Failed to cancel order"); } } catch (error: any) { console.error("Failed to cancel order:", error); toast.error(error.message || "Failed to cancel order"); } finally { setIsCancelling(false); } }; useEffect(() => { const fetchOrderDetails = async () => { try { if (!orderId) return; const authToken = document.cookie.split("Authorization=")[1]; const res = await fetchData( `${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}?includeInsights=true`, { method: "GET", headers: { Authorization: `Bearer ${authToken}` }, } ); if (!res) throw new Error("Failed to fetch order details"); const data: OrderResponse = await res; setOrder(data.order); setCustomerInsights(data.customerInsights); console.log("Fresh order data:", data.order); console.log("Customer insights:", data.customerInsights); const productIds = data.order.products.map((product) => product.productId); const productNamesMap = await fetchProductNames(productIds, authToken); setProductNames(productNamesMap); setTimeout(() => { setProductNames(prev => { const newMap = {...prev}; productIds.forEach(id => { if (!newMap[id] || newMap[id] === "Loading...") { newMap[id] = "Unknown Product (Deleted)"; } }); return newMap; }); }, 3000); if (data.order.status === "paid") { setIsPaid(true); } } catch (err: any) { router.push("/dashboard/orders"); setError(err.message); } finally { setLoading(false); } }; fetchOrderDetails(); }, [orderId, refreshTrigger]); useEffect(() => { const fetchAdjacentOrders = async () => { try { const authToken = document.cookie.split("Authorization=")[1]; if (!order?.orderId) return; // Get the current numerical orderId const currentOrderId = parseInt(order.orderId); console.log('Current orderId (numerical):', currentOrderId); // Use the new optimized backend endpoint to get adjacent orders const adjacentOrdersUrl = `${process.env.NEXT_PUBLIC_API_URL}/orders/adjacent/${currentOrderId}`; console.log('Fetching adjacent orders:', adjacentOrdersUrl); const adjacentOrdersRes = await fetchData( adjacentOrdersUrl, { method: "GET", headers: { Authorization: `Bearer ${authToken}` }, } ); console.log('Adjacent orders response:', adjacentOrdersRes); if (!adjacentOrdersRes) { console.error("Invalid response from adjacent orders endpoint"); return; } // Set the next and previous order IDs const { newer, older } = adjacentOrdersRes; // Set IDs for navigation setPrevOrderId(newer?._id || null); setNextOrderId(older?._id || null); if (newer) { console.log(`Newer order: ${newer.orderId} (ID: ${newer._id})`); } else { console.log('No newer order found'); } if (older) { console.log(`Older order: ${older.orderId} (ID: ${older._id})`); } else { console.log('No older order found'); } } catch (error) { console.error("Failed to fetch adjacent orders:", error); } }; if (order) { fetchAdjacentOrders(); } }, [order]); const handleAddTracking = async () => { if (!trackingNumber) { toast.error("Please enter a tracking number"); return; } try { setIsSending(true); const authToken = document.cookie.split("Authorization=")[1]; const response = await fetchData( `${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}/tracking`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ trackingNumber }), } ); if (response.error) throw new Error(response.error); // Update the local state setOrder(prevOrder => prevOrder ? { ...prevOrder, trackingNumber: trackingNumber } : null); toast.success("Tracking number updated successfully!"); setTrackingNumber(""); // Clear the input } catch (err: any) { console.error("Failed to update tracking:", err); toast.error(err.message || "Failed to update tracking number"); } finally { setIsSending(false); } }; const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); toast.success("Payment address copied to clipboard"); } catch (error) { toast.error("Failed to copy to clipboard"); } }; // Helper function to check if order is underpaid const isOrderUnderpaid = (order: Order | null) => { // More robust check - only show underpaid if status is NOT paid and underpayment exists return order?.underpaid === true && order?.underpaymentAmount && order.underpaymentAmount > 0 && order.status !== "paid" && order.status !== "completed" && order.status !== "shipped"; }; // Helper function to get underpaid information const getUnderpaidInfo = (order: Order | null) => { if (!isOrderUnderpaid(order)) return null; const received = order?.lastBalanceReceived || 0; const required = order?.cryptoTotal || 0; const missing = order?.underpaymentAmount || 0; // Calculate LTC to GBP exchange rate from order data const ltcToGbpRate = required > 0 ? (order?.totalPrice || 0) / required : 0; const receivedGbp = received * ltcToGbpRate; const requiredGbp = order?.totalPrice || 0; const missingGbp = missing * ltcToGbpRate; return { received, required, missing, receivedGbp, requiredGbp, missingGbp, percentage: required > 0 ? ((received / required) * 100).toFixed(1) : 0 }; }; const underpaidInfo = getUnderpaidInfo(order); // Add order refresh subscription useEffect(() => { const unsubscribe = cacheUtils.onOrderRefresh(() => { setRefreshTrigger(prev => prev + 1); }); return unsubscribe; }, []); if (loading) return (
Loading order details...
); if (error) return (
Error: {error}
); return (
{/* Header Section */}

Order {order?.orderId}

{order?.status?.toUpperCase()}
{isOrderUnderpaid(order) && (
UNDERPAID ({underpaidInfo?.percentage}%)
)}
{prevOrderId && ( )} {nextOrderId && ( )}
{/* Underpaid Alert Card */} {isOrderUnderpaid(order) && underpaidInfo && ( Payment Underpaid

Required Amount

£{underpaidInfo.requiredGbp.toFixed(2)}

{underpaidInfo.required.toFixed(8)} LTC

Received Amount

£{underpaidInfo.receivedGbp.toFixed(2)}

{underpaidInfo.received.toFixed(8)} LTC

Missing Amount

£{underpaidInfo.missingGbp.toFixed(2)}

{underpaidInfo.missing.toFixed(8)} LTC

Payment Progress

{underpaidInfo.percentage}% paid

{order?.paymentAddress && (

Payment Address:

{order.paymentAddress}
)}
)}
{/* Left Column - Order Details */}
{/* Products Card */} Products Product Quantity Price/Unit Total {order?.products.map((product) => ( {productNames[product.productId] || "Loading..."} {product.quantity} £{product.pricePerUnit.toFixed(2)} £{product.totalItemPrice.toFixed(2)} ))} Shipping ({order?.shippingMethod.type}) £{order?.shippingMethod.price.toFixed(2)} {order?.promotionCode && order?.discountAmount && order?.discountAmount > 0 && ( Promotion ({order.promotionCode}) -£{order.discountAmount.toFixed(2)} )} Total £{order?.totalPrice.toFixed(2)}
{/* Customer Details Card */} Customer Details {(order?.telegramUsername || order?.telegramBuyerId) && (
{order?.telegramUsername && (
Username
@{order.telegramUsername}
)} {order?.telegramBuyerId && (
Buyer ID
{order?.telegramBuyerId}
)}
)} {/* Customer History */} {customerInsights && (
Total Orders {customerInsights.totalOrders}
Total Spent £{customerInsights.totalSpent.toFixed(2)}
Success Rate {customerInsights.paymentSuccessRate.toFixed(1)}%
Customer Since {customerInsights.firstOrder ? new Date(customerInsights.firstOrder).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : 'N/A' }
{customerInsights.cancellationRate > 20 && (
High cancellation rate: {customerInsights.cancellationRate.toFixed(1)}%
)}
)}