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

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);
}
});