332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
const db = require("../models");
|
|
const IPICalculationService = require("../services/ipi_calculation_service");
|
|
|
|
const ManufacturingIpi = db.ManufacturingIpi;
|
|
const Isic2DigitIndices = db.Isic2DigitIndices;
|
|
const Isic3DigitIndices = db.Isic3DigitIndices;
|
|
const Isic4DigitIndices = db.Isic4DigitIndices;
|
|
const CalculationLog = db.CalculationLog;
|
|
|
|
const dbConfig = {
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
dialect: process.env.DB_DIALECT || "mysql",
|
|
pool: {
|
|
max: 5,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000,
|
|
},
|
|
};
|
|
|
|
const CALCULATION_STEPS = [
|
|
"monthly_production",
|
|
"item_level_indices",
|
|
"isic_4digit_indices",
|
|
"isic_3digit_indices",
|
|
"isic_2digit_indices",
|
|
"manufacturing_ipi",
|
|
];
|
|
|
|
const formatNumber = (value) => {
|
|
if (value === null || value === undefined || value === "") return null;
|
|
const parsed = Number(value);
|
|
if (Number.isNaN(parsed)) return null;
|
|
return parsed.toFixed(2);
|
|
};
|
|
|
|
const monthToQuarter = (month) => {
|
|
if (month >= 1 && month <= 3) return "Q1";
|
|
if (month >= 4 && month <= 6) return "Q2";
|
|
if (month >= 7 && month <= 9) return "Q3";
|
|
if (month >= 10 && month <= 12) return "Q4";
|
|
return null;
|
|
};
|
|
|
|
exports.renderPage = async (req, res) => {
|
|
const now = new Date();
|
|
const year = Number(req.query.year) || now.getFullYear();
|
|
const month = Number(req.query.month) || now.getMonth() + 1;
|
|
|
|
return res.render("ipi-demo", {
|
|
title: "IPI Calculation Demo Dashboard",
|
|
defaultYear: year,
|
|
defaultMonth: month,
|
|
});
|
|
};
|
|
|
|
exports.getData = async (req, res) => {
|
|
try {
|
|
const year = Number(req.query.year);
|
|
const month = Number(req.query.month);
|
|
|
|
if (!year || !month || month < 1 || month > 12) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "Valid year and month are required",
|
|
});
|
|
}
|
|
|
|
const quarter = monthToQuarter(month);
|
|
const whereClause = { year, quarter };
|
|
|
|
const [manufacturing, isic2, isic3, isic4, itemCount, logs] = await Promise.all([
|
|
ManufacturingIpi.findOne({ where: whereClause, raw: true }),
|
|
Isic2DigitIndices.findAll({ where: whereClause, order: [["isic_2digit_code", "ASC"]], raw: true }),
|
|
Isic3DigitIndices.findAll({ where: whereClause, order: [["isic_3digit_code", "ASC"]], raw: true }),
|
|
Isic4DigitIndices.findAll({ where: whereClause, order: [["isic_4digit_code", "ASC"]], raw: true }),
|
|
db.sequelize.query(
|
|
`SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND quarter = ?`,
|
|
{ replacements: [year, quarter], type: db.Sequelize.QueryTypes.SELECT }
|
|
),
|
|
CalculationLog.findAll({
|
|
where: { reference_year: year, reference_month: quarter },
|
|
order: [["started_at", "DESC"]],
|
|
raw: true,
|
|
})
|
|
]);
|
|
|
|
return res.json({
|
|
success: true,
|
|
year,
|
|
month,
|
|
quarter,
|
|
summary: {
|
|
item_count: Number(itemCount[0]?.total || 0),
|
|
isic4_count: isic4.length,
|
|
isic3_count: isic3.length,
|
|
isic2_count: isic2.length,
|
|
},
|
|
manufacturing: manufacturing
|
|
? {
|
|
...manufacturing,
|
|
total_weight: formatNumber(manufacturing.total_weight),
|
|
weighted_index_sum: formatNumber(manufacturing.weighted_index_sum),
|
|
manufacturing_index: formatNumber(manufacturing.manufacturing_index),
|
|
mom_change: formatNumber(manufacturing.mom_change),
|
|
yoy_change: formatNumber(manufacturing.yoy_change),
|
|
}
|
|
: null,
|
|
isic2: isic2.map((row) => ({
|
|
...row,
|
|
total_weight: formatNumber(row.total_weight),
|
|
weighted_index_sum: formatNumber(row.weighted_index_sum),
|
|
isic_2digit_index: formatNumber(row.isic_2digit_index),
|
|
})),
|
|
isic3: isic3.map((row) => ({
|
|
...row,
|
|
total_weight: formatNumber(row.total_weight),
|
|
weighted_index_sum: formatNumber(row.weighted_index_sum),
|
|
isic_3digit_index: formatNumber(row.isic_3digit_index),
|
|
})),
|
|
isic4: isic4.map((row) => ({
|
|
...row,
|
|
total_weight: formatNumber(row.total_weight),
|
|
weighted_index_sum: formatNumber(row.weighted_index_sum),
|
|
isic_4digit_index: formatNumber(row.isic_4digit_index),
|
|
})),
|
|
logs,
|
|
});
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "Failed to load IPI demo data",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
exports.getTableData = async (req, res) => {
|
|
try {
|
|
const table = String(req.query.table || "");
|
|
const year = Number(req.query.year);
|
|
const month = Number(req.query.month);
|
|
const quarter = monthToQuarter(month);
|
|
const baseYear = Number(req.query.baseYear) || 2022;
|
|
const page = Math.max(1, Number(req.query.page) || 1);
|
|
const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50));
|
|
const search = String(req.query.search || "").trim();
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const queryConfig = {
|
|
monthly: {
|
|
whereSql: `year = ? AND quarter = ?`,
|
|
whereParams: [year, quarter],
|
|
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ? OR quarter_name LIKE ?)` : "",
|
|
searchParams: search ? [`%${search}%`, `%${search}%`, `%${search}%`] : [],
|
|
countSql: `SELECT COUNT(*) AS total FROM monthly_production WHERE year = ? AND quarter = ?`,
|
|
dataSql: `SELECT product_id, product_hs_code, year, quarter, quarter_name, production_quantity, unit
|
|
FROM monthly_production
|
|
WHERE year = ? AND quarter = ?`,
|
|
orderSql: ` ORDER BY product_id`,
|
|
},
|
|
baseyear: {
|
|
whereSql: `base_year = ?`,
|
|
whereParams: [baseYear],
|
|
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "",
|
|
searchParams: search ? [`%${search}%`, `%${search}%`] : [],
|
|
countSql: `SELECT COUNT(*) AS total FROM base_year_production WHERE base_year = ?`,
|
|
dataSql: `SELECT product_id, product_hs_code, base_year, avg_by_production, q1, q2, q3, q4
|
|
FROM base_year_production
|
|
WHERE base_year = ?`,
|
|
orderSql: ` ORDER BY product_id`,
|
|
},
|
|
items: {
|
|
whereSql: `year = ? AND quarter = ?`,
|
|
whereParams: [year, quarter],
|
|
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "",
|
|
searchParams: search ? [`%${search}%`, `%${search}%`] : [],
|
|
countSql: `SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND quarter = ?`,
|
|
dataSql: `SELECT product_id, product_hs_code, current_production, base_year_avg_production, production_relative, item_index
|
|
FROM item_level_indices
|
|
WHERE year = ? AND quarter = ?`,
|
|
orderSql: ` ORDER BY product_id`,
|
|
},
|
|
};
|
|
|
|
if (!queryConfig[table]) {
|
|
return res.status(400).json({ success: false, message: "Invalid table parameter" });
|
|
}
|
|
|
|
if ((table === "monthly" || table === "items") && (!year || !month || month < 1 || month > 12 || !quarter)) {
|
|
return res.status(400).json({ success: false, message: "Valid year and month are required" });
|
|
}
|
|
|
|
const cfg = queryConfig[table];
|
|
const whereParams = [...cfg.whereParams, ...cfg.searchParams];
|
|
|
|
const [countResult] = await db.sequelize.query(
|
|
cfg.countSql + (search ? cfg.searchSql.replace(" AND ", " AND ") : ""),
|
|
{ replacements: whereParams, type: db.Sequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
const rows = await db.sequelize.query(
|
|
cfg.dataSql + cfg.searchSql + cfg.orderSql + " LIMIT ? OFFSET ?",
|
|
{ replacements: [...whereParams, pageSize, offset], type: db.Sequelize.QueryTypes.SELECT }
|
|
);
|
|
|
|
const formattedRows = rows.map((row) => {
|
|
const copy = { ...row };
|
|
Object.keys(copy).forEach((k) => {
|
|
if (
|
|
["production_quantity", "avg_by_production", "current_production", "base_year_avg_production", "production_relative", "item_index", "q1", "q2", "q3", "q4"].includes(k)
|
|
) {
|
|
copy[k] = formatNumber(copy[k]);
|
|
}
|
|
});
|
|
return copy;
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
table,
|
|
page,
|
|
pageSize,
|
|
total: Number(countResult.total || 0),
|
|
rows: formattedRows,
|
|
});
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "Failed to load table data",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
exports.runMonthCalculation = async (req, res) => {
|
|
try {
|
|
const year = Number(req.body.year);
|
|
const month = Number(req.body.month);
|
|
const quarter = monthToQuarter(month);
|
|
|
|
if (!year || !month || month < 1 || month > 12 || !quarter) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "Valid year and month are required",
|
|
});
|
|
}
|
|
|
|
const service = new IPICalculationService(dbConfig);
|
|
const result = await service.runCompleteCalculation(year, quarter);
|
|
await service.close();
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: `Calculation completed for ${year}-${quarter}`,
|
|
data: result,
|
|
});
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "Failed to run month calculation",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
exports.getRunStatus = async (req, res) => {
|
|
try {
|
|
const year = Number(req.query.year);
|
|
const month = Number(req.query.month);
|
|
const quarter = monthToQuarter(month);
|
|
|
|
if (!year || !month || month < 1 || month > 12 || !quarter) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "Valid year and month are required",
|
|
});
|
|
}
|
|
|
|
const logs = await CalculationLog.findAll({
|
|
where: {
|
|
reference_year: year,
|
|
reference_month: quarter,
|
|
},
|
|
order: [["started_at", "ASC"]],
|
|
raw: true,
|
|
});
|
|
|
|
const map = new Map(logs.map((l) => [l.calculation_type, l]));
|
|
const steps = CALCULATION_STEPS.map((step) => {
|
|
const row = map.get(step);
|
|
return {
|
|
step,
|
|
status: row?.status || "Pending",
|
|
records_processed: row?.records_processed ?? 0,
|
|
started_at: row?.started_at || null,
|
|
completed_at: row?.completed_at || null,
|
|
error_message: row?.error_message || null,
|
|
};
|
|
});
|
|
|
|
const completedCount = steps.filter((s) => s.status === "Completed").length;
|
|
const failedStep = steps.find((s) => s.status === "Failed");
|
|
const startedAny = steps.some((s) => s.status !== "Pending");
|
|
const isFinished = completedCount === CALCULATION_STEPS.length || Boolean(failedStep);
|
|
|
|
return res.json({
|
|
success: true,
|
|
year,
|
|
month,
|
|
quarter,
|
|
started: startedAny,
|
|
isFinished,
|
|
completedCount,
|
|
totalSteps: CALCULATION_STEPS.length,
|
|
progressPercent: Math.round((completedCount / CALCULATION_STEPS.length) * 100),
|
|
failedStep: failedStep?.step || null,
|
|
steps,
|
|
});
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "Failed to fetch run status",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|