diff --git a/app/controllers/unitMasterController.js b/app/controllers/unitMasterController.js index a9f3e6f..1adcbb4 100644 --- a/app/controllers/unitMasterController.js +++ b/app/controllers/unitMasterController.js @@ -75,15 +75,10 @@ exports.deleteUnit = async (req, res) => { } }; - exports.uploadUnitMasterFromCSV = async (req, res) => { try { - if (!req.file) { - return res.status(400).send({ - status: "failed", - message: "No file uploaded.", - }); - } + if (!req.file) + return res.status(400).send({ status: "failed", message: "No file uploaded." }); const filePath = req.file.path; @@ -96,8 +91,8 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { }); } - // Validate created_by - if (!req.user.id || isNaN(req.user.id)) { + // Validate user + if (!req.user?.id || isNaN(req.user.id)) { fs.unlinkSync(filePath); return res.status(400).send({ status: "failed", @@ -105,7 +100,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { }); } - // Check if file is empty + // Validate file not empty const stats = fs.statSync(filePath); if (stats.size === 0) { fs.unlinkSync(filePath); @@ -116,6 +111,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { } const results = []; + fs.createReadStream(filePath) .pipe(csv()) .on("data", (row) => results.push(row)) @@ -129,10 +125,9 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { }); } - const requiredCols = ["Display Name", "Unit Name", "Description"]; + //Expected columns + const requiredCols = ["Unit Name", "Description"]; const headers = Object.keys(results[0] || {}); - - //Check for missing or extra columns const missingCols = requiredCols.filter((col) => !headers.includes(col)); const extraCols = headers.filter((col) => !requiredCols.includes(col)); @@ -142,135 +137,170 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { if (missingCols.length > 0) msg += `Missing required columns: ${missingCols.join(", ")}. `; if (extraCols.length > 0) - msg += `Unexpected columns found: ${extraCols.join(", ")}. Only 'Display Name', 'Unit Name', and 'Description' are allowed.`; + msg += `Unexpected columns found: ${extraCols.join( + ", " + )}. Only 'Unit Name' and 'Description' are allowed.`; return res.status(400).send({ status: "failed", message: msg.trim() }); } const inserted = []; - const duplicates = []; // DB duplicates + const duplicates = []; const errors = []; - - //Detect duplicates within uploaded file - const seenShortNames = new Set(); const seenUnitNames = new Set(); + const seenShortNames = new Set(); const fileDuplicates = []; + //generate smart short name (letters + numbers) + const generateShortName = (unitName, existingShorts = new Set()) => { + const cleaned = unitName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase(); + + const abbreviationMap = { + METER: "MT", + METRE: "MT", + KILOGRAM: "KG", + GRAM: "GM", + LITER: "LTR", + LITRE: "LTR", + CENTIMETER: "CM", + MILLIMETER: "MM", + SECOND: "SEC", + MINUTE: "MIN", + HOUR: "HR", + DAY: "DY", + PIECE: "PC", + BOX: "BX", + USER: "USR", + ITEM: "ITM", + UNIT: "UNT", + }; + + let shortName = abbreviationMap[cleaned] || cleaned.substring(0, 3); + if (shortName.length < 2) shortName = shortName.padEnd(2, "X"); + + let finalShort = `${shortName}${Math.floor(Math.random() * 100)}`; + while (existingShorts.has(finalShort.toLowerCase())) { + finalShort = `${shortName}${Math.floor(Math.random() * 100)}`; + } + + return finalShort.substring(0, 5); + }; + + //Validate & prepare records for (const [index, row] of results.entries()) { - const uomShort = row["Display Name"]?.trim().toUpperCase(); const uom = row["Unit Name"]?.trim(); - const description = row["Description"]?.trim(); + const description = row["Description"]?.trim() || null; - if (!uomShort || !uom) { - errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." }); + if (!uom) { + errors.push({ row: index + 1, reason: "Missing 'Unit Name'." }); continue; } - if (!/^[A-Z]+$/i.test(uomShort)) { - errors.push({ row: index + 1, reason: "Display Name must contain only alphabetic characters (A–Z)." }); - continue; - } - - if (uomShort.length > 5) { - errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." }); - continue; - } - - const shortKey = uomShort.toLowerCase(); const uomKey = uom.toLowerCase(); - - if (seenShortNames.has(shortKey) || seenUnitNames.has(uomKey)) { - fileDuplicates.push({ row: index + 1, uom_short_name: uomShort, uom }); + if (seenUnitNames.has(uomKey)) { + fileDuplicates.push({ + row: index + 1, + reason: "Duplicate Unit Name in file.", + uom, + }); continue; } - seenShortNames.add(shortKey); + let uomShort = generateShortName(uom, seenShortNames); + seenUnitNames.add(uomKey); + seenShortNames.add(uomShort.toLowerCase()); + results[index]._generatedShort = uomShort; + results[index]._description = description; } - // Stop if file has duplicate rows + // Stop if file has internal duplicates if (fileDuplicates.length > 0) { fs.unlinkSync(filePath); return res.status(400).send({ status: "failed", - message: "Duplicate entries found within uploaded file.", + message: "Duplicate Unit Names found within uploaded file.", duplicate_rows: fileDuplicates, }); } - // Process each row and check against DB + //Insert into DB with safe duplicate handling for (const [index, row] of results.entries()) { try { - const uomShort = row["Display Name"]?.trim().toUpperCase(); const uom = row["Unit Name"]?.trim(); - const description = row["Description"]?.trim(); + let uomShort = row._generatedShort; + const description = row._description; - if (!uomShort || !uom) { - errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." }); - continue; - } - - if (uomShort.length > 5) { - errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." }); - continue; - } - - //Check for existing record (duplicate) in DB const existing = await UnitMaster.findOne({ where: { [Op.or]: [ - Sequelize.where( - Sequelize.fn("LOWER", Sequelize.col("uom_short_name")), - uomShort.toLowerCase() - ), Sequelize.where( Sequelize.fn("LOWER", Sequelize.col("uom")), uom.toLowerCase() ), + Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("uom_short_name")), + uomShort.toLowerCase() + ), ], }, }); if (existing) { - duplicates.push({ - id: existing.id, - uom_short_name: existing.uom_short_name, - uom: existing.uom, - }); - continue; + // Skip if same Unit Name already exists + if (existing.uom.toLowerCase() === uom.toLowerCase()) { + duplicates.push({ + id: existing.id, + uom: existing.uom, + uom_short_name: existing.uom_short_name, + }); + continue; + } + + //Safe regeneration using ternary inside while loop + let newShort = uomShort; + let existsInDb; + + do { + existsInDb = await UnitMaster.findOne({ + where: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("uom_short_name")), + newShort.toLowerCase() + ), + }); + + newShort = existsInDb + ? `${uomShort}${Math.floor(Math.random() * 1000)}`.substring(0, 5) + : newShort; + } while (existsInDb); + + uomShort = newShort; } - // Insert record const newUnit = await UnitMaster.create({ uom_short_name: uomShort, - uom: uom, - description: description || null, + uom, + description, created_by: parseInt(req.user.id), created_at: new Date(), }); inserted.push(newUnit); } catch (err) { - errors.push({ - row: index + 1, - reason: err.message, - }); + errors.push({ row: index + 1, reason: err.message }); } } - // Cleanup file fs.unlinkSync(filePath); - // Final response summary let finalStatus = "success"; let message = `${inserted.length} units inserted successfully.`; if (errors.length > 0 || duplicates.length > 0) { finalStatus = inserted.length > 0 ? "partial_success" : "failed"; - if (finalStatus === "partial_success") { - message = `${inserted.length} units inserted, ${duplicates.length} duplicates skipped, ${errors.length} validation errors found.`; - } else { - message = `No units imported. ${duplicates.length} duplicates and ${errors.length} validation errors found.`; - } + message = + finalStatus === "partial_success" + ? `${inserted.length} inserted, ${duplicates.length} duplicates skipped, ${errors.length} errors found.` + : `No units imported. ${duplicates.length} duplicates and ${errors.length} errors found.`; } return res.status(200).send({ @@ -282,7 +312,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => { skipped: duplicates.length, errors: errors.length, }, - duplicates, + duplicates, error_details: errors, }); } catch (err) {