diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index 2823be1..e7e8de5 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -16,6 +16,7 @@ import { ProductModal } from "@/components/modals/product-modal"; import ProductTable from "@/components/tables/product-table"; import { Category } from "@/models/categories"; import ImportProductsModal from "@/components/modals/import-products-modal"; +import { toast } from "sonner"; export default function ProductsPage() { const router = useRouter(); @@ -106,51 +107,76 @@ export default function ProductsPage() { const handleSaveProduct = async (data: Product, file?: File | null) => { - console.log("handleSaveProduct:", data, file); - - const adjustedPricing = data.pricing.map((tier) => ({ - minQuantity: tier.minQuantity, - pricePerUnit: - typeof tier.pricePerUnit === "string" - ? parseFloat(tier.pricePerUnit) - : tier.pricePerUnit, - })); - - const productToSave: Product = { - ...data, - pricing: adjustedPricing, - }; - + const authToken = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + if (!authToken) { + router.push("/login"); + return; + } + + setLoading(true); + try { - const authToken = document.cookie.split("Authorization=")[1]; - const apiUrl = editing - ? `${process.env.NEXT_PUBLIC_API_URL}/products/${data._id}` - : `${process.env.NEXT_PUBLIC_API_URL}/products`; - + const url = editing + ? `/api/products/${data._id}` + : "/api/products"; + + // Save product data const savedProduct = await saveProductData( - apiUrl, - productToSave, + url, + { + name: data.name, + description: data.description, + unitType: data.unitType, + category: data.category, + pricing: data.pricing, + stockTracking: data.stockTracking, + currentStock: data.currentStock, + lowStockThreshold: data.lowStockThreshold + }, authToken, editing ? "PUT" : "POST" ); - + if (file) { - await saveProductImage(`${process.env.NEXT_PUBLIC_API_URL}/products/${savedProduct._id}/image`, file, authToken); + await saveProductImage( + `/api/products/${savedProduct._id}/image`, + file, + authToken + ); } - - setProducts((prevProducts) => { - if (editing) { - return prevProducts.map((product) => - product._id === savedProduct._id ? savedProduct : product - ); - } else { - return [...prevProducts, savedProduct]; - } - }); - + + // If editing and stock values were updated, update stock in the dedicated endpoint + if (editing && data.stockTracking !== undefined) { + await saveProductData( + `/api/stock/${data._id}`, + { + stockTracking: data.stockTracking, + currentStock: data.currentStock || 0, + lowStockThreshold: data.lowStockThreshold || 10 + }, + authToken, + "PUT" + ); + } + + // Refresh products list + const fetchedProducts = await fetchProductData("/api/products", authToken); + setProducts(fetchedProducts); + setModalOpen(false); + setLoading(false); + + toast.success( + editing ? "Product updated successfully" : "Product added successfully" + ); } catch (error) { - console.error("Error saving product:", error); + console.error(error); + setLoading(false); + toast.error("Failed to save product"); } }; @@ -194,11 +220,14 @@ export default function ProductsPage() { description: "", unitType: "pcs", category: "", + stockTracking: true, + currentStock: 0, + lowStockThreshold: 10, pricing: [{ minQuantity: 1, pricePerUnit: 0 }], image: null, }); setEditing(false); - setAddProductOpen(true); + setModalOpen(true); }; // Get category name by ID diff --git a/app/dashboard/stock/page.tsx b/app/dashboard/stock/page.tsx new file mode 100644 index 0000000..69a5e13 --- /dev/null +++ b/app/dashboard/stock/page.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Layout from "@/components/layout/layout"; +import { Button } from "@/components/ui/button"; +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Product } from "@/models/products"; +import { Package, RefreshCw } from "lucide-react"; +import { fetchProductData, updateProductStock } from "@/lib/productData"; +import { toast } from "sonner"; + +export default function StockManagementPage() { + const router = useRouter(); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [editingStock, setEditingStock] = useState>({}); + const [stockValues, setStockValues] = useState>({}); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const authToken = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + if (!authToken) { + router.push("/login"); + return; + } + + const fetchDataAsync = async () => { + try { + const fetchedProducts = await fetchProductData("/api/products", authToken); + setProducts(fetchedProducts); + + // Initialize stock values + const initialStockValues: Record = {}; + fetchedProducts.forEach((product: Product) => { + if (product._id) { + initialStockValues[product._id] = product.currentStock || 0; + } + }); + setStockValues(initialStockValues); + + setLoading(false); + } catch (error) { + console.error("Error fetching products:", error); + setLoading(false); + } + }; + + fetchDataAsync(); + }, [router]); + + const handleEditStock = (productId: string) => { + setEditingStock({ + ...editingStock, + [productId]: true, + }); + }; + + const handleSaveStock = async (product: Product) => { + if (!product._id) return; + + const authToken = document.cookie + .split("; ") + .find((row) => row.startsWith("Authorization=")) + ?.split("=")[1]; + + if (!authToken) { + router.push("/login"); + return; + } + + try { + const newStockValue = stockValues[product._id] || 0; + + await updateProductStock( + product._id, + { + currentStock: newStockValue, + stockTracking: product.stockTracking + }, + authToken + ); + + // Update local products state + setProducts(products.map(p => { + if (p._id === product._id) { + return { + ...p, + currentStock: newStockValue + }; + } + return p; + })); + + setEditingStock({ + ...editingStock, + [product._id]: false, + }); + + toast.success("Stock updated successfully"); + } catch (error) { + console.error("Error updating stock:", error); + toast.error("Failed to update stock"); + } + }; + + const handleStockChange = (productId: string, value: number) => { + setStockValues({ + ...stockValues, + [productId]: value, + }); + }; + + const getStockStatus = (product: Product) => { + if (!product.stockTracking) return "Not Tracked"; + + if (!product.currentStock || product.currentStock <= 0) { + return "Out of Stock"; + } else if (product.lowStockThreshold && product.currentStock <= product.lowStockThreshold) { + return "Low Stock"; + } else { + return "In Stock"; + } + }; + + const filteredProducts = products.filter(product => { + const matchesSearch = + !searchTerm || + product.name.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesSearch; + }); + + const statsData = { + total: products.length, + inStock: products.filter(p => p.stockTracking && p.currentStock && p.currentStock > 0 && (!p.lowStockThreshold || p.currentStock > p.lowStockThreshold)).length, + lowStock: products.filter(p => p.stockTracking && p.lowStockThreshold && p.currentStock && p.currentStock <= p.lowStockThreshold && p.currentStock > 0).length, + outOfStock: products.filter(p => p.stockTracking && (!p.currentStock || p.currentStock <= 0)).length, + notTracked: products.filter(p => p.stockTracking === false).length + }; + + return ( + +
+
+

Stock Management

+ +
+ +
+
+
{statsData.total}
+
Total Products
+
+ +
+
{statsData.inStock}
+
In Stock
+
+ +
+
{statsData.lowStock}
+
Low Stock
+
+ +
+
{statsData.outOfStock}
+
Out of Stock
+
+ +
+
{statsData.notTracked}
+
Not Tracked
+
+
+ +
+
+

Inventory List

+
+ setSearchTerm(e.target.value)} + className="w-64" + /> + +
+
+ +
+ + + + Product + Unit + Status + Current Stock + Low Stock Threshold + Actions + + + + {loading ? ( + + + Loading... + + + ) : filteredProducts.length > 0 ? ( + filteredProducts.map((product) => ( + + {product.name} + {product.unitType} + + {getStockStatus(product)} + + + {editingStock[product._id as string] ? ( + handleStockChange(product._id as string, parseFloat(e.target.value))} + className="max-w-[100px]" + /> + ) : ( + product.stockTracking ? + `${product.currentStock || 0} ${product.unitType}` : + "N/A" + )} + + + {product.stockTracking ? + `${product.lowStockThreshold || 0} ${product.unitType}` : + "N/A"} + + + {product.stockTracking && ( + editingStock[product._id as string] ? ( + + ) : ( + + ) + )} + + + )) + ) : ( + + + No products found. + + + )} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index 0652c5d..b506965 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -102,15 +102,32 @@ export const ProductModal: React.FC = ({ }; const handleSave = async () => { - if (!productData.category) { - toast.error("Please select or add a category"); - return; + try { + // Validate required fields + if (!productData.name || !productData.category || !productData.unitType) { + toast.error("Please fill in all required fields"); + return; + } + + // Validate pricing tiers + if (!productData.pricing || productData.pricing.length === 0) { + toast.error("At least one pricing tier is required"); + return; + } + + // Make sure stock values are numbers + let stockData = { ...productData }; + if (stockData.stockTracking) { + stockData.currentStock = Number(stockData.currentStock) || 0; + stockData.lowStockThreshold = Number(stockData.lowStockThreshold) || 10; + } + + await onSave(stockData, selectedFile); + onClose(); + } catch (error) { + console.error("Error saving product:", error); + toast.error("Failed to save product"); } - - - onSave(productData, selectedFile); - toast.success(editing ? "Product updated!" : "Product added!"); - onClose(); }; const handleAddCategory = (newCategory: { _id: string; name: string; parentId?: string }) => { @@ -183,42 +200,111 @@ const ProductBasicInfo: React.FC<{ setProductData: React.Dispatch>; onAddCategory: (newCategory: { _id: string; name: string; parentId?: string }) => void; }> = ({ productData, handleChange, categories, setProductData, onAddCategory }) => ( -
+
- +
- +