diff --git a/app.js b/app.js index 6f12e23..90c940c 100644 --- a/app.js +++ b/app.js @@ -203,7 +203,7 @@ import TaskRoute from "./resources/Task/TaskRoute.js"; // visit RD and PD import VisitRDandPDRoute from "./resources/VisitRD&PD/VisitRD&PDRoute.js"; //Stock -import Stock from "./resources/Stock/PdStockRoute.js"; +import Stock from "./resources/Stock/StockRoute.js"; app.use("/api/v1", user); //Product diff --git a/public/temp/tmp-1-1728283415292 b/public/temp/tmp-1-1728283415292 new file mode 100644 index 0000000..501cf6e Binary files /dev/null and b/public/temp/tmp-1-1728283415292 differ diff --git a/public/temp/tmp-1-1728363850151 b/public/temp/tmp-1-1728363850151 new file mode 100644 index 0000000..49e6ae6 Binary files /dev/null and b/public/temp/tmp-1-1728363850151 differ diff --git a/public/uploads/Add-RD.xlsx b/public/uploads/Add-RD.xlsx new file mode 100644 index 0000000..38576dd Binary files /dev/null and b/public/uploads/Add-RD.xlsx differ diff --git a/public/uploads/Add-SC.xlsx b/public/uploads/Add-SC.xlsx new file mode 100644 index 0000000..744e40a Binary files /dev/null and b/public/uploads/Add-SC.xlsx differ diff --git a/public/uploads/Add-TM.xlsx b/public/uploads/Add-TM.xlsx new file mode 100644 index 0000000..49e6ae6 Binary files /dev/null and b/public/uploads/Add-TM.xlsx differ diff --git a/resources/KYC/KycModel.js b/resources/KYC/KycModel.js index 00461a0..91c9adf 100644 --- a/resources/KYC/KycModel.js +++ b/resources/KYC/KycModel.js @@ -52,7 +52,7 @@ const KycSchema = new Schema( }, pan_img: { type: Object, - required: true, + // required: true, }, aadhar_number: { type: String, @@ -60,7 +60,7 @@ const KycSchema = new Schema( }, aadhar_img: { type: Object, - required: true, + // required: true, }, gst_number: { type: String, @@ -68,18 +68,18 @@ const KycSchema = new Schema( }, gst_img: { type: Object, - required: true, + // required: true, }, pesticide_license_img: { type: Object, - required: true, + // required: true, }, fertilizer_license_img: { type: Object, }, selfie_entrance_img: { type: Object, - required: true, + // required: true, }, status: { type: String, diff --git a/resources/RetailDistributor/RetailDistributerRoutes.js b/resources/RetailDistributor/RetailDistributerRoutes.js index 7034933..7c48c09 100644 --- a/resources/RetailDistributor/RetailDistributerRoutes.js +++ b/resources/RetailDistributor/RetailDistributerRoutes.js @@ -14,12 +14,16 @@ import { UpdateProfileRD, updateRDMapped, updateunmapRD, + uploadRetaildistributors, } from "./RetailDistributorController.js"; import { isAuthenticatedRD } from "../../middlewares/rdAuth.js"; import { authorizeRoles, isAuthenticatedUser } from "../../middlewares/auth.js"; const router = express.Router(); +router + .route("/retaildistributor/upload") + .post(isAuthenticatedUser, authorizeRoles("admin"), uploadRetaildistributors); router.route("/rd-login").post(loginRD); router.route("/rd-get-me").get(isAuthenticatedRD, getmyProfileRD); router.post("/forgot-password", forgotPasswordRD); diff --git a/resources/RetailDistributor/RetailDistributorController.js b/resources/RetailDistributor/RetailDistributorController.js index 989ead4..40ddb9a 100644 --- a/resources/RetailDistributor/RetailDistributorController.js +++ b/resources/RetailDistributor/RetailDistributorController.js @@ -5,6 +5,254 @@ import password from "secure-random-password"; import crypto from "crypto"; import { RdOrder } from "../RD_Ordes/rdOrderModal.js"; import sendEmail, { sendOtp } from "../../Utils/sendEmail.js"; +import { KYC } from "../KYC/KycModel.js"; +import { generatePassword } from "../../Utils/generatepassword.js"; +import XLSX from "xlsx"; +import fs from "fs"; +import path from "path"; + +export const uploadRetaildistributors = async (req, res) => { + try { + if (!mongoose.Types.ObjectId.isValid(req.user._id)) { + return res.status(400).json({ message: "Please login again" }); + } + 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 = { + "Retail Distributor Name": "name", + Email: "email", + "Phone Number": "mobile_number", + "PAN Number": "pan_number", + "Trade Name": "trade_name", + "GST Number": "gst_number", + "Aadhar Number": "aadhar_number", + State: "state", + City: "city", + District: "district", + Address: "address", + Pincode: "pincode", + }; + + 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 updatedDistributors = []; + + 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 validationErrors = new Set(); + + // Validate required fields + if (!item.name) missingFields.add("name"); + if (!item.email) missingFields.add("email"); + if (!item.mobile_number) missingFields.add("mobile_number"); + if (!item.pan_number) missingFields.add("pan_number"); + if (!item.gst_number) missingFields.add("gst_number"); + if (!item.trade_name) missingFields.add("trade_name"); + if (!item.aadhar_number) missingFields.add("aadhar_number"); + if (!item.state) missingFields.add("state"); + if (!item.city) missingFields.add("city"); + if (!item.pincode) missingFields.add("pincode"); + if (!item.district) missingFields.add("district"); + if (!item.address) missingFields.add("address"); + + // Check email validity + if (item.email && !validator.isEmail(item.email)) { + validationErrors.add("incorrect mail"); + } + + // Validate mobile number + if (item.mobile_number && !/^\d{10}$/.test(item.mobile_number)) { + validationErrors.add("Invalid Mobile Number (should be 10 digits)"); + } + + // Check GST, PAN, and postal code validation + item.pan_number = item.pan_number ? item.pan_number.toUpperCase() : ""; + item.gst_number = item.gst_number ? item.gst_number.toUpperCase() : ""; + + // Validate PAN Number + if ( + item.pan_number && + !/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(item.pan_number) + ) { + validationErrors.add("Invalid PAN Number"); + } + + // Validate GST Number + if ( + item.gst_number && + !/^(\d{2}[A-Z]{5}\d{4}[A-Z]{1}\d[Z]{1}[A-Z\d]{1})$/.test( + item.gst_number + ) + ) { + validationErrors.add("Invalid GST Number"); + } + // Validate Aadhar number + if (item.aadhar_number && !/^\d{12}$/.test(item.aadhar_number)) { + validationErrors.add("Invalid Aadhar Number (should be 12 digits)"); + } + // Validate Postal Code + if (item.pincode && !/^\d{6}$/.test(item.pincode)) { + validationErrors.add("Invalid Postal Code"); + } + + // Combine all errors into a single message + let errorMessage = ""; + if (missingFields.size > 0) { + errorMessage += `Missing fields: ${Array.from(missingFields).join( + ", " + )}. `; + } + if (validationErrors.size > 0) { + errorMessage += `Validation errors: ${Array.from(validationErrors).join( + ", " + )}.`; + } + + // If there are errors, push them to the errors array + if (errorMessage.trim()) { + errors.push({ + name: item.name || "N/A", + email: item.email || "N/A", + TradeName: item.trade_name || "N/A", + phone: item.mobile_number || "N/A", + panNumber: item.pan_number || "N/A", + gstNumber: item.gst_number || "N/A", + AadharNumber: item.aadhar_number || "N/A", + message: errorMessage.trim(), + }); + continue; + } + + // Generate a password + const password = generatePassword(item.name, item.email); + + // Check for existing user by uniqueId + let Kyc = await KYC.findOne({ email: item.email }); + let Retaildistributor = await RetailDistributor.findOne({ + email: item.email, + }); + if (Kyc) { + // Track updated fields + const updatedFields = []; + + // Check for changes in user details + let kycUpdated = false; + for (let field in item) { + const currentValue = Kyc[field]?.toString(); + const newValue = item[field]?.toString(); + + if (currentValue !== newValue) { + updatedFields.push(field); + Kyc[field] = item[field]; // Update Kyc with the new value + + if (Retaildistributor && field !== 'email') { + Retaildistributor[field] = item[field]; + } + kycUpdated = true; + } + } + + // Update Kyc and Retaildistributor if there are any changes + if (kycUpdated) { + await Kyc.save(); + await Retaildistributor.save(); + updatedDistributors.push({ + ...Kyc._doc, + updatedFields: updatedFields.join(", "), + }); + } + } else { + // Create a new Kyc + Kyc = new KYC({ + ...item, + status: "approved", + }); + const newkyc = await Kyc.save(); + const retailDistributorData = { + name: item.name, + email: item.email, + mobile_number: item.mobile_number, + kyc: newkyc._id, + password, + }; + Retaildistributor = new RetailDistributor(retailDistributorData); + await Retaildistributor.save(); + // Send email with the new password + await sendEmail({ + to: `${item.email}`, // Change to your recipient + from: `${process.env.SEND_EMAIL_FROM}`, // Change to your verified sender + subject: `Cheminova Account Created`, + html: `Your Retail Distributor Account is created successfully. +
name is: ${item.name} +
+
MobileNumber is: ${item.mobile_number}
+
Email is: ${item.email}
+
password is: ${password}

If you have not requested this email, please ignore it.`, + }); + newlyCreated.push({ Kyc }); + } + } + + res.status(200).json({ + message: "File processed successfully", + newlyCreated, + updatedDistributors, + errors, + }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + export const loginRD = async (req, res) => { const { email, password } = req.body; diff --git a/resources/SalesCoOrdinators/SalesCoOrdinatorController.js b/resources/SalesCoOrdinators/SalesCoOrdinatorController.js index 56cce6d..bef5b45 100644 --- a/resources/SalesCoOrdinators/SalesCoOrdinatorController.js +++ b/resources/SalesCoOrdinators/SalesCoOrdinatorController.js @@ -1,11 +1,192 @@ // import hashPassword from '../utils/hashPassword'; import crypto from "crypto"; +import mongoose from "mongoose"; import SalesCoOrdinator from "./SalesCoOrdinatorModel.js"; import sendEmail, { sendOtp } from "../../Utils/sendEmail.js"; import validator from "validator"; import password from "secure-random-password"; import catchAsyncErrors from "../../middlewares/catchAsyncErrors.js"; +import { generatePassword } from "../../Utils/generatepassword.js"; +import XLSX from "xlsx"; +import fs from "fs"; +import path from "path"; +export const uploadSalesCoordinators = async (req, res) => { + try { + if (!mongoose.Types.ObjectId.isValid(req.user._id)) { + return res.status(400).json({ message: "Please login again" }); + } + 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 = { + "Sales Coordinator Name": "name", + Email: "email", + "Phone Number": "mobileNumber", + }; + + 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 updatedsalesCoordinators = []; + + 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(); + const validationErrors = new Set(); + + // Validate required fields + if (!item.name) missingFields.add("name"); + if (!item.email) missingFields.add("email"); + if (!item.mobileNumber) missingFields.add("mobileNumber"); + + // Check email validity + if (item.email && !validator.isEmail(item.email)) { + validationErrors.add("incorrect mail"); + } + + // Validate mobile number + if (item.mobileNumber && !/^\d{10}$/.test(item.mobileNumber)) { + validationErrors.add("Invalid Mobile Number (should be 10 digits)"); + } + + // Combine all errors into a single message + let errorMessage = ""; + if (missingFields.size > 0) { + errorMessage += `Missing fields: ${Array.from(missingFields).join( + ", " + )}. `; + } + if (validationErrors.size > 0) { + errorMessage += `Validation errors: ${Array.from(validationErrors).join( + ", " + )}.`; + } + + // If there are errors, push them to the errors array + if (errorMessage.trim()) { + errors.push({ + name: item.name || "N/A", + email: item.email || "N/A", + phone: item.mobileNumber || "N/A", + message: errorMessage.trim(), + }); + continue; + } + + // Generate a password + const password = generatePassword(item.name, item.email); + + // Check for existing user by uniqueId + let salesCoordinator = await SalesCoOrdinator.findOne({ + email: item.email, + }); + + if (salesCoordinator) { + // Track updated fields + const updatedFields = []; + + // Check for changes in user details + let territoryManagerUpdated = false; + for (let field in item) { + const currentValue = salesCoordinator[field]?.toString(); + const newValue = item[field]?.toString(); + + if (currentValue !== newValue) { + updatedFields.push(field); + salesCoordinator[field] = item[field]; + territoryManagerUpdated = true; + } + } + + if (territoryManagerUpdated) { + await salesCoordinator.save(); + updatedsalesCoordinators.push({ + ...salesCoordinator._doc, + updatedFields: updatedFields.join(", "), + }); + } + } else { + // Create a new salesCoordinator + salesCoordinator = new SalesCoOrdinator({ + ...item, + password, + isVerified: true, + }); + await salesCoordinator.save(); + // Send email with the new password + await sendEmail({ + to: `${item?.email}`, // Change to your recipient + from: `${process.env.SEND_EMAIL_FROM}`, // Change to your verified sender + subject: `Cheminova Account Created`, + html: `Your Sales Coordinator Account is created successfully. +
name is: ${item?.name}
+
MobileNumber is: ${item?.mobileNumber}
+
password is: ${password}

If you have not requested this email, please ignore it.`, + }); + newlyCreated.push({ salesCoordinator }); + } + } + + res.status(200).json({ + message: "File processed successfully", + newlyCreated, + updatedsalesCoordinators, + errors, + }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; export const register = async (req, res) => { let { name, email, countryCode, mobileNumber, territoryManager } = req.body; // console.log(req.body); diff --git a/resources/SalesCoOrdinators/SalesCoOrdinatorModel.js b/resources/SalesCoOrdinators/SalesCoOrdinatorModel.js index bbd6a74..ca2ef55 100644 --- a/resources/SalesCoOrdinators/SalesCoOrdinatorModel.js +++ b/resources/SalesCoOrdinators/SalesCoOrdinatorModel.js @@ -50,11 +50,11 @@ const salescoordinatorSchema = new mongoose.Schema( uniqueId: { type: String, unique: true, - required: true, }, fcm_token: { type: String, default: null, + sparse: true, }, mappedby: { type: mongoose.Schema.Types.ObjectId, diff --git a/resources/SalesCoOrdinators/SalesCoOrdinatorRoute.js b/resources/SalesCoOrdinators/SalesCoOrdinatorRoute.js index a543544..3ff3a91 100644 --- a/resources/SalesCoOrdinators/SalesCoOrdinatorRoute.js +++ b/resources/SalesCoOrdinators/SalesCoOrdinatorRoute.js @@ -20,12 +20,16 @@ import { mappedbyTM, unmapSalesCoOrdinator, getAllSalesCoOrdinatorforTM_App, + uploadSalesCoordinators, } from "./SalesCoOrdinatorController.js"; import { isAuthenticatedSalesCoOrdinator } from "../../middlewares/SalesCoOrdinatorAuth.js"; import { isAuthenticatedTerritoryManager } from "../../middlewares/TerritoryManagerAuth.js"; import { authorizeRoles, isAuthenticatedUser } from "../../middlewares/auth.js"; router.post("/register", register); +router + .route("/upload") + .post(isAuthenticatedUser, authorizeRoles("admin"), uploadSalesCoordinators); router.post("/verify-otp", verifyOtp); router.post("/login", loginSalesCoOrdinator); router.route("/logout").get(logout); diff --git a/resources/Stock/PdStockController.js b/resources/Stock/PdStockController.js deleted file mode 100644 index 0aebe55..0000000 --- a/resources/Stock/PdStockController.js +++ /dev/null @@ -1,103 +0,0 @@ -import mongoose from "mongoose"; -import { PDStock } from "./PdStockModel.js"; -import { Product } from "../Products/ProductModel.js"; - -export const getProductsAndStockByUser = async (req, res) => { - try { - const { userId } = req.params; - - // Pagination parameters - const PAGE_SIZE = parseInt(req.query.show) || 10; - const page = parseInt(req.query.page) || 1; - const skip = (page - 1) * PAGE_SIZE; - - // Filtering criteria - const 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); - } - - // Fetch user's PDStock data and products concurrently - const [userStock, products] = await Promise.all([ - PDStock.findOne({ userId: mongoose.Types.ObjectId(userId) }), - Product.aggregate([ - { $match: filter }, - { - $lookup: { - from: "categorymodels", - localField: "category", - foreignField: "_id", - as: "categoryDetails", - }, - }, - { - $lookup: { - from: "brandmodels", - localField: "brand", - foreignField: "_id", - as: "brandDetails", - }, - }, - { - $project: { - category: { $arrayElemAt: ["$categoryDetails.categoryName", 0] }, - brand: { $arrayElemAt: ["$brandDetails.brandName", 0] }, - GST: 1, - HSN_Code: 1, - SKU: 1, - addedBy: 1, - createdAt: 1, - description: 1, - image: 1, - name: 1, - price: 1, - product_Status: 1, - updatedAt: 1, - }, - }, - { $skip: skip }, - { $limit: PAGE_SIZE }, - ]), - ]); - - // Create a stock map for easy lookup - const stockMap = {}; - if (userStock && userStock.products) { - userStock.products.forEach((product) => { - stockMap[product.productid.toString()] = product.Stock; - }); - } - - // Combine products with their respective stock - const productsWithStock = products.map((product) => ({ - ...product, - stock: stockMap[product._id.toString()] || 0, - })); - - // Get total count for pagination purposes - const total = await Product.countDocuments(filter); - - return res.status(200).json({ - success: true, - totalProducts: total, - totalPages: Math.ceil(total / PAGE_SIZE), - currentPage: page, - products: productsWithStock, - }); - } catch (error) { - console.error("Error fetching products with stock:", error); - return res.status(500).json({ - success: false, - message: "Error fetching products and stock", - }); - } -}; - diff --git a/resources/Stock/RdStockModel.js b/resources/Stock/RdStockModel.js new file mode 100644 index 0000000..20a3dfa --- /dev/null +++ b/resources/Stock/RdStockModel.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose'; + +// Define Product record schema +const ProductRecordSchema = new mongoose.Schema({ + productid: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Product', + required: true, + }, + Stock: { + type: Number, + default: 0, + }, +}); + +// Define main Stock schema +const StockSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + refPath: 'RetailDistributor', + required: true, + }, + products: [ProductRecordSchema], +}, { timestamps: true, versionKey: false }); + +export const RDStock = mongoose.model('RDStock', StockSchema); diff --git a/resources/Stock/StockController.js b/resources/Stock/StockController.js new file mode 100644 index 0000000..2a4b9b1 --- /dev/null +++ b/resources/Stock/StockController.js @@ -0,0 +1,202 @@ +import mongoose from "mongoose"; +import { PDStock } from "./PdStockModel.js"; +import { Product } from "../Products/ProductModel.js"; +import { RDStock } from "./RdStockModel.js"; + +export const getProductsAndStockByPD = async (req, res) => { + try { + const { userId } = req.params; + + // Pagination parameters + const PAGE_SIZE = parseInt(req.query.show) || 10; + const page = parseInt(req.query.page) || 1; + const skip = (page - 1) * PAGE_SIZE; + + // Filtering criteria + const 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); + } + + // Fetch user's PDStock data and products concurrently + const [userStock, products] = await Promise.all([ + PDStock.findOne({ userId: mongoose.Types.ObjectId(userId) }), + Product.aggregate([ + { $match: filter }, + { + $lookup: { + from: "categorymodels", + localField: "category", + foreignField: "_id", + as: "categoryDetails", + }, + }, + { + $lookup: { + from: "brandmodels", + localField: "brand", + foreignField: "_id", + as: "brandDetails", + }, + }, + { + $project: { + category: { $arrayElemAt: ["$categoryDetails.categoryName", 0] }, + brand: { $arrayElemAt: ["$brandDetails.brandName", 0] }, + GST: 1, + HSN_Code: 1, + SKU: 1, + addedBy: 1, + createdAt: 1, + description: 1, + image: 1, + name: 1, + price: 1, + product_Status: 1, + updatedAt: 1, + }, + }, + { $skip: skip }, + { $limit: PAGE_SIZE }, + ]), + ]); + + // Create a stock map for easy lookup + const stockMap = {}; + if (userStock && userStock.products) { + userStock.products.forEach((product) => { + stockMap[product.productid.toString()] = product.Stock; + }); + } + + // Combine products with their respective stock + const productsWithStock = products.map((product) => ({ + ...product, + stock: stockMap[product._id.toString()] || 0, + })); + + // Get total count for pagination purposes + const total = await Product.countDocuments(filter); + + return res.status(200).json({ + success: true, + totalProducts: total, + totalPages: Math.ceil(total / PAGE_SIZE), + currentPage: page, + products: productsWithStock, + }); + } catch (error) { + console.error("Error fetching products with stock:", error); + return res.status(500).json({ + success: false, + message: "Error fetching products and stock", + }); + } +}; + +export const getProductsAndStockByRD = async (req, res) => { + try { + const { userId } = req.params; + + // Pagination parameters + const PAGE_SIZE = parseInt(req.query.show) || 10; + const page = parseInt(req.query.page) || 1; + const skip = (page - 1) * PAGE_SIZE; + + // Filtering criteria + const 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); + } + + // Fetch user's RDStock data and products concurrently + const [userStock, products] = await Promise.all([ + RDStock.findOne({ userId: mongoose.Types.ObjectId(userId) }), + Product.aggregate([ + { $match: filter }, + { + $lookup: { + from: "categorymodels", + localField: "category", + foreignField: "_id", + as: "categoryDetails", + }, + }, + { + $lookup: { + from: "brandmodels", + localField: "brand", + foreignField: "_id", + as: "brandDetails", + }, + }, + { + $project: { + category: { $arrayElemAt: ["$categoryDetails.categoryName", 0] }, + brand: { $arrayElemAt: ["$brandDetails.brandName", 0] }, + GST: 1, + HSN_Code: 1, + SKU: 1, + addedBy: 1, + createdAt: 1, + description: 1, + image: 1, + name: 1, + price: 1, + product_Status: 1, + updatedAt: 1, + }, + }, + { $skip: skip }, + { $limit: PAGE_SIZE }, + ]), + ]); + + // Create a stock map for easy lookup + const stockMap = {}; + if (userStock && userStock.products) { + userStock.products.forEach((product) => { + stockMap[product.productid.toString()] = product.Stock; + }); + } + + // Combine products with their respective stock + const productsWithStock = products.map((product) => ({ + ...product, + stock: stockMap[product._id.toString()] || 0, + })); + + // Get total count for pagination purposes + const total = await Product.countDocuments(filter); + + return res.status(200).json({ + success: true, + totalProducts: total, + totalPages: Math.ceil(total / PAGE_SIZE), + currentPage: page, + products: productsWithStock, + }); + } catch (error) { + console.error("Error fetching products with stock:", error); + return res.status(500).json({ + success: false, + message: "Error fetching products and stock", + }); + } +}; \ No newline at end of file diff --git a/resources/Stock/PdStockRoute.js b/resources/Stock/StockRoute.js similarity index 52% rename from resources/Stock/PdStockRoute.js rename to resources/Stock/StockRoute.js index d49a778..4d014c6 100644 --- a/resources/Stock/PdStockRoute.js +++ b/resources/Stock/StockRoute.js @@ -1,5 +1,5 @@ import express from "express"; -import { getProductsAndStockByUser } from "./PdStockController.js"; +import { getProductsAndStockByPD ,getProductsAndStockByRD} from "./StockController.js"; import { authorizeRoles, isAuthenticatedUser } from "../../middlewares/auth.js"; const router = express.Router(); @@ -7,6 +7,12 @@ router.get( "/pd/stock/:userId", isAuthenticatedUser, authorizeRoles("admin"), - getProductsAndStockByUser + getProductsAndStockByPD +); +router.get( + "/rd/stock/:userId", + isAuthenticatedUser, + authorizeRoles("admin"), + getProductsAndStockByRD ); export default router; diff --git a/resources/TerritoryManagers/TerritoryManagerController.js b/resources/TerritoryManagers/TerritoryManagerController.js index 163feff..3194761 100644 --- a/resources/TerritoryManagers/TerritoryManagerController.js +++ b/resources/TerritoryManagers/TerritoryManagerController.js @@ -1,11 +1,192 @@ // import hashPassword from '../utils/hashPassword'; import crypto from "crypto"; +import mongoose from "mongoose"; import TerritoryManager from "./TerritoryManagerModel.js"; import sendEmail, { sendOtp } from "../../Utils/sendEmail.js"; import validator from "validator"; import password from "secure-random-password"; import catchAsyncErrors from "../../middlewares/catchAsyncErrors.js"; +import { generatePassword } from "../../Utils/generatepassword.js"; +import XLSX from "xlsx"; +import fs from "fs"; +import path from "path"; +export const uploadTerritoryManagers = async (req, res) => { + try { + if (!mongoose.Types.ObjectId.isValid(req.user._id)) { + return res.status(400).json({ message: "Please login again" }); + } + 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 = { + "Territory Manager Name": "name", + Email: "email", + "Phone Number": "mobileNumber", + }; + + 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 updatedtrritoryManagers = []; + + 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(); + const validationErrors = new Set(); + + // Validate required fields + if (!item.name) missingFields.add("name"); + if (!item.email) missingFields.add("email"); + if (!item.mobileNumber) missingFields.add("mobileNumber"); + + // Check email validity + if (item.email && !validator.isEmail(item.email)) { + validationErrors.add("incorrect mail"); + } + + // Validate mobile number + if (item.mobileNumber && !/^\d{10}$/.test(item.mobileNumber)) { + validationErrors.add("Invalid Mobile Number (should be 10 digits)"); + } + + // Combine all errors into a single message + let errorMessage = ""; + if (missingFields.size > 0) { + errorMessage += `Missing fields: ${Array.from(missingFields).join( + ", " + )}. `; + } + if (validationErrors.size > 0) { + errorMessage += `Validation errors: ${Array.from(validationErrors).join( + ", " + )}.`; + } + + // If there are errors, push them to the errors array + if (errorMessage.trim()) { + errors.push({ + name: item.name || "N/A", + email: item.email || "N/A", + phone: item.mobileNumber || "N/A", + message: errorMessage.trim(), + }); + continue; + } + + // Generate a password + const password = generatePassword(item.name, item.email); + + // Check for existing user by uniqueId + let territoryManager = await TerritoryManager.findOne({ + email: item.email, + }); + + if (territoryManager) { + // Track updated fields + const updatedFields = []; + + // Check for changes in user details + let territoryManagerUpdated = false; + for (let field in item) { + const currentValue = territoryManager[field]?.toString(); + const newValue = item[field]?.toString(); + + if (currentValue !== newValue) { + updatedFields.push(field); + territoryManager[field] = item[field]; + territoryManagerUpdated = true; + } + } + + if (territoryManagerUpdated) { + await territoryManager.save(); + updatedtrritoryManagers.push({ + ...territoryManager._doc, + updatedFields: updatedFields.join(", "), + }); + } + } else { + // Create a new territoryManager + territoryManager = new TerritoryManager({ + ...item, + password, + isVerified: true, + }); + await territoryManager.save(); + // Send email with the new password + await sendEmail({ + to: `${item?.email}`, // Change to your recipient + from: `${process.env.SEND_EMAIL_FROM}`, // Change to your verified sender + subject: `Cheminova Account Created`, + html: `Your Territory Manager Account is created successfully. +
name is: ${item?.name}
+
MobileNumber is: ${item?.mobileNumber}
+
password is: ${password}

If you have not requested this email, please ignore it.`, + }); + newlyCreated.push({ territoryManager }); + } + } + + res.status(200).json({ + message: "File processed successfully", + newlyCreated, + updatedtrritoryManagers, + errors, + }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; export const register = async (req, res) => { let { name, email, countryCode, mobileNumber } = req.body; countryCode = countryCode?.trim(); diff --git a/resources/TerritoryManagers/TerritoryManagerModel.js b/resources/TerritoryManagers/TerritoryManagerModel.js index 0d6436f..250c174 100644 --- a/resources/TerritoryManagers/TerritoryManagerModel.js +++ b/resources/TerritoryManagers/TerritoryManagerModel.js @@ -50,11 +50,11 @@ const territorymanagerSchema = new mongoose.Schema( uniqueId: { type: String, unique: true, - required: true, }, fcm_token: { type: String, default: null, + sparse: true, }, }, { timestamps: true } diff --git a/resources/TerritoryManagers/TerritoryManagerRoute.js b/resources/TerritoryManagers/TerritoryManagerRoute.js index 82ee8c5..8c24a17 100644 --- a/resources/TerritoryManagers/TerritoryManagerRoute.js +++ b/resources/TerritoryManagers/TerritoryManagerRoute.js @@ -16,11 +16,15 @@ import { ChangePassword, getOneTerritoryManager, logout, + uploadTerritoryManagers, } from "./TerritoryManagerController.js"; import { isAuthenticatedTerritoryManager } from "../../middlewares/TerritoryManagerAuth.js"; import { authorizeRoles, isAuthenticatedUser } from "../../middlewares/auth.js"; router.post("/register", register); +router + .route("/upload") + .post(isAuthenticatedUser, authorizeRoles("admin"), uploadTerritoryManagers); router.post("/verify-otp", verifyOtp); router.post("/login", loginTerritoryManager); router.route("/logout").get(logout); @@ -69,11 +73,7 @@ router.patch( authorizeRoles("admin"), UpdateProfile ); -router.patch( - "/profile/update", - isAuthenticatedTerritoryManager, - UpdateProfile -); +router.patch("/profile/update", isAuthenticatedTerritoryManager, UpdateProfile); //change password router.put( "/password/update/:id", @@ -82,11 +82,7 @@ router.put( ChangePassword ); -router.put( - "/password/update", - isAuthenticatedTerritoryManager, - ChangePassword -); +router.put("/password/update", isAuthenticatedTerritoryManager, ChangePassword); //delete TerritoryManager router.delete( "/delete/:id",