IIP calculation corrected : GWM

This commit is contained in:
Gowtham M 2026-04-16 14:08:23 +05:30
parent 028f728ebd
commit 7608e93334
3 changed files with 537 additions and 0 deletions

View File

@ -0,0 +1,219 @@
const db = require("../models");
const ManufacturingIpi = db.ManufacturingIpi;
const Isic2DigitIndices = db.Isic2DigitIndices;
const Isic3DigitIndices = db.Isic3DigitIndices;
const Isic4DigitIndices = db.Isic4DigitIndices;
const CalculationLog = db.CalculationLog;
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);
};
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 whereClause = { year, month };
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 month = ?`,
{ replacements: [year, month], type: db.Sequelize.QueryTypes.SELECT }
),
CalculationLog.findAll({
where: { reference_year: year, reference_month: month },
order: [["started_at", "DESC"]],
raw: true,
}),
db.sequelize.query(
`SELECT product_id, product_hs_code, base_year, avg_by_production, jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, \`dec\`
FROM base_year_production
WHERE base_year = 2022
ORDER BY product_id
LIMIT 25`,
{ type: db.Sequelize.QueryTypes.SELECT }
),
db.sequelize.query(
`SELECT product_id, product_hs_code, year, month, month_name, production_quantity, unit
FROM monthly_production
WHERE year = ? AND month = ?
ORDER BY product_id
LIMIT 50`,
{ replacements: [year, month], type: db.Sequelize.QueryTypes.SELECT }
),
]);
return res.json({
success: true,
year,
month,
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 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 month = ?`,
whereParams: [year, month],
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ? OR month_name LIKE ?)` : "",
searchParams: search ? [`%${search}%`, `%${search}%`, `%${search}%`] : [],
countSql: `SELECT COUNT(*) AS total FROM monthly_production WHERE year = ? AND month = ?`,
dataSql: `SELECT product_id, product_hs_code, year, month, month_name, production_quantity, unit
FROM monthly_production
WHERE year = ? AND month = ?`,
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, jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, \`dec\`
FROM base_year_production
WHERE base_year = ?`,
orderSql: ` ORDER BY product_id`,
},
items: {
whereSql: `year = ? AND month = ?`,
whereParams: [year, month],
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 month = ?`,
dataSql: `SELECT product_id, product_hs_code, current_production, base_year_avg_production, production_relative, item_index
FROM item_level_indices
WHERE year = ? AND month = ?`,
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)) {
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", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"].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,
});
}
};

308
app/views/ipi-demo.ejs Normal file
View File

@ -0,0 +1,308 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<style>
body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fb; color: #1f2937; }
.wrap { max-width: 1300px; margin: 20px auto; padding: 0 16px; }
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px; margin-bottom: 14px; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
input, button { padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; }
button { background: #1d4ed8; border-color: #1d4ed8; color: #fff; cursor: pointer; }
button:hover { background: #1e40af; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { border: 1px solid #e5e7eb; padding: 8px; text-align: left; }
th { background: #f9fafb; }
.muted { color: #6b7280; font-size: 13px; }
.grid4 { display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr)); gap: 10px; }
.kpi { background: #eef2ff; border: 1px solid #c7d2fe; border-radius: 8px; padding: 10px; }
.kpi b { display: block; font-size: 18px; margin-top: 2px; }
.err { color: #b91c1c; }
.ok { color: #065f46; }
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.tabs button { background: #fff; color: #111827; border: 1px solid #d1d5db; }
.tabs button.active { background: #1d4ed8; color: #fff; border-color: #1d4ed8; }
.panel { display: none; }
.panel.active { display: block; }
.pager { display: flex; gap: 8px; align-items: center; margin: 10px 0; flex-wrap: wrap; }
.pager input { width: 220px; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h2 style="margin:0 0 8px 0;">IPI Calculation Demo Dashboard</h2>
<div class="muted">Use Swagger to trigger calculation first, then view data here by year/month.</div>
<div class="row" style="margin-top:12px;">
<label>Year <input id="year" type="number" value="<%= defaultYear %>" min="2000" max="2100" /></label>
<label>Month <input id="month" type="number" value="<%= defaultMonth %>" min="1" max="12" /></label>
<button id="loadBtn">Load Data</button>
<span id="status" class="muted"></span>
</div>
</div>
<div class="card">
<div class="grid4">
<div class="kpi">Item-level rows<b id="kpiItem">-</b></div>
<div class="kpi">ISIC 4-digit rows<b id="kpi4">-</b></div>
<div class="kpi">ISIC 3-digit rows<b id="kpi3">-</b></div>
<div class="kpi">ISIC 2-digit rows<b id="kpi2">-</b></div>
</div>
<div style="margin-top:12px;">
<table>
<thead><tr><th>Manufacturing Index</th><th>MoM %</th><th>YoY %</th><th>Total Weight</th><th>Weighted Index Sum</th></tr></thead>
<tbody id="manufacturingBody"><tr><td colspan="5" class="muted">No data loaded.</td></tr></tbody>
</table>
</div>
</div>
<div class="card">
<div class="tabs">
<button class="tab active" data-tab="isic4">ISIC 4-digit</button>
<button class="tab" data-tab="isic3">ISIC 3-digit</button>
<button class="tab" data-tab="isic2">ISIC 2-digit</button>
<button class="tab" data-tab="monthly">Monthly Production</button>
<button class="tab" data-tab="baseyear">Base Year Production</button>
<button class="tab" data-tab="items">Item Level Indices</button>
<button class="tab" data-tab="logs">Calculation Logs</button>
</div>
<div id="tableControls" class="pager" style="display:none;">
<input id="tableSearch" type="text" placeholder="Search product_id / hs_code..." />
<button id="searchBtn">Search</button>
<button id="prevBtn">Prev</button>
<button id="nextBtn">Next</button>
<span id="pageInfo" class="muted"></span>
</div>
<div id="panel-isic4" class="panel active"></div>
<div id="panel-isic3" class="panel"></div>
<div id="panel-isic2" class="panel"></div>
<div id="panel-monthly" class="panel"></div>
<div id="panel-baseyear" class="panel"></div>
<div id="panel-items" class="panel"></div>
<div id="panel-logs" class="panel"></div>
</div>
</div>
<script>
const statusEl = document.getElementById("status");
const pagedTabs = new Set(["monthly", "baseyear", "items"]);
const tableState = {
monthly: { page: 1, pageSize: 50, total: 0, search: "" },
baseyear: { page: 1, pageSize: 50, total: 0, search: "" },
items: { page: 1, pageSize: 50, total: 0, search: "" },
};
let activeTab = "isic4";
function tableHtml(columns, rows) {
if (!rows || rows.length === 0) return '<div class="muted">No rows found.</div>';
const head = `<thead><tr>${columns.map(c => `<th>${c.label}</th>`).join("")}</tr></thead>`;
const bodyRows = rows.map(r => `<tr>${columns.map(c => `<td>${r[c.key] ?? ""}</td>`).join("")}</tr>`).join("");
return `<table>${head}<tbody>${bodyRows}</tbody></table>`;
}
function render(data) {
document.getElementById("kpiItem").textContent = data.summary.item_count;
document.getElementById("kpi4").textContent = data.summary.isic4_count;
document.getElementById("kpi3").textContent = data.summary.isic3_count;
document.getElementById("kpi2").textContent = data.summary.isic2_count;
const m = data.manufacturing;
const mBody = document.getElementById("manufacturingBody");
if (!m) {
mBody.innerHTML = `<tr><td colspan="5" class="muted">No manufacturing index for selected period.</td></tr>`;
} else {
mBody.innerHTML = `<tr>
<td>${m.manufacturing_index ?? ""}</td>
<td>${m.mom_change ?? ""}</td>
<td>${m.yoy_change ?? ""}</td>
<td>${m.total_weight ?? ""}</td>
<td>${m.weighted_index_sum ?? ""}</td>
</tr>`;
}
document.getElementById("panel-isic4").innerHTML = tableHtml(
[
{ key: "isic_4digit_code", label: "ISIC 4 Code" },
{ key: "isic_description", label: "Description" },
{ key: "total_weight", label: "Total Weight" },
{ key: "weighted_index_sum", label: "Weighted Index Sum" },
{ key: "isic_4digit_index", label: "ISIC 4 Index" },
],
data.isic4
);
document.getElementById("panel-isic3").innerHTML = tableHtml(
[
{ key: "isic_3digit_code", label: "ISIC 3 Code" },
{ key: "isic_description", label: "Description" },
{ key: "total_weight", label: "Total Weight" },
{ key: "weighted_index_sum", label: "Weighted Index Sum" },
{ key: "isic_3digit_index", label: "ISIC 3 Index" },
],
data.isic3
);
document.getElementById("panel-isic2").innerHTML = tableHtml(
[
{ key: "isic_2digit_code", label: "ISIC 2 Code" },
{ key: "isic_description", label: "Description" },
{ key: "total_weight", label: "Total Weight" },
{ key: "weighted_index_sum", label: "Weighted Index Sum" },
{ key: "isic_2digit_index", label: "ISIC 2 Index" },
],
data.isic2
);
document.getElementById("panel-monthly").innerHTML = '<div class="muted">Loading monthly production...</div>';
document.getElementById("panel-baseyear").innerHTML = '<div class="muted">Loading base year production...</div>';
document.getElementById("panel-items").innerHTML = '<div class="muted">Loading item level indices...</div>';
document.getElementById("panel-logs").innerHTML = tableHtml(
[
{ key: "calculation_type", label: "Type" },
{ key: "status", label: "Status" },
{ key: "records_processed", label: "Records" },
{ key: "started_at", label: "Started At" },
{ key: "completed_at", label: "Completed At" },
{ key: "error_message", label: "Error" },
],
data.logs
);
loadPagedTable("monthly");
loadPagedTable("baseyear");
loadPagedTable("items");
refreshTableControls();
}
function getColumnsForTable(tab) {
if (tab === "monthly") {
return [
{ key: "product_id", label: "Product ID" },
{ key: "product_hs_code", label: "HS Code" },
{ key: "year", label: "Year" },
{ key: "month", label: "Month" },
{ key: "month_name", label: "Month Name" },
{ key: "production_quantity", label: "Production Qty" },
{ key: "unit", label: "Unit" },
];
}
if (tab === "baseyear") {
return [
{ key: "product_id", label: "Product ID" },
{ key: "product_hs_code", label: "HS Code" },
{ key: "base_year", label: "Base Year" },
{ key: "avg_by_production", label: "Avg Base Year Prod." },
{ key: "jan", label: "Jan" }, { key: "feb", label: "Feb" }, { key: "mar", label: "Mar" },
{ key: "apr", label: "Apr" }, { key: "may", label: "May" }, { key: "jun", label: "Jun" },
{ key: "jul", label: "Jul" }, { key: "aug", label: "Aug" }, { key: "sep", label: "Sep" },
{ key: "oct", label: "Oct" }, { key: "nov", label: "Nov" }, { key: "dec", label: "Dec" },
];
}
return [
{ key: "product_id", label: "Product ID" },
{ key: "product_hs_code", label: "HS Code" },
{ key: "current_production", label: "Current Production" },
{ key: "base_year_avg_production", label: "Base Avg" },
{ key: "production_relative", label: "Relative" },
{ key: "item_index", label: "Item Index" },
];
}
async function loadPagedTable(tab) {
const year = document.getElementById("year").value;
const month = document.getElementById("month").value;
const panel = document.getElementById(`panel-${tab}`);
const state = tableState[tab];
panel.innerHTML = '<div class="muted">Loading...</div>';
try {
const url = `/ipi-demo/table-data?table=${tab}&year=${encodeURIComponent(year)}&month=${encodeURIComponent(month)}&page=${state.page}&pageSize=${state.pageSize}&search=${encodeURIComponent(state.search)}`;
const resp = await fetch(url);
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.message || "Load failed");
state.total = data.total;
panel.innerHTML = tableHtml(getColumnsForTable(tab), data.rows);
if (activeTab === tab) refreshTableControls();
} catch (err) {
panel.innerHTML = `<div class="err">${err.message}</div>`;
}
}
function refreshTableControls() {
const controls = document.getElementById("tableControls");
if (!pagedTabs.has(activeTab)) {
controls.style.display = "none";
return;
}
controls.style.display = "flex";
const st = tableState[activeTab];
document.getElementById("tableSearch").value = st.search;
const totalPages = Math.max(1, Math.ceil(st.total / st.pageSize));
document.getElementById("pageInfo").textContent = `Page ${st.page} / ${totalPages} (Total: ${st.total}, Page size: ${st.pageSize})`;
}
async function loadData() {
const year = document.getElementById("year").value;
const month = document.getElementById("month").value;
statusEl.textContent = "Loading...";
statusEl.className = "muted";
try {
const resp = await fetch(`/ipi-demo/data?year=${encodeURIComponent(year)}&month=${encodeURIComponent(month)}`);
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.message || "Failed to load");
render(data);
statusEl.textContent = "Loaded successfully";
statusEl.className = "ok";
} catch (err) {
statusEl.textContent = err.message;
statusEl.className = "err";
}
}
document.getElementById("loadBtn").addEventListener("click", loadData);
document.querySelectorAll(".tab").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach((b) => b.classList.remove("active"));
document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
document.getElementById(`panel-${btn.dataset.tab}`).classList.add("active");
activeTab = btn.dataset.tab;
refreshTableControls();
});
});
document.getElementById("searchBtn").addEventListener("click", () => {
if (!pagedTabs.has(activeTab)) return;
tableState[activeTab].search = document.getElementById("tableSearch").value.trim();
tableState[activeTab].page = 1;
loadPagedTable(activeTab);
});
document.getElementById("prevBtn").addEventListener("click", () => {
if (!pagedTabs.has(activeTab)) return;
const st = tableState[activeTab];
if (st.page > 1) {
st.page -= 1;
loadPagedTable(activeTab);
}
});
document.getElementById("nextBtn").addEventListener("click", () => {
if (!pagedTabs.has(activeTab)) return;
const st = tableState[activeTab];
const totalPages = Math.max(1, Math.ceil(st.total / st.pageSize));
if (st.page < totalPages) {
st.page += 1;
loadPagedTable(activeTab);
}
});
loadData();
</script>
</body>
</html>

View File

@ -13,6 +13,7 @@ const verifySignature = require("./app/middleware/app.middleware");
const authController = require("./app/controllers/auth.controller");
const establishmentController = require("./app/controllers/establishment.controller");
const AutomatedSchedulerService = require('./app/services/scheduler.service');
const ipiDemoController = require("./app/controllers/ipiDemo.controller");
require("dotenv").config();
@ -299,6 +300,15 @@ app.get("/api/test", (req, res) => {
const deploymentController = require("./app/controllers/deployment.controller");
app.post("/deploy", deploymentController.deployment);
/**
* =========================
* IPI DEMO UI
* =========================
*/
app.get("/ipi-demo", ipiDemoController.renderPage);
app.get("/ipi-demo/data", ipiDemoController.getData);
app.get("/ipi-demo/table-data", ipiDemoController.getTableData);
/**
* =========================