import express from "express"; import Product from "../models/Product.model.js"; import multer from "multer"; import sharp from "sharp"; import { protectVendor } from "../middleware/authMiddleware.js"; import fs from "fs"; import path, { dirname } from "path"; import Store from "../models/Store.model.js"; import mongoose from "mongoose"; import { fileURLToPath } from "url"; const router = express.Router(); // Get the current directory and set up a relative uploads path const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const uploadsDir = path.join(process.cwd(), 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 15 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith("image/")) { cb(null, true); } else { cb(new Error("Only image files are allowed!"), false); } }, }); const handleMulterError = (err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === "LIMIT_FILE_SIZE") { return res.status(400).json({ message: "File size too large. Maximum allowed size is 15 MB." }); } return res.status(400).json({ message: "Multer error occurred.", error: err.message }); } next(err); }; router.use(handleMulterError) // 📌 Upload Image for Product router.put("/:id/image", protectVendor, upload.single("file"), handleMulterError, async (req, res) => { try { const productId = req.params.id; if (!productId) return res.status(400).json({ message: "Missing product ID." }); const product = await Product.findOne({ _id: productId, storeId: req.user.storeId }); if (!product) { return res.status(404).json({ message: "Product not found." }); } const file = req.file; if (!file) return res.status(400).json({ message: "No file uploaded." }); // Generate a new filename for the uploaded image const outputFileName = `${Date.now()}-${file.originalname.split('.').slice(0, -1).join('.')}.jpg`; const outputFilePath = path.join(uploadsDir, outputFileName); await sharp(file.buffer) .jpeg({ quality: 80 }) .toFile(outputFilePath); // Check the final file size const stats = fs.statSync(outputFilePath); if (stats.size > 10 * 1024 * 1024) { fs.unlinkSync(outputFilePath); return res.status(400).json({ message: "File size too large after processing." }); } // Update the product in the database const updatedProduct = await Product.findOneAndUpdate( { _id: productId, storeId: req.user.storeId }, { image: outputFileName }, { new: true } ); if (!updatedProduct) { fs.unlinkSync(outputFilePath); // Clean up uploaded file if product not found return res.status(404).json({ message: "Product not found." }); } res.json(updatedProduct); } catch (error) { if(req.file){ const outputFileName = `${Date.now()}-${req.file.originalname.split('.').slice(0, -1).join('.')}.jpg`; const outputFilePath = path.join(uploadsDir, outputFileName); if (fs.existsSync(outputFilePath)) { fs.unlinkSync(outputFilePath); } } res.status(500).json({ message: "Failed to upload image.", error }); } }); router.use(express.json({ limit: "50mb" })); router.get("/", protectVendor, async (req, res) => { try { if (!req.user.storeId) { return res.status(400).json({ message: "Store not found for this vendor" }); } const products = await Product.find({ storeId: req.user.storeId }).select("-base64Image"); res.json(products); } catch (error) { res.status(500).json({ message: "Failed to fetch products", error }); } }); // 📌 Add New Product router.post("/", protectVendor, async (req, res) => { try { if (!req.user.storeId) { return res.status(400).json({ message: "Store not found for this vendor" }); } const { name, description, category, unitType, pricing } = req.body; if (!name || !category || !unitType || !pricing || !pricing.length) { return res.status(400).json({ message: "Missing required fields" }); } const formattedPricing = pricing.map((tier) => ({ minQuantity: tier.minQuantity, pricePerUnit: tier.pricePerUnit, })); const newProduct = new Product({ storeId: req.user.storeId, name, description, category, unitType, pricing: formattedPricing, }); const savedProduct = await newProduct.save(); res.status(201).json(savedProduct); } catch (error) { res.status(400).json({ message: "Failed to add product", error }); } }); // 📌 Edit a Product by ID router.put("/:id", protectVendor, async (req, res) => { try { const productId = req.params.id; const updatableFields = ["name", "description", "category", "unitType"]; const updateFields = {}; for (const field of updatableFields) { if (req.body[field] !== undefined) { updateFields[field] = req.body[field]; } } if (req.body.pricing !== undefined) { if (!Array.isArray(req.body.pricing)) { return res.status(400).json({ message: "Pricing must be an array" }); } updateFields.pricing = req.body.pricing.map((tier) => ({ minQuantity: tier.minQuantity, pricePerUnit: tier.pricePerUnit, })); } const updatedProduct = await Product.findOneAndUpdate( { _id: productId, storeId: req.user.storeId }, updateFields, { new: true } ); if (!updatedProduct) { return res .status(404) .json({ message: "Product not found or not owned by this store" }); } return res.json(updatedProduct); } catch (error) { res.status(400).json({ message: "Failed to update product", error }); } }); router.get("/:id/image", async (req, res) => { try { const productId = req.params.id; const product = await Product.findById(productId); if (!product || !product.image) { return res.status(404).json({ message: "Image not found" }); } const imagePath = path.join(uploadsDir, product.image); console.log("Image path:", imagePath); if (fs.existsSync(imagePath)) { res.sendFile(imagePath); } else { res.status(404).json({ message: "Image file does not exist" }); } } catch (error) { console.error("Error fetching image:", error); res.status(400).json({ message: "Failed to fetch image", error }); } }); router.delete("/:id", protectVendor, async (req, res) => { try { const productId = req.params.id; const deletedProduct = await Product.findByIdAndDelete({ _id: productId, storeId: req.user.storeId }); if (!deletedProduct) { return res.status(404).json({ message: "Product not found" }); } res.json({ message: "Product deleted successfully" }); } catch (error) { res.status(400).json({ message: "Failed to delete product", error }); } }); router.get("/:id", protectVendor, async (req, res) => { try { const productId = req.params.id; const product = await Product.findById(productId); if (!product) { return res.status(404).json({ message: "Product not found" }); } res.status(200).json(product); } catch (error) { res.status(400).json({ message: "Failed to fetch product", error }); } }); // Helper function to escape special regex characters function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Helper function to standardize unit types function standardizeUnitType(unit) { const unitMap = { 'gram': 'gr', 'grams': 'gr', 'g': 'gr', 'gr': 'gr', 'piece': 'pcs', 'pieces': 'pcs', 'pcs': 'pcs', 'kilo': 'kg', 'kilos': 'kg', 'kilogram': 'kg', 'kilograms': 'kg', 'kg': 'kg' }; return unitMap[unit.toLowerCase()] || unit.toLowerCase(); } // 📌 Batch Add Products router.post("/batch", protectVendor, async (req, res) => { try { if (!req.user.storeId) { return res.status(400).json({ message: "Store not found for this vendor" }); } const { products } = req.body; if (!Array.isArray(products) || products.length === 0) { return res.status(400).json({ message: "Products array is required and cannot be empty" }); } // Get store to validate categories const store = await Store.findById(req.user.storeId); if (!store) { return res.status(404).json({ message: "Store not found" }); } const processedProducts = []; const skippedProducts = []; let completedCount = 0; console.log(`Starting batch processing of ${products.length} products...`); for (const product of products) { const { name, category, subcategory, prices } = product; completedCount++; // Validate required fields if (!name || !category || !prices || !Array.isArray(prices) || prices.length === 0) { console.log(`Skipping product ${completedCount}/${products.length}: Missing required fields`); skippedProducts.push({ name: name || 'Unknown', reason: 'Missing required fields' }); continue; } // Check if product already exists - with escaped special characters const escapedName = escapeRegExp(name); const existingProduct = await Product.findOne({ storeId: req.user.storeId, name: { $regex: new RegExp(`^${escapedName}$`, 'i') } }); if (existingProduct) { console.log(`Skipping product ${completedCount}/${products.length}: "${name}" already exists`); skippedProducts.push({ name, reason: 'Product already exists' }); continue; } // Find or create main category let targetCategory; const existingMainCategory = store.categories.find(cat => cat.name.toLowerCase() === category.toLowerCase() && !cat.parentId ); if (!existingMainCategory) { const newCategory = { _id: new mongoose.Types.ObjectId(), name: category, parentId: null }; store.categories.push(newCategory); targetCategory = newCategory; console.log(`Created new main category: ${category}`); } else { targetCategory = existingMainCategory; } // If subcategory is provided, find or create it if (subcategory) { const existingSubcategory = store.categories.find(cat => cat.name.toLowerCase() === subcategory.toLowerCase() && cat.parentId?.toString() === targetCategory._id.toString() ); if (existingSubcategory) { targetCategory = existingSubcategory; } else { const newSubcategory = { _id: new mongoose.Types.ObjectId(), name: subcategory, parentId: targetCategory._id }; store.categories.push(newSubcategory); targetCategory = newSubcategory; console.log(`Created new subcategory: ${subcategory} under ${category}`); } } console.log(targetCategory._id) // Convert prices array to pricing format const pricing = prices.map(price => ({ minQuantity: price.quantity, pricePerUnit: price.pricePerUnit })); const standardizedUnitType = standardizeUnitType(prices[0].unit); // Validate unit type if (!["pcs", "gr", "kg"].includes(standardizedUnitType)) { console.log(`Skipping product ${completedCount}/${products.length}: Invalid unit type "${prices[0].unit}"`); skippedProducts.push({ name, reason: `Invalid unit type: ${prices[0].unit}` }); continue; } const newProduct = new Product({ storeId: req.user.storeId, name, category: targetCategory._id, unitType: standardizedUnitType, pricing }); try { await store.save(); const savedProduct = await newProduct.save(); processedProducts.push(savedProduct); console.log(`Processed ${completedCount}/${products.length}: Successfully added "${name}"`); } catch (error) { console.error(`Failed to save product ${completedCount}/${products.length}: "${name}"`, error); skippedProducts.push({ name, reason: `Save error: ${error.message}` }); } } console.log(`Batch processing completed. Processed: ${processedProducts.length}, Skipped: ${skippedProducts.length}`); if (processedProducts.length === 0) { return res.status(400).json({ message: "No valid products were processed", totalSubmitted: products.length, totalProcessed: 0, skippedProducts }); } res.status(201).json({ message: "Products batch processed successfully", totalSubmitted: products.length, totalProcessed: processedProducts.length, totalSkipped: skippedProducts.length, products: processedProducts, skippedProducts, categories: store.categories }); } catch (error) { console.error("Batch processing error:", error); res.status(500).json({ message: "Failed to process product batch", error: error.message }); } }); // 📌 Disable stock tracking for all products (one-time setup route) router.post("/disable-stock-tracking", protectVendor, async (req, res) => { try { // Ensure the user has admin privileges or apply appropriate restrictions here if (!req.user.storeId) { return res.status(400).json({ message: "Store not found for this vendor" }); } // Update all products for this store to disable stock tracking const result = await Product.updateMany( { storeId: req.user.storeId }, { stockTracking: false, stockStatus: "in_stock" // Set a default status to avoid UI issues } ); res.status(200).json({ message: "Stock tracking disabled for all products", modifiedCount: result.modifiedCount, matchedCount: result.matchedCount }); } catch (error) { console.error("Error disabling stock tracking:", error); res.status(500).json({ message: "Failed to disable stock tracking", error: error.message }); } }); export default router;