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:
@@ -84,493 +84,11 @@ block content %}
|
||||
endif
|
||||
%}
|
||||
></canvas>
|
||||
<p
|
||||
id="activity-chart-empty"
|
||||
class="empty-state"
|
||||
{%
|
||||
if
|
||||
activity_chart_has_data
|
||||
%}
|
||||
hidden{%
|
||||
endif
|
||||
%}
|
||||
>
|
||||
Add production or consumption records to display this chart.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel dashboard-panel">
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h3>Scenario Snapshot</h3>
|
||||
<p class="chart-subtitle">
|
||||
Operational and financial highlights per scenario
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="table-container">
|
||||
<table id="scenario-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scenario</th>
|
||||
<th>Parameters</th>
|
||||
<th>Equipment</th>
|
||||
<th>CAPEX</th>
|
||||
<th>OPEX</th>
|
||||
<th>Production</th>
|
||||
<th>Consumption</th>
|
||||
<th>Maintenance</th>
|
||||
<th>Iterations</th>
|
||||
<th>Simulation Mean</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in scenario_rows %}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="scenario-table-empty" class="empty-state" {% if scenario_rows|length>
|
||||
0 %} hidden{% endif %}> Create scenarios and populate domain data to see the
|
||||
snapshot overview.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-columns">
|
||||
<section class="panel dashboard-panel">
|
||||
<header class="panel-header">
|
||||
<h3>Overall Simulation Metrics</h3>
|
||||
</header>
|
||||
<ul id="overall-metrics" class="metric-list">
|
||||
{% for metric in overall_report_metrics %}
|
||||
<li>
|
||||
<span class="metric-label">{{ metric.label }}</span>
|
||||
<span class="metric-value">{{ metric.value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p
|
||||
id="overall-metrics-empty"
|
||||
class="empty-state"
|
||||
{%
|
||||
if
|
||||
report_available
|
||||
%}
|
||||
hidden{%
|
||||
endif
|
||||
%}
|
||||
>
|
||||
Run a simulation to surface aggregate reporting metrics.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="panel dashboard-panel">
|
||||
<header class="panel-header">
|
||||
<h3>Recent Simulation Runs</h3>
|
||||
</header>
|
||||
<ul id="recent-simulations" class="striped-list">
|
||||
{% for run in recent_simulations %}
|
||||
<li>
|
||||
<span class="list-title">{{ run.scenario_name }}</span>
|
||||
<span class="list-detail"
|
||||
>Iterations: {{ run.iterations_display }} · Mean: {{ run.mean_display
|
||||
}} · P95: {{ run.p95_display }}</span
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p
|
||||
id="recent-simulations-empty"
|
||||
class="empty-state"
|
||||
{%
|
||||
if
|
||||
recent_simulations|length
|
||||
>
|
||||
0 %} hidden{% endif %}> Trigger simulations to populate recent run
|
||||
statistics.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel dashboard-panel">
|
||||
<header class="panel-header">
|
||||
<h3>Upcoming Maintenance</h3>
|
||||
</header>
|
||||
<ul id="upcoming-maintenance" class="striped-list">
|
||||
{% for item in upcoming_maintenance %}
|
||||
<li>
|
||||
<span class="list-title"
|
||||
>{{ item.equipment_name }} · {{ item.scenario_name }}</span
|
||||
>
|
||||
<span class="list-detail"
|
||||
>{{ item.date_display }} · {{ item.cost_display }} · {{ item.description
|
||||
}}</span
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p
|
||||
id="upcoming-maintenance-empty"
|
||||
class="empty-state"
|
||||
{%
|
||||
if
|
||||
upcoming_maintenance|length
|
||||
>
|
||||
0 %} hidden{% endif %}> Schedule maintenance activities to track upcoming
|
||||
events.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %} {% block scripts %} {{ super() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script id="dashboard-data" type="application/json">
|
||||
{{ {
|
||||
"summary_metrics": summary_metrics,
|
||||
"scenario_rows": scenario_rows,
|
||||
"overall_report_metrics": overall_report_metrics,
|
||||
"recent_simulations": recent_simulations,
|
||||
"upcoming_maintenance": upcoming_maintenance,
|
||||
"scenario_cost_chart": scenario_cost_chart,
|
||||
"scenario_activity_chart": scenario_activity_chart,
|
||||
"cost_chart_has_data": cost_chart_has_data,
|
||||
"activity_chart_has_data": activity_chart_has_data,
|
||||
"report_available": report_available
|
||||
} | tojson }}
|
||||
</script>
|
||||
<script>
|
||||
(() => {
|
||||
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;
|
||||
|
||||
function 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");
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
function renderOverallMetrics() {
|
||||
if (!overallMetricsList || !overallMetricsEmpty) {
|
||||
return;
|
||||
}
|
||||
overallMetricsList.innerHTML = "";
|
||||
const items = Array.isArray(state.overall_report_metrics)
|
||||
? state.overall_report_metrics
|
||||
: [];
|
||||
items.forEach((metric) => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `
|
||||
<span class="metric-label">${metric.label}</span>
|
||||
<span class="metric-value">${metric.value}</span>
|
||||
`;
|
||||
overallMetricsList.appendChild(li);
|
||||
});
|
||||
overallMetricsEmpty.hidden =
|
||||
Boolean(state.report_available) && items.length > 0;
|
||||
if (!Boolean(state.report_available)) {
|
||||
overallMetricsEmpty.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecentSimulations() {
|
||||
if (!recentList || !recentEmpty) {
|
||||
return;
|
||||
}
|
||||
recentList.innerHTML = "";
|
||||
const runs = Array.isArray(state.recent_simulations)
|
||||
? state.recent_simulations
|
||||
: [];
|
||||
runs.forEach((run) => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `
|
||||
<span class="list-title">${run.scenario_name}</span>
|
||||
<span class="list-detail">Iterations: ${run.iterations_display} · Mean: ${run.mean_display} · P95: ${run.p95_display}</span>
|
||||
`;
|
||||
recentList.appendChild(li);
|
||||
});
|
||||
recentEmpty.hidden = runs.length > 0;
|
||||
}
|
||||
|
||||
function renderMaintenance() {
|
||||
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;
|
||||
}
|
||||
|
||||
function renderCostChart() {
|
||||
if (!costChartCanvas || !costChartEmpty) {
|
||||
return;
|
||||
}
|
||||
if (costChartInstance) {
|
||||
costChartInstance.destroy();
|
||||
costChartInstance = null;
|
||||
}
|
||||
const hasData =
|
||||
Boolean(state.cost_chart_has_data) &&
|
||||
Array.isArray(state.scenario_cost_chart?.labels) &&
|
||||
state.scenario_cost_chart.labels.length > 0;
|
||||
costChartCanvas.hidden = !hasData;
|
||||
costChartEmpty.hidden = hasData;
|
||||
if (!hasData) {
|
||||
return;
|
||||
}
|
||||
costChartInstance = new Chart(costChartCanvas.getContext("2d"), {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: state.scenario_cost_chart.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "CAPEX",
|
||||
data: state.scenario_cost_chart.capex,
|
||||
backgroundColor: "#1d4ed8",
|
||||
},
|
||||
{
|
||||
label: "OPEX",
|
||||
data: state.scenario_cost_chart.opex,
|
||||
backgroundColor: "#38bdf8",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: false,
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderActivityChart() {
|
||||
if (!activityChartCanvas || !activityChartEmpty) {
|
||||
return;
|
||||
}
|
||||
if (activityChartInstance) {
|
||||
activityChartInstance.destroy();
|
||||
activityChartInstance = null;
|
||||
}
|
||||
const hasData =
|
||||
Boolean(state.activity_chart_has_data) &&
|
||||
Array.isArray(state.scenario_activity_chart?.labels) &&
|
||||
state.scenario_activity_chart.labels.length > 0;
|
||||
activityChartCanvas.hidden = !hasData;
|
||||
activityChartEmpty.hidden = hasData;
|
||||
if (!hasData) {
|
||||
return;
|
||||
}
|
||||
activityChartInstance = new Chart(activityChartCanvas.getContext("2d"), {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: state.scenario_activity_chart.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Production",
|
||||
data: state.scenario_activity_chart.production,
|
||||
borderColor: "#22c55e",
|
||||
backgroundColor: "rgba(34, 197, 94, 0.18)",
|
||||
tension: 0.35,
|
||||
fill: true,
|
||||
},
|
||||
{
|
||||
label: "Consumption",
|
||||
data: state.scenario_activity_chart.consumption,
|
||||
borderColor: "#ef4444",
|
||||
backgroundColor: "rgba(239, 68, 68, 0.18)",
|
||||
tension: 0.35,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
renderSummaryMetrics();
|
||||
renderScenarioTable();
|
||||
renderOverallMetrics();
|
||||
renderRecentSimulations();
|
||||
renderMaintenance();
|
||||
renderCostChart();
|
||||
renderActivityChart();
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
if (!refreshButton) {
|
||||
return;
|
||||
}
|
||||
refreshButton.disabled = isLoading;
|
||||
refreshButton.classList.toggle("is-loading", isLoading);
|
||||
refreshButton.textContent = isLoading
|
||||
? "Refreshing…"
|
||||
: "Refresh Dashboard";
|
||||
}
|
||||
|
||||
async function refreshDashboard() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatus("");
|
||||
const response = await fetch("/ui/dashboard/data", {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to refresh dashboard data.");
|
||||
}
|
||||
const payload = await response.json();
|
||||
state = payload;
|
||||
renderDashboard();
|
||||
setStatus("Dashboard updated.", "success");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || "Unable to refresh dashboard.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener("click", refreshDashboard);
|
||||
}
|
||||
|
||||
renderDashboard();
|
||||
})();
|
||||
{{ {"summary_metrics": summary_metrics, "scenario_rows": scenario_rows, "overall_report_metrics": overall_report_metrics, "recent_simulations": recent_simulations, "upcoming_maintenance": upcoming_maintenance} | tojson }}
|
||||
</script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user