Introduces an admin-only OrderDetailsModal component for viewing and managing order details, including status updates and transaction info. Updates OrdersTable to support the modal, and enhances the admin dashboard page with lazy loading and skeletons for better UX. Also fixes API client base URL handling for /api prefix.
257 lines
9.2 KiB
TypeScript
257 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
|
|
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 [currentPage, setCurrentPage] = useState(1);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [selectedOrderId, setSelectedOrderId] = useState<number | string | null>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const itemsPerPage = 10;
|
|
|
|
// Filter orders based on search and status
|
|
const filteredOrders = orders.filter(order => {
|
|
const matchesSearch = 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;
|
|
});
|
|
|
|
// Calculate pagination
|
|
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
|
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
const endIndex = startIndex + itemsPerPage;
|
|
const currentOrders = filteredOrders.slice(startIndex, endIndex);
|
|
|
|
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>
|
|
<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>
|
|
);
|
|
}
|