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.
This commit is contained in:
@@ -1,261 +1,576 @@
|
||||
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
|
||||
block head_extra %} {{ super() }}
|
||||
<style>
|
||||
.summary-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
}
|
||||
.metric {
|
||||
text-align: center;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #52606d;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
#chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
#error-message {
|
||||
color: #b91d47;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block content %}
|
||||
<h2>Simulation Results Dashboard</h2>
|
||||
<div class="summary-card">
|
||||
<h3>Summary Statistics</h3>
|
||||
<div id="summary-grid" class="summary-grid"></div>
|
||||
<p id="error-message" hidden></p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>Sample Results Input</h3>
|
||||
<p>
|
||||
Provide simulation outputs as JSON (array of objects containing the
|
||||
<code>result</code> field) and refresh the dashboard to preview metrics.
|
||||
</p>
|
||||
<textarea id="results-input" rows="6" class="monospace-input"></textarea>
|
||||
<div class="button-row">
|
||||
<button id="load-sample" type="button" class="btn">Load Sample Data</button>
|
||||
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>
|
||||
<div id="chart-container">
|
||||
<h3>Result Distribution</h3>
|
||||
<canvas id="summary-chart" height="120"></canvas>
|
||||
|
||||
<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 SUMMARY_FIELDS = [
|
||||
{ key: "mean", label: "Mean" },
|
||||
{ key: "median", label: "Median" },
|
||||
{ key: "min", label: "Min" },
|
||||
{ key: "max", label: "Max" },
|
||||
{ key: "std_dev", label: "Std Dev" },
|
||||
{ key: "variance", label: "Variance" },
|
||||
{ key: "percentile_5", label: "5th Percentile" },
|
||||
{ key: "percentile_10", label: "10th Percentile" },
|
||||
{ key: "percentile_90", label: "90th Percentile" },
|
||||
{ key: "percentile_95", label: "95th Percentile" },
|
||||
{ key: "value_at_risk_95", label: "VaR (95%)" },
|
||||
{
|
||||
key: "expected_shortfall_95",
|
||||
label: "Expected Shortfall (95%)",
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchSummary(results) {
|
||||
const response = await fetch("/api/reporting/summary", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(results),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.json();
|
||||
throw new Error(message.detail || "Failed to retrieve summary");
|
||||
(() => {
|
||||
const dataElement = document.getElementById("dashboard-data");
|
||||
if (!dataElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function getResultsFromInput() {
|
||||
const textarea = document.getElementById("results-input");
|
||||
let state = {};
|
||||
try {
|
||||
const parsed = JSON.parse(textarea.value || "[]");
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("Input must be a JSON array");
|
||||
}
|
||||
return parsed;
|
||||
state = JSON.parse(dataElement.textContent || "{}");
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input: ${error.message}`);
|
||||
console.error("Failed to parse dashboard data", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSummary(summary) {
|
||||
const grid = document.getElementById("summary-grid");
|
||||
grid.innerHTML = "";
|
||||
SUMMARY_FIELDS.forEach(({ key, label }) => {
|
||||
const rawValue = summary[key];
|
||||
const numericValue = Number(rawValue);
|
||||
const display = Number.isFinite(numericValue)
|
||||
? numericValue.toFixed(2)
|
||||
: "—";
|
||||
const metric = document.createElement("div");
|
||||
metric.className = "metric";
|
||||
metric.innerHTML = `
|
||||
<div class="metric-label">${label}</div>
|
||||
<div class="metric-value">${display}</div>
|
||||
`;
|
||||
grid.appendChild(metric);
|
||||
});
|
||||
}
|
||||
|
||||
let chartInstance = null;
|
||||
|
||||
function renderChart(summary) {
|
||||
const ctx = document.getElementById("summary-chart").getContext("2d");
|
||||
const percentilePoints = [
|
||||
{ label: "Min", value: summary.min },
|
||||
{ label: "P5", value: summary.percentile_5 },
|
||||
{ label: "P10", value: summary.percentile_10 },
|
||||
{ label: "Median", value: summary.median },
|
||||
{ label: "Mean", value: summary.mean },
|
||||
{ label: "P90", value: summary.percentile_90 },
|
||||
{ label: "P95", value: summary.percentile_95 },
|
||||
{ label: "Max", value: summary.max },
|
||||
];
|
||||
|
||||
const labels = percentilePoints.map((point) => point.label);
|
||||
const dataPoints = percentilePoints.map((point) =>
|
||||
Number(point.value ?? 0)
|
||||
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");
|
||||
|
||||
const tailRiskLines = [
|
||||
{ label: "VaR (95%)", value: summary.value_at_risk_95 },
|
||||
{ label: "ES (95%)", value: summary.expected_shortfall_95 },
|
||||
]
|
||||
.map(({ label, value }) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return null;
|
||||
}
|
||||
return `${label}: ${numeric.toFixed(2)}`;
|
||||
})
|
||||
.filter((line) => line !== null);
|
||||
let costChartInstance = null;
|
||||
let activityChartInstance = null;
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
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");
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Result Summary",
|
||||
data: dataPoints,
|
||||
borderColor: "#2563eb",
|
||||
backgroundColor: "rgba(37, 99, 235, 0.2)",
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
afterBody: () => tailRiskLines,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
});
|
||||
}
|
||||
|
||||
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 showError(message) {
|
||||
const errorElement = document.getElementById("error-message");
|
||||
errorElement.textContent = message;
|
||||
errorElement.hidden = false;
|
||||
}
|
||||
function renderDashboard() {
|
||||
renderSummaryMetrics();
|
||||
renderScenarioTable();
|
||||
renderOverallMetrics();
|
||||
renderRecentSimulations();
|
||||
renderMaintenance();
|
||||
renderCostChart();
|
||||
renderActivityChart();
|
||||
}
|
||||
|
||||
function attachHandlers() {
|
||||
const loadSampleButton = document.getElementById("load-sample");
|
||||
const refreshButton = document.getElementById("refresh-dashboard");
|
||||
function setLoading(isLoading) {
|
||||
if (!refreshButton) {
|
||||
return;
|
||||
}
|
||||
refreshButton.disabled = isLoading;
|
||||
refreshButton.classList.toggle("is-loading", isLoading);
|
||||
refreshButton.textContent = isLoading
|
||||
? "Refreshing…"
|
||||
: "Refresh Dashboard";
|
||||
}
|
||||
|
||||
const sampleData = JSON.stringify(
|
||||
[
|
||||
{ result: 18.2 },
|
||||
{ result: 22.1 },
|
||||
{ result: 30.4 },
|
||||
{ result: 25.7 },
|
||||
{ result: 28.3 },
|
||||
],
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
loadSampleButton.addEventListener("click", () => {
|
||||
document.getElementById("results-input").value = sampleData;
|
||||
});
|
||||
|
||||
refreshButton.addEventListener("click", async () => {
|
||||
async function refreshDashboard() {
|
||||
try {
|
||||
const results = getResultsFromInput();
|
||||
const summary = await fetchSummary(results);
|
||||
renderSummary(summary);
|
||||
renderChart(summary);
|
||||
document.getElementById("error-message").hidden = true;
|
||||
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);
|
||||
showError(error.message);
|
||||
setStatus(error.message || "Unable to refresh dashboard.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("results-input").value = sampleData;
|
||||
}
|
||||
|
||||
async function initializeDashboard() {
|
||||
try {
|
||||
attachHandlers();
|
||||
const initialResults = getResultsFromInput();
|
||||
const summary = await fetchSummary(initialResults);
|
||||
renderSummary(summary);
|
||||
renderChart(summary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
initializeDashboard();
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener("click", refreshDashboard);
|
||||
}
|
||||
|
||||
renderDashboard();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user