Files
ember-market-frontend/components/tables/product-table.tsx
2026-01-12 06:54:28 +00:00

277 lines
10 KiB
TypeScript

Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Edit,
Trash,
AlertTriangle,
CheckCircle,
AlertCircle,
Calculator,
Copy,
PackageOffset,
Archive
} 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";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { motion, AnimatePresence } from "framer-motion";
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) => {
// Separate enabled and disabled products
const enabledProducts = products.filter((p) => p.enabled !== false);
const disabledProducts = products.filter((p) => p.enabled === false);
const sortByCategory = (productList: Product[]) => {
return [...productList].sort((a, b) => {
const categoryNameA = getCategoryNameById(a.category);
const categoryNameB = getCategoryNameById(b.category);
return categoryNameA.localeCompare(categoryNameB);
});
};
const sortedEnabledProducts = sortByCategory(enabledProducts);
const sortedDisabledProducts = sortByCategory(disabledProducts);
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" />;
}
};
const renderProductRow = (product: Product, index: number, isDisabled: boolean = false) => (
<motion.tr
key={product._id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className={`group hover:bg-muted/40 border-b border-border/50 transition-colors ${isDisabled ? "opacity-60 bg-muted/20" : ""}`}
>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded bg-muted/50 flex items-center justify-center text-muted-foreground">
{product.image ? (
<img src={product.image} alt={product.name} className="h-full w-full object-cover rounded" />
) : (
<span className="text-xs font-bold">{product.name.charAt(0).toUpperCase()}</span>
)}
</div>
<div>
<div className="truncate max-w-[180px]">{product.name}</div>
<div className="sm:hidden text-xs text-muted-foreground">
{getCategoryNameById(product.category)}
</div>
</div>
</div>
</TableCell>
<TableCell className="hidden sm:table-cell text-center">
<Badge variant="outline" className="font-normal bg-background/50">
{getCategoryNameById(product.category)}
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell text-center text-muted-foreground text-sm">
{product.unitType}
</TableCell>
<TableCell className="text-center">
{product.stockTracking ? (
<div className="flex items-center justify-center gap-1.5">
{getStockIcon(product)}
<span className={`text-sm font-medium ${product.stockStatus === 'out_of_stock' ? 'text-destructive' :
product.stockStatus === 'low_stock' ? 'text-amber-500' : 'text-foreground'
}`}>
{product.currentStock !== undefined ? product.currentStock : 0}
</span>
</div>
) : (
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-muted-foreground bg-muted/50">
Unlimited
</Badge>
)}
</TableCell>
<TableCell className="hidden lg:table-cell text-center">
<Switch
checked={product.enabled !== false}
onCheckedChange={(checked) =>
onToggleEnabled(product._id as string, checked)
}
className="data-[state=checked]:bg-primary"
/>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{onProfitAnalysis && (
<Button
variant="ghost"
size="icon"
onClick={() =>
onProfitAnalysis(product._id as string, product.name)
}
className="h-8 w-8 text-emerald-500 hover:text-emerald-600 hover:bg-emerald-500/10"
title="Profit Analysis"
>
<Calculator className="h-4 w-4" />
</Button>
)}
{onClone && (
<Button
variant="ghost"
size="icon"
onClick={() => onClone(product)}
className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-500/10"
title="Clone Listing"
>
<Copy className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(product)}
className="h-8 w-8 text-muted-foreground hover:text-foreground"
title="Edit Product"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(product._id as string)}
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
title="Delete Product"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</TableCell>
</motion.tr>
);
const renderTableHeader = () => (
<TableHeader className="bg-muted/50 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-border/50">
<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 pr-6">Actions</TableHead>
</TableRow>
</TableHeader>
);
return (
<div className="space-y-8">
{/* Enabled Products Table */}
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/30">
<CardTitle className="text-lg font-medium flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-primary" />
Active Products
<Badge variant="secondary" className="ml-2 bg-background/80 backdrop-blur-sm">
{sortedEnabledProducts.length}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-[600px] overflow-auto">
<Table>
{renderTableHeader()}
<TableBody>
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
Loading products...
</TableCell>
</TableRow>
) : sortedEnabledProducts.length > 0 ? (
sortedEnabledProducts.map((product, index) => renderProductRow(product, index))
) : (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<PackageOffset className="h-8 w-8 opacity-50" />
<p>No active products found</p>
</div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Disabled Products Section */}
{!loading && disabledProducts.length > 0 && (
<Card className="border-border/40 bg-background/30 backdrop-blur-sm shadow-sm overflow-hidden opacity-90">
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/20">
<CardTitle className="text-lg font-medium flex items-center gap-2 text-muted-foreground">
<Archive className="h-5 w-5" />
Archived / Disabled
<Badge variant="outline" className="ml-2">
{sortedDisabledProducts.length}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-[400px] overflow-auto">
<Table>
{renderTableHeader()}
<TableBody>
<AnimatePresence mode="popLayout">
{sortedDisabledProducts.map((product, index) =>
renderProductRow(product, index, true),
)}
</AnimatePresence>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
</div>
);
};
export default ProductTable;