diff --git a/app.js b/app.js index c31536f..6de17c2 100644 --- a/app.js +++ b/app.js @@ -39,6 +39,7 @@ app.use(express.static(publicPath)); app.use( fileUpload({ useTempFiles: true, + tempFileDir: join(publicPath, 'temp'), }) ); diff --git a/package-lock.json b/package-lock.json index 5056ee6..e07c4b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "secure-random-password": "^0.2.3", "stripe": "^14.16.0", "uuid": "^9.0.1", - "validator": "^13.7.0" + "validator": "^13.7.0", + "xlsx": "^0.18.5" } }, "node_modules/@aws-crypto/sha256-browser": { @@ -1714,6 +1715,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -2071,6 +2081,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -2170,6 +2193,15 @@ "lodash": ">=4.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2306,6 +2338,18 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/croner": { "version": "4.1.97", "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", @@ -2744,6 +2788,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -4805,6 +4858,18 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5125,6 +5190,24 @@ "node": ">=12" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -5146,6 +5229,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 97038a3..314bc6d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "secure-random-password": "^0.2.3", "stripe": "^14.16.0", "uuid": "^9.0.1", - "validator": "^13.7.0" + "validator": "^13.7.0", + "xlsx": "^0.18.5" } } diff --git a/public/temp/tmp-2-1722948449609 b/public/temp/tmp-2-1722948449609 new file mode 100644 index 0000000..da090b5 Binary files /dev/null and b/public/temp/tmp-2-1722948449609 differ diff --git a/resources/Products/ProductController.js b/resources/Products/ProductController.js index 03f4267..01a611e 100644 --- a/resources/Products/ProductController.js +++ b/resources/Products/ProductController.js @@ -1,12 +1,236 @@ +// import ExcelJS from 'exceljs'; import { Product } from "./ProductModel.js"; import cloudinary from "../../Utils/cloudinary.js"; import { v4 as uuidv4 } from "uuid"; import { CategoryModel } from "../Category/CategoryModel.js"; +import { Tax } from "../Tax/tax_model.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 = { + "Product Name": "name", + "category Name": "category", + price: "price", + "GST Name": "GST", + description: "description", + special_instructions: "special_instructions", + }; + + 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 productsProcessed = []; + + for (let i = 1; i < data.length; i++) { + const row = data[i]; + 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(); + const notFoundErrors = new Set(); + + let { name, category, price, GST, description, special_instructions } = item; + + // Trim leading and trailing spaces from product name and GST + name = name ? name.trim() : ""; + GST = GST ? GST.toString().trim() : ""; + + // Validate required fields + if (!name) missingFields.add("name"); + if (!category) missingFields.add("category"); + if (price === undefined || price === "") missingFields.add("price"); + if (!GST) missingFields.add("GST"); + + // Validate category + if (category) { + const categoryDoc = await CategoryModel.findOne({ + categoryName: { $regex: new RegExp(`^${category.trim()}$`, "i") }, + }).exec(); + if (categoryDoc) { + item.category = categoryDoc._id; + } else { + notFoundErrors.add("category"); + } + } else { + missingFields.add("category"); + } + + // Validate GST + if (GST) { + const gstDoc = await Tax.findOne({ + name: { $regex: new RegExp(`^${GST}$`, "i") }, + }).exec(); + if (gstDoc) { + item.GST = gstDoc._id; + } else { + notFoundErrors.add("GST"); + } + } else { + missingFields.add("GST"); + } + + // Combine all errors into a single message + let errorMessage = ""; + if (missingFields.size > 0) { + errorMessage += `Missing fields: ${Array.from(missingFields).join(", ")}. `; + } + if (notFoundErrors.size > 0) { + errorMessage += `Not found: ${Array.from(notFoundErrors).join(", ")}.`; + } + + // If there are errors, push them to the errors array + if (errorMessage.trim()) { + errors.push({ + productName: name || "N/A", + category: category || "N/A", + GST: GST || "N/A", + message: errorMessage.trim(), + }); + continue; + } + + // Ensure fields are set to empty strings if not provided + description = description !== undefined ? description : ""; + special_instructions = special_instructions !== undefined ? special_instructions : ""; + + // Check for existing product + let existingProduct = await Product.findOne({ + name: new RegExp(`^${name}$`, "i"), + }).exec(); + + if (existingProduct) { + // Validate that the existing product can be updated + const updateErrors = []; + if (missingFields.size > 0) { + updateErrors.push(`Missing fields: ${Array.from(missingFields).join(", ")}`); + } + if (notFoundErrors.size > 0) { + updateErrors.push(`Not found: ${Array.from(notFoundErrors).join(", ")}`); + } + + if (updateErrors.length > 0) { + errors.push({ + productName: name, + message: updateErrors.join(". "), + }); + continue; + } + + // Update existing product + try { + await Product.updateOne( + { _id: existingProduct._id }, + { + $set: { + category: item.category || existingProduct.category, + price: price !== undefined && price !== "" ? price : existingProduct.price, + GST: item.GST || existingProduct.GST, + description: description, // Ensure description is included + special_instructions: special_instructions, // Ensure special_instructions is included + product_Status: item.product_Status || existingProduct.product_Status || "Active", + }, + } + ); + productsProcessed.push({ ...existingProduct._doc, ...item }); // Track updated product + } catch (error) { + errors.push({ + productName: name, + message: "Failed to update product", + }); + } + continue; + } + + // Create new product + if (item.category && item.GST) { + const productData = { + name, + category: item.category, + price, + GST: item.GST, + description: description, // Ensure description is included + special_instructions: special_instructions, // Ensure special_instructions is included + product_Status: item.product_Status || "Active", + addedBy: req.user._id, + }; + try { + const newProduct = await Product.create(productData); + productsProcessed.push(newProduct); // Track new product + } catch (error) { + errors.push({ + productName: name, + message: "Failed to create product", + }); + } + } + } + + fs.unlinkSync(filePath); // Clean up uploaded file + + res.status(201).json({ + message: + errors.length > 0 + ? "Products processed with errors!" + : "Products processed successfully!", + productsProcessed: productsProcessed.length, // Total processed products + errors, + }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ message: error.message || "Something went wrong!" }); + } +}; + + export const createProduct = async (req, res) => { try { let findProduct = ""; let product = { _id: "" }; - + // console.log("req.body", req.body); if (req.body?.product_id) { findProduct = await Product.findById(req.body.product_id); } @@ -71,11 +295,15 @@ export const updateProduct = async (req, res) => { // Process new images for (const file of filesArray) { - if (file && file.tempFilePath) { // Check if file has tempFilePath + if (file && file.tempFilePath) { + // Check if file has tempFilePath try { - const result = await cloudinary.v2.uploader.upload(file.tempFilePath, { - folder: "chemiNova/product", - }); + const result = await cloudinary.v2.uploader.upload( + file.tempFilePath, + { + folder: "chemiNova/product", + } + ); newImagesLinks.push({ public_id: result.public_id, url: result.secure_url, @@ -83,7 +311,10 @@ export const updateProduct = async (req, res) => { // 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 }); + return res.status(500).json({ + message: "Failed to upload image", + error: uploadError.message, + }); } } } @@ -109,7 +340,9 @@ export const updateProduct = async (req, res) => { product.image = allImages; await product.save(); - res.status(200).json({ message: "Product updated successfully!", images: allImages }); + 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 }); @@ -120,55 +353,49 @@ export const updateProduct = async (req, res) => { //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 || "0"); + const PAGE_SIZE = parseInt(req.query.show) || 10; + const page = parseInt(req.query.page) || 1; + const skip = (page - 1) * PAGE_SIZE; - // Create filter object based on query parameters let filter = {}; - if (req.query?.name) { + if (req.query.name) { filter.name = { - $regex: new RegExp(req.query.name, "i"), // Case-insensitive search + $regex: new RegExp(req.query.name, "i"), }; } - if (req.query?.category) { - filter.category = mongoose.Types.ObjectId(req.query.category); // Ensure category is an ObjectId - } - if (req.query?.FeatureProduct) { - filter.featured_Product = req.query.FeatureProduct === "true"; // Convert string to boolean + if (req.query.category) { + filter.category = mongoose.Types.ObjectId(req.query.category); } - // Count total products matching the filter const total = await Product.countDocuments(filter); - // Fetch products with pagination and sorting const products = await Product.find(filter) .populate({ path: "category addedBy GST", select: "categoryName name tax", }) .limit(PAGE_SIZE) - .skip(PAGE_SIZE * page) - .sort({ - featured_Product: -1, - createdAt: -1, - }) + .skip(skip) + .sort({ createdAt: -1 }) .exec(); return res.status(200).json({ success: true, total_data: total, total_pages: Math.ceil(total / PAGE_SIZE), - products, // Changed from `product` to `products` to match the response variable + products, }); } catch (error) { + console.error(error); // Add logging for better debugging res.status(500).json({ success: false, - msg: error.message ? error.message : "Something went wrong!", + msg: error.message || "Something went wrong!", }); } }; + //get All Product User(website) export const getAllProductUser = async (req, res) => { try { @@ -181,8 +408,6 @@ export const getAllProductUser = async (req, res) => { $options: "i", }; if (req.query?.category) obj.category = req.query.category; - if (req.query?.FeatureProduct) - obj.featured_Product = req.query.FeatureProduct; obj.product_Status = "Active"; const total = await Product.countDocuments(obj); const product = await Product.find(obj) @@ -194,7 +419,6 @@ export const getAllProductUser = async (req, res) => { .skip(PAGE_SIZE * page) // .sort("name") .sort({ - featured_Product: -1, createdAt: -1, }) .exec(); @@ -248,49 +472,6 @@ export const ChangeProductStatus = async (req, res) => { }); } }; -//Change Product status -export const ChangeFeatueProductStatus = async (req, res) => { - try { - const data = await Product.findById(req.params.id); - if (data) { - if (data?.featured_Product === false) { - const totalFeatueProduct = await Product.countDocuments({ - featured_Product: true, - }); - if (totalFeatueProduct > 2) { - return res.status(400).json({ - success: false, - msg: "Maximum 3 Featue Product can be..", - }); - } - let product = await Product.findByIdAndUpdate( - req.params.id, - { featured_Product: true }, - { new: true } // Return the updated document - ); - return res.status(200).json({ - success: true, - msg: "Changed status as Featue Product", - }); - } else { - let product = await Product.findByIdAndUpdate( - req.params.id, - { featured_Product: false }, - { new: true } // Return the updated document - ); - return res.status(200).json({ - success: true, - msg: "Changed status as not Featue Product", - }); - } - } - } 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 { @@ -534,7 +715,6 @@ export const getAllProductsDevicesFirst = async (req, res) => { // } // }; - export const deleteImageFromCloudinary = async (req, res) => { const { public_id } = req.params; diff --git a/resources/Products/ProductModel.js b/resources/Products/ProductModel.js index 4c9c593..bf8c86c 100644 --- a/resources/Products/ProductModel.js +++ b/resources/Products/ProductModel.js @@ -26,16 +26,12 @@ const productSchema = new Schema( description: { type: String, maxLength: [400, "description cannot exceed 100 characters"], - required: [true, "Please Enter product Description"], + // required: [true, "Please Enter product Description"], }, special_instructions: { type: String, }, - featured_Product: { - type: Boolean, - default: false, // Initially, products are not featured - }, image: [ { public_id: { diff --git a/resources/Products/ProductRoute.js b/resources/Products/ProductRoute.js index ea0908d..39213ff 100644 --- a/resources/Products/ProductRoute.js +++ b/resources/Products/ProductRoute.js @@ -10,10 +10,21 @@ import { getAllProductUser, getAllProductsDevicesFirst, ChangeProductStatus, - ChangeFeatueProductStatus, + uploadProducts, } from "./ProductController.js"; -const router = express.Router(); + import { isAuthenticatedUser, authorizeRoles } from "../../middlewares/auth.js"; + +const router = express.Router(); + +router + .route('/products/upload').post( + isAuthenticatedUser, + authorizeRoles('admin'), + uploadProducts +); + + router .route("/product/create/") .post(isAuthenticatedUser, authorizeRoles("admin"), createProduct); @@ -23,9 +34,6 @@ router //change Product status router.route("/product/admin/status/:id").patch(ChangeProductStatus); -router - .route("/product/admin/feature_product/status/:id") - .patch(ChangeFeatueProductStatus); //get all product user router.route("/product/getAll/user/").get(getAllProductUser);