Refines the admin ban page with better dialog state management and feedback during ban/unban actions. Adds a product cloning feature to the products dashboard and updates the product table to support cloning. Improves error handling in ChatDetail for authentication errors, and enhances middleware to handle auth check timeouts and network errors more gracefully. Also updates BanUserCard to validate user ID and ensure correct request formatting.
154 lines
6.4 KiB
TypeScript
154 lines
6.4 KiB
TypeScript
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator, Copy } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Product } from "@/models/products";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
interface ProductTableProps {
|
|
products: Product[];
|
|
loading: boolean;
|
|
onEdit: (product: Product) => void;
|
|
onClone?: (product: Product) => void;
|
|
onDelete: (productId: string) => void;
|
|
onToggleEnabled: (productId: string, enabled: boolean) => void;
|
|
onProfitAnalysis?: (productId: string, productName: string) => void;
|
|
getCategoryNameById: (categoryId: string) => string;
|
|
}
|
|
|
|
const ProductTable = ({
|
|
products,
|
|
loading,
|
|
onEdit,
|
|
onClone,
|
|
onDelete,
|
|
onToggleEnabled,
|
|
onProfitAnalysis,
|
|
getCategoryNameById
|
|
}: ProductTableProps) => {
|
|
|
|
const sortedProducts = [...products].sort((a, b) => {
|
|
const categoryNameA = getCategoryNameById(a.category);
|
|
const categoryNameB = getCategoryNameById(b.category);
|
|
return categoryNameA.localeCompare(categoryNameB);
|
|
});
|
|
|
|
const getStockIcon = (product: Product) => {
|
|
if (!product.stockTracking) return null;
|
|
|
|
if (product.stockStatus === 'out_of_stock') {
|
|
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
|
} else if (product.stockStatus === 'low_stock') {
|
|
return <AlertCircle className="h-4 w-4 text-amber-500" />;
|
|
} else {
|
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
|
<Table className="relative">
|
|
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead className="w-[200px]">Product</TableHead>
|
|
<TableHead className="hidden sm:table-cell text-center">Category</TableHead>
|
|
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
|
|
<TableHead className="text-center">Stock</TableHead>
|
|
<TableHead className="hidden lg:table-cell text-center">Enabled</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
Array.from({ length: 1 }).map((_, index) => (
|
|
<TableRow key={index}>
|
|
<TableCell>Loading...</TableCell>
|
|
<TableCell>Loading...</TableCell>
|
|
<TableCell>Loading...</TableCell>
|
|
<TableCell>Loading...</TableCell>
|
|
<TableCell>Loading...</TableCell>
|
|
<TableCell>Loading...</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : sortedProducts.length > 0 ? (
|
|
sortedProducts.map((product) => (
|
|
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
|
|
<TableCell>
|
|
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
|
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
|
{getCategoryNameById(product.category)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="hidden sm:table-cell text-center">{getCategoryNameById(product.category)}</TableCell>
|
|
<TableCell className="hidden md:table-cell text-center">{product.unitType}</TableCell>
|
|
<TableCell className="text-center">
|
|
{product.stockTracking ? (
|
|
<div className="flex items-center justify-center gap-1">
|
|
{getStockIcon(product)}
|
|
<span className="text-sm">
|
|
{product.currentStock !== undefined ? product.currentStock : 0} {product.unitType}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<Badge variant="outline" className="text-xs">Not Tracked</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="hidden lg:table-cell text-center">
|
|
<Switch
|
|
checked={product.enabled !== false}
|
|
onCheckedChange={(checked) => onToggleEnabled(product._id as string, checked)}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-right flex justify-end space-x-1">
|
|
{onProfitAnalysis && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onProfitAnalysis(product._id as string, product.name)}
|
|
className="text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
|
title="Profit Analysis"
|
|
>
|
|
<Calculator className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
{onClone && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onClone(product)}
|
|
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20"
|
|
title="Clone Listing"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button variant="ghost" size="sm" onClick={() => onEdit(product)} title="Edit Product">
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onDelete(product._id as string)}
|
|
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
|
|
title="Delete Product"
|
|
>
|
|
<Trash className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="h-24 text-center">
|
|
No products found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductTable;
|