diff --git a/app/controllers/establishment.controller.js b/app/controllers/establishment.controller.js index 22fd194..0750ca4 100644 --- a/app/controllers/establishment.controller.js +++ b/app/controllers/establishment.controller.js @@ -61,6 +61,7 @@ exports.createEstablishment = async (req, res) => { "industry_code", "license_number", "isic_code", + "zone", ].forEach(key => { if (req.body[key] === "") req.body[key] = null; }); @@ -109,7 +110,8 @@ exports.createEstablishment = async (req, res) => { created_by, establishment_user, establishment_products, - ERN + ERN, + zone, } = req.body; // Basic Validation @@ -243,6 +245,10 @@ exports.createEstablishment = async (req, res) => { total_emirati, total_employees, ERN: sanitizeStringValue(ERN), + zone: + zone === undefined || zone === null || zone === "" + ? null + : sanitizeStringValue(zone), created_by: req.body.created_by || req.user.id, created_at: new Date(), }); @@ -421,6 +427,7 @@ exports.getAllEstablishments = async (req, res) => { worksheet.columns = [ { header: "ID", key: "id", width: 10 }, { header: "Establishment Name", key: "factory_name", width: 30 }, + { header: "Zone", key: "zone", width: 16 }, { header: "Contact Name", key: "establishment_contact_person_name", width: 30 }, { header: "Emirate", key: "emirate_name", width: 20 }, { header: "ISIC Code", key: "isic_code", width: 20 }, @@ -438,6 +445,7 @@ exports.getAllEstablishments = async (req, res) => { worksheet.addRow({ id: item.id, factory_name: item.factory_name, + zone: item.zone ?? "-", establishment_contact_person_name: item.establishment_contact_person_name, emirate_name: item.establishment_emirate?.name || "-", isic_code: item.isic_code || "-", @@ -600,7 +608,8 @@ exports.updateEstablishment = async (req, res) => { "permanent_factory_code", "industry_code", "license_number", - "isic_code" + "isic_code", + "zone", ].forEach(key => { if (req.body[key] === "") req.body[key] = null; }); @@ -723,6 +732,10 @@ exports.updateEstablishment = async (req, res) => { if (isic_code !== undefined) { estData.isic_code = isic_code; } + + if (estData.zone !== undefined && estData.zone !== null && estData.zone !== "") { + estData.zone = sanitizeStringValue(estData.zone); + } estData.updated_by = estData.updated_by || req.user.id; estData.updated_at = estData.updated_at || new Date(); @@ -1913,6 +1926,10 @@ exports.establishmentBulkUpload = async (req, res) => { Number(r.number_of_non_emirati_male || 0) + Number(r.number_of_non_emirati_female || 0), + zone: + r.zone && String(r.zone).trim() !== "" + ? sanitizeStringValue(String(r.zone).trim()) + : null, created_by: req.user.id }, { transaction: transaction }); diff --git a/app/controllers/products.controller.js b/app/controllers/products.controller.js index 50f609d..c65d5ca 100644 --- a/app/controllers/products.controller.js +++ b/app/controllers/products.controller.js @@ -14,6 +14,21 @@ const cleanString = (value) => ? sanitize(value, { allowedTags: [], allowedAttributes: {} }) : value; +/** Parses optional products.weight_in_ib (nullable integer, non-negative). */ +function parseWeightInIb(value) { + if (value === undefined || value === null || value === "") { + return { ok: true, value: null }; + } + const n = parseInt(String(value).trim(), 10); + if (Number.isNaN(n)) { + return { ok: false, message: "weight_in_ib must be a valid integer." }; + } + if (n < 0) { + return { ok: false, message: "weight_in_ib must be non-negative." }; + } + return { ok: true, value: n }; +} + exports.createProduct = async (req, res) => { try { const product_name = cleanString(req.body.product_name); @@ -21,6 +36,11 @@ exports.createProduct = async (req, res) => { const hs_code = req.body.hs_code; const unit_id = req.body.unit_id; + const weightParsed = parseWeightInIb(req.body.weight_in_ib); + if (!weightParsed.ok) { + return res.status(400).json({ status: "failed", message: weightParsed.message }); + } + if (hs_description && hs_description.length > 1000) { return res.status(400).json({ status: "failed", @@ -67,6 +87,7 @@ exports.createProduct = async (req, res) => { hs_code, hs_description, unit_id, + weight_in_ib: weightParsed.value, created_by: req.user.id }); @@ -150,6 +171,14 @@ exports.updateProduct = async (req, res) => { updated_at: new Date() }; + if (Object.prototype.hasOwnProperty.call(req.body, "weight_in_ib")) { + const weightParsed = parseWeightInIb(req.body.weight_in_ib); + if (!weightParsed.ok) { + return res.status(400).json({ status: "failed", message: weightParsed.message }); + } + object.weight_in_ib = weightParsed.value; + } + const [updated] = await Product.update(object, { where: { id: req.params.id } }); if (!updated) res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" }); @@ -348,6 +377,12 @@ exports.uploadProductsFromCSV = async (req, res) => { mappedKey = "unit"; } else if (/desc(ription)?/i.test(normalizedKey)) { mappedKey = "description"; + } else if ( + normalizedKey === "weight_in_ib" || + /^weight.*(ib|lb)/i.test(normalizedKey) || + /^weight$/i.test(normalizedKey) + ) { + mappedKey = "weight_in_ib"; } cleanRow[mappedKey] = row[key] ? row[key].trim() : null; @@ -368,10 +403,12 @@ exports.uploadProductsFromCSV = async (req, res) => { // Validate required columns const requiredCols = ["hs_code", "product_name", "unit", "description"]; + const optionalCols = ["weight_in_ib"]; + const allowedCols = requiredCols.concat(optionalCols); const headers = Object.keys(csvResults[0]); const missingCols = requiredCols.filter(col => !headers.includes(col)); - const extraCols = headers.filter(col => !requiredCols.includes(col)); + const extraCols = headers.filter(col => !allowedCols.includes(col)); if (missingCols.length > 0 || extraCols.length > 0) { deleteFileSecure(sanitizedPath); @@ -398,6 +435,30 @@ exports.uploadProductsFromCSV = async (req, res) => { const unit = row.unit ? row.unit.trim().toLowerCase() : ""; const description = row.description ? row.description.trim().toLowerCase() : ""; + let weightInIb = null; + if ( + row.weight_in_ib !== undefined && + row.weight_in_ib !== null && + String(row.weight_in_ib).trim() !== "" + ) { + const w = parseInt(String(row.weight_in_ib).trim(), 10); + if (Number.isNaN(w)) { + validationErrors.push({ + row: rowNumber, + error: "weight_in_ib must be numeric", + }); + continue; + } + if (w < 0) { + validationErrors.push({ + row: rowNumber, + error: "weight_in_ib must be non-negative", + }); + continue; + } + weightInIb = w; + } + // Validate required fields if (!hsCode || !productName) { validationErrors.push({ @@ -455,7 +516,8 @@ exports.uploadProductsFromCSV = async (req, res) => { hsCode: hsCode, productName: productName, unit: unit, - description: description + description: description, + weightInIb, }); } @@ -560,7 +622,8 @@ exports.uploadProductsFromCSV = async (req, res) => { hs_code: row.hsCode, product_name: row.productName, unit_id: unitId, - hs_description: row.description, + hs_description: row.description, + weight_in_ib: row.weightInIb, created_by: userId, created_at: new Date(), }); diff --git a/app/downloads_csv/company_profile_upload_sample.csv b/app/downloads_csv/company_profile_upload_sample.csv index 8c171a7..fb61d25 100644 --- a/app/downloads_csv/company_profile_upload_sample.csv +++ b/app/downloads_csv/company_profile_upload_sample.csv @@ -1 +1 @@ -Establishment Id * (Mandatory),Factory Name * (Mandatory),User Name * (Mandatory),Email * (Mandatory),Emirate * (Mandatory),City/Town * (Mandatory),Principal activity Code (ISIC Rev 4 - 4 digits) * (Mandatory),HS Code 1 * (Mandatory),HS Code 2,HS Code 3,HS Code 4,HS Code 5,Permanent Factory Code,Industry Code Business Register,Description,Industry Code Mismatch Remarks,Establishment Address,Postal Code,PO Box,Makani Number,Contact Person Name,Contact Person Designation,Mobile Number,Website,Number of Emirati Male,Number of Emirati Female,Number of Non-Emirati Male,Number of Non-Emirati Female +Establishment Id * (Mandatory),Factory Name * (Mandatory),User Name * (Mandatory),Email * (Mandatory),Emirate * (Mandatory),City/Town * (Mandatory),Principal activity Code (ISIC Rev 4 - 4 digits) * (Mandatory),HS Code 1 * (Mandatory),HS Code 2,HS Code 3,HS Code 4,HS Code 5,Permanent Factory Code,Industry Code Business Register,Description,Industry Code Mismatch Remarks,Establishment Address,Postal Code,PO Box,Makani Number,Contact Person Name,Contact Person Designation,Mobile Number,Website,Number of Emirati Male,Number of Emirati Female,Number of Non-Emirati Male,Number of Non-Emirati Female,Zone (Optional) diff --git a/app/downloads_csv/products_upload_sample.csv b/app/downloads_csv/products_upload_sample.csv index fa2ee9e..7dc6984 100644 --- a/app/downloads_csv/products_upload_sample.csv +++ b/app/downloads_csv/products_upload_sample.csv @@ -1 +1 @@ -HS Code * (Mandatory),Product Name * (Mandatory),Unit * (Mandatory),Description +HS Code * (Mandatory),Product Name * (Mandatory),Unit * (Mandatory),Description,Weight in lb (Optional) diff --git a/app/models/establishment.model.js b/app/models/establishment.model.js index 577a47c..4673f93 100644 --- a/app/models/establishment.model.js +++ b/app/models/establishment.model.js @@ -74,6 +74,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, ERN: { type: DataTypes.STRING }, + zone: { type: DataTypes.STRING }, }, { timestamps: false, diff --git a/app/models/product.model.js b/app/models/product.model.js index cb7d0d5..f361973 100644 --- a/app/models/product.model.js +++ b/app/models/product.model.js @@ -42,10 +42,6 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.INTEGER, allowNull: true, }, - isic_code: { - type: DataTypes.INTEGER, - allowNull: true, - }, is_active: { type: DataTypes.BOOLEAN, defaultValue: true, diff --git a/app/routes/routes.js b/app/routes/routes.js index 448ac1f..f60942d 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -688,6 +688,7 @@ router.post("/test/submissions-with-products",[verifySignature], testController. * total_emirati: { type: integer } * total_employees: { type: integer } * ERN: { type: string } + * zone: { type: string, description: "Establishment zone (optional)" } * created_by: { type: integer } * establishment_user: * type: object @@ -865,6 +866,7 @@ router.get("/establishments/:id",[verifySignature, verifyToken], establishmentCo * total_emirati: { type: integer } * total_employees: { type: integer } * ERN: { type: string } + * zone: { type: string, description: "Establishment zone (optional)" } * updated_by: { type: integer } * establishment_products: * type: array @@ -1278,6 +1280,10 @@ router.post("/trigger-establishment-users-welcome-email",[verifySignature, verif * unit_id: * type: integer * example: "select data from unit master" + * weight_in_ib: + * type: integer + * description: Optional weight used in IPI weighting (integer). + * example: 100 * is_active: * type: boolean * example: true @@ -1361,6 +1367,10 @@ router.get("/products/:id", [verifySignature, verifyToken],productController.get * unit_id: * type: integer * example: "select data from unit master" + * weight_in_ib: + * type: integer + * description: Optional weight used in IPI weighting (integer). Omit to leave unchanged; send null to clear. + * example: 100 * is_active: * type: boolean * example: true