Automated UOM shot name

This commit is contained in:
unknown 2025-11-11 09:57:57 +05:30
parent 9d3c006343
commit 3692b7bf4c

View File

@ -75,15 +75,10 @@ exports.deleteUnit = async (req, res) => {
} }
}; };
exports.uploadUnitMasterFromCSV = async (req, res) => { exports.uploadUnitMasterFromCSV = async (req, res) => {
try { try {
if (!req.file) { if (!req.file)
return res.status(400).send({ return res.status(400).send({ status: "failed", message: "No file uploaded." });
status: "failed",
message: "No file uploaded.",
});
}
const filePath = req.file.path; const filePath = req.file.path;
@ -96,8 +91,8 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
}); });
} }
// Validate created_by // Validate user
if (!req.user.id || isNaN(req.user.id)) { if (!req.user?.id || isNaN(req.user.id)) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
return res.status(400).send({ return res.status(400).send({
status: "failed", 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); const stats = fs.statSync(filePath);
if (stats.size === 0) { if (stats.size === 0) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
@ -116,6 +111,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
} }
const results = []; const results = [];
fs.createReadStream(filePath) fs.createReadStream(filePath)
.pipe(csv()) .pipe(csv())
.on("data", (row) => results.push(row)) .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] || {}); const headers = Object.keys(results[0] || {});
//Check for missing or extra columns
const missingCols = requiredCols.filter((col) => !headers.includes(col)); const missingCols = requiredCols.filter((col) => !headers.includes(col));
const extraCols = headers.filter((col) => !requiredCols.includes(col)); const extraCols = headers.filter((col) => !requiredCols.includes(col));
@ -142,135 +137,170 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
if (missingCols.length > 0) if (missingCols.length > 0)
msg += `Missing required columns: ${missingCols.join(", ")}. `; msg += `Missing required columns: ${missingCols.join(", ")}. `;
if (extraCols.length > 0) 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() }); return res.status(400).send({ status: "failed", message: msg.trim() });
} }
const inserted = []; const inserted = [];
const duplicates = []; // DB duplicates const duplicates = [];
const errors = []; const errors = [];
//Detect duplicates within uploaded file
const seenShortNames = new Set();
const seenUnitNames = new Set(); const seenUnitNames = new Set();
const seenShortNames = new Set();
const fileDuplicates = []; 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()) { for (const [index, row] of results.entries()) {
const uomShort = row["Display Name"]?.trim().toUpperCase();
const uom = row["Unit Name"]?.trim(); const uom = row["Unit Name"]?.trim();
const description = row["Description"]?.trim(); const description = row["Description"]?.trim() || null;
if (!uomShort || !uom) { if (!uom) {
errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." }); errors.push({ row: index + 1, reason: "Missing 'Unit Name'." });
continue; continue;
} }
if (!/^[A-Z]+$/i.test(uomShort)) {
errors.push({ row: index + 1, reason: "Display Name must contain only alphabetic characters (AZ)." });
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(); const uomKey = uom.toLowerCase();
if (seenUnitNames.has(uomKey)) {
if (seenShortNames.has(shortKey) || seenUnitNames.has(uomKey)) { fileDuplicates.push({
fileDuplicates.push({ row: index + 1, uom_short_name: uomShort, uom }); row: index + 1,
reason: "Duplicate Unit Name in file.",
uom,
});
continue; continue;
} }
seenShortNames.add(shortKey); let uomShort = generateShortName(uom, seenShortNames);
seenUnitNames.add(uomKey); 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) { if (fileDuplicates.length > 0) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
return res.status(400).send({ return res.status(400).send({
status: "failed", status: "failed",
message: "Duplicate entries found within uploaded file.", message: "Duplicate Unit Names found within uploaded file.",
duplicate_rows: fileDuplicates, duplicate_rows: fileDuplicates,
}); });
} }
// Process each row and check against DB //Insert into DB with safe duplicate handling
for (const [index, row] of results.entries()) { for (const [index, row] of results.entries()) {
try { try {
const uomShort = row["Display Name"]?.trim().toUpperCase();
const uom = row["Unit Name"]?.trim(); 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({ const existing = await UnitMaster.findOne({
where: { where: {
[Op.or]: [ [Op.or]: [
Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
uomShort.toLowerCase()
),
Sequelize.where( Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom")), Sequelize.fn("LOWER", Sequelize.col("uom")),
uom.toLowerCase() uom.toLowerCase()
), ),
Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
uomShort.toLowerCase()
),
], ],
}, },
}); });
if (existing) { if (existing) {
// Skip if same Unit Name already exists
if (existing.uom.toLowerCase() === uom.toLowerCase()) {
duplicates.push({ duplicates.push({
id: existing.id, id: existing.id,
uom_short_name: existing.uom_short_name,
uom: existing.uom, uom: existing.uom,
uom_short_name: existing.uom_short_name,
}); });
continue; continue;
} }
// Insert record //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;
}
const newUnit = await UnitMaster.create({ const newUnit = await UnitMaster.create({
uom_short_name: uomShort, uom_short_name: uomShort,
uom: uom, uom,
description: description || null, description,
created_by: parseInt(req.user.id), created_by: parseInt(req.user.id),
created_at: new Date(), created_at: new Date(),
}); });
inserted.push(newUnit); inserted.push(newUnit);
} catch (err) { } catch (err) {
errors.push({ errors.push({ row: index + 1, reason: err.message });
row: index + 1,
reason: err.message,
});
} }
} }
// Cleanup file
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
// Final response summary
let finalStatus = "success"; let finalStatus = "success";
let message = `${inserted.length} units inserted successfully.`; let message = `${inserted.length} units inserted successfully.`;
if (errors.length > 0 || duplicates.length > 0) { if (errors.length > 0 || duplicates.length > 0) {
finalStatus = inserted.length > 0 ? "partial_success" : "failed"; finalStatus = inserted.length > 0 ? "partial_success" : "failed";
if (finalStatus === "partial_success") { message =
message = `${inserted.length} units inserted, ${duplicates.length} duplicates skipped, ${errors.length} validation errors found.`; finalStatus === "partial_success"
} else { ? `${inserted.length} inserted, ${duplicates.length} duplicates skipped, ${errors.length} errors found.`
message = `No units imported. ${duplicates.length} duplicates and ${errors.length} validation errors found.`; : `No units imported. ${duplicates.length} duplicates and ${errors.length} errors found.`;
}
} }
return res.status(200).send({ return res.status(200).send({