Files
ember-market-frontend/components/admin/OrdersTable.tsx
g 2db13cc9b7 Add admin order details modal and improve admin UI
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.
2025-12-17 23:38:17 +00:00

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>
);
}