Some checks failed
Build Frontend / build (push) Failing after 7s
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.
370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
|
|
import { Button } from "@/components/common/button";
|
|
import { Input } from "@/components/common/input";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
|
|
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { List } from 'react-window';
|
|
import OrderDetailsModal from "./OrderDetailsModal";
|
|
|
|
interface Order {
|
|
orderId: string | number;
|
|
userId: string;
|
|
total: number;
|
|
createdAt: string;
|
|
status: string;
|
|
items: Array<{
|
|
name: string;
|
|
quantity: number;
|
|
}>;
|
|
vendorUsername?: string;
|
|
}
|
|
|
|
interface OrdersTableProps {
|
|
orders: Order[];
|
|
/**
|
|
* Enable order details modal (admin-only feature)
|
|
* @default true
|
|
*/
|
|
enableModal?: boolean;
|
|
}
|
|
|
|
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';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Admin-only orders table component with order details modal
|
|
* This component should only be used in admin contexts
|
|
*/
|
|
export default function OrdersTable({ orders, enableModal = true }: OrdersTableProps) {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [selectedOrderId, setSelectedOrderId] = useState<string | number | null>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
const itemsPerPage = 20;
|
|
|
|
// Filter orders based on search and status
|
|
const filteredOrders = useMemo(() => {
|
|
return orders.filter((order) => {
|
|
const matchesSearch = searchTerm === "" ||
|
|
order.orderId.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
order.userId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(order.vendorUsername && order.vendorUsername.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
|
|
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
|
|
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
}, [orders, searchTerm, statusFilter]);
|
|
|
|
// Pagination calculations
|
|
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = startIndex + itemsPerPage;
|
|
const currentOrders = filteredOrders.slice(startIndex, endIndex);
|
|
|
|
// Virtual scrolling row renderer
|
|
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
const order = currentOrders[index];
|
|
if (!order) return null;
|
|
|
|
return (
|
|
<div style={style}>
|
|
<TableRow>
|
|
<TableCell className="font-medium">{order.orderId}</TableCell>
|
|
<TableCell>{order.userId}</TableCell>
|
|
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
|
|
<TableCell className="max-w-[200px] truncate">
|
|
{order.items.length > 0 ? order.items[0].name : 'No items'}
|
|
</TableCell>
|
|
<TableCell>£{order.total.toFixed(2)}</TableCell>
|
|
<TableCell>
|
|
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
|
|
{order.status.toUpperCase()}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>N/A</TableCell>
|
|
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
|
|
<TableCell className="text-right">
|
|
{enableModal ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleViewOrder(order.orderId)}
|
|
title="View order details (Admin only)"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
title="Order details modal disabled"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Determine if we should use virtual scrolling (for performance with large datasets)
|
|
const useVirtualScrolling = currentOrders.length > 50;
|
|
|
|
const handlePageChange = (page: number) => {
|
|
setCurrentPage(page);
|
|
};
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
setSearchTerm(value);
|
|
setCurrentPage(1); // Reset to first page when searching
|
|
};
|
|
|
|
const handleStatusFilterChange = (value: string) => {
|
|
setStatusFilter(value);
|
|
setCurrentPage(1); // Reset to first page when filtering
|
|
};
|
|
|
|
const handleViewOrder = (orderId: number | string) => {
|
|
setSelectedOrderId(orderId);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Order Management</CardTitle>
|
|
<CardDescription>View and manage platform orders</CardDescription>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search orders..."
|
|
className="pl-8 w-64"
|
|
value={searchTerm}
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Select value={statusFilter} onValueChange={handleStatusFilterChange}>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Status</SelectItem>
|
|
<SelectItem value="acknowledged">Acknowledged</SelectItem>
|
|
<SelectItem value="paid">Paid</SelectItem>
|
|
<SelectItem value="completed">Completed</SelectItem>
|
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button variant="outline" size="sm">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
Filters
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Order ID</TableHead>
|
|
<TableHead>Customer</TableHead>
|
|
<TableHead>Vendor</TableHead>
|
|
<TableHead>Product</TableHead>
|
|
<TableHead>Amount</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Payment</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
{useVirtualScrolling ? (
|
|
<List
|
|
height={400}
|
|
itemCount={currentOrders.length}
|
|
itemSize={60}
|
|
className="border"
|
|
>
|
|
{({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
const order = currentOrders[index];
|
|
if (!order) return null;
|
|
|
|
return (
|
|
<div style={style}>
|
|
<TableRow>
|
|
<TableCell className="font-medium">{order.orderId}</TableCell>
|
|
<TableCell>{order.userId}</TableCell>
|
|
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
|
|
<TableCell className="max-w-[200px] truncate">
|
|
{order.items.length > 0 ? order.items[0].name : 'No items'}
|
|
</TableCell>
|
|
<TableCell>£{order.total.toFixed(2)}</TableCell>
|
|
<TableCell>
|
|
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
|
|
{order.status.toUpperCase()}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>N/A</TableCell>
|
|
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
|
|
<TableCell className="text-right">
|
|
{enableModal ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleViewOrder(order.orderId)}
|
|
title="View order details (Admin only)"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
title="Order details modal disabled"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
</div>
|
|
);
|
|
}}
|
|
</List>
|
|
) : (
|
|
<TableBody>
|
|
{currentOrders.map((order) => (
|
|
<TableRow key={order.orderId}>
|
|
<TableCell className="font-medium">{order.orderId}</TableCell>
|
|
<TableCell>{order.userId}</TableCell>
|
|
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
|
|
<TableCell className="max-w-[200px] truncate">
|
|
{order.items.length > 0 ? order.items[0].name : 'No items'}
|
|
</TableCell>
|
|
<TableCell>£{order.total.toFixed(2)}</TableCell>
|
|
<TableCell>
|
|
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
|
|
{order.status.toUpperCase()}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>N/A</TableCell>
|
|
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
|
|
<TableCell className="text-right">
|
|
{enableModal ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleViewOrder(order.orderId)}
|
|
title="View order details (Admin only)"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
title="Order details modal disabled"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
)}
|
|
</Table>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-between mt-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
Showing {startIndex + 1} to {Math.min(endIndex, filteredOrders.length)} of {filteredOrders.length} orders
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePageChange(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
Previous
|
|
</Button>
|
|
|
|
<div className="flex items-center space-x-1">
|
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
const pageNum = i + 1;
|
|
return (
|
|
<Button
|
|
key={pageNum}
|
|
variant={currentPage === pageNum ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => handlePageChange(pageNum)}
|
|
className="w-8 h-8 p-0"
|
|
>
|
|
{pageNum}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePageChange(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
>
|
|
Next
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
|
|
{/* Order Details Modal - Admin only */}
|
|
{enableModal && selectedOrderId && (
|
|
<OrderDetailsModal
|
|
orderId={selectedOrderId}
|
|
open={isModalOpen}
|
|
onOpenChange={setIsModalOpen}
|
|
/>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|