upload spreadsheet and create product

This commit is contained in:
Sibunnayak 2024-08-07 16:54:19 +05:30
parent c82b44ce33
commit 16425a2879
7 changed files with 375 additions and 85 deletions

1
app.js
View File

@ -39,6 +39,7 @@ app.use(express.static(publicPath));
app.use( app.use(
fileUpload({ fileUpload({
useTempFiles: true, useTempFiles: true,
tempFileDir: join(publicPath, 'temp'),
}) })
); );

106
package-lock.json generated
View File

@ -33,7 +33,8 @@
"secure-random-password": "^0.2.3", "secure-random-password": "^0.2.3",
"stripe": "^14.16.0", "stripe": "^14.16.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"validator": "^13.7.0" "validator": "^13.7.0",
"xlsx": "^0.18.5"
} }
}, },
"node_modules/@aws-crypto/sha256-browser": { "node_modules/@aws-crypto/sha256-browser": {
@ -1714,6 +1715,15 @@
"node": ">= 0.6" "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": { "node_modules/agent-base": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
@ -2071,6 +2081,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/chalk": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@ -2170,6 +2193,15 @@
"lodash": ">=4.0" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2306,6 +2338,18 @@
"node": ">= 0.10" "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": { "node_modules/croner": {
"version": "4.1.97", "version": "4.1.97",
"resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz",
@ -2744,6 +2788,15 @@
"node": ">= 0.6" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -4805,6 +4858,18 @@
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"license": "BSD-3-Clause" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -5125,6 +5190,24 @@
"node": ">=12" "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": { "node_modules/ws": {
"version": "7.5.10", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -38,6 +38,7 @@
"secure-random-password": "^0.2.3", "secure-random-password": "^0.2.3",
"stripe": "^14.16.0", "stripe": "^14.16.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"validator": "^13.7.0" "validator": "^13.7.0",
"xlsx": "^0.18.5"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,12 +1,236 @@
// import ExcelJS from 'exceljs';
import { Product } from "./ProductModel.js"; import { Product } from "./ProductModel.js";
import cloudinary from "../../Utils/cloudinary.js"; import cloudinary from "../../Utils/cloudinary.js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { CategoryModel } from "../Category/CategoryModel.js"; 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) => { export const createProduct = async (req, res) => {
try { try {
let findProduct = ""; let findProduct = "";
let product = { _id: "" }; let product = { _id: "" };
// console.log("req.body", req.body);
if (req.body?.product_id) { if (req.body?.product_id) {
findProduct = await Product.findById(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 // Process new images
for (const file of filesArray) { for (const file of filesArray) {
if (file && file.tempFilePath) { // Check if file has tempFilePath if (file && file.tempFilePath) {
// Check if file has tempFilePath
try { try {
const result = await cloudinary.v2.uploader.upload(file.tempFilePath, { const result = await cloudinary.v2.uploader.upload(
file.tempFilePath,
{
folder: "chemiNova/product", folder: "chemiNova/product",
}); }
);
newImagesLinks.push({ newImagesLinks.push({
public_id: result.public_id, public_id: result.public_id,
url: result.secure_url, url: result.secure_url,
@ -83,7 +311,10 @@ export const updateProduct = async (req, res) => {
// console.log("Uploaded image:", result.secure_url); // console.log("Uploaded image:", result.secure_url);
} catch (uploadError) { } catch (uploadError) {
console.error("Error uploading image:", 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; product.image = allImages;
await product.save(); 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) { } catch (error) {
console.error(error); // Log error for debugging console.error(error); // Log error for debugging
res.status(500).json({ message: "Server error!", error: error.message }); res.status(500).json({ message: "Server error!", error: error.message });
@ -120,55 +353,49 @@ export const updateProduct = async (req, res) => {
//get All Product //get All Product
export const getAllProductAdmin = async (req, res) => { export const getAllProductAdmin = async (req, res) => {
try { try {
const PAGE_SIZE = parseInt(req.query?.show || "10"); const PAGE_SIZE = parseInt(req.query.show) || 10;
const page = parseInt(req.query?.page - 1 || "0"); const page = parseInt(req.query.page) || 1;
const skip = (page - 1) * PAGE_SIZE;
// Create filter object based on query parameters
let filter = {}; let filter = {};
if (req.query?.name) { if (req.query.name) {
filter.name = { filter.name = {
$regex: new RegExp(req.query.name, "i"), // Case-insensitive search $regex: new RegExp(req.query.name, "i"),
}; };
} }
if (req.query?.category) { if (req.query.category) {
filter.category = mongoose.Types.ObjectId(req.query.category); // Ensure category is an ObjectId filter.category = mongoose.Types.ObjectId(req.query.category);
}
if (req.query?.FeatureProduct) {
filter.featured_Product = req.query.FeatureProduct === "true"; // Convert string to boolean
} }
// Count total products matching the filter
const total = await Product.countDocuments(filter); const total = await Product.countDocuments(filter);
// Fetch products with pagination and sorting
const products = await Product.find(filter) const products = await Product.find(filter)
.populate({ .populate({
path: "category addedBy GST", path: "category addedBy GST",
select: "categoryName name tax", select: "categoryName name tax",
}) })
.limit(PAGE_SIZE) .limit(PAGE_SIZE)
.skip(PAGE_SIZE * page) .skip(skip)
.sort({ .sort({ createdAt: -1 })
featured_Product: -1,
createdAt: -1,
})
.exec(); .exec();
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
total_data: total, total_data: total,
total_pages: Math.ceil(total / PAGE_SIZE), total_pages: Math.ceil(total / PAGE_SIZE),
products, // Changed from `product` to `products` to match the response variable products,
}); });
} catch (error) { } catch (error) {
console.error(error); // Add logging for better debugging
res.status(500).json({ res.status(500).json({
success: false, success: false,
msg: error.message ? error.message : "Something went wrong!", msg: error.message || "Something went wrong!",
}); });
} }
}; };
//get All Product User(website) //get All Product User(website)
export const getAllProductUser = async (req, res) => { export const getAllProductUser = async (req, res) => {
try { try {
@ -181,8 +408,6 @@ export const getAllProductUser = async (req, res) => {
$options: "i", $options: "i",
}; };
if (req.query?.category) obj.category = req.query.category; if (req.query?.category) obj.category = req.query.category;
if (req.query?.FeatureProduct)
obj.featured_Product = req.query.FeatureProduct;
obj.product_Status = "Active"; obj.product_Status = "Active";
const total = await Product.countDocuments(obj); const total = await Product.countDocuments(obj);
const product = await Product.find(obj) const product = await Product.find(obj)
@ -194,7 +419,6 @@ export const getAllProductUser = async (req, res) => {
.skip(PAGE_SIZE * page) .skip(PAGE_SIZE * page)
// .sort("name") // .sort("name")
.sort({ .sort({
featured_Product: -1,
createdAt: -1, createdAt: -1,
}) })
.exec(); .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 //get One Product
export const getOneProduct = async (req, res) => { export const getOneProduct = async (req, res) => {
try { try {
@ -534,7 +715,6 @@ export const getAllProductsDevicesFirst = async (req, res) => {
// } // }
// }; // };
export const deleteImageFromCloudinary = async (req, res) => { export const deleteImageFromCloudinary = async (req, res) => {
const { public_id } = req.params; const { public_id } = req.params;

View File

@ -26,16 +26,12 @@ const productSchema = new Schema(
description: { description: {
type: String, type: String,
maxLength: [400, "description cannot exceed 100 characters"], maxLength: [400, "description cannot exceed 100 characters"],
required: [true, "Please Enter product Description"], // required: [true, "Please Enter product Description"],
}, },
special_instructions: { special_instructions: {
type: String, type: String,
}, },
featured_Product: {
type: Boolean,
default: false, // Initially, products are not featured
},
image: [ image: [
{ {
public_id: { public_id: {

View File

@ -10,10 +10,21 @@ import {
getAllProductUser, getAllProductUser,
getAllProductsDevicesFirst, getAllProductsDevicesFirst,
ChangeProductStatus, ChangeProductStatus,
ChangeFeatueProductStatus, uploadProducts,
} from "./ProductController.js"; } from "./ProductController.js";
const router = express.Router();
import { isAuthenticatedUser, authorizeRoles } from "../../middlewares/auth.js"; import { isAuthenticatedUser, authorizeRoles } from "../../middlewares/auth.js";
const router = express.Router();
router
.route('/products/upload').post(
isAuthenticatedUser,
authorizeRoles('admin'),
uploadProducts
);
router router
.route("/product/create/") .route("/product/create/")
.post(isAuthenticatedUser, authorizeRoles("admin"), createProduct); .post(isAuthenticatedUser, authorizeRoles("admin"), createProduct);
@ -23,9 +34,6 @@ router
//change Product status //change Product status
router.route("/product/admin/status/:id").patch(ChangeProductStatus); router.route("/product/admin/status/:id").patch(ChangeProductStatus);
router
.route("/product/admin/feature_product/status/:id")
.patch(ChangeFeatueProductStatus);
//get all product user //get all product user
router.route("/product/getAll/user/").get(getAllProductUser); router.route("/product/getAll/user/").get(getAllProductUser);