"use client"; import { useState, useEffect, useCallback } from "react"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Eye, Loader2, CheckCircle2, XCircle, ChevronLeft, ChevronRight, ArrowUpDown, Truck, MessageCircle, AlertTriangle, Tag, Percent } from "lucide-react"; import Link from "next/link"; import { clientFetch } from '@/lib/api'; import { exportOrdersToCSV } from '@/lib/api-client'; import { toast } from "sonner"; import { Download } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { cacheUtils } from '@/lib/api-client'; interface Order { _id: string; orderId: string; status: string; totalPrice: number; orderDate: Date; paidAt?: Date; telegramUsername?: string; telegramBuyerId?: string; underpaid?: boolean; underpaymentAmount?: number; lastBalanceReceived?: number; cryptoTotal?: number; // Promotion fields promotion?: string; promotionCode?: string; discountAmount?: number; subtotalBeforeDiscount?: number; } type SortableColumns = "orderId" | "totalPrice" | "status" | "orderDate" | "paidAt"; interface StatusConfig { icon: React.ElementType; color: string; bgColor: string; animate?: string; } type OrderStatus = "paid" | "unpaid" | "acknowledged" | "shipped" | "completed" | "cancelled" | "confirming"; // Create a StatusFilter component to replace the missing component const StatusFilter = ({ currentStatus, onChange }: { currentStatus: string, onChange: (value: string) => void }) => { return ( ); }; // Create a PageSizeSelector component const PageSizeSelector = ({ currentSize, onChange, options }: { currentSize: number, onChange: (value: string) => void, options: number[] }) => { return (
Show:
); }; export default function OrderTable() { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [statusFilter, setStatusFilter] = useState("all"); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalOrders, setTotalOrders] = useState(0); const [sortConfig, setSortConfig] = useState<{ column: SortableColumns; direction: "asc" | "desc"; }>({ column: "orderDate", direction: "desc" }); const [selectedOrders, setSelectedOrders] = useState>(new Set()); const [isShipping, setIsShipping] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(20); const pageSizeOptions = [5, 10, 15, 20, 25, 50, 75, 100]; const [refreshTrigger, setRefreshTrigger] = useState(0); const [exporting, setExporting] = useState(false); // Add order refresh subscription useEffect(() => { const unsubscribe = cacheUtils.onOrderRefresh(() => { console.log("Order data refresh triggered in OrderTable"); setRefreshTrigger(prev => prev + 1); }); return unsubscribe; }, []); // Fetch orders with server-side pagination const fetchOrders = useCallback(async () => { try { setLoading(true); const queryParams = new URLSearchParams({ page: currentPage.toString(), limit: itemsPerPage.toString(), sortBy: sortConfig.column, sortOrder: sortConfig.direction, ...(statusFilter !== "all" && { status: statusFilter }), }); const data = await clientFetch(`/orders?${queryParams}`); console.log("Fetched orders with fresh data:", data.orders?.length || 0); setOrders(data.orders || []); setTotalPages(data.totalPages || 1); setTotalOrders(data.totalOrders || 0); } catch (error) { toast.error("Failed to fetch orders"); console.error("Fetch error:", error); } finally { setLoading(false); } }, [currentPage, statusFilter, itemsPerPage, sortConfig, refreshTrigger]); useEffect(() => { fetchOrders(); }, [fetchOrders]); // Reset to first page when filter changes useEffect(() => { setCurrentPage(1); }, [statusFilter]); const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; const handleItemsPerPageChange = (e: React.ChangeEvent) => { const newSize = parseInt(e.target.value, 10); setItemsPerPage(newSize); setCurrentPage(1); // Reset to first page when changing items per page }; // Derived data calculations const filteredOrders = orders.filter( (order) => statusFilter === "all" || order.status === statusFilter ); // Use the orders directly as they're already sorted by the server const paginatedOrders = filteredOrders; // Handlers const handleSort = (column: SortableColumns) => { setSortConfig(prev => ({ column, direction: prev.column === column && prev.direction === "asc" ? "desc" : "asc" })); setCurrentPage(1); // Reset to first page when changing sort order }; const toggleSelection = (orderId: string) => { setSelectedOrders(prev => { const newSet = new Set(prev); newSet.has(orderId) ? newSet.delete(orderId) : newSet.add(orderId); return newSet; }); }; const toggleAll = () => { if (selectedOrders.size === paginatedOrders.length) { setSelectedOrders(new Set()); } else { setSelectedOrders(new Set(paginatedOrders.map(o => o._id))); } }; const markAsShipped = async () => { if (selectedOrders.size === 0) { toast.warning("Please select orders to mark as shipped"); return; } try { setIsShipping(true); const response = await clientFetch("/orders/mark-shipped", { method: "POST", body: JSON.stringify({ orderIds: Array.from(selectedOrders) }) }); // Only update orders that were successfully marked as shipped if (response.success && response.success.orders) { const successfulOrderIds = new Set(response.success.orders.map((o: any) => o.id)); setOrders(prev => prev.map(order => successfulOrderIds.has(order._id) ? { ...order, status: "shipped" } : order ) ); if (response.failed && response.failed.count > 0) { toast.warning(`${response.failed.count} orders could not be marked as shipped`); } if (response.success.count > 0) { toast.success(`${response.success.count} orders marked as shipped`); } } setSelectedOrders(new Set()); } catch (error) { toast.error("Failed to update orders"); console.error("Shipping error:", error); } finally { setIsShipping(false); } }; const statusConfig: Record = { acknowledged: { icon: CheckCircle2, color: "text-purple-100", bgColor: "bg-purple-600/90 shadow-[0_0_10px_rgba(147,51,234,0.3)]" }, paid: { icon: CheckCircle2, color: "text-emerald-100", bgColor: "bg-emerald-600/90 shadow-[0_0_10px_rgba(16,185,129,0.3)]", animate: "animate-pulse" }, unpaid: { icon: XCircle, color: "text-amber-100", bgColor: "bg-amber-500/90" }, confirming: { icon: Loader2, color: "text-blue-100", bgColor: "bg-blue-500/90", animate: "animate-spin" }, shipped: { icon: Truck, color: "text-indigo-100", bgColor: "bg-indigo-600/90 shadow-[0_0_10px_rgba(79,70,229,0.3)]" }, completed: { icon: CheckCircle2, color: "text-green-100", bgColor: "bg-green-600/90" }, cancelled: { icon: XCircle, color: "text-gray-100", bgColor: "bg-gray-600/90" } }; // Helper function to determine if order is underpaid const isOrderUnderpaid = (order: Order) => { // More robust check - only show underpaid if status allows it and underpayment exists 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 display info const getUnderpaidInfo = (order: Order) => { 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 / required : 0; const missingGbp = missing * ltcToGbpRate; return { received, required, missing, missingGbp, percentage: required > 0 ? ((received / required) * 100).toFixed(1) : 0 }; }; // Add manual refresh function const handleRefresh = () => { console.log("Manual refresh triggered"); setRefreshTrigger(prev => prev + 1); toast.success("Orders refreshed"); }; // Handle CSV export const handleExportCSV = async () => { if (statusFilter === "all") { toast.error("Please select a specific status to export"); return; } try { setExporting(true); await exportOrdersToCSV(statusFilter); toast.success(`Orders exported successfully!`); } catch (error) { console.error("Error exporting orders:", error); toast.error("Failed to export orders"); } finally { setExporting(false); } }; // Add periodic refresh for underpaid orders useEffect(() => { // Check if we have any underpaid orders const hasUnderpaidOrders = orders.some(order => isOrderUnderpaid(order)); if (hasUnderpaidOrders) { console.log("Found underpaid orders, setting up refresh interval"); const interval = setInterval(() => { console.log("Auto-refreshing due to underpaid orders"); setRefreshTrigger(prev => prev + 1); }, 30000); // Refresh every 30 seconds if there are underpaid orders return () => clearInterval(interval); } }, [orders]); return (
{/* Filters header */}
handleItemsPerPageChange({ target: { value } } as React.ChangeEvent)} options={pageSizeOptions} /> {statusFilter !== "all" && ( )}
Mark Orders as Shipped Are you sure you want to mark {selectedOrders.size} order{selectedOrders.size !== 1 ? 's' : ''} as shipped? This action cannot be undone. Cancel Confirm
{/* Table */} {loading && (
)}
0} onCheckedChange={toggleAll} /> handleSort("orderId")}> Order ID handleSort("totalPrice")}> Total Promotion handleSort("status")}> Status handleSort("orderDate")}> Date handleSort("paidAt")}> Paid At Buyer Actions {paginatedOrders.map((order, index) => { const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle; const underpaidInfo = getUnderpaidInfo(order); return ( toggleSelection(order._id)} disabled={order.status !== "paid" && order.status !== "acknowledged"} /> #{order.orderId}
£{order.totalPrice.toFixed(2)} {underpaidInfo && ( -£{underpaidInfo.missingGbp.toFixed(2)} )}
{order.promotionCode ? (
{order.promotionCode}
-£{(order.discountAmount || 0).toFixed(2)}
) : ( - )}
{React.createElement(statusConfig[order.status as OrderStatus]?.icon || XCircle, { className: `h-3.5 w-3.5 ${statusConfig[order.status as OrderStatus]?.animate || ""}` })} {order.status.charAt(0).toUpperCase() + order.status.slice(1)}
{isOrderUnderpaid(order) && (
{underpaidInfo?.percentage}%
)}
{new Date(order.orderDate).toLocaleDateString("en-GB", { day: '2-digit', month: 'short', year: 'numeric', })} {new Date(order.orderDate).toLocaleTimeString("en-GB", { hour: '2-digit', minute: '2-digit' })} {order.paidAt ? new Date(order.paidAt).toLocaleDateString("en-GB", { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : "-"} {order.telegramUsername ? ( @{order.telegramUsername} ) : ( Guest )}
{(order.telegramBuyerId || order.telegramUsername) && ( )}
); })}
{/* Pagination */}
Page {currentPage} of {totalPages} ({totalOrders} total)
); }