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"); 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}`}); } 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 }, {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, // } // 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 }}); // Shared placeholder data const basePlaceholder = { 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 }; // --- 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", data: result }); } catch (err) { res.status(500).json({ status: "failed", message: err.message }); } }; 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" }); // 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) { await existingProduct.update(updatedProductData); } else { await SubmissionProduct.create(updatedProductData); } } else { 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", data: updatedSubmission, }); } catch (err) { console.error(err); res.status(500).json({ status: "failed", message: err.message }); } }; exports.submissionHistory = async (req, res) => { try { const { establishment_id } = req.params; const 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 SUM(sp2.current_cost) FROM submission_products AS sp2 WHERE sp2.submission_id = submission.id)`), "total_cost" ] ] }, where: { establishment_id }, order: [["id", "DESC"]], }); res.status(200).json({ status: "success", data }); } catch (err) { res.status(500).json({ status: "failed", message: err.message }); } }; // exports.submissionList = async (req, res) => { // try { // const data = await Submission.findAll({ // include: [ // { // model: Establishment, // as: "establishment" , // attributes: ["factory_name","establishment_code"], // 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", // ], // ], // }, // order: [["id", "DESC"]], // }); // res.status(200).json({ status: "success", data }); // } catch (err) { // res.status(500).json({ status: "failed", message: err.message }); // } // }; 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", ], ], }, 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); res.status(500).json({ status: "failed", message: err.message }); } }; 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, }; } const 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"], }, ], }, ], }); if (!data) return res.status(404).json({ status: "failed", message: "Not found" }); const quarter_periods = getQuarterPeriods(Number(data.year), data.quarter); res.status(200).json({ status: "success", data , quarter_periods }); } catch (err) { res.status(500).json({ status: "failed", message: err.message }); } }; // 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) { console.log(err); return res.status(500).json({ status:"failed", message: err.message }); } } 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 }); res.status(200).json({ status: "success", message: "Submission edit request updated", data: "", }); } catch (err) { console.error(err); res.status(500).json({ status: "failed", message: err.message }); } }; 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}); res.status(200).json({ status: "success", message: "Submission edit access updated", data: result, }); } catch (err) { console.error(err); res.status(500).json({ status: "failed", message: err.message }); } }; exports.approveOrRejectSubmission = async (req, res) => { try { const { id } = req.params; approve_reject_status = req.body.approve_reject_status; reject_reason = req.body.reject_reason; approve_or_reject_by = req.body.approve_or_reject_by; if(approve_reject_status == 1) { statusString = 'Approved'; }else{ statusString = 'Rejected'; } // 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({ approve_reject_status : approve_reject_status , status : statusString, reject_reason, approve_or_reject_by }); //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 == 1) { for await (const userObj of EstablishmentUserData) { placeHolderData = { user_name : userObj.name, portal_url : process.env.FE_BASE_URL, quarter : result['quarter'], year : result['year'], establishment_name : EstablishmentData['factory_name'], submission_date : result['created_at'], rejection_reason : result['reject_reason'], support_email : process.env.SUPPORT_EMAIL, support_phone : process.env.SUPPORT_PHONE, } await sendEmailService(userObj.email, 'submission_approved_mail_to_establishment_user', placeHolderData); } }else{ for await (const userObj of EstablishmentUserData) { placeHolderData = { user_name : userObj.name, portal_url : process.env.FE_BASE_URL, quarter : result['quarter'], year : result['year'], establishment_name : EstablishmentData['factory_name'], submission_date : result['created_at'], rejection_reason : result['reject_reason'], support_email : process.env.SUPPORT_EMAIL, support_phone : process.env.SUPPORT_PHONE, } await sendEmailService(userObj.email, 'submission_rejected_mail_to_establishment_user', placeHolderData); } } res.status(200).json({ status: "success", message: "Submission edit access updated", data: result, }); } catch (err) { console.error(err); res.status(500).json({ status: "failed", message: err.message }); } }; 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 quarter_periods = getQuarterPeriods(Number(current_year), current_quarter); return res.status(200).json({ status: "success", data: quarter_periods }); } catch (err) { return res.status(500).json({ status: "failed", message: err.message }); } }; 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" }); } return res.status(200).json({ status:"success", data: submissionProduct }); } catch (err) { return res.status(500).json({ status:"failed", message: err.message }); } };