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.
This commit is contained in:
g
2026-01-12 06:59:21 +00:00
parent 7c7db0fc09
commit f7e768f6d6
3 changed files with 60 additions and 47 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" />
@@ -152,7 +152,7 @@ export default function ProductsPage() {
const [importModalOpen, setImportModalOpen] = useState(false);
const [addProductOpen, setAddProductOpen] = useState(false);
const [profitAnalysisOpen, setProfitAnalysisOpen] = useState(false);
const [selectedProductForAnalysis, setSelectedProductForAnalysis] = useState<{id: string, name: string} | null>(null);
const [selectedProductForAnalysis, setSelectedProductForAnalysis] = useState<{ id: string, name: string } | null>(null);
// Fetch products and categories
useEffect(() => {
@@ -210,7 +210,7 @@ export default function ProductsPage() {
}));
};
// Handle input changes
// Handle input changes
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => setProductData({ ...productData, [e.target.name]: e.target.value });
@@ -323,9 +323,9 @@ export default function ProductsPage() {
...product,
pricing: product.pricing
? product.pricing.map((tier) => ({
minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit,
}))
minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit,
}))
: [{ minQuantity: 1, pricePerUnit: 0 }],
costPerUnit: product.costPerUnit || 0,
});
@@ -343,9 +343,9 @@ export default function ProductsPage() {
image: null, // Clear image so user can upload a new one
pricing: product.pricing
? product.pricing.map((tier) => ({
minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit,
}))
minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit,
}))
: [{ minQuantity: 1, pricePerUnit: 0 }],
costPerUnit: product.costPerUnit || 0,
// Reset stock to defaults for cloned product

View File

@@ -1,4 +1,5 @@
Table,
import {
Table,
TableBody,
TableCell,
TableHead,
@@ -13,7 +14,7 @@ import {
AlertCircle,
Calculator,
Copy,
PackageOffset,
PackageX,
Archive
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -23,6 +24,13 @@ 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[];
loading: boolean;
@@ -82,9 +90,13 @@ const ProductTable = ({
>
<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" />
<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>
)}
@@ -110,7 +122,7 @@ const ProductTable = ({
<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.stockStatus === 'low_stock' ? 'text-amber-500' : 'text-foreground'
}`}>
{product.currentStock !== undefined ? product.currentStock : 0}
</span>
@@ -227,7 +239,7 @@ const ProductTable = ({
<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" />
<PackageX className="h-8 w-8 opacity-50" />
<p>No active products found</p>
</div>
</TableCell>

View File

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