fcsc_ipi_backend/app/views/ipi-demo.ejs
2026-04-29 10:39:44 +05:30

402 lines
18 KiB
Plaintext

<!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; }
.run-monitor { margin-top: 12px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; background: #f8fafc; }
.progress { width: 100%; height: 10px; background: #e5e7eb; border-radius: 999px; overflow: hidden; margin: 10px 0; }
.progress > div { height: 100%; background: #2563eb; width: 0%; transition: width 0.3s ease; }
.step-grid { display: grid; grid-template-columns: repeat(3, minmax(220px, 1fr)); gap: 8px; }
.step-card { border: 1px solid #d1d5db; border-radius: 6px; padding: 8px; background: #fff; }
.badge { display: inline-block; font-size: 11px; padding: 2px 6px; border-radius: 999px; }
.badge-pending { background: #f3f4f6; color: #374151; }
.badge-started { background: #dbeafe; color: #1d4ed8; }
.badge-completed { background: #dcfce7; color: #166534; }
.badge-failed { background: #fee2e2; color: #991b1b; }
</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 by year + month (month is mapped to quarter).</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>
<button id="runBtn">Run Month Calculation</button>
<span id="status" class="muted"></span>
</div>
<div class="run-monitor">
<div class="row" style="justify-content: space-between;">
<b>Live Run Monitor</b>
<span id="runState" class="muted">Idle</span>
</div>
<div class="progress"><div id="runProgress"></div></div>
<div id="stepGrid" class="step-grid"></div>
</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";
let pollTimer = null;
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 statusBadge(status) {
const s = String(status || "Pending");
const lower = s.toLowerCase();
if (lower === "completed") return `<span class="badge badge-completed">Completed</span>`;
if (lower === "started") return `<span class="badge badge-started">Started</span>`;
if (lower === "failed") return `<span class="badge badge-failed">Failed</span>`;
return `<span class="badge badge-pending">Pending</span>`;
}
function renderRunStatus(data) {
document.getElementById("runProgress").style.width = `${data.progressPercent || 0}%`;
const state = data.isFinished
? (data.failedStep ? `Failed at ${data.failedStep}` : "Completed")
: (data.started ? "Running..." : "Idle");
document.getElementById("runState").textContent = `${state} (${data.completedCount || 0}/${data.totalSteps || 6})`;
const html = (data.steps || []).map((s) => `
<div class="step-card">
<div><b>${s.step}</b> ${statusBadge(s.status)}</div>
<div class="muted">Records: ${s.records_processed ?? 0}</div>
<div class="muted">Started: ${s.started_at || "-"}</div>
<div class="muted">Completed: ${s.completed_at || "-"}</div>
${s.error_message ? `<div class="err">Error: ${s.error_message}</div>` : ""}
</div>
`).join("");
document.getElementById("stepGrid").innerHTML = html || '<div class="muted">No run status yet.</div>';
}
async function fetchRunStatus() {
const year = document.getElementById("year").value;
const month = document.getElementById("month").value;
try {
const resp = await fetch(`/ipi-demo/run-status?year=${encodeURIComponent(year)}&month=${encodeURIComponent(month)}`);
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.message || "Status fetch failed");
renderRunStatus(data);
if (data.isFinished && pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
loadData();
}
} catch (err) {
document.getElementById("runState").textContent = err.message;
}
}
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: "quarter", label: "Quarter" },
{ key: "quarter_name", label: "Quarter 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: "q1", label: "Q1" },
{ key: "q2", label: "Q2" },
{ key: "q3", label: "Q3" },
{ key: "q4", label: "Q4" },
];
}
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.getElementById("runBtn").addEventListener("click", async () => {
const year = Number(document.getElementById("year").value);
const month = Number(document.getElementById("month").value);
statusEl.textContent = "Starting run...";
statusEl.className = "muted";
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(fetchRunStatus, 2500);
try {
const resp = await fetch("/ipi-demo/run-month", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ year, month }),
});
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.message || "Run failed");
statusEl.textContent = "Run completed";
statusEl.className = "ok";
await fetchRunStatus();
await loadData();
} catch (err) {
statusEl.textContent = err.message;
statusEl.className = "err";
}
});
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();
fetchRunStatus();
pollTimer = setInterval(fetchRunStatus, 2500);
</script>
</body>
</html>