1133 lines
37 KiB
JavaScript
1133 lines
37 KiB
JavaScript
const db = require("../models");
|
|
const Submission = db.Submission;
|
|
const SubmissionProduct = db.SubmissionProduct;
|
|
const SubmissionHistory = db.SubmissionHistory;
|
|
const Establishment = db.Establishment;
|
|
const EstablishmentUser = db.EstablishmentUser;
|
|
const UnitMaster = db.UnitMaster;
|
|
const VariationReasonMaster = db.VariationReasonMaster;
|
|
const ZeroTargetReasonMaster = db.ZeroTargetReasonMaster;
|
|
const Product = db.Product;
|
|
const CityTown = db.CityTown;
|
|
const Emirate = db.Emirate;
|
|
const QuarterlyWindowsConfiguration = db.QuarterlyWindowsConfiguration;
|
|
const User = db.user;
|
|
const ExcelJS = require("exceljs");
|
|
const { Op } = require("sequelize");
|
|
const { Sequelize } = require("sequelize");
|
|
const { sendEmail } = require("../services/email.service");
|
|
const { sendEmailService } = require("../services/email.service");
|
|
const { getQuarterPeriods } = require("../services/quarterService");
|
|
const { sanitizeForLog } = require("../utils/sanitize");
|
|
const logger = require("../services/logger");
|
|
|
|
exports.createSubmission = async (req, res) => {
|
|
try {
|
|
|
|
const { products = [], establishment_id, quarter, year, ...submissionData } = req.body;
|
|
if (products.length > 10)
|
|
return res.status(400).json({ status: "failed", message: "Max 10 products allowed" });
|
|
|
|
// check duplicate submission
|
|
const exists = await Submission.findOne({ where: {establishment_id,quarter,year} });
|
|
|
|
if (exists) {
|
|
return res.status(400).json({status: "failed", message: `Submission already exists for establishment_id: ${establishment_id}, quarter: ${quarter}, year: ${year}`});
|
|
}
|
|
submissionData.created_by = submissionData.created_by || req.user.id;
|
|
const numericFields = [
|
|
"annual_installed_capacity",
|
|
"previous_quantity_period_one",
|
|
"previous_quantity_period_two",
|
|
"previous_quantity_period_three",
|
|
"previous_cost_period_one",
|
|
"previous_cost_period_two",
|
|
"previous_cost_period_three",
|
|
|
|
"current_quantity_period_one",
|
|
"current_quantity_period_two",
|
|
"current_quantity_period_three",
|
|
"current_cost_period_one",
|
|
"current_cost_period_two",
|
|
"current_cost_period_three",
|
|
|
|
"forecast_quantity_period_one",
|
|
"forecast_quantity_period_two",
|
|
"forecast_quantity_period_three",
|
|
"forecast_cost_period_one",
|
|
"forecast_cost_period_two",
|
|
"forecast_cost_period_three",
|
|
];
|
|
|
|
const hasNegativeNumber = (obj, fields) => {
|
|
return fields.find((field) => {
|
|
const val = obj[field];
|
|
if (val === null || val === undefined || val === "") return false;
|
|
|
|
const num = Number(val);
|
|
return !isNaN(num) && num < 0;
|
|
});
|
|
};
|
|
|
|
for (let i = 0; i < products.length; i++) {
|
|
const invalidField = hasNegativeNumber(products[i], numericFields);
|
|
|
|
if (invalidField) {
|
|
return res.status(400).json({
|
|
status: "failed",
|
|
message: `Product ${i + 1}: ${invalidField} cannot be negative`,
|
|
});
|
|
}
|
|
}
|
|
|
|
const submission = await Submission.create({establishment_id, quarter, year, ...submissionData});
|
|
|
|
// find config
|
|
const config = await QuarterlyWindowsConfiguration.findOne({where: { quarter, year }});
|
|
if (config) {
|
|
const currentResponded = config.responded ?? 0;
|
|
const currentNotResponded = config.not_responded ?? 0;
|
|
// respond increase, not responded decrease
|
|
await QuarterlyWindowsConfiguration.update({responded: currentResponded + 1, not_responded: currentNotResponded - 1, updated_at: new Date() }, {where: { id: config.id }} );
|
|
}
|
|
|
|
if (products.length) {
|
|
// products.forEach((p) => (p.submission_id = submission.id));
|
|
// await SubmissionProduct.bulkCreate(products);
|
|
|
|
// Process products before insert
|
|
const processedProducts = products.map((p) => {
|
|
const num = (v) => (isNaN(parseFloat(v)) ? 0 : parseFloat(v)); // safe number conversion
|
|
|
|
// Calculate totals for each section
|
|
const previous_quantity =
|
|
num(p.previous_quantity_period_one) +
|
|
num(p.previous_quantity_period_two) +
|
|
num(p.previous_quantity_period_three);
|
|
|
|
const previous_cost =
|
|
num(p.previous_cost_period_one) +
|
|
num(p.previous_cost_period_two) +
|
|
num(p.previous_cost_period_three);
|
|
|
|
const current_quantity =
|
|
num(p.current_quantity_period_one) +
|
|
num(p.current_quantity_period_two) +
|
|
num(p.current_quantity_period_three);
|
|
|
|
const current_cost =
|
|
num(p.current_cost_period_one) +
|
|
num(p.current_cost_period_two) +
|
|
num(p.current_cost_period_three);
|
|
|
|
const forecast_quantity =
|
|
num(p.forecast_quantity_period_one) +
|
|
num(p.forecast_quantity_period_two) +
|
|
num(p.forecast_quantity_period_three);
|
|
|
|
const forecast_cost =
|
|
num(p.forecast_cost_period_one) +
|
|
num(p.forecast_cost_period_two) +
|
|
num(p.forecast_cost_period_three);
|
|
|
|
return {
|
|
...p,
|
|
submission_id: submission.id,
|
|
previous_quantity: previous_quantity.toString(),
|
|
previous_cost: previous_cost.toString(),
|
|
current_quantity: current_quantity.toString(),
|
|
current_cost: current_cost.toString(),
|
|
forecast_quantity: forecast_quantity.toString(),
|
|
forecast_cost: forecast_cost.toString(),
|
|
};
|
|
});
|
|
|
|
// Bulk insert products
|
|
await SubmissionProduct.bulkCreate(processedProducts);
|
|
|
|
}
|
|
|
|
// const result = await Submission.findByPk(submission.id, {
|
|
// include: [{ model: SubmissionProduct, as: "products" },{ model: EstablishmentUser,as: "created_user",attributes: ["name"],}],
|
|
// });
|
|
|
|
|
|
// //send email
|
|
// const EstablishmentData = await Establishment.findOne({ where: { id :result['establishment_id'] } });
|
|
// const EstablishmentUserData = await EstablishmentUser.findAll({ where: { establishment_id :result['establishment_id'] , is_active : 1} });
|
|
// const AdminUsersData = await User.findAll({ where: { is_active : 1} });
|
|
|
|
// for await (const userObj of EstablishmentUserData)
|
|
// {
|
|
// placeHolderData = {
|
|
// user_name : userObj.name,
|
|
// quarter : result['quarter'],
|
|
// year : result['year'],
|
|
// establishment_name : EstablishmentData['factory_name'],
|
|
// submitted_by : result['created_user.name'],
|
|
// submission_date : result['created_at'],
|
|
// support_email : process.env.SUPPORT_EMAIL,
|
|
// support_phone : process.env.SUPPORT_PHONE,
|
|
// portal_url : process.env.FE_BASE_URL,
|
|
// }
|
|
|
|
// await sendEmailService(userObj.email, 'submission_created_mail_to_establishment_user', placeHolderData);
|
|
// }
|
|
|
|
// for await (const userObj of AdminUsersData)
|
|
// {
|
|
// placeHolderData = {
|
|
// user_name : userObj.name,
|
|
// quarter : result['quarter'],
|
|
// year : result['year'],
|
|
// establishment_name : EstablishmentData['factory_name'],
|
|
// submitted_by : result['created_user.name'],
|
|
// submission_date : result['created_at'],
|
|
// support_email : process.env.SUPPORT_EMAIL,
|
|
// support_phone : process.env.SUPPORT_PHONE,
|
|
// portal_url : process.env.FE_BASE_URL,
|
|
// logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
|
|
// }
|
|
|
|
// await sendEmailService(userObj.email, 'submission_created_mail_to_admin', placeHolderData);
|
|
// }
|
|
|
|
|
|
const result = await Submission.findByPk(submission.id, {
|
|
include: [
|
|
{ model: SubmissionProduct, as: "products" },
|
|
{ model: EstablishmentUser, as: "created_user", attributes: ["name"] },
|
|
],
|
|
});
|
|
|
|
// Fetch required data for email
|
|
const EstablishmentData = await Establishment.findOne({ where: { id: result.establishment_id }});
|
|
const EstablishmentUserData = await EstablishmentUser.findAll({where: { establishment_id: result.establishment_id, is_active: 1 }});
|
|
const AdminUsersData = await User.findAll({where: { is_active: 1 }});
|
|
|
|
const formatSubmissionDate = (date) => {
|
|
const formatted = new Intl.DateTimeFormat("en-GB", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
timeZone: "Asia/Dubai"
|
|
}).format(new Date(date));
|
|
const [datePart, timePart] = formatted.split(", ");
|
|
const upperTime = timePart.toUpperCase();
|
|
return `${datePart}, ${upperTime} GST (UAE)`;
|
|
};
|
|
|
|
const surveyData = await QuarterlyWindowsConfiguration.findOne({ where: { year :result['year'] , quarter : result['quarter'] } });
|
|
|
|
// Shared placeholder data
|
|
const basePlaceholder = {
|
|
quarter: result.quarter,
|
|
year: result.year,
|
|
establishment_name: EstablishmentData.factory_name,
|
|
submitted_by: result.created_user?.name,
|
|
submission_date: formatSubmissionDate(result.created_at),
|
|
support_email: process.env.SUPPORT_EMAIL,
|
|
support_phone: process.env.SUPPORT_PHONE,
|
|
portal_url: process.env.FE_BASE_URL,
|
|
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`,
|
|
survey_name: surveyData.survey_name
|
|
};
|
|
|
|
// --- SEND EMAILS TO ESTABLISHMENT USERS ---
|
|
const establishmentEmailPromises = EstablishmentUserData.map((userObj) => {
|
|
return sendEmailService(userObj.email,"submission_created_mail_to_establishment_user",{...basePlaceholder,user_name: userObj.name,});
|
|
});
|
|
|
|
// --- SEND EMAILS TO ADMINS ---
|
|
const adminEmailPromises = AdminUsersData.map((userObj) => {
|
|
return sendEmailService(userObj.email,"submission_created_mail_to_admin",{...basePlaceholder,user_name: userObj.name,} );
|
|
});
|
|
|
|
// Execute ALL emails in parallel
|
|
await Promise.all([
|
|
...establishmentEmailPromises,
|
|
...adminEmailPromises,
|
|
]);
|
|
|
|
|
|
|
|
|
|
res.status(201).json({ status: "success", message: "Submission created sucessfully."});
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.updateSubmission = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { products = [], ...submissionData } = req.body;
|
|
|
|
// Check if submission exists
|
|
const submission = await Submission.findByPk(id);
|
|
if (!submission)
|
|
return res.status(404).json({ status: "failed", message: "Submission not found" });
|
|
|
|
submissionData.updated_by = submissionData.updated_by || req.user.id;
|
|
submissionData.updated_at = submissionData.updated_at || new Date();
|
|
|
|
// Update main submission record
|
|
await submission.update(submissionData);
|
|
|
|
// Helper: safe numeric conversion
|
|
const num = (v) => (isNaN(parseFloat(v)) ? 0 : parseFloat(v));
|
|
|
|
// Loop through products
|
|
for (const p of products) {
|
|
// --- Auto-calculate totals ---
|
|
const previous_quantity =
|
|
num(p.previous_quantity_period_one) +
|
|
num(p.previous_quantity_period_two) +
|
|
num(p.previous_quantity_period_three);
|
|
|
|
const previous_cost =
|
|
num(p.previous_cost_period_one) +
|
|
num(p.previous_cost_period_two) +
|
|
num(p.previous_cost_period_three);
|
|
|
|
const current_quantity =
|
|
num(p.current_quantity_period_one) +
|
|
num(p.current_quantity_period_two) +
|
|
num(p.current_quantity_period_three);
|
|
|
|
const current_cost =
|
|
num(p.current_cost_period_one) +
|
|
num(p.current_cost_period_two) +
|
|
num(p.current_cost_period_three);
|
|
|
|
const forecast_quantity =
|
|
num(p.forecast_quantity_period_one) +
|
|
num(p.forecast_quantity_period_two) +
|
|
num(p.forecast_quantity_period_three);
|
|
|
|
const forecast_cost =
|
|
num(p.forecast_cost_period_one) +
|
|
num(p.forecast_cost_period_two) +
|
|
num(p.forecast_cost_period_three);
|
|
|
|
// Attach calculated fields
|
|
const updatedProductData = {
|
|
...p,
|
|
submission_id: id,
|
|
previous_quantity: previous_quantity.toString(),
|
|
previous_cost: previous_cost.toString(),
|
|
current_quantity: current_quantity.toString(),
|
|
current_cost: current_cost.toString(),
|
|
forecast_quantity: forecast_quantity.toString(),
|
|
forecast_cost: forecast_cost.toString(),
|
|
};
|
|
|
|
// --- Update or Create ---
|
|
if (p.id) {
|
|
const existingProduct = await SubmissionProduct.findOne({
|
|
where: { id: p.id, submission_id: id },
|
|
});
|
|
|
|
if (existingProduct) {
|
|
updatedProductData.updated_by = updatedProductData.updated_by || req.user.id;
|
|
updatedProductData.updated_at = updatedProductData.updated_at || new Date();
|
|
await existingProduct.update(updatedProductData);
|
|
} else {
|
|
updatedProductData.created_by = updatedProductData.created_by || req.user.id;
|
|
await SubmissionProduct.create(updatedProductData);
|
|
}
|
|
} else {
|
|
updatedProductData.created_by = updatedProductData.created_by || req.user.id;
|
|
await SubmissionProduct.create(updatedProductData);
|
|
}
|
|
}
|
|
|
|
// Fetch updated data with associations
|
|
const updatedSubmission = await Submission.findByPk(id, {
|
|
include: [{ model: SubmissionProduct, as: "products" }],
|
|
});
|
|
|
|
res.status(200).json({
|
|
status: "success",
|
|
message: "Submission updated successfully",
|
|
});
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).json({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.submissionHistory = async (req, res) => {
|
|
try {
|
|
|
|
const { establishment_id } = req.params;
|
|
|
|
let data = await Submission.findAll({
|
|
attributes: {
|
|
include: [
|
|
[
|
|
Sequelize.literal(`(SELECT COUNT(*) FROM submission_products AS sp1 WHERE sp1.submission_id = submission.id)`),
|
|
"product_count"
|
|
],
|
|
[
|
|
Sequelize.literal(`(SELECT ROUND(COALESCE(SUM(sp2.current_cost), 0), 2) FROM submission_products AS sp2 WHERE sp2.submission_id = submission.id)`),
|
|
"total_cost"
|
|
]
|
|
]
|
|
},
|
|
where: { establishment_id },
|
|
order: [["id", "DESC"]],
|
|
});
|
|
|
|
data = data.map(item => ({
|
|
...item.get(),
|
|
total_cost: Number(item.get("total_cost") || 0).toFixed(2)
|
|
}));
|
|
|
|
res.status(200).json({ status: "success", data });
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.submissionList = async (req, res) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
limit = 10,
|
|
search = "",
|
|
quarter,
|
|
year,
|
|
status,
|
|
emirate_id,
|
|
export_excel = false,
|
|
} = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const where = {};
|
|
|
|
// 🔹 Filters
|
|
if (year) where.year = year;
|
|
if (quarter) where.quarter = quarter;
|
|
if (status) where.status = status;
|
|
|
|
// 🔹 Search — establishment name or code or submitted user
|
|
const establishmentWhere = {};
|
|
if (search) {
|
|
establishmentWhere[Op.or] = [
|
|
{ factory_name: { [Op.like]: `%${search}%` } },
|
|
{ establishment_code: { [Op.like]: `%${search}%` } },
|
|
];
|
|
}
|
|
|
|
// 🔹 Emirate filter
|
|
if (emirate_id) establishmentWhere.establishment_emirate_id = emirate_id;
|
|
|
|
// 🔹 Query
|
|
const { rows, count } = await Submission.findAndCountAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: Establishment,
|
|
as: "establishment",
|
|
attributes: ["factory_name", "establishment_code"],
|
|
where: establishmentWhere,
|
|
include: [
|
|
{ model: Emirate, as: "establishment_emirate", attributes: ["name"] },
|
|
],
|
|
},
|
|
{
|
|
model: EstablishmentUser,
|
|
as: "created_user",
|
|
attributes: ["name"],
|
|
},
|
|
],
|
|
attributes: {
|
|
include: [
|
|
[
|
|
Sequelize.literal(`(
|
|
SELECT COUNT(*) FROM submission_products AS sp
|
|
WHERE sp.submission_id = submission.id
|
|
)`),
|
|
"product_count",
|
|
],
|
|
[
|
|
Sequelize.literal(`(
|
|
SELECT eu.name
|
|
FROM establishment_users eu
|
|
WHERE eu.id = submission.created_by
|
|
LIMIT 1
|
|
)`),
|
|
"created_by_name"
|
|
],
|
|
[
|
|
Sequelize.literal(`(
|
|
SELECT au.name
|
|
FROM admin_users au
|
|
WHERE au.id = submission.approve_or_reject_by
|
|
LIMIT 1
|
|
)`),
|
|
"reviewer_name"
|
|
],
|
|
],
|
|
},
|
|
order: [["id", "DESC"]],
|
|
offset: parseInt(offset),
|
|
limit: parseInt(limit),
|
|
});
|
|
|
|
// 🔹 Export to Excel
|
|
if (export_excel && export_excel === "true") {
|
|
const excelData = rows.map((s) => ({
|
|
Establishment: s.establishment?.factory_name,
|
|
Emirate: s.establishment?.establishment_emirate?.name,
|
|
Year: s.year,
|
|
Quarter: s.quarter,
|
|
Products: s.dataValues.product_count,
|
|
Status: s.status,
|
|
Submitted_By: s.created_user?.name,
|
|
Submission_Date: s.created_at,
|
|
}));
|
|
|
|
const filePath = await exportToExcel("Submission_List", excelData);
|
|
return res.download(filePath);
|
|
}
|
|
|
|
|
|
// status summary count
|
|
const total = count;
|
|
const submitted = await Submission.count({
|
|
where: { ...where, status: "Submitted" },
|
|
include: [{model: Establishment,as: "establishment",where: establishmentWhere }]
|
|
});
|
|
|
|
const resubmitted = await Submission.count({
|
|
where: { ...where, status: "Resubmitted" },
|
|
include: [{model: Establishment,as: "establishment",where: establishmentWhere }]
|
|
});
|
|
|
|
const approved = await Submission.count({
|
|
where: { ...where, status: "Approved" },
|
|
include: [{model: Establishment,as: "establishment",where: establishmentWhere }]
|
|
});
|
|
|
|
const rejected = await Submission.count({
|
|
where: { ...where, status: "Rejected" },
|
|
include: [{model: Establishment,as: "establishment",where: establishmentWhere }]
|
|
});
|
|
|
|
// 🔹 Paginated Response
|
|
return res.status(200).json({
|
|
status: "success",
|
|
message: "Fetched successfully",
|
|
pagination: {
|
|
total: count,
|
|
page: Number(page),
|
|
limit: Number(limit),
|
|
totalPages: Math.ceil(count / limit),
|
|
},
|
|
summary: {
|
|
total,
|
|
submitted,
|
|
resubmitted,
|
|
approved,
|
|
rejected
|
|
},
|
|
data: rows,
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.viewSubmissionDetails = async (req, res) => {
|
|
try {
|
|
|
|
const { id } = req.params;
|
|
const { establishment_id, year, quarter } = req.query; // optional search params
|
|
|
|
let whereCondition = {};
|
|
|
|
// Primary search by ID (default)
|
|
if (id) {
|
|
whereCondition.id = id;
|
|
}
|
|
|
|
// Secondary combination search if establishment_id, year, quarter given
|
|
if (establishment_id && year && quarter) {
|
|
whereCondition = {
|
|
establishment_id,
|
|
year,
|
|
quarter,
|
|
};
|
|
}
|
|
|
|
let data = await Submission.findOne( {
|
|
where: whereCondition,
|
|
include: [
|
|
{
|
|
model: Establishment,
|
|
as: "establishment" ,
|
|
},
|
|
{
|
|
model: SubmissionProduct,
|
|
as: "products",
|
|
include: [
|
|
{
|
|
model: Product,
|
|
as: "product",
|
|
attributes: ["id", "product_name", "hs_code", "hs_description"],
|
|
},
|
|
{
|
|
model: UnitMaster,
|
|
as: "unit",
|
|
attributes: ["uom"],
|
|
},
|
|
{
|
|
model: VariationReasonMaster,
|
|
as: "variation_reason",
|
|
attributes: ["reason"],
|
|
},
|
|
{
|
|
model: ZeroTargetReasonMaster,
|
|
as: "zero_target_reason",
|
|
attributes: ["reason"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
let plainData = data.get({ plain: true });
|
|
plainData.products = plainData.products.map(p => {
|
|
const format = v => Number(v || 0).toFixed(2);
|
|
return {
|
|
...p,
|
|
current_cost: format(p.current_cost)
|
|
};
|
|
});
|
|
const quarter_periods = getQuarterPeriods(Number(plainData.year), plainData.quarter);
|
|
|
|
const quarter_window = await QuarterlyWindowsConfiguration.findOne({
|
|
attributes : ['survey_name','year','quarter','start_date','end_date','grace_periods_days'],
|
|
where: { is_active: true , year : data.year , quarter : data.quarter }
|
|
});
|
|
|
|
res.status(200).json({ status: "success", data: plainData, quarter_periods , quarter_window });
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// GET Submission History
|
|
exports.getSubmissionHistory = async (req, res) => {
|
|
try {
|
|
|
|
const { establishment_id, year, quarter, product_id } = req.query;
|
|
|
|
let whereSubmission = {};
|
|
let whereProduct = {};
|
|
|
|
// dynamic filter user can pass 0, null, empty -> no filter
|
|
if (establishment_id) whereSubmission.establishment_id = establishment_id;
|
|
if (year) whereSubmission.year = year;
|
|
if (quarter) whereSubmission.quarter = quarter;
|
|
if (product_id) whereProduct.product_id = product_id;
|
|
|
|
// 1) Get all filtered submissions (or all)
|
|
const submissions = await Submission.findAll({
|
|
where: whereSubmission,
|
|
include: [
|
|
{
|
|
model: Establishment,
|
|
as: "establishment",
|
|
attributes: ["factory_name", "establishment_code"],
|
|
}
|
|
],
|
|
});
|
|
|
|
if (submissions.length === 0) {
|
|
return res.status(200).json({ status:"success", data: [] });
|
|
}
|
|
|
|
const submissionIds = submissions.map(x => x.id);
|
|
|
|
// 2) Get submission products
|
|
const products = await SubmissionProduct.findAll({
|
|
where: {
|
|
submission_id: submissionIds,
|
|
...whereProduct
|
|
},
|
|
include: [
|
|
{ model: Product, as: "product", attributes: ["product_name","hs_code"] },
|
|
{ model: UnitMaster, as: "unit", attributes: ["uom"], },
|
|
{ model: VariationReasonMaster, as: "variation_reason", attributes: ["reason"],},
|
|
{ model: ZeroTargetReasonMaster, as: "zero_target_reason", attributes: ["reason"], },
|
|
]
|
|
});
|
|
|
|
if (products.length === 0) {
|
|
return res.status(200).json({ status:"success", data: [] });
|
|
}
|
|
|
|
const productPKs = products.map(x => x.id);
|
|
|
|
// 3) history table
|
|
const history = await SubmissionHistory.findAll({
|
|
where: { primary_key: productPKs, table_name: "submission_products" },
|
|
order:[["created_at","DESC"]]
|
|
});
|
|
|
|
// 4) merge here
|
|
let finalData = products.map(prod => {
|
|
const sub = submissions.find(s => s.id === prod.submission_id);
|
|
const productHistory = history.filter(h => h.primary_key === prod.id);
|
|
|
|
return {
|
|
submission_date: sub.created_at,
|
|
establishment_id: sub.establishment_id,
|
|
factory_name: sub.establishment.factory_name,
|
|
establishment_code: sub.establishment.establishment_code,
|
|
year: sub.year,
|
|
quarter: sub.quarter,
|
|
hs_code: prod.product.hs_code,
|
|
product_name: prod.product.product_name,
|
|
unit: prod.unit.umo,
|
|
variation_reason: prod.variation_reason.reason,
|
|
zero_target_reason: prod.zero_target_reason.reason,
|
|
status: sub.status,
|
|
actor: "Establishment",
|
|
details: productHistory.map(h => ({
|
|
column: h.column_name,
|
|
old: h.old_value,
|
|
new: h.new_value,
|
|
date: h.created_at
|
|
}))
|
|
};
|
|
});
|
|
|
|
|
|
// remove empty details rows
|
|
finalData = finalData.filter(item => item.details.length > 0);
|
|
|
|
return res.status(200).json({ status:"success", data: finalData });
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
}
|
|
|
|
exports.submissionEditRequest = async (req, res) => {
|
|
try {
|
|
|
|
const { id } = req.params;
|
|
edit_request = req.body.edit_request;
|
|
|
|
// Check submission exists
|
|
const submission = await Submission.findByPk(id);
|
|
if (!submission)
|
|
return res.status(404).json({ status: "failed", message: "Submission not found" });
|
|
|
|
// Update submission main data
|
|
await submission.update({ edit_request, updated_by: req.user.id, updated_at: new Date() });
|
|
|
|
res.status(200).json({
|
|
status: "success",
|
|
message: "Submission edit request updated",
|
|
data: "",
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.enableOrDisableSubmissionEditAccess = async (req, res) => {
|
|
try {
|
|
|
|
const { id } = req.params;
|
|
edit_access = req.body.edit_access;
|
|
|
|
// Check submission exists
|
|
const submission = await Submission.findByPk(id);
|
|
if (!submission)
|
|
return res.status(404).json({ status: "failed", message: "Submission not found" });
|
|
|
|
// Update submission main data
|
|
const result = await submission.update({ edit_request : 0 , edit_access : edit_access, updated_by: req.user.id, updated_at: new Date()});
|
|
|
|
res.status(200).json({
|
|
status: "success",
|
|
message: "Submission edit access updated",
|
|
data: result,
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.approveOrRejectSubmission = async (req, res) => {
|
|
try {
|
|
|
|
const { id } = req.params;
|
|
const approve_reject_status = sanitizeForLog(req.body.approve_reject_status);
|
|
const reject_reason = sanitizeForLog(req.body.reject_reason || "");
|
|
const approve_or_reject_by = sanitizeForLog(req.body.approve_or_reject_by || req.user.id);
|
|
|
|
if (![1, "1", 0, "0"].includes(approve_reject_status)) {
|
|
return res.status(400).json({
|
|
status: "failed",
|
|
message: "approve_reject_status must be 1 or 0"
|
|
});
|
|
}
|
|
|
|
const statusString = approve_reject_status == 1 ? "Approved" : "Rejected";
|
|
const submission = await Submission.findByPk(id);
|
|
if (!submission)
|
|
return res.status(404).json({ status: "failed", message: "Submission not found" });
|
|
|
|
const result = await submission.update({
|
|
approve_reject_status,
|
|
status: statusString,
|
|
reject_reason,
|
|
approve_or_reject_by,
|
|
updated_by: req.user.id,
|
|
updated_at: new Date()
|
|
});
|
|
|
|
const QuarterlyWindowsConfiguration = db.QuarterlyWindowsConfiguration;
|
|
//send email to establishment user
|
|
const EstablishmentData = await Establishment.findOne({ where: { id :result['establishment_id'] } });
|
|
const EstablishmentUserData = await EstablishmentUser.findAll({ where: { establishment_id :result['establishment_id'] , is_active : 1} });
|
|
|
|
if(approve_reject_status == 0 || approve_reject_status == 1)
|
|
{
|
|
for await (const userObj of EstablishmentUserData)
|
|
{
|
|
const formatSubmissionDate = (date) => {
|
|
const formatted = new Intl.DateTimeFormat("en-GB", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
timeZone: "Asia/Dubai"
|
|
}).format(new Date(date));
|
|
const [datePart, timePart] = formatted.split(", ");
|
|
const upperTime = timePart.toUpperCase();
|
|
return `${datePart}, ${upperTime} GST (UAE)`;
|
|
};
|
|
|
|
const surveyData = await QuarterlyWindowsConfiguration.findOne({ where: { year :result['year'] , quarter : result['quarter'] } });
|
|
function formatName(name) {
|
|
return name
|
|
.replace(/([A-Z])/g, ' $1')
|
|
.replace(/\b\w/g, char => char.toUpperCase())
|
|
.trim();
|
|
}
|
|
const userName = sanitizeForLog(userObj.name),
|
|
placeHolderData = {
|
|
user_name: userName,
|
|
portal_url : process.env.FE_BASE_URL,
|
|
quarter : result['quarter'],
|
|
year : result['year'],
|
|
establishment_name : EstablishmentData['factory_name'],
|
|
submission_date : formatSubmissionDate(result['created_at']),
|
|
rejection_reason : result['reject_reason'],
|
|
support_email : process.env.SUPPORT_EMAIL,
|
|
support_phone : process.env.SUPPORT_PHONE,
|
|
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`,
|
|
survey_name: surveyData.survey_name
|
|
};
|
|
|
|
const template = approve_reject_status === 1
|
|
? "submission_approved_mail_to_establishment_user"
|
|
: "submission_rejected_mail_to_establishment_user";
|
|
|
|
await sendEmailService(sanitizeForLog(userObj.email), template, placeHolderData);
|
|
logger.info(`Submission approved or rejected status email triggered for: ${sanitizeForLog(userObj.email)}`);
|
|
}
|
|
}
|
|
return res.status(200).json({
|
|
status: "success",
|
|
message: "Submission edit access updated",
|
|
data: result,
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
|
|
};
|
|
|
|
|
|
exports.getQuarterPeriods = async (req, res) => {
|
|
try {
|
|
const { current_year, current_quarter } = req.body;
|
|
|
|
if (!current_year || !current_quarter) {
|
|
return res
|
|
.status(400)
|
|
.json({ status: "failed", message: "current_year and current_quarter are required" });
|
|
}
|
|
|
|
const config = await QuarterlyWindowsConfiguration.findOne({
|
|
where: {
|
|
year: current_year,
|
|
quarter: current_quarter,
|
|
is_active: true
|
|
}
|
|
});
|
|
|
|
if (!config) {
|
|
return res.status(404).json({
|
|
status: "failed",
|
|
message: `No configuration found for Quarter ${current_quarter} ${current_year}`
|
|
});
|
|
}
|
|
|
|
const quarter_periods = getQuarterPeriods(Number(current_year), current_quarter);
|
|
return res.status(200).json({ status: "success", data: quarter_periods });
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: err.message, stack:err.stack });
|
|
}
|
|
};
|
|
|
|
exports.getPreviousForecastData = async (req, res) => {
|
|
try {
|
|
|
|
const { establishment_id, quarter, year, product_id } = req.query;
|
|
|
|
if(!establishment_id || !quarter || !year || !product_id){
|
|
return res.status(400).json({ status:"failed", message:"Missing required query params" });
|
|
}
|
|
|
|
// find submission id
|
|
const submission = await Submission.findOne({
|
|
where : { establishment_id, quarter, year },
|
|
});
|
|
|
|
if(!submission){
|
|
return res.status(404).json({ status:"failed", message:"No previous submission found" });
|
|
}
|
|
|
|
// find submission product row
|
|
const submissionProduct = await SubmissionProduct.findOne({
|
|
where : {
|
|
submission_id : submission.id,
|
|
product_id
|
|
},
|
|
include:[
|
|
{
|
|
model: Product,
|
|
as: "product",
|
|
attributes:["product_name","hs_code"]
|
|
}
|
|
]
|
|
});
|
|
|
|
if(!submissionProduct){
|
|
return res.status(404).json({ status:"failed", message:"No previous product row found" });
|
|
}
|
|
|
|
submissionProduct.dataValues.quarter_periods = getQuarterPeriods(
|
|
Number(year),
|
|
quarter
|
|
);
|
|
|
|
return res.status(200).json({ status:"success", data: submissionProduct });
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
exports.getBeforePreviousData = async (req, res) => {
|
|
try {
|
|
const { establishment_id, current_quarter, current_year, product_id } = req.query;
|
|
|
|
if (!establishment_id || !current_quarter || !current_year || !product_id) {
|
|
return res.status(400).json({ status: "failed", message: "Missing required query params" });
|
|
}
|
|
|
|
// normalize and validate current_quarter (accept "Q1" or "1")
|
|
const quarterMatch = String(current_quarter).trim().match(/(\d)/);
|
|
if (!quarterMatch) {
|
|
return res.status(400).json({ status: "failed", message: "Invalid current_quarter format" });
|
|
}
|
|
let currentQuarterNum = parseInt(quarterMatch[1], 10);
|
|
if (![1, 2, 3, 4].includes(currentQuarterNum)) {
|
|
return res.status(400).json({ status: "failed", message: "current_quarter must be Q1..Q4 or 1..4" });
|
|
}
|
|
|
|
// normalize and validate year
|
|
let yearNum = Number(current_year);
|
|
if (!Number.isInteger(yearNum) || yearNum <= 0) {
|
|
return res.status(400).json({ status: "failed", message: "Invalid current_year" });
|
|
}
|
|
|
|
// compute the quarter two steps before (before-previous)
|
|
// e.g. current Q1 -> before-previous = Q3 (year - 1)
|
|
// current Q4 -> before-previous = Q2 (same year)
|
|
let beforePrevQuarterNum = currentQuarterNum - 1;
|
|
let targetYear = yearNum;
|
|
if (beforePrevQuarterNum <= 0) {
|
|
beforePrevQuarterNum += 4;
|
|
targetYear = yearNum - 1;
|
|
}
|
|
|
|
const quarter = `Q${beforePrevQuarterNum}`;
|
|
const year = String(targetYear);
|
|
|
|
|
|
|
|
|
|
// find submission id
|
|
const submission = await Submission.findOne({
|
|
where: { establishment_id, quarter, year },
|
|
});
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({ status: "failed", message: "No previous submission found" });
|
|
}
|
|
|
|
// find submission product row
|
|
const submissionProduct = await SubmissionProduct.findOne({
|
|
where: {
|
|
submission_id: submission.id,
|
|
product_id
|
|
},
|
|
include: [
|
|
{
|
|
model: Product,
|
|
as: "product",
|
|
attributes: ["product_name", "hs_code"]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!submissionProduct) {
|
|
return res.status(404).json({ status: "failed", message: "No previous product row found" });
|
|
}
|
|
|
|
submissionProduct.dataValues.quarter_periods = getQuarterPeriods(
|
|
Number(year),
|
|
quarter
|
|
);
|
|
|
|
return res.status(200).json({ status: "success", data: submissionProduct });
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
|
|
exports.getProductSubmissionHistory = async (req, res) => {
|
|
try {
|
|
|
|
const { establishment_id, product_id } = req.query;
|
|
|
|
if(!establishment_id || !product_id){
|
|
return res.status(400).json({ status:"failed", message:"Missing required query params" });
|
|
}
|
|
|
|
// find submission id
|
|
const submissions = await Submission.findAll({
|
|
where: { establishment_id },
|
|
attributes: ["id"]
|
|
});
|
|
|
|
if(!submissions){
|
|
return res.status(404).json({ status:"failed", message:"No submissions found for this establishment" });
|
|
}
|
|
|
|
const submissionIds = submissions.map(s => s.id);
|
|
|
|
// 2. Find submission product history with quarter + year + sorting
|
|
let submissionProductsData = await SubmissionProduct.findAll({
|
|
where: {
|
|
submission_id: submissionIds,
|
|
product_id
|
|
},
|
|
include: [
|
|
{
|
|
model: Submission,
|
|
as: "submission",
|
|
attributes: ["quarter", "year", "created_at"]
|
|
},
|
|
{
|
|
model: Product,
|
|
as: "product",
|
|
attributes: ["product_name", "hs_code"]
|
|
}
|
|
],
|
|
order: [
|
|
[{ model: Submission, as: "submission" }, "year", "ASC"],
|
|
[
|
|
Sequelize.literal(`
|
|
FIELD(submission.quarter, 'Q4', 'Q3', 'Q2', 'Q1')
|
|
`),
|
|
"DESC"
|
|
],
|
|
]
|
|
});
|
|
|
|
if(!submissionProductsData){
|
|
return res.status(404).json({ status:"failed", message:"No previous product row found" });
|
|
}
|
|
|
|
for (const d of submissionProductsData) {
|
|
d.dataValues.quarter_periods = getQuarterPeriods(
|
|
Number(d.submission.year),
|
|
d.submission.quarter
|
|
);
|
|
d.dataValues.current_cost = Number(d.current_cost || 0).toFixed(2);
|
|
}
|
|
|
|
|
|
return res.status(200).json({ status:"success", data: submissionProductsData });
|
|
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.error(`Stack trace: ${err.stack}`);
|
|
res.status(500).send({ status: "failed", message: "Internal server error" });
|
|
}
|
|
};
|
|
|