456 lines
14 KiB
JavaScript
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; |