import { Product } from "./ProductModel.js"; import cloudinary from "../../Utils/cloudinary.js"; import { v4 as uuidv4 } from "uuid"; import { CategoryModel } from "../Category/CategoryModel.js"; import { BrandModel } from "../Brands/BrandsModel.js"; import XLSX from "xlsx"; import fs from "fs"; import path from "path"; import mongoose from "mongoose"; // Function to handle product upload export const uploadProducts = async (req, res) => { try { if (!req.files || !req.files.file) { return res.status(400).json({ message: "No file uploaded" }); } const file = req.files.file; const filePath = path.join("public", "uploads", file.name); // Ensure 'uploads' directory exists if (!fs.existsSync(path.dirname(filePath))) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } // Move the file from temp to the uploads directory await file.mv(filePath); // Process the file const fileBuffer = fs.readFileSync(filePath); const workbook = XLSX.read(fileBuffer, { type: "buffer" }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); if (data.length <= 1) { return res .status(400) .json({ message: "Empty spreadsheet or no data found" }); } const headers = data[0]; // Map headers from the Excel file to your schema const headerMapping = { SKU: "SKU", "Product Name": "name", "Category Name": "category", "Brand Name": "brand", Price: "price", "GST (in %)": "GST", "HSN Code": "HSN_Code", "Description (Optional)": "description", }; const requiredHeaders = Object.keys(headerMapping); if (!requiredHeaders.every((header) => headers.includes(header))) { return res .status(400) .json({ message: "Missing required columns in spreadsheet" }); } const errors = []; const newlyCreated = []; const updatedProducts = []; const capitalizeWords = (str) => str .toLowerCase() .replace(/\b\w/g, (char) => char.toUpperCase()) .trim(); for (let i = 1; i < data.length; i++) { const row = data[i]; // Skip the row if it's completely empty if (row.every((cell) => cell === undefined || cell === "")) { continue; } const item = {}; headers.forEach((header, index) => { if (headerMapping[header]) { item[headerMapping[header]] = row[index] !== undefined ? row[index] : ""; } }); // Initialize error tracking for each item const missingFields = new Set(); let { SKU, name, category, brand, price, GST, HSN_Code, description } = item; // Trim leading and trailing spaces and apply case formatting name = name ? name.trim() : ""; // category = category ? capitalizeWords(category) : ""; // brand = brand ? capitalizeWords(brand) : ""; category = category ? category.trim() : ""; brand = brand ? brand.trim() : ""; // Validate required fields if (!SKU) missingFields.add("SKU"); if (!name) missingFields.add("name"); if (!category) missingFields.add("category"); if (!brand) missingFields.add("brand"); if (price === undefined || price === "") missingFields.add("price"); if (!GST) missingFields.add("GST"); if (!HSN_Code) missingFields.add("HSN_Code"); // Validate or create category let categoryDoc = null; let categoryname = ""; if (category) { categoryDoc = await CategoryModel.findOne({ categoryName: { $regex: new RegExp(`^${category}$`, "i") }, }).exec(); if (!categoryDoc) { categoryDoc = await CategoryModel.create({ categoryName: category, addedBy: req.user._id, }); } categoryname = categoryDoc.categoryName; item.category = categoryDoc._id; } // Validate or create brand let brandDoc = null; let brandname = ""; if (brand) { brandDoc = await BrandModel.findOne({ brandName: { $regex: new RegExp(`^${brand}$`, "i") }, }).exec(); if (!brandDoc) { brandDoc = await BrandModel.create({ brandName: brand, addedBy: req.user._id, }); } brandname = brandDoc.brandName; item.brand = brandDoc._id; } // Combine all errors into a single message let errorMessage = ""; if (missingFields.size > 0) { errorMessage += `Missing fields: ${Array.from(missingFields).join( ", " )}. `; } // If there are errors, push them to the errors array if (errorMessage.trim()) { errors.push({ SKU: SKU || "N/A", productName: name || "N/A", category: categoryname || "N/A", brand: brandname || "N/A", GST: GST || "N/A", HSN_Code: HSN_Code || "N/A", price: price || "N/A", message: errorMessage.trim(), }); continue; } // Ensure fields are set to empty strings if not provided description = description !== undefined ? description : ""; // Check for existing product by SKU let existingProduct = await Product.findOne({ SKU }).exec(); if (existingProduct) { // Track changes const updatedFields = []; let updatedProduct = { ...existingProduct._doc }; // Update product fields if they have changed if (name !== existingProduct.name) { updatedFields.push("name"); updatedProduct.name = name; } if ( categoryDoc && existingProduct.category.toString() !== item.category.toString() ) { updatedFields.push("category"); updatedProduct.category = categoryname; } else { updatedProduct.category = categoryDoc.categoryName; } if ( price !== undefined && price !== "" && existingProduct.price !== price ) { updatedFields.push("price"); updatedProduct.price = price; } if ( brandDoc && existingProduct.brand.toString() !== item.brand.toString() ) { updatedFields.push("brand"); updatedProduct.brand = brandname; } else { updatedProduct.brand = brandDoc.brandName; } if (HSN_Code !== existingProduct.HSN_Code) { updatedFields.push("HSN_Code"); updatedProduct.HSN_Code = HSN_Code; } if (GST !== existingProduct.GST) { updatedFields.push("GST"); updatedProduct.GST = GST; } if (description !== existingProduct.description) { updatedFields.push("description"); updatedProduct.description = description; } // Only update if there are changes if (updatedFields.length > 0) { try { await Product.updateOne( { SKU: existingProduct.SKU }, { $set: { category: item.category || existingProduct.category, brand: item.brand || existingProduct.brand, price: price !== undefined && price !== "" ? price : existingProduct.price, GST: GST || existingProduct.GST, HSN_Code: HSN_Code || existingProduct.HSN_Code, name: name, description: description, product_Status: item.product_Status || existingProduct.product_Status || "Active", }, } ); updatedProducts.push({ ...updatedProduct, updatedFields: updatedFields.join(", "), // Track updated fields }); } catch (error) { errors.push({ SKU, message: "Failed to update product", }); } } continue; } // Create new product if (item.category && item.brand) { const productData = { SKU, name, category: item.category, brand: item.brand, price, GST, HSN_Code, description: description, product_Status: item.product_Status || "Active", addedBy: req.user._id, }; try { const newProduct = await Product.create(productData); newlyCreated.push({ ...newProduct._doc, category: categoryDoc.categoryName, brand: brandDoc.brandName, }); } catch (error) { errors.push({ SKU, message: "Failed to create product", }); } } } // Clean up uploaded file if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } res.status(201).json({ message: errors.length > 0 ? "Products processed with errors!" : "Products processed successfully!", newlyCreated: newlyCreated, updatedProducts: updatedProducts, errors, }); } catch (error) { console.error("Error uploading products:", error); // Clean up uploaded file if any error occurs if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } res.status(500).json({ message: "Server error" }); } }; export const createProduct = async (req, res) => { try { let findProduct = ""; let product = { _id: "" }; const sku = req.body?.SKU; // console.log(req.body); if (!sku) { return res.status(400).json({ message: "SKU is required!" }); } // Check if SKU exists in the request body if (req.body?.product_id) { findProduct = await Product.findById(req.body.product_id); } if (!findProduct) { // Check if a product with the same SKU already exists const existingProduct = await Product.findOne({ SKU: { $regex: new RegExp(`^${sku}$`, "i") }, }).exec(); if (existingProduct) { return res .status(400) .json({ message: "Product with this SKU already exists!" }); } // Add user ID to the request body req.body.addedBy = req.user._id; product = await Product.create(req.body); } else { // Check if another product with the same SKU exists, excluding the current one const existingProduct = await Product.findOne({ _id: { $ne: findProduct._id }, SKU: { $regex: new RegExp(`^${sku}$`, "i") }, }).exec(); if (existingProduct) { return res .status(400) .json({ message: "Product with this SKU already exists!" }); } product = await Product.findByIdAndUpdate(req.body.product_id, req.body, { new: true, }); } res.status(201).json({ message: "Product details added/updated successfully!", product_id: product._id, }); } catch (error) { res.status(500).json({ message: error.message ? error.message : "Something went wrong!", }); } }; /////////////////////////////////////////////////////////////////////////////////////// export const updateProduct = async (req, res) => { try { // console.log("Product ID:", req.params.id); // console.log("Files:", req.files); // console.log("Body:", req.body); const { removedImages } = req.body; const files = req.files?.images || []; // Parse removedImages const removedImageIds = removedImages ? JSON.parse(removedImages) : []; // Find the product const product = await Product.findById(req.params.id); if (!product) { return res.status(404).json({ message: "Product not found!" }); } // Existing images let existingImages = product.image || []; let newImagesLinks = []; // Ensure files is always an array const filesArray = Array.isArray(files) ? files : [files]; // Process new images for (const file of filesArray) { if (file && file.tempFilePath) { // Check if file has tempFilePath try { const result = await cloudinary.v2.uploader.upload( file.tempFilePath, { folder: "chemiNova/product", } ); newImagesLinks.push({ public_id: result.public_id, url: result.secure_url, }); // console.log("Uploaded image:", result.secure_url); } catch (uploadError) { console.error("Error uploading image:", uploadError); return res.status(500).json({ message: "Failed to upload image", error: uploadError.message, }); } } } // Remove images from Cloudinary and database // for (const public_id of removedImageIds) { // try { // await cloudinary.v2.uploader.destroy(public_id); // // console.log("Deleted image from Cloudinary:", public_id); // } catch (deleteError) { // console.error("Error deleting image from Cloudinary:", deleteError); // return res.status(500).json({ message: "Failed to delete image", error: deleteError.message }); // } // } // Filter out removed images const updatedImages = existingImages.filter( (img) => !removedImageIds.includes(img.public_id) ); const allImages = [...updatedImages, ...newImagesLinks]; // Update product product.image = allImages; await product.save(); res .status(200) .json({ message: "Product updated successfully!", images: allImages }); } catch (error) { console.error(error); // Log error for debugging res.status(500).json({ message: "Server error!", error: error.message }); } }; //////////////////////////////////////////////////////////////////////////// //get All Product export const getAllProductAdmin = async (req, res) => { try { const PAGE_SIZE = parseInt(req.query.show) || 10; const page = parseInt(req.query.page) || 1; const skip = (page - 1) * PAGE_SIZE; let filter = {}; if (req.query.name) { filter.name = { $regex: new RegExp(req.query.name, "i"), }; } if (req.query.category) { filter.category = mongoose.Types.ObjectId(req.query.category); } if (req.query.brand) { filter.brand = mongoose.Types.ObjectId(req.query.brand); } const total = await Product.countDocuments(filter); const products = await Product.find(filter) .populate({ path: "category addedBy brand", select: "categoryName name brandName image", }) .limit(PAGE_SIZE) .skip(skip) .sort({ createdAt: -1 }) .exec(); return res.status(200).json({ success: true, total_data: total, total_pages: Math.ceil(total / PAGE_SIZE), products, }); } catch (error) { console.error(error); // Add logging for better debugging res.status(500).json({ success: false, msg: error.message || "Something went wrong!", }); } }; //get All Product User(website) export const getAllProductUser = async (req, res) => { try { // Set default values for pagination const PAGE_SIZE = parseInt(req.query?.show || "10"); const page = parseInt(req.query?.page || "1") - 1; let filter = {}; // Filter by category name if (req.query?.category) { const category = await CategoryModel.findOne({ categoryName: req.query.category, }); if (category) { filter.category = category._id; } else { return res.status(400).json({ success: false, msg: "Category not found!", }); } } if (req.query.brand) { filter.brand = mongoose.Types.ObjectId(req.query.brand); } // Filter by SKU if (req.query?.SKU) { filter.SKU = req.query.SKU; } // Filter by product name using regex for case-insensitive partial matching if (req.query?.name) { filter.name = { $regex: new RegExp(req.query.name, "i"), }; } // Only get active products filter.product_Status = "Active"; // Get total count of filtered products const total = await Product.countDocuments(filter); // Retrieve products with pagination, filtering, and sorting const products = await Product.find(filter) .populate({ path: "category addedBy brand", select: "categoryName name brandName image", }) .limit(PAGE_SIZE) .skip(PAGE_SIZE * page) .sort({ createdAt: -1 }) .exec(); return res.status(200).json({ success: true, total_data: total, total_pages: Math.ceil(total / PAGE_SIZE), products, }); } catch (error) { return res.status(500).json({ success: false, msg: error.message || "Something went wrong!", }); } }; //Change Product status export const ChangeProductStatus = async (req, res) => { try { const data = await Product.findById(req.params.id); if (data) { if (data?.product_Status === "Active") { let product = await Product.findByIdAndUpdate( req.params.id, { product_Status: "inActive" }, { new: true } // Return the updated document ); return res.status(200).json({ success: true, msg: "Changed status inActive", }); } else { let product = await Product.findByIdAndUpdate( req.params.id, { product_Status: "Active" }, { new: true } // Return the updated document ); return res.status(200).json({ success: true, msg: "Changed status Active", }); } } } catch (error) { res.status(500).json({ success: false, msg: error.message ? error.message : "Something went wrong!", }); } }; //get One Product export const getOneProduct = async (req, res) => { try { const data = await Product.findById(req.params.id).populate({ path: "category addedBy brand", select: "categoryName name brandName image", }); if (data) { return res.status(200).json({ success: true, data, }); } } catch (error) { // console.log(error) res.status(500).json({ success: false, msg: error.message ? error.message : "Something went wrong!", }); } }; // get all product with device products first export const getAllProductsDevicesFirst = async (req, res) => { try { // we want products with category name Device to be displayed first, so i have first found the products with category name Devices then made another request to find all products and filtered products with category devices , then merged both arrays so we get devices first then all other categories const categoryName = "Devices"; // Find the category object by name first const category = await CategoryModel.findOne({ categoryName }); if (!category) { throw new Error("Category not found"); } // products with device category const deviceProducts = await Product.find({ category: category._id, }).populate("category"); // all products const allProducts = await Product.find() .populate({ path: "category gst addedBy", select: "name categoryName tax", }) .sort({ createdAt: -1, }); // filtering out products with device category const filteredProducts = allProducts.filter((ele) => { return ele.category?.categoryName !== categoryName; }); // merging both deviceProcuts and filtered products const product = deviceProducts.concat(filteredProducts); if (product) { return res.status(200).json({ success: true, product, }); } } catch (error) { res.status(500).json({ success: false, msg: error.message ? error.message : "Something went wrong!", }); } }; // 3.update Product // export const updateProduct = async (req, res) => { // const { // name, // description, // price, // category, // image, // gst_amount, // gst, // total_amount, // } = req.body; // console.log(gst_amount, gst, total_amount); // try { // // Prepare an array for the images // const jsonArray = JSON.parse(image); // const AllImages = jsonArray.map(({ public_id, url }) => ({ // public_id, // url, // })); // if (req.files && req.files.newImages) { // const newuploadImages = Array.isArray(req.files.newImages) // ? req.files.newImages // : [req.files.newImages]; // const imagesLinks = []; // for (let i = 0; i < newuploadImages.length; i++) { // const result = await cloudinary.v2.uploader.upload( // newuploadImages[i].tempFilePath, // { // folder: "chemiNova/product", // } // ); // imagesLinks.push({ // public_id: result.public_id, // url: result.secure_url, // }); // } // // Combine the existing images and the newly uploaded images // const updatedImages = [...AllImages, ...imagesLinks]; // // Perform the product update // const ModifyProduct = await Product.findOneAndUpdate( // { _id: req.params.id }, // { // $set: { // name, // description, // price, // category, // image: updatedImages, // gst, // gst_amount, // total_amount, // }, // }, // { new: true } // ); // return res.status(200).json({ // success: true, // ModifyProduct, // }); // } else { // const ModifyProduct = await Product.findOneAndUpdate( // { _id: req.params.id }, // { // $set: { // name, // description, // price, // category, // image: AllImages, // }, // }, // { new: true } // ); // return res.status(200).json({ // success: true, // ModifyProduct, // }); // } // } catch (error) { // res.status(500).json({ // success: false, // msg: error.message ? error.message : "Something went wrong!", // }); // } // }; // export const updateProduct = async (req, res) => { // const { // name, // description, // price, // category, // image, // gst_amount, // product_Status, // gst, // total_amount, // } = req.body; // try { // // Prepare an array for the images // const jsonArray = JSON.parse(image); // const AllImages = jsonArray.map(({ public_id, url }) => ({ // public_id, // url, // })); // let updatedImages = AllImages; // if (req.files && req.files.newImages) { // const newUploadImages = Array.isArray(req.files.newImages) // ? req.files.newImages // : [req.files.newImages]; // const imagesLinks = []; // for (let i = 0; i < newUploadImages.length; i++) { // const result = await cloudinary.v2.uploader.upload( // newUploadImages[i].tempFilePath, // { // folder: "chemiNova/product", // } // ); // imagesLinks.push({ // public_id: result.public_id, // url: result.secure_url, // }); // } // // Combine the existing images and the newly uploaded images // updatedImages = [...AllImages, ...imagesLinks]; // } // // Perform the product update // const updatedProduct = await Product.findOneAndUpdate( // { _id: req.params.id }, // { // $set: { // name, // description, // product_Status, // price, // category, // image: updatedImages, // gst, // gst_amount, // total_amount, // }, // }, // { new: true } // ); // if (!updatedProduct) { // return res.status(404).json({ success: false, msg: "Product not found" }); // } // return res.status(200).json({ // success: true, // updatedProduct, // }); // } catch (error) { // console.error("Error updating product:", error); // res.status(500).json({ // success: false, // msg: error.message ? error.message : "Something went wrong!", // }); // } // }; export const deleteImageFromCloudinary = async (req, res) => { const { public_id } = req.params; // console.log("Received public_id:", public_id); // Debugging log // Ensure public_id is not empty if (!public_id) { return res.status(400).json({ success: false, msg: "Public ID is required!", }); } const decodedPublicId = decodeURIComponent(public_id); try { const response = await cloudinary.v2.uploader.destroy(decodedPublicId); if (response.result === "ok") { return res.status(200).json({ success: true, msg: "Image Deleted Successfully!", }); } else { return res.status(400).json({ success: false, msg: "Image deletion failed!", }); } } catch (error) { console.error("Error deleting image:", error); // Log error for debugging return res.status(500).json({ success: false, msg: error.message || "Something went wrong!", }); } }; //delete one Product export const deleteProduct = async (req, res) => { try { if (!req.params.id) { return res.status(400).json({ success: false, message: "Please Provide Product ID!", }); } const getProduct = await Product.findById(req.params.id); if (!getProduct) { return res.status(404).json({ success: false, message: "Product not Found!", }); } // Deleting Images From Cloudinary if (getProduct?.image?.length > 0) { for (let i = 0; i < getProduct.image.length; i++) { await cloudinary.v2.uploader.destroy(getProduct.image[i].public_id); } } //-------------------------// const product = await Product.findByIdAndDelete(req.params.id); if (!product) { return res.status(404).json({ message: "Product Not Found" }); } await product.remove(); res.status(200).json({ success: true, message: "Product Deleted Successfully!!", }); } catch (error) { res.status(500).json({ success: false, msg: error.message ? error.message : "Something went wrong!", }); } }; export const getProductsByCategory = async (req, res) => { const { categoryName } = req.params; // Assuming category name is in the route // console.log(categoryName); try { // Find the category object by name first const category = await CategoryModel.findOne({ categoryName }); if (!category) { throw new Error("Category not found"); } const products = await Product.find({ category: category._id }).populate( "category" ); // console.log(products); if (products && products.length > 0) { return res.status(200).json({ success: true, products, }); } else { return res.status(404).json({ success: false, msg: "No products found for this category", }); } } catch (error) { res.status(500).json({ success: false, msg: error.message ? error.message : "Something went wrong!", }); } };