Improve product image handling and add costPerUnit
All checks were successful
Build Frontend / build (push) Successful in 1m10s
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:
@@ -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" />
|
||||
@@ -60,8 +60,8 @@ function ProductTableSkeleton() {
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{['Product', 'Category', 'Price', 'Stock', 'Status', 'Actions'].map((header, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="h-4 w-20 flex-1 animate-in fade-in"
|
||||
style={{
|
||||
animationDelay: `${i * 50}ms`,
|
||||
@@ -72,10 +72,10 @@ function ProductTableSkeleton() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
<div
|
||||
key={i}
|
||||
className="border-b last:border-b-0 p-4 animate-in fade-in"
|
||||
style={{
|
||||
animationDelay: `${300 + i * 50}ms`,
|
||||
@@ -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(() => {
|
||||
@@ -169,7 +169,7 @@ export default function ProductsPage() {
|
||||
const fetchDataAsync = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
const [fetchedProducts, fetchedCategories] = await Promise.all([
|
||||
clientFetch('/products'),
|
||||
clientFetch('/categories'),
|
||||
@@ -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 });
|
||||
@@ -226,7 +226,7 @@ export default function ProductsPage() {
|
||||
setProductData({ ...productData, pricing: updatedPricing });
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -247,7 +247,7 @@ export default function ProductsPage() {
|
||||
// Save the product data
|
||||
const endpoint = editing ? `/products/${data._id}` : "/products";
|
||||
const method = editing ? "PUT" : "POST";
|
||||
|
||||
|
||||
const productResponse = await clientFetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
@@ -259,10 +259,10 @@ export default function ProductsPage() {
|
||||
// If there's a new image to upload
|
||||
if (file) {
|
||||
const imageEndpoint = `/products/${productResponse._id || data._id}/image`;
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL}${imageEndpoint}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -279,10 +279,10 @@ export default function ProductsPage() {
|
||||
// Refresh products list
|
||||
const fetchedProducts = await clientFetch('/products');
|
||||
setProducts(fetchedProducts);
|
||||
|
||||
|
||||
setModalOpen(false);
|
||||
setLoading(false);
|
||||
|
||||
|
||||
toast.success(
|
||||
editing ? "Product updated successfully" : "Product added successfully"
|
||||
);
|
||||
@@ -296,18 +296,18 @@ export default function ProductsPage() {
|
||||
// Handle delete product
|
||||
const handleDeleteProduct = async (productId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this product?")) return;
|
||||
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
await clientFetch(`/products/${productId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
|
||||
// Refresh products list
|
||||
const fetchedProducts = await clientFetch('/products');
|
||||
setProducts(fetchedProducts);
|
||||
|
||||
|
||||
toast.success("Product deleted successfully");
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
@@ -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,16 +343,16 @@ 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
|
||||
currentStock: 0,
|
||||
stockStatus: 'out_of_stock' as const,
|
||||
};
|
||||
|
||||
|
||||
setProductData(clonedProduct);
|
||||
setEditing(false); // Set to false so it creates a new product
|
||||
setAddProductOpen(true);
|
||||
@@ -390,19 +390,19 @@ export default function ProductsPage() {
|
||||
// Filter products based on search term
|
||||
const filteredProducts = products.filter(product => {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
|
||||
// Search in product name
|
||||
if (product.name.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
|
||||
// Search in product description if it exists
|
||||
if (product.description && product.description.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
|
||||
// Search in category name
|
||||
const categoryName = getCategoryNameById(product.category).toLowerCase();
|
||||
if (categoryName.includes(searchLower)) return true;
|
||||
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -437,19 +437,19 @@ export default function ProductsPage() {
|
||||
const handleToggleEnabled = async (productId: string, enabled: boolean) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
await clientFetch(`/products/${productId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
|
||||
|
||||
// Update the local state
|
||||
setProducts(products.map(product =>
|
||||
product._id === productId
|
||||
? { ...product, enabled }
|
||||
setProducts(products.map(product =>
|
||||
product._id === productId
|
||||
? { ...product, enabled }
|
||||
: product
|
||||
));
|
||||
|
||||
|
||||
toast.success(`Product ${enabled ? 'enabled' : 'disabled'} successfully`);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
@@ -489,9 +489,9 @@ export default function ProductsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
variant="outline"
|
||||
<Button
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
variant="outline"
|
||||
className="gap-2 flex-1 sm:flex-none"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user