Files
calminer/templates/Dashboard.html
zwitschi 5a84445e90 feat: Enhance equipment, maintenance, production, and simulation management interfaces
- Updated equipment.html to include scenario filtering, equipment addition form, and dynamic equipment listing.
- Enhanced maintenance.html with scenario filtering, maintenance entry form, and dynamic equipment selection.
- Improved production.html to allow scenario filtering and production output entry.
- Revamped reporting.html to display scenario KPI summaries with refresh functionality.
- Expanded simulations.html to support scenario selection, simulation run history, and detailed results display.
- Refactored base_header.html for improved navigation structure and active link highlighting.
2025-10-21 00:09:00 +02:00

577 lines
16 KiB
HTML

{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
block content %}
<div class="dashboard-header">
<div>
<h2>Operations Overview</h2>
<p class="dashboard-subtitle">
Unified insight across scenarios, costs, production, maintenance, and
simulations.
</p>
</div>
<div class="dashboard-actions">
<button id="refresh-dashboard" type="button" class="btn primary">
Refresh Dashboard
</button>
</div>
</div>
<p id="dashboard-status" class="feedback" hidden></p>
<section>
<div id="summary-metrics" class="dashboard-metrics-grid">
{% for metric in summary_metrics %}
<article class="metric-card">
<span class="metric-label">{{ metric.label }}</span>
<span class="metric-value">{{ metric.value }}</span>
</article>
{% endfor %}
</div>
<p id="summary-empty" class="empty-state" {% if summary_metrics|length>
0 %} hidden{% endif %}> Add project inputs to populate summary metrics.
</p>
</section>
<section class="dashboard-charts">
<article class="panel chart-card">
<header class="panel-header">
<div>
<h3>Scenario Cost Mix</h3>
<p class="chart-subtitle">CAPEX vs OPEX totals per scenario</p>
</div>
</header>
<canvas
id="cost-chart"
height="220"
{%
if
not
cost_chart_has_data
%}
hidden{%
endif
%}
></canvas>
<p
id="cost-chart-empty"
class="empty-state"
{%
if
cost_chart_has_data
%}
hidden{%
endif
%}
>
Add CAPEX or OPEX entries to display this chart.
</p>
</article>
<article class="panel chart-card">
<header class="panel-header">
<div>
<h3>Production vs Consumption</h3>
<p class="chart-subtitle">Throughput comparison by scenario</p>
</div>
</header>
<canvas
id="activity-chart"
height="220"
{%
if
not
activity_chart_has_data
%}
hidden{%
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();
})();
</script>
{% endblock %}