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:
2025-10-21 07:20:02 +02:00
parent 5a84445e90
commit 18f4ae7278
21 changed files with 1963 additions and 1986 deletions

156
static/js/consumption.js Normal file
View File

@@ -0,0 +1,156 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("consumption-data");
let data = { scenarios: [], consumption: {} };
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
data = {
scenarios: Array.isArray(parsed.scenarios) ? parsed.scenarios : [],
consumption:
parsed.consumption && typeof parsed.consumption === "object"
? parsed.consumption
: {},
};
}
} catch (error) {
console.error("Unable to parse consumption data", error);
}
}
const consumptionByScenario = data.consumption;
const filterSelect = document.getElementById("consumption-scenario-filter");
const tableWrapper = document.getElementById("consumption-table-wrapper");
const tableBody = document.getElementById("consumption-table-body");
const emptyState = document.getElementById("consumption-empty");
const form = document.getElementById("consumption-form");
const feedbackEl = document.getElementById("consumption-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const renderConsumptionRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = consumptionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No consumption records for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review its consumption records.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderConsumptionRows(value);
});
}
const submitConsumption = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/consumption/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add consumption record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(consumptionByScenario[mapKey])) {
consumptionByScenario[mapKey] = [];
}
consumptionByScenario[mapKey].push(result);
form.reset();
showFeedback("Consumption record saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderConsumptionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitConsumption);
}
if (filterSelect && filterSelect.value) {
renderConsumptionRows(filterSelect.value);
}
});

240
static/js/costs.js Normal file
View File

@@ -0,0 +1,240 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("costs-payload");
let capexByScenario = {};
let opexByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.capex && typeof parsed.capex === "object") {
capexByScenario = parsed.capex;
}
if (parsed.opex && typeof parsed.opex === "object") {
opexByScenario = parsed.opex;
}
}
} catch (error) {
console.error("Unable to parse cost data", error);
}
}
const filterSelect = document.getElementById("costs-scenario-filter");
const costsEmptyState = document.getElementById("costs-empty");
const costsDataWrapper = document.getElementById("costs-data");
const capexTableBody = document.getElementById("capex-table-body");
const opexTableBody = document.getElementById("opex-table-body");
const capexEmpty = document.getElementById("capex-empty");
const opexEmpty = document.getElementById("opex-empty");
const capexTotal = document.getElementById("capex-total");
const opexTotal = document.getElementById("opex-total");
const capexForm = document.getElementById("capex-form");
const opexForm = document.getElementById("opex-form");
const capexFeedback = document.getElementById("capex-feedback");
const opexFeedback = document.getElementById("opex-feedback");
const capexFormScenario = document.getElementById("capex-form-scenario");
const opexFormScenario = document.getElementById("opex-form-scenario");
const showFeedback = (element, message, type = "success") => {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
};
const hideFeedback = (element) => {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const sumAmount = (records) =>
records.reduce((total, record) => total + Number(record.amount || 0), 0);
const renderCostTables = (scenarioId) => {
if (
!capexTableBody ||
!opexTableBody ||
!capexEmpty ||
!opexEmpty ||
!capexTotal ||
!opexTotal
) {
return;
}
const capexRecords = capexByScenario[String(scenarioId)] || [];
const opexRecords = opexByScenario[String(scenarioId)] || [];
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
if (!capexRecords.length) {
capexEmpty.classList.remove("hidden");
} else {
capexEmpty.classList.add("hidden");
capexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
capexTableBody.appendChild(row);
});
}
if (!opexRecords.length) {
opexEmpty.classList.remove("hidden");
} else {
opexEmpty.classList.add("hidden");
opexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
opexTableBody.appendChild(row);
});
}
capexTotal.textContent = formatAmount(sumAmount(capexRecords));
opexTotal.textContent = formatAmount(sumAmount(opexRecords));
};
const toggleCostView = (show) => {
if (
!costsEmptyState ||
!costsDataWrapper ||
!capexTableBody ||
!opexTableBody
) {
return;
}
if (show) {
costsEmptyState.classList.add("hidden");
costsDataWrapper.classList.remove("hidden");
} else {
costsEmptyState.classList.remove("hidden");
costsDataWrapper.classList.add("hidden");
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
if (capexTotal) {
capexTotal.textContent = "—";
}
if (opexTotal) {
opexTotal.textContent = "—";
}
if (capexEmpty) {
capexEmpty.classList.add("hidden");
}
if (opexEmpty) {
opexEmpty.classList.add("hidden");
}
}
};
const syncFormSelections = (value) => {
if (capexFormScenario) {
capexFormScenario.value = value || "";
}
if (opexFormScenario) {
opexFormScenario.value = value || "";
}
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
toggleCostView(false);
syncFormSelections("");
return;
}
toggleCostView(true);
renderCostTables(value);
syncFormSelections(value);
});
}
const submitCostEntry = async (event, targetUrl, storageMap, feedbackEl) => {
event.preventDefault();
hideFeedback(feedbackEl);
const formData = new FormData(event.target);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
if (!payload.scenario_id) {
showFeedback(feedbackEl, "Select a scenario before submitting.", "error");
return;
}
try {
const response = await fetch(targetUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to save cost entry.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(storageMap[mapKey])) {
storageMap[mapKey] = [];
}
storageMap[mapKey].push(result);
event.target.reset();
showFeedback(feedbackEl, "Entry saved successfully.", "success");
if (filterSelect && filterSelect.value === mapKey) {
renderCostTables(mapKey);
}
} catch (error) {
showFeedback(
feedbackEl,
error.message || "An unexpected error occurred.",
"error"
);
}
};
if (capexForm) {
capexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/capex", capexByScenario, capexFeedback)
);
}
if (opexForm) {
opexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/opex", opexByScenario, opexFeedback)
);
}
if (filterSelect && filterSelect.value) {
toggleCostView(true);
renderCostTables(filterSelect.value);
syncFormSelections(filterSelect.value);
}
});

289
static/js/dashboard.js Normal file
View 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);
}
})();

145
static/js/equipment.js Normal file
View File

@@ -0,0 +1,145 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("equipment-data");
let equipmentByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.equipment && typeof parsed.equipment === "object") {
equipmentByScenario = parsed.equipment;
}
}
} catch (error) {
console.error("Unable to parse equipment data", error);
}
}
const filterSelect = document.getElementById("equipment-scenario-filter");
const tableWrapper = document.getElementById("equipment-table-wrapper");
const tableBody = document.getElementById("equipment-table-body");
const emptyState = document.getElementById("equipment-empty");
const form = document.getElementById("equipment-form");
const feedbackEl = document.getElementById("equipment-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const renderEquipmentRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = equipmentByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No equipment recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${record.name || "—"}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review the equipment list.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderEquipmentRows(value);
});
}
const submitEquipment = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
name: formData.get("name"),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/equipment/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add equipment record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(equipmentByScenario[mapKey])) {
equipmentByScenario[mapKey] = [];
}
equipmentByScenario[mapKey].push(result);
form.reset();
showFeedback("Equipment saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderEquipmentRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitEquipment);
}
if (filterSelect && filterSelect.value) {
renderEquipmentRows(filterSelect.value);
}
});

243
static/js/maintenance.js Normal file
View File

@@ -0,0 +1,243 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("maintenance-data");
let equipmentByScenario = {};
let maintenanceByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.equipment && typeof parsed.equipment === "object") {
equipmentByScenario = parsed.equipment;
}
if (parsed.maintenance && typeof parsed.maintenance === "object") {
maintenanceByScenario = parsed.maintenance;
}
}
} catch (error) {
console.error("Unable to parse maintenance data", error);
}
}
const filterSelect = document.getElementById("maintenance-scenario-filter");
const tableWrapper = document.getElementById("maintenance-table-wrapper");
const tableBody = document.getElementById("maintenance-table-body");
const emptyState = document.getElementById("maintenance-empty");
const form = document.getElementById("maintenance-form");
const feedbackEl = document.getElementById("maintenance-feedback");
const formScenarioSelect = document.getElementById(
"maintenance-form-scenario"
);
const equipmentSelect = document.getElementById("maintenance-form-equipment");
const equipmentEmptyState = document.getElementById(
"maintenance-equipment-empty"
);
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatCost = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const formatDate = (value) => {
if (!value) {
return "—";
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleDateString();
};
const renderMaintenanceRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = maintenanceByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent =
"No maintenance entries recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatDate(record.maintenance_date)}</td>
<td>${record.equipment_name || "—"}</td>
<td>${formatCost(record.cost)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
const populateEquipmentOptions = (scenarioId) => {
if (!equipmentSelect) {
return;
}
equipmentSelect.innerHTML =
'<option value="" disabled selected>Select equipment</option>';
equipmentSelect.disabled = true;
if (equipmentEmptyState) {
equipmentEmptyState.classList.add("hidden");
}
if (!scenarioId) {
return;
}
const list = equipmentByScenario[String(scenarioId)] || [];
if (!list.length) {
if (equipmentEmptyState) {
equipmentEmptyState.textContent =
"Add equipment for this scenario before scheduling maintenance.";
equipmentEmptyState.classList.remove("hidden");
}
return;
}
list.forEach((item) => {
const option = document.createElement("option");
option.value = item.id;
option.textContent = item.name || `Equipment ${item.id}`;
equipmentSelect.appendChild(option);
});
equipmentSelect.disabled = false;
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review upcoming or completed maintenance.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderMaintenanceRows(value);
});
}
if (formScenarioSelect) {
formScenarioSelect.addEventListener("change", (event) => {
const value = event.target.value;
populateEquipmentOptions(value);
});
}
const submitMaintenance = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const equipmentId = formData.get("equipment_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
equipment_id: equipmentId ? Number(equipmentId) : null,
maintenance_date: formData.get("maintenance_date"),
cost: Number(formData.get("cost")),
description: formData.get("description") || null,
};
if (!payload.scenario_id || !payload.equipment_id) {
showFeedback(
"Select a scenario and equipment before submitting.",
"error"
);
return;
}
try {
const response = await fetch("/api/maintenance/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add maintenance entry."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(maintenanceByScenario[mapKey])) {
maintenanceByScenario[mapKey] = [];
}
const equipmentList = equipmentByScenario[mapKey] || [];
const matchedEquipment = equipmentList.find(
(item) => Number(item.id) === Number(result.equipment_id)
);
result.equipment_name = matchedEquipment ? matchedEquipment.name : "";
maintenanceByScenario[mapKey].push(result);
form.reset();
populateEquipmentOptions(null);
showFeedback("Maintenance entry saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderMaintenanceRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitMaintenance);
}
if (filterSelect && filterSelect.value) {
renderMaintenanceRows(filterSelect.value);
}
if (formScenarioSelect && formScenarioSelect.value) {
populateEquipmentOptions(formScenarioSelect.value);
}
});

124
static/js/parameters.js Normal file
View File

@@ -0,0 +1,124 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("parameters-data");
let parametersByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
parametersByScenario = parsed;
}
} catch (error) {
console.error("Unable to parse parameter data", error);
}
}
const form = document.getElementById("parameter-form");
const scenarioSelect = /** @type {HTMLSelectElement | null} */ (
document.getElementById("scenario_id")
);
const nameInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("name")
);
const valueInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("value")
);
const feedback = document.getElementById("parameter-feedback");
const tableBody = document.getElementById("parameter-table-body");
const setFeedback = (message, variant) => {
if (!feedback) {
return;
}
feedback.textContent = message;
feedback.classList.remove("success", "error");
if (variant) {
feedback.classList.add(variant);
}
};
const renderTable = (scenarioId) => {
if (!tableBody) {
return;
}
tableBody.innerHTML = "";
const rows = parametersByScenario[String(scenarioId)] || [];
if (!rows.length) {
const emptyRow = document.createElement("tr");
emptyRow.id = "parameter-empty-state";
emptyRow.innerHTML =
'<td colspan="4">No parameters recorded for this scenario yet.</td>';
tableBody.appendChild(emptyRow);
return;
}
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${row.name}</td>
<td>${row.value}</td>
<td>${row.distribution_type ?? "—"}</td>
<td>${
row.distribution_parameters
? JSON.stringify(row.distribution_parameters)
: "—"
}</td>
`;
tableBody.appendChild(tr);
});
};
if (scenarioSelect) {
renderTable(scenarioSelect.value);
scenarioSelect.addEventListener("change", () =>
renderTable(scenarioSelect.value)
);
}
if (!form || !scenarioSelect || !nameInput || !valueInput) {
return;
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const scenarioId = scenarioSelect.value;
const payload = {
scenario_id: Number(scenarioId),
name: nameInput.value.trim(),
value: Number(valueInput.value),
};
if (!payload.name) {
setFeedback("Parameter name is required.", "error");
return;
}
if (!Number.isFinite(payload.value)) {
setFeedback("Enter a numeric value.", "error");
return;
}
const response = await fetch("/api/parameters/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
setFeedback(`Error saving parameter: ${errorText}`, "error");
return;
}
const data = await response.json();
const scenarioKey = String(scenarioId);
parametersByScenario[scenarioKey] = parametersByScenario[scenarioKey] || [];
parametersByScenario[scenarioKey].push(data);
form.reset();
scenarioSelect.value = scenarioKey;
renderTable(scenarioKey);
nameInput.focus();
setFeedback("Parameter saved.", "success");
});
});

157
static/js/production.js Normal file
View File

@@ -0,0 +1,157 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("production-data");
let data = { scenarios: [], production: {} };
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
data = {
scenarios: Array.isArray(parsed.scenarios) ? parsed.scenarios : [],
production:
parsed.production && typeof parsed.production === "object"
? parsed.production
: {},
};
}
} catch (error) {
console.error("Unable to parse production data", error);
}
}
const productionByScenario = data.production;
const filterSelect = document.getElementById("production-scenario-filter");
const tableWrapper = document.getElementById("production-table-wrapper");
const tableBody = document.getElementById("production-table-body");
const emptyState = document.getElementById("production-empty");
const form = document.getElementById("production-form");
const feedbackEl = document.getElementById("production-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const renderProductionRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = productionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent =
"No production output recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review its production output.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderProductionRows(value);
});
}
const submitProduction = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/production/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add production output record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(productionByScenario[mapKey])) {
productionByScenario[mapKey] = [];
}
productionByScenario[mapKey].push(result);
form.reset();
showFeedback("Production output saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderProductionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitProduction);
}
if (filterSelect && filterSelect.value) {
renderProductionRows(filterSelect.value);
}
});

149
static/js/reporting.js Normal file
View File

@@ -0,0 +1,149 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("reporting-data");
let reportingSummaries = [];
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "[]");
if (Array.isArray(parsed)) {
reportingSummaries = parsed;
}
} catch (error) {
console.error("Unable to parse reporting data", error);
}
}
const REPORT_FIELDS = [
{ key: "iterations", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Std. Dev", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{
key: "expected_shortfall_95",
label: "Expected Shortfall (95%)",
decimals: 2,
},
];
const tableWrapper = document.getElementById("reporting-table-wrapper");
const tableBody = document.getElementById("reporting-table-body");
const emptyState = document.getElementById("reporting-empty");
const refreshButton = document.getElementById("report-refresh");
const feedbackEl = document.getElementById("report-feedback");
const formatNumber = (value, decimals = 2) => {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
};
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const renderReportingTable = (summaryData) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
tableBody.innerHTML = "";
if (!summaryData.length) {
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
summaryData.forEach((entry) => {
const row = document.createElement("tr");
const scenarioCell = document.createElement("td");
scenarioCell.textContent = entry.scenario_name;
row.appendChild(scenarioCell);
REPORT_FIELDS.forEach((field) => {
const cell = document.createElement("td");
const source = field.key === "iterations" ? entry : entry.summary || {};
cell.textContent = formatNumber(source[field.key], field.decimals);
row.appendChild(cell);
});
tableBody.appendChild(row);
});
};
const refreshMetrics = async () => {
hideFeedback();
showFeedback("Refreshing metrics…", "success");
try {
const response = await fetch("/ui/reporting", {
method: "GET",
headers: { "X-Requested-With": "XMLHttpRequest" },
});
if (!response.ok) {
throw new Error("Unable to refresh reporting data.");
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const newTable = doc.querySelector("#reporting-table-wrapper");
const newFeedback = doc.querySelector("#report-feedback");
if (!newTable) {
throw new Error("Unexpected response while refreshing.");
}
const newEmptyState = doc.querySelector("#reporting-empty");
if (emptyState && newEmptyState) {
emptyState.className = newEmptyState.className;
emptyState.textContent = newEmptyState.textContent;
}
if (tableWrapper) {
tableWrapper.className = newTable.className;
tableWrapper.innerHTML = newTable.innerHTML;
}
if (newFeedback && feedbackEl) {
feedbackEl.className = newFeedback.className;
feedbackEl.textContent = newFeedback.textContent;
}
showFeedback("Metrics refreshed.", "success");
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
renderReportingTable(reportingSummaries);
if (refreshButton) {
refreshButton.addEventListener("click", refreshMetrics);
}
});

View File

@@ -0,0 +1,69 @@
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("scenario-form");
if (!form) {
return;
}
const nameInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("name")
);
const descriptionInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("description")
);
const table = document.getElementById("scenario-table");
const tableBody = document.getElementById("scenario-table-body");
const emptyState = document.getElementById("empty-state");
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!nameInput || !descriptionInput) {
return;
}
const payload = {
name: nameInput.value.trim(),
description: descriptionInput.value.trim() || null,
};
if (!payload.name) {
return;
}
const response = await fetch("/api/scenarios/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
console.error("Scenario creation failed", errorText);
return;
}
const data = await response.json();
const row = document.createElement("tr");
row.dataset.scenarioId = String(data.id);
row.innerHTML = `
<td>${data.name}</td>
<td>${data.description ?? "—"}</td>
`;
if (emptyState) {
emptyState.remove();
}
if (table) {
table.classList.remove("hidden");
table.removeAttribute("aria-hidden");
}
if (tableBody) {
tableBody.appendChild(row);
}
form.reset();
nameInput.focus();
});
});

354
static/js/simulations.js Normal file
View File

@@ -0,0 +1,354 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("simulations-data");
let simulationScenarios = [];
let initialRuns = [];
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (Array.isArray(parsed.scenarios)) {
simulationScenarios = parsed.scenarios;
}
if (Array.isArray(parsed.runs)) {
initialRuns = parsed.runs;
}
}
} catch (error) {
console.error("Unable to parse simulations data", error);
}
}
const SUMMARY_FIELDS = [
{ key: "count", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "median", label: "Median Result", decimals: 2 },
{ key: "min", label: "Minimum", decimals: 2 },
{ key: "max", label: "Maximum", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Standard Deviation", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{
key: "expected_shortfall_95",
label: "Expected Shortfall (95%)",
decimals: 2,
},
];
const SAMPLE_RESULT_LIMIT = 20;
const filterSelect = document.getElementById("simulations-scenario-filter");
const overviewWrapper = document.getElementById(
"simulations-overview-wrapper"
);
const overviewBody = document.getElementById("simulations-overview-body");
const overviewEmpty = document.getElementById("simulations-overview-empty");
const emptyState = document.getElementById("simulations-empty");
const summaryWrapper = document.getElementById("simulations-summary-wrapper");
const summaryBody = document.getElementById("simulations-summary-body");
const summaryEmpty = document.getElementById("simulations-summary-empty");
const resultsWrapper = document.getElementById("simulations-results-wrapper");
const resultsBody = document.getElementById("simulations-results-body");
const resultsEmpty = document.getElementById("simulations-results-empty");
const simulationForm = document.getElementById("simulation-run-form");
const simulationFeedback = document.getElementById("simulation-feedback");
const formScenarioSelect = document.getElementById(
"simulation-form-scenario"
);
const simulationRunsMap = Object.create(null);
const getScenarioName = (id) => {
const match = simulationScenarios.find(
(scenario) => String(scenario.id) === String(id)
);
return match ? match.name : `Scenario ${id}`;
};
const formatNumber = (value, decimals = 2) => {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
};
const showFeedback = (element, message, type = "success") => {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
};
const hideFeedback = (element) => {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
};
const initializeRunsMap = () => {
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
simulationRunsMap[key] = {
scenario_id: scenario.id,
scenario_name: scenario.name,
iterations: 0,
summary: null,
sample_results: [],
};
});
initialRuns.forEach((run) => {
const key = String(run.scenario_id);
simulationRunsMap[key] = {
scenario_id: run.scenario_id,
scenario_name: run.scenario_name || getScenarioName(key),
iterations: run.iterations || 0,
summary: run.summary || null,
sample_results: Array.isArray(run.sample_results)
? run.sample_results
: [],
};
});
};
const renderOverviewTable = () => {
if (!overviewBody) {
return;
}
overviewBody.innerHTML = "";
if (!simulationScenarios.length) {
if (overviewWrapper) {
overviewWrapper.classList.add("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.remove("hidden");
}
return;
}
if (overviewWrapper) {
overviewWrapper.classList.remove("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.add("hidden");
}
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
const run = simulationRunsMap[key];
const iterations = run && run.iterations ? run.iterations : 0;
const meanValue =
iterations && run && run.summary ? run.summary.mean : null;
const row = document.createElement("tr");
row.innerHTML = `
<td>${scenario.name}</td>
<td>${iterations || 0}</td>
<td>${iterations ? formatNumber(meanValue) : "—"}</td>
`;
overviewBody.appendChild(row);
});
};
const renderScenarioDetails = (scenarioId) => {
if (!scenarioId) {
if (emptyState) {
emptyState.classList.remove("hidden");
}
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
return;
}
if (emptyState) {
emptyState.classList.add("hidden");
}
const run = simulationRunsMap[String(scenarioId)];
const summary = run ? run.summary : null;
const samples = run ? run.sample_results || [] : [];
if (!summary) {
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.remove("hidden");
}
} else {
if (summaryWrapper) {
summaryWrapper.classList.remove("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (summaryBody) {
summaryBody.innerHTML = "";
SUMMARY_FIELDS.forEach((field) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${field.label}</td>
<td>${formatNumber(summary[field.key], field.decimals)}</td>
`;
summaryBody.appendChild(row);
});
}
}
if (!samples.length) {
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.remove("hidden");
}
} else {
if (resultsWrapper) {
resultsWrapper.classList.remove("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
if (resultsBody) {
resultsBody.innerHTML = "";
samples.slice(0, SAMPLE_RESULT_LIMIT).forEach((item, index) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${index + 1}</td>
<td>${formatNumber(item)}</td>
`;
resultsBody.appendChild(row);
});
}
}
};
const runSimulation = async (event) => {
event.preventDefault();
hideFeedback(simulationFeedback);
if (!simulationForm) {
return;
}
const formData = new FormData(simulationForm);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
iterations: Number(formData.get("iterations")),
seed: formData.get("seed") ? Number(formData.get("seed")) : null,
};
if (!payload.scenario_id) {
showFeedback(
simulationFeedback,
"Select a scenario before running a simulation.",
"error"
);
return;
}
try {
const response = await fetch("/api/simulations/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to run simulation.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
const summary =
result.summary && typeof result.summary === "object"
? result.summary
: null;
const iterations =
summary && typeof summary.count === "number"
? summary.count
: payload.iterations || 0;
simulationRunsMap[mapKey] = {
scenario_id: result.scenario_id,
scenario_name: getScenarioName(mapKey),
iterations,
summary,
sample_results: Array.isArray(result.sample_results)
? result.sample_results
: [],
};
renderOverviewTable();
renderScenarioDetails(mapKey);
if (filterSelect) {
filterSelect.value = mapKey;
}
if (formScenarioSelect) {
formScenarioSelect.value = mapKey;
}
simulationForm.reset();
showFeedback(simulationFeedback, "Simulation completed.", "success");
} catch (error) {
showFeedback(
simulationFeedback,
error.message || "An unexpected error occurred.",
"error"
);
}
};
initializeRunsMap();
renderOverviewTable();
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
renderScenarioDetails(value);
});
}
if (formScenarioSelect) {
formScenarioSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (filterSelect) {
filterSelect.value = value;
}
renderScenarioDetails(value);
});
}
if (simulationForm) {
simulationForm.addEventListener("submit", runSimulation);
}
if (filterSelect && filterSelect.value) {
renderScenarioDetails(filterSelect.value);
}
});