Refactor templates to externalize JavaScript: Moved inline scripts to separate JS files and added JSON data attributes for better maintainability and performance. Updated consumption, costs, equipment, maintenance, production, reporting, and simulations templates accordingly.
This commit is contained in:
289
static/js/dashboard.js
Normal file
289
static/js/dashboard.js
Normal file
@@ -0,0 +1,289 @@
|
||||
(() => {
|
||||
const dataElement = document.getElementById("dashboard-data");
|
||||
if (!dataElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = {};
|
||||
try {
|
||||
state = JSON.parse(dataElement.textContent || "{}");
|
||||
} catch (error) {
|
||||
console.error("Failed to parse dashboard data", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const statusElement = document.getElementById("dashboard-status");
|
||||
const summaryContainer = document.getElementById("summary-metrics");
|
||||
const summaryEmpty = document.getElementById("summary-empty");
|
||||
const scenarioTableBody = document.querySelector("#scenario-table tbody");
|
||||
const scenarioEmpty = document.getElementById("scenario-table-empty");
|
||||
const overallMetricsList = document.getElementById("overall-metrics");
|
||||
const overallMetricsEmpty = document.getElementById("overall-metrics-empty");
|
||||
const recentList = document.getElementById("recent-simulations");
|
||||
const recentEmpty = document.getElementById("recent-simulations-empty");
|
||||
const maintenanceList = document.getElementById("upcoming-maintenance");
|
||||
const maintenanceEmpty = document.getElementById(
|
||||
"upcoming-maintenance-empty"
|
||||
);
|
||||
const refreshButton = document.getElementById("refresh-dashboard");
|
||||
const costChartCanvas = document.getElementById("cost-chart");
|
||||
const costChartEmpty = document.getElementById("cost-chart-empty");
|
||||
const activityChartCanvas = document.getElementById("activity-chart");
|
||||
const activityChartEmpty = document.getElementById("activity-chart-empty");
|
||||
|
||||
let costChartInstance = null;
|
||||
let activityChartInstance = null;
|
||||
|
||||
const setStatus = (message, variant = "success") => {
|
||||
if (!statusElement) {
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
statusElement.hidden = true;
|
||||
statusElement.textContent = "";
|
||||
statusElement.classList.remove("success", "error");
|
||||
return;
|
||||
}
|
||||
statusElement.textContent = message;
|
||||
statusElement.hidden = false;
|
||||
statusElement.classList.toggle("success", variant === "success");
|
||||
statusElement.classList.toggle("error", variant !== "success");
|
||||
};
|
||||
|
||||
const renderSummaryMetrics = () => {
|
||||
if (!summaryContainer || !summaryEmpty) {
|
||||
return;
|
||||
}
|
||||
summaryContainer.innerHTML = "";
|
||||
const metrics = Array.isArray(state.summary_metrics)
|
||||
? state.summary_metrics
|
||||
: [];
|
||||
metrics.forEach((metric) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "metric-card";
|
||||
card.innerHTML = `
|
||||
<span class="metric-label">${metric.label}</span>
|
||||
<span class="metric-value">${metric.value}</span>
|
||||
`;
|
||||
summaryContainer.appendChild(card);
|
||||
});
|
||||
summaryEmpty.hidden = metrics.length > 0;
|
||||
};
|
||||
|
||||
const renderScenarioTable = () => {
|
||||
if (!scenarioTableBody || !scenarioEmpty) {
|
||||
return;
|
||||
}
|
||||
scenarioTableBody.innerHTML = "";
|
||||
const rows = Array.isArray(state.scenario_rows) ? state.scenario_rows : [];
|
||||
rows.forEach((row) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${row.scenario_name}</td>
|
||||
<td>${row.parameter_display}</td>
|
||||
<td>${row.equipment_display}</td>
|
||||
<td>${row.capex_display}</td>
|
||||
<td>${row.opex_display}</td>
|
||||
<td>${row.production_display}</td>
|
||||
<td>${row.consumption_display}</td>
|
||||
<td>${row.maintenance_display}</td>
|
||||
<td>${row.iterations_display}</td>
|
||||
<td>${row.simulation_mean_display}</td>
|
||||
`;
|
||||
scenarioTableBody.appendChild(tr);
|
||||
});
|
||||
scenarioEmpty.hidden = rows.length > 0;
|
||||
};
|
||||
|
||||
const renderOverallMetrics = () => {
|
||||
if (!overallMetricsList || !overallMetricsEmpty) {
|
||||
return;
|
||||
}
|
||||
overallMetricsList.innerHTML = "";
|
||||
const items = Array.isArray(state.overall_report_metrics)
|
||||
? state.overall_report_metrics
|
||||
: [];
|
||||
items.forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "metric-list-item";
|
||||
li.textContent = `${item.label}: ${item.value}`;
|
||||
overallMetricsList.appendChild(li);
|
||||
});
|
||||
overallMetricsEmpty.hidden = items.length > 0;
|
||||
};
|
||||
|
||||
const renderRecentSimulations = () => {
|
||||
if (!recentList || !recentEmpty) {
|
||||
return;
|
||||
}
|
||||
recentList.innerHTML = "";
|
||||
const runs = Array.isArray(state.recent_simulations)
|
||||
? state.recent_simulations
|
||||
: [];
|
||||
runs.forEach((run) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = "metric-list-item";
|
||||
item.textContent = `${run.scenario_name} · ${run.iterations_display} iterations · ${run.mean_display}`;
|
||||
recentList.appendChild(item);
|
||||
});
|
||||
recentEmpty.hidden = runs.length > 0;
|
||||
};
|
||||
|
||||
const renderMaintenanceReminders = () => {
|
||||
if (!maintenanceList || !maintenanceEmpty) {
|
||||
return;
|
||||
}
|
||||
maintenanceList.innerHTML = "";
|
||||
const items = Array.isArray(state.upcoming_maintenance)
|
||||
? state.upcoming_maintenance
|
||||
: [];
|
||||
items.forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `
|
||||
<span class="list-title">${item.equipment_name} · ${item.scenario_name}</span>
|
||||
<span class="list-detail">${item.date_display} · ${item.cost_display} · ${item.description}</span>
|
||||
`;
|
||||
maintenanceList.appendChild(li);
|
||||
});
|
||||
maintenanceEmpty.hidden = items.length > 0;
|
||||
};
|
||||
|
||||
const buildChartConfig = (dataset, overrides = {}) => ({
|
||||
type: dataset.type || "bar",
|
||||
data: {
|
||||
labels: dataset.labels || [],
|
||||
datasets: dataset.datasets || [],
|
||||
},
|
||||
options: Object.assign(
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: "top" },
|
||||
tooltip: { enabled: true },
|
||||
},
|
||||
scales: {
|
||||
x: { stacked: dataset.stacked ?? false },
|
||||
y: { stacked: dataset.stacked ?? false, beginAtZero: true },
|
||||
},
|
||||
},
|
||||
overrides.options || {}
|
||||
),
|
||||
});
|
||||
|
||||
const renderCharts = () => {
|
||||
if (costChartInstance) {
|
||||
costChartInstance.destroy();
|
||||
}
|
||||
if (activityChartInstance) {
|
||||
activityChartInstance.destroy();
|
||||
}
|
||||
|
||||
const costData = state.scenario_cost_chart || {};
|
||||
const activityData = state.scenario_activity_chart || {};
|
||||
|
||||
if (costChartCanvas && state.cost_chart_has_data) {
|
||||
costChartInstance = new Chart(
|
||||
costChartCanvas,
|
||||
buildChartConfig(costData, {
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (value) =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
if (costChartEmpty) {
|
||||
costChartEmpty.hidden = true;
|
||||
}
|
||||
costChartCanvas.classList.remove("hidden");
|
||||
} else if (costChartEmpty && costChartCanvas) {
|
||||
costChartEmpty.hidden = false;
|
||||
costChartCanvas.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (activityChartCanvas && state.activity_chart_has_data) {
|
||||
activityChartInstance = new Chart(
|
||||
activityChartCanvas,
|
||||
buildChartConfig(activityData, {
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (value) =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
if (activityChartEmpty) {
|
||||
activityChartEmpty.hidden = true;
|
||||
}
|
||||
activityChartCanvas.classList.remove("hidden");
|
||||
} else if (activityChartEmpty && activityChartCanvas) {
|
||||
activityChartEmpty.hidden = false;
|
||||
activityChartCanvas.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const renderView = () => {
|
||||
renderSummaryMetrics();
|
||||
renderScenarioTable();
|
||||
renderOverallMetrics();
|
||||
renderRecentSimulations();
|
||||
renderMaintenanceReminders();
|
||||
renderCharts();
|
||||
};
|
||||
|
||||
const refreshDashboard = async () => {
|
||||
setStatus("Refreshing dashboard…", "success");
|
||||
if (refreshButton) {
|
||||
refreshButton.classList.add("is-loading");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/ui/dashboard/data", {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Unable to refresh dashboard data.");
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
state = payload || {};
|
||||
renderView();
|
||||
setStatus("Dashboard updated.", "success");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || "Failed to refresh dashboard.", "error");
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.classList.remove("is-loading");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderView();
|
||||
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener("click", refreshDashboard);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user