other
This commit is contained in:
456
backend/routes/products.routes.js
Normal file
456
backend/routes/products.routes.js
Normal file
@@ -0,0 +1,456 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user