fcsc_ipi_backend/app/controllers/unitMasterController.js

429 lines
14 KiB
JavaScript

const db = require("../models");
const { Op } = require("sequelize");
const { Sequelize } = require("sequelize");
const { UnitMaster } = require("../models");
const fs = require("fs");
const csv = require("csv-parser");
// Create new Unit
exports.createUnit = async (req, res) => {
try {
const { uom } = req.body;
if (!uom || typeof uom !== "string" || !uom.trim()) {
return res.status(400).json({
status: "error",
message: "UOM (Unit Name) is required and must be a non-empty string.",
});
}
const cleaned = uom.replace(/[^a-zA-Z]/g, "").toUpperCase();
if (!cleaned) {
return res.status(400).json({
status: "error",
message: "UOM must contain at least one letter (A-Z).",
});
}
const existingUOM = await UnitMaster.findOne({
where: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom")),
uom.toLowerCase()
),
});
if (existingUOM)
return res.status(400).json({
status: "error",
message: `Unit '${uom}' already exists with short key '${existingUOM.uom_short_name}'.`,
});
//Generate short name (same rule as CSV)
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: "DAY",
PIECE: "PC",
BOX: "BX",
USER: "USR",
ITEM: "ITM",
UNIT: "UNT",
};
let uomShort;
if (cleaned.length <= 5) {
uomShort = cleaned;
} else {
// If >5 letters, use abbreviation or auto-generate
const baseShort = abbreviationMap[cleaned] || cleaned.substring(0, 3);
const randomLetters = () =>
Array.from({ length: 2 }, () =>
String.fromCharCode(65 + Math.floor(Math.random() * 26))
).join("");
uomShort = `${baseShort}${randomLetters()}`.substring(0, 5);
}
let exists = await UnitMaster.findOne({
where: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
uomShort.toLowerCase()
),
});
while (exists) {
const randomSuffix = Math.random().toString(36).substring(2, 3).toUpperCase();
uomShort = (uomShort.substring(0, 4) + randomSuffix).substring(0, 5);
exists = await UnitMaster.findOne({
where: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
uomShort.toLowerCase()
),
});
}
req.body.uom_short_name = uomShort;
req.body.created_by = req.body.created_by || req.user.id;
const unit = await UnitMaster.create(req.body);
res.status(201).json({ status: "success", data: unit });
} catch (err) {
res.status(400).json({ status: "error", message: err.message });
}
};
// Get all Units
exports.getAllUnits = async (req, res) => {
try {
const units = await UnitMaster.findAll({
attributes: {
include: [
[
Sequelize.literal(
`(SELECT COUNT(*) FROM products AS P WHERE P.unit_id = UnitMaster.id)`
),
"mapped_products_count",
],
],
},
});
res.status(200).json({ status: "success",unit_count : units.length, data: units });
} catch (err) {
res.status(500).json({ status: "error", message: err.message });
}
};
// Get Unit by ID
exports.getUnitById = async (req, res) => {
try {
const unit = await UnitMaster.findByPk(req.params.id);
if (!unit) return res.status(404).json({ status: "error", message: "Unit not found" });
res.status(200).json({ status: "success", data: unit });
} catch (err) {
res.status(500).json({ status: "error", message: err.message });
}
};
// Update Unit
exports.updateUnit = async (req, res) => {
try {
const unit = await UnitMaster.findByPk(req.params.id);
if (!unit) return res.status(404).json({ status: "error", message: "Unit not found" });
req.body.updated_by = req.body.updated_by || req.user.id;
req.body.updated_at = req.body.updated_at || new Date();
await unit.update(req.body);
res.status(200).json({ status: "success", data: unit });
} catch (err) {
res.status(400).json({ status: "error", message: err.message });
}
};
// Delete Unit
exports.deleteUnit = async (req, res) => {
try {
const unit = await UnitMaster.findByPk(req.params.id);
if (!unit) return res.status(404).json({ status: "error", message: "Unit not found" });
await unit.destroy();
res.status(200).json({ status: "success", message: "Unit deleted successfully" });
} catch (err) {
res.status(500).json({ status: "error", message: err.message });
}
};
exports.uploadUnitMasterFromCSV = async (req, res) => {
try {
if (!req.file)
return res.status(400).send({ status: "failed", message: "No file uploaded." });
const filePath = req.file.path;
// Validate file type
if (!req.file.originalname.endsWith(".csv")) {
fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Invalid file type. Only CSV allowed.",
});
}
// Validate user
if (!req.user?.id || isNaN(req.user.id)) {
fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Invalid or missing User Id.",
});
}
// Validate file not empty
const stats = fs.statSync(filePath);
if (stats.size === 0) {
fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Uploaded file is empty.",
});
}
const results = [];
fs.createReadStream(filePath)
.pipe(csv())
.on("data", (row) => results.push(row))
.on("end", async () => {
try {
if (results.length === 0) {
fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "CSV file is empty or invalid.",
});
}
//Expected columns
const requiredCols = ["Unit Name", "Description"];
const headers = Object.keys(results[0] || {});
const missingCols = requiredCols.filter((col) => !headers.includes(col));
const extraCols = headers.filter((col) => !requiredCols.includes(col));
if (missingCols.length > 0 || extraCols.length > 0) {
fs.unlinkSync(filePath);
let msg = "";
if (missingCols.length > 0)
msg += `Missing required columns: ${missingCols.join(", ")}. `;
if (extraCols.length > 0)
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 = [];
const errors = [];
const seenUnitNames = new Set();
const seenShortNames = new Set();
const fileDuplicates = [];
const generateShortName = (unitName, existingShorts = new Set()) => {
const cleaned = unitName.replace(/[^a-zA-Z]/g, "").toUpperCase();
// Predefined abbreviations
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",
};
//if Unit Name is ≤ 5 characters, just use it in uppercase
if (cleaned.length <= 5) return cleaned;
// Otherwise, use abbreviation map or generate automatically
let shortName = abbreviationMap[cleaned] || cleaned.substring(0, 3);
if (shortName.length < 2) shortName = shortName.padEnd(2, "X");
// Random 2-letter suffix for uniqueness
const randomLetters = () =>
Array.from({ length: 2 }, () =>
String.fromCharCode(65 + Math.floor(Math.random() * 26))
).join("");
let finalShort = `${shortName}${randomLetters()}`;
while (existingShorts.has(finalShort.toLowerCase())) {
finalShort = `${shortName}${randomLetters()}`;
}
return finalShort.substring(0, 5);
};
//Validate & prepare records
for (const [index, row] of results.entries()) {
const uom = row["Unit Name"]?.trim();
const description = row["Description"]?.trim() || null;
if (!uom) {
errors.push({ row: index + 1, reason: "Missing 'Unit Name'." });
continue;
}
const uomKey = uom.toLowerCase();
if (seenUnitNames.has(uomKey)) {
fileDuplicates.push({
row: index + 1,
reason: "Duplicate Unit Name in file.",
uom,
});
continue;
}
let uomShort = generateShortName(uom, seenShortNames);
seenUnitNames.add(uomKey);
seenShortNames.add(uomShort.toLowerCase());
results[index]._generatedShort = uomShort;
results[index]._description = description;
}
// Stop if file has internal duplicates
if (fileDuplicates.length > 0) {
fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Duplicate Unit Names found within uploaded file.",
duplicate_rows: fileDuplicates,
});
}
//Insert into DB with safe duplicate handling
for (const [index, row] of results.entries()) {
try {
const uom = row["Unit Name"]?.trim();
let uomShort = row._generatedShort;
const description = row._description;
const existing = await UnitMaster.findOne({
where: {
[Op.or]: [
Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom")),
uom.toLowerCase()
),
Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
uomShort.toLowerCase()
),
],
},
});
if (existing) {
// 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;
}
const newUnit = await UnitMaster.create({
uom_short_name: uomShort,
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 });
}
}
fs.unlinkSync(filePath);
let finalStatus = "success";
let message = `${inserted.length} units inserted successfully.`;
if (errors.length > 0 || duplicates.length > 0) {
finalStatus = inserted.length > 0 ? "partial_success" : "failed";
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({
status: finalStatus,
message,
summary: {
total_records: results.length,
imported: inserted.length,
skipped: duplicates.length,
errors: errors.length,
},
duplicates,
error_details: errors,
});
} catch (err) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(500).send({
status: "failed",
message: err.message,
});
}
});
} catch (error) {
console.error("Error uploading Unit Master CSV:", error);
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
return res.status(500).send({
status: "failed",
message: "Error processing CSV file.",
error: error.message,
});
}
};