Files
ember-market-frontend/backend/routes/products.routes.js
2025-03-10 17:39:37 +00:00

456 lines
14 KiB
JavaScript

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;