Compare commits

..

2 Commits

Author SHA1 Message Date
g
f7e768f6d6 Improve product image handling and add costPerUnit
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Added a utility to generate product image URLs, ensuring images are displayed correctly in the product table. Updated the Product model to include an optional costPerUnit field. Minor UI and code formatting improvements were made for consistency.
2026-01-12 06:59:21 +00:00
g
7c7db0fc09 Update product-table.tsx 2026-01-12 06:54:28 +00:00
3 changed files with 198 additions and 131 deletions

View File

@@ -45,7 +45,7 @@ const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-ana
function ProductTableSkeleton() {
return (
<Card className="animate-in fade-in duration-500">
<Card className="animate-in fade-in duration-500 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" />

View File

@@ -14,11 +14,22 @@ import {
AlertCircle,
Calculator,
Copy,
PackageX,
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";
const getProductImageUrl = (product: Product) => {
if (!product.image) return null;
if (typeof product.image === 'string' && product.image.startsWith('http')) return product.image;
// Use the API endpoint to serve the image
return `${process.env.NEXT_PUBLIC_API_URL}/products/${product._id}/image`;
};
interface ProductTableProps {
products: Product[];
@@ -68,35 +79,57 @@ const ProductTable = ({
}
};
const renderProductRow = (product: Product, isDisabled: boolean = false) => (
<TableRow
const renderProductRow = (product: Product, index: number, isDisabled: boolean = false) => (
<motion.tr
key={product._id}
className={`transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70 ${isDisabled ? "opacity-60" : ""}`}
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>
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
<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 overflow-hidden relative">
{getProductImageUrl(product) ? (
<img
src={getProductImageUrl(product)!}
alt={product.name}
className="h-full w-full object-cover"
/>
) : (
<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">
<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">
<div className="flex items-center justify-center gap-1.5">
{getStockIcon(product)}
<span className="text-sm">
{product.currentStock !== undefined ? product.currentStock : 0}{" "}
{product.unitType}
<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="outline" className="text-xs">
Not Tracked
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-muted-foreground bg-muted/50">
Unlimited
</Badge>
)}
</TableCell>
@@ -106,17 +139,19 @@ const ProductTable = ({
onCheckedChange={(checked) =>
onToggleEnabled(product._id as string, checked)
}
className="data-[state=checked]:bg-primary"
/>
</TableCell>
<TableCell className="text-right flex justify-end space-x-1">
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{onProfitAnalysis && (
<Button
variant="ghost"
size="sm"
size="icon"
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"
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" />
@@ -125,9 +160,9 @@ const ProductTable = ({
{onClone && (
<Button
variant="ghost"
size="sm"
size="icon"
onClick={() => onClone(product)}
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20"
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" />
@@ -135,28 +170,30 @@ const ProductTable = ({
)}
<Button
variant="ghost"
size="sm"
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="sm"
size="icon"
onClick={() => onDelete(product._id as string)}
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
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>
</TableRow>
</motion.tr>
);
const renderTableHeader = () => (
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
<TableRow className="hover:bg-transparent">
<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
@@ -166,57 +203,86 @@ const ProductTable = ({
<TableHead className="hidden lg:table-cell text-center">
Enabled
</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead className="text-right pr-6">Actions</TableHead>
</TableRow>
</TableHeader>
);
return (
<div className="space-y-6">
<div className="space-y-8">
{/* Enabled Products Table */}
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
<Table className="relative">
<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 ? (
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>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
Loading products...
</TableCell>
</TableRow>
))
) : sortedEnabledProducts.length > 0 ? (
sortedEnabledProducts.map((product) => renderProductRow(product))
sortedEnabledProducts.map((product, index) => renderProductRow(product, index))
) : (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
No enabled products found.
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<PackageX 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 && (
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden bg-gray-50/30 dark:bg-zinc-900/30">
<Table className="relative">
<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>
{sortedDisabledProducts.map((product) =>
renderProductRow(product, true),
<AnimatePresence mode="popLayout">
{sortedDisabledProducts.map((product, index) =>
renderProductRow(product, index, true),
)}
</AnimatePresence>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
</div>
);
};
export default ProductTable;

View File

@@ -15,4 +15,5 @@ export interface Product {
pricePerUnit: number;
}>;
image?: string | File | null | undefined;
costPerUnit?: number;
}