v2 init
This commit is contained in:
@@ -1,205 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const dataElement = document.getElementById("consumption-data");
|
|
||||||
let data = { scenarios: [], consumption: {}, unit_options: [] };
|
|
||||||
|
|
||||||
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
|
|
||||||
: {},
|
|
||||||
unit_options: Array.isArray(parsed.unit_options)
|
|
||||||
? parsed.unit_options
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} 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 unitSelect = document.getElementById("consumption-form-unit");
|
|
||||||
const unitSymbolInput = document.getElementById(
|
|
||||||
"consumption-form-unit-symbol"
|
|
||||||
);
|
|
||||||
|
|
||||||
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 formatMeasurement = (amount, symbol, name) => {
|
|
||||||
if (symbol) {
|
|
||||||
return `${formatAmount(amount)} ${symbol}`;
|
|
||||||
}
|
|
||||||
if (name) {
|
|
||||||
return `${formatAmount(amount)} ${name}`;
|
|
||||||
}
|
|
||||||
return formatAmount(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>${formatMeasurement(
|
|
||||||
record.amount,
|
|
||||||
record.unit_symbol,
|
|
||||||
record.unit_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 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 unitName = formData.get("unit_name");
|
|
||||||
const unitSymbol = formData.get("unit_symbol");
|
|
||||||
const payload = {
|
|
||||||
scenario_id: scenarioId ? Number(scenarioId) : null,
|
|
||||||
amount: Number(formData.get("amount")),
|
|
||||||
description: formData.get("description") || null,
|
|
||||||
unit_name: unitName ? String(unitName) : null,
|
|
||||||
unit_symbol: unitSymbol ? String(unitSymbol) : 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();
|
|
||||||
syncUnitSelection();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncUnitSelection = () => {
|
|
||||||
if (!unitSelect || !unitSymbolInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!unitSelect.value && unitSelect.options.length > 0) {
|
|
||||||
const firstOption = Array.from(unitSelect.options).find(
|
|
||||||
(option) => option.value
|
|
||||||
);
|
|
||||||
if (firstOption) {
|
|
||||||
firstOption.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selectedOption = unitSelect.options[unitSelect.selectedIndex];
|
|
||||||
unitSymbolInput.value = selectedOption
|
|
||||||
? selectedOption.getAttribute("data-symbol") || ""
|
|
||||||
: "";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (unitSelect) {
|
|
||||||
unitSelect.addEventListener("change", syncUnitSelection);
|
|
||||||
syncUnitSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterSelect && filterSelect.value) {
|
|
||||||
renderConsumptionRows(filterSelect.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const dataElement = document.getElementById("costs-payload");
|
|
||||||
let capexByScenario = {};
|
|
||||||
let opexByScenario = {};
|
|
||||||
let currencyOptions = [];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (Array.isArray(parsed.currency_options)) {
|
|
||||||
currencyOptions = parsed.currency_options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 capexCurrencySelect = document.getElementById("capex-form-currency");
|
|
||||||
const opexCurrencySelect = document.getElementById("opex-form-currency");
|
|
||||||
|
|
||||||
// If no currency options were injected server-side, fetch from API
|
|
||||||
const fetchCurrencyOptions = async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/currencies/");
|
|
||||||
if (!resp.ok) return;
|
|
||||||
const list = await resp.json();
|
|
||||||
if (Array.isArray(list) && list.length) {
|
|
||||||
currencyOptions = list;
|
|
||||||
populateCurrencySelects();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Unable to fetch currency options", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const populateCurrencySelects = () => {
|
|
||||||
const selectElements = [capexCurrencySelect, opexCurrencySelect].filter(Boolean);
|
|
||||||
selectElements.forEach((sel) => {
|
|
||||||
if (!sel) return;
|
|
||||||
// Clear non-empty options except the empty placeholder
|
|
||||||
const placeholder = sel.querySelector("option[value='']");
|
|
||||||
sel.innerHTML = "";
|
|
||||||
if (placeholder) sel.appendChild(placeholder);
|
|
||||||
currencyOptions.forEach((opt) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = opt.id;
|
|
||||||
option.textContent = opt.name || opt.id;
|
|
||||||
sel.appendChild(option);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// populate from injected options first, then fetch to refresh
|
|
||||||
if (currencyOptions && currencyOptions.length) populateCurrencySelects();
|
|
||||||
else fetchCurrencyOptions();
|
|
||||||
|
|
||||||
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 formatCurrencyAmount = (value, currencyCode) => {
|
|
||||||
if (!currencyCode) {
|
|
||||||
return formatAmount(value);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new Intl.NumberFormat(undefined, {
|
|
||||||
style: "currency",
|
|
||||||
currency: currencyCode,
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(Number(value));
|
|
||||||
} catch (error) {
|
|
||||||
return `${currencyCode} ${formatAmount(value)}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sumAmount = (records) =>
|
|
||||||
records.reduce((total, record) => total + Number(record.amount || 0), 0);
|
|
||||||
|
|
||||||
const describeTotal = (records) => {
|
|
||||||
if (!records || records.length === 0) {
|
|
||||||
return "—";
|
|
||||||
}
|
|
||||||
const total = sumAmount(records);
|
|
||||||
const currencyCodes = Array.from(
|
|
||||||
new Set(
|
|
||||||
records
|
|
||||||
.map((record) => (record.currency_code || "").trim().toUpperCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currencyCodes.length === 1) {
|
|
||||||
return formatCurrencyAmount(total, currencyCodes[0]);
|
|
||||||
}
|
|
||||||
return `${formatAmount(total)} (mixed)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
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>${formatCurrencyAmount(record.amount, record.currency_code)}</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>${formatCurrencyAmount(record.amount, record.currency_code)}</td>
|
|
||||||
<td>${record.description || "—"}</td>
|
|
||||||
`;
|
|
||||||
opexTableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
capexTotal.textContent = describeTotal(capexRecords);
|
|
||||||
opexTotal.textContent = describeTotal(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 || "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureCurrencySelection = (selectElement) => {
|
|
||||||
if (!selectElement || selectElement.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstOption = selectElement.querySelector(
|
|
||||||
"option[value]:not([value=''])"
|
|
||||||
);
|
|
||||||
if (firstOption && firstOption.value) {
|
|
||||||
selectElement.value = firstOption.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 currencyCode = formData.get("currency_code");
|
|
||||||
const payload = {
|
|
||||||
scenario_id: scenarioId ? Number(scenarioId) : null,
|
|
||||||
amount: Number(formData.get("amount")),
|
|
||||||
description: formData.get("description") || null,
|
|
||||||
currency_code: currencyCode ? String(currencyCode).toUpperCase() : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!payload.scenario_id) {
|
|
||||||
showFeedback(feedbackEl, "Select a scenario before submitting.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.currency_code) {
|
|
||||||
showFeedback(feedbackEl, "Choose a currency 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();
|
|
||||||
ensureCurrencySelection(event.target.querySelector("select[name='currency_code']"));
|
|
||||||
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) {
|
|
||||||
ensureCurrencySelection(capexCurrencySelect);
|
|
||||||
capexForm.addEventListener("submit", (event) =>
|
|
||||||
submitCostEntry(event, "/api/costs/capex", capexByScenario, capexFeedback)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opexForm) {
|
|
||||||
ensureCurrencySelection(opexCurrencySelect);
|
|
||||||
opexForm.addEventListener("submit", (event) =>
|
|
||||||
submitCostEntry(event, "/api/costs/opex", opexByScenario, opexFeedback)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterSelect && filterSelect.value) {
|
|
||||||
toggleCostView(true);
|
|
||||||
renderCostTables(filterSelect.value);
|
|
||||||
syncFormSelections(filterSelect.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,537 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const dataElement = document.getElementById("currencies-data");
|
|
||||||
const editorSection = document.getElementById("currencies-editor");
|
|
||||||
const tableBody = document.getElementById("currencies-table-body");
|
|
||||||
const tableEmptyState = document.getElementById("currencies-table-empty");
|
|
||||||
const metrics = {
|
|
||||||
total: document.getElementById("currency-metric-total"),
|
|
||||||
active: document.getElementById("currency-metric-active"),
|
|
||||||
inactive: document.getElementById("currency-metric-inactive"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = document.getElementById("currency-form");
|
|
||||||
const existingSelect = document.getElementById("currency-form-existing");
|
|
||||||
const codeInput = document.getElementById("currency-form-code");
|
|
||||||
const nameInput = document.getElementById("currency-form-name");
|
|
||||||
const symbolInput = document.getElementById("currency-form-symbol");
|
|
||||||
const statusSelect = document.getElementById("currency-form-status");
|
|
||||||
const resetButton = document.getElementById("currency-form-reset");
|
|
||||||
const feedbackElement = document.getElementById("currency-form-feedback");
|
|
||||||
|
|
||||||
const saveButton = form ? form.querySelector("button[type='submit']") : null;
|
|
||||||
|
|
||||||
const uppercaseCode = (value) =>
|
|
||||||
(value || "").toString().trim().toUpperCase();
|
|
||||||
const normalizeSymbol = (value) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = String(value).trim();
|
|
||||||
return trimmed ? trimmed : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeApiBase = (value) => {
|
|
||||||
if (!value || typeof value !== "string") {
|
|
||||||
return "/api/currencies";
|
|
||||||
}
|
|
||||||
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
let currencies = [];
|
|
||||||
let apiBase = "/api/currencies";
|
|
||||||
let defaultCurrencyCode = "USD";
|
|
||||||
|
|
||||||
const buildCurrencyRecord = (record) => {
|
|
||||||
if (!record || typeof record !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const code = uppercaseCode(record.code);
|
|
||||||
return {
|
|
||||||
id: record.id ?? null,
|
|
||||||
code,
|
|
||||||
name: record.name || "",
|
|
||||||
symbol: record.symbol || "",
|
|
||||||
is_active: Boolean(record.is_active),
|
|
||||||
is_default: code === defaultCurrencyCode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const findCurrencyIndex = (code) => {
|
|
||||||
return currencies.findIndex((item) => item.code === code);
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertCurrency = (record) => {
|
|
||||||
const normalized = buildCurrencyRecord(record);
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const existingIndex = findCurrencyIndex(normalized.code);
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
currencies[existingIndex] = normalized;
|
|
||||||
} else {
|
|
||||||
currencies.push(normalized);
|
|
||||||
}
|
|
||||||
currencies.sort((a, b) => a.code.localeCompare(b.code));
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceCurrencyList = (records) => {
|
|
||||||
if (!Array.isArray(records)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currencies = records
|
|
||||||
.map((record) => buildCurrencyRecord(record))
|
|
||||||
.filter((record) => record !== null)
|
|
||||||
.sort((a, b) => a.code.localeCompare(b.code));
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyPayload = () => {
|
|
||||||
if (!dataElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(dataElement.textContent || "{}");
|
|
||||||
if (parsed && typeof parsed === "object") {
|
|
||||||
if (parsed.default_currency_code) {
|
|
||||||
defaultCurrencyCode = uppercaseCode(parsed.default_currency_code);
|
|
||||||
}
|
|
||||||
if (parsed.currency_api_base) {
|
|
||||||
apiBase = normalizeApiBase(parsed.currency_api_base);
|
|
||||||
}
|
|
||||||
if (Array.isArray(parsed.currencies)) {
|
|
||||||
replaceCurrencyList(parsed.currencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Unable to parse currencies payload", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showFeedback = (message, type = "success") => {
|
|
||||||
if (!feedbackElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
feedbackElement.textContent = message;
|
|
||||||
feedbackElement.classList.remove("hidden", "success", "error");
|
|
||||||
feedbackElement.classList.add(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideFeedback = () => {
|
|
||||||
if (!feedbackElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
feedbackElement.classList.add("hidden");
|
|
||||||
feedbackElement.classList.remove("success", "error");
|
|
||||||
feedbackElement.textContent = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const setButtonLoading = (button, isLoading) => {
|
|
||||||
if (!button) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
button.disabled = isLoading;
|
|
||||||
button.classList.toggle("is-loading", isLoading);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMetrics = () => {
|
|
||||||
const total = currencies.length;
|
|
||||||
const active = currencies.filter((item) => item.is_active).length;
|
|
||||||
const inactive = total - active;
|
|
||||||
if (metrics.total) {
|
|
||||||
metrics.total.textContent = String(total);
|
|
||||||
}
|
|
||||||
if (metrics.active) {
|
|
||||||
metrics.active.textContent = String(active);
|
|
||||||
}
|
|
||||||
if (metrics.inactive) {
|
|
||||||
metrics.inactive.textContent = String(inactive);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderExistingOptions = (
|
|
||||||
selectedCode = existingSelect ? existingSelect.value : ""
|
|
||||||
) => {
|
|
||||||
if (!existingSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const placeholder = existingSelect.querySelector("option[value='']");
|
|
||||||
const placeholderClone = placeholder ? placeholder.cloneNode(true) : null;
|
|
||||||
existingSelect.innerHTML = "";
|
|
||||||
if (placeholderClone) {
|
|
||||||
existingSelect.appendChild(placeholderClone);
|
|
||||||
}
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
currencies.forEach((currency) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = currency.code;
|
|
||||||
option.textContent = currency.name
|
|
||||||
? `${currency.name} (${currency.code})`
|
|
||||||
: currency.code;
|
|
||||||
if (selectedCode === currency.code) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
fragment.appendChild(option);
|
|
||||||
});
|
|
||||||
existingSelect.appendChild(fragment);
|
|
||||||
if (
|
|
||||||
selectedCode &&
|
|
||||||
!currencies.some((item) => item.code === selectedCode)
|
|
||||||
) {
|
|
||||||
existingSelect.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTable = () => {
|
|
||||||
if (!tableBody) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tableBody.innerHTML = "";
|
|
||||||
if (!currencies.length) {
|
|
||||||
if (tableEmptyState) {
|
|
||||||
tableEmptyState.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tableEmptyState) {
|
|
||||||
tableEmptyState.classList.add("hidden");
|
|
||||||
}
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
currencies.forEach((currency) => {
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
|
|
||||||
const codeCell = document.createElement("td");
|
|
||||||
codeCell.textContent = currency.code;
|
|
||||||
row.appendChild(codeCell);
|
|
||||||
|
|
||||||
const nameCell = document.createElement("td");
|
|
||||||
nameCell.textContent = currency.name || "—";
|
|
||||||
row.appendChild(nameCell);
|
|
||||||
|
|
||||||
const symbolCell = document.createElement("td");
|
|
||||||
symbolCell.textContent = currency.symbol || "—";
|
|
||||||
row.appendChild(symbolCell);
|
|
||||||
|
|
||||||
const statusCell = document.createElement("td");
|
|
||||||
statusCell.textContent = currency.is_active ? "Active" : "Inactive";
|
|
||||||
if (currency.is_default) {
|
|
||||||
statusCell.textContent += " (Default)";
|
|
||||||
}
|
|
||||||
row.appendChild(statusCell);
|
|
||||||
|
|
||||||
const actionsCell = document.createElement("td");
|
|
||||||
const editButton = document.createElement("button");
|
|
||||||
editButton.type = "button";
|
|
||||||
editButton.className = "btn";
|
|
||||||
editButton.dataset.action = "edit";
|
|
||||||
editButton.dataset.code = currency.code;
|
|
||||||
editButton.textContent = "Edit";
|
|
||||||
editButton.style.marginRight = "0.5rem";
|
|
||||||
|
|
||||||
const toggleButton = document.createElement("button");
|
|
||||||
toggleButton.type = "button";
|
|
||||||
toggleButton.className = "btn";
|
|
||||||
toggleButton.dataset.action = "toggle";
|
|
||||||
toggleButton.dataset.code = currency.code;
|
|
||||||
toggleButton.textContent = currency.is_active ? "Deactivate" : "Activate";
|
|
||||||
if (currency.is_default && currency.is_active) {
|
|
||||||
toggleButton.disabled = true;
|
|
||||||
toggleButton.title = "The default currency must remain active.";
|
|
||||||
}
|
|
||||||
|
|
||||||
actionsCell.appendChild(editButton);
|
|
||||||
actionsCell.appendChild(toggleButton);
|
|
||||||
|
|
||||||
row.appendChild(actionsCell);
|
|
||||||
fragment.appendChild(row);
|
|
||||||
});
|
|
||||||
tableBody.appendChild(fragment);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshUI = (selectedCode) => {
|
|
||||||
currencies.sort((a, b) => a.code.localeCompare(b.code));
|
|
||||||
renderTable();
|
|
||||||
renderExistingOptions(selectedCode);
|
|
||||||
updateMetrics();
|
|
||||||
};
|
|
||||||
|
|
||||||
const findCurrency = (code) =>
|
|
||||||
currencies.find((item) => item.code === code) || null;
|
|
||||||
|
|
||||||
const setFormForCurrency = (currency) => {
|
|
||||||
if (!form || !codeInput || !nameInput || !symbolInput || !statusSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currency) {
|
|
||||||
form.reset();
|
|
||||||
if (existingSelect) {
|
|
||||||
existingSelect.value = "";
|
|
||||||
}
|
|
||||||
codeInput.readOnly = false;
|
|
||||||
codeInput.value = "";
|
|
||||||
nameInput.value = "";
|
|
||||||
symbolInput.value = "";
|
|
||||||
statusSelect.disabled = false;
|
|
||||||
statusSelect.value = "true";
|
|
||||||
statusSelect.title = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSelect) {
|
|
||||||
existingSelect.value = currency.code;
|
|
||||||
}
|
|
||||||
codeInput.readOnly = true;
|
|
||||||
codeInput.value = currency.code;
|
|
||||||
nameInput.value = currency.name || "";
|
|
||||||
symbolInput.value = currency.symbol || "";
|
|
||||||
statusSelect.value = currency.is_active ? "true" : "false";
|
|
||||||
if (currency.is_default) {
|
|
||||||
statusSelect.disabled = true;
|
|
||||||
statusSelect.value = "true";
|
|
||||||
statusSelect.title = "The default currency must remain active.";
|
|
||||||
} else {
|
|
||||||
statusSelect.disabled = false;
|
|
||||||
statusSelect.title = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFormState = () => {
|
|
||||||
setFormForCurrency(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseError = async (response, fallbackMessage) => {
|
|
||||||
try {
|
|
||||||
const detail = await response.json();
|
|
||||||
if (detail && typeof detail === "object" && detail.detail) {
|
|
||||||
return detail.detail;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// ignore JSON parse errors
|
|
||||||
}
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchCurrenciesFromApi = async () => {
|
|
||||||
const url = `${apiBase}/?include_inactive=true`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = await response.json();
|
|
||||||
if (Array.isArray(list)) {
|
|
||||||
replaceCurrencyList(list);
|
|
||||||
refreshUI(existingSelect ? existingSelect.value : undefined);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Unable to refresh currency list", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
hideFeedback();
|
|
||||||
if (!form || !codeInput || !nameInput || !statusSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const editingCode = existingSelect
|
|
||||||
? uppercaseCode(existingSelect.value)
|
|
||||||
: "";
|
|
||||||
const codeValue = uppercaseCode(codeInput.value);
|
|
||||||
const nameValue = (nameInput.value || "").trim();
|
|
||||||
const symbolValue = normalizeSymbol(symbolInput ? symbolInput.value : "");
|
|
||||||
const isActive = statusSelect.value !== "false";
|
|
||||||
|
|
||||||
if (!nameValue) {
|
|
||||||
showFeedback("Provide a currency name.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editingCode) {
|
|
||||||
if (!codeValue || codeValue.length !== 3) {
|
|
||||||
showFeedback("Provide a three-letter currency code.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = editingCode
|
|
||||||
? {
|
|
||||||
name: nameValue,
|
|
||||||
symbol: symbolValue,
|
|
||||||
is_active: isActive,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
code: codeValue,
|
|
||||||
name: nameValue,
|
|
||||||
symbol: symbolValue,
|
|
||||||
is_active: isActive,
|
|
||||||
};
|
|
||||||
|
|
||||||
const targetCode = editingCode || codeValue;
|
|
||||||
const url = editingCode
|
|
||||||
? `${apiBase}/${encodeURIComponent(editingCode)}`
|
|
||||||
: `${apiBase}/`;
|
|
||||||
|
|
||||||
setButtonLoading(saveButton, true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: editingCode ? "PUT" : "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const message = await parseError(
|
|
||||||
response,
|
|
||||||
editingCode
|
|
||||||
? "Unable to update the currency."
|
|
||||||
: "Unable to create the currency."
|
|
||||||
);
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
const updated = upsertCurrency(result);
|
|
||||||
defaultCurrencyCode = uppercaseCode(defaultCurrencyCode);
|
|
||||||
refreshUI(updated ? updated.code : targetCode);
|
|
||||||
|
|
||||||
if (editingCode) {
|
|
||||||
showFeedback("Currency updated successfully.");
|
|
||||||
if (updated) {
|
|
||||||
setFormForCurrency(updated);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showFeedback("Currency created successfully.");
|
|
||||||
resetFormState();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showFeedback(error.message || "An unexpected error occurred.", "error");
|
|
||||||
} finally {
|
|
||||||
setButtonLoading(saveButton, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggle = async (code, button) => {
|
|
||||||
const record = findCurrency(code);
|
|
||||||
if (!record) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hideFeedback();
|
|
||||||
const nextState = !record.is_active;
|
|
||||||
const url = `${apiBase}/${encodeURIComponent(code)}/activation`;
|
|
||||||
setButtonLoading(button, true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ is_active: nextState }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const message = await parseError(
|
|
||||||
response,
|
|
||||||
nextState
|
|
||||||
? "Unable to activate the currency."
|
|
||||||
: "Unable to deactivate the currency."
|
|
||||||
);
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
const updated = upsertCurrency(result);
|
|
||||||
refreshUI(updated ? updated.code : code);
|
|
||||||
if (existingSelect && existingSelect.value === code && updated) {
|
|
||||||
setFormForCurrency(updated);
|
|
||||||
}
|
|
||||||
const actionMessage = nextState
|
|
||||||
? `Currency ${code} activated.`
|
|
||||||
: `Currency ${code} deactivated.`;
|
|
||||||
showFeedback(actionMessage);
|
|
||||||
} catch (error) {
|
|
||||||
showFeedback(error.message || "An unexpected error occurred.", "error");
|
|
||||||
} finally {
|
|
||||||
setButtonLoading(button, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableClick = (event) => {
|
|
||||||
const button = event.target.closest("button[data-action]");
|
|
||||||
if (!button) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const code = uppercaseCode(button.dataset.code);
|
|
||||||
const action = button.dataset.action;
|
|
||||||
if (!code || !action) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === "edit") {
|
|
||||||
const currency = findCurrency(code);
|
|
||||||
if (currency) {
|
|
||||||
setFormForCurrency(currency);
|
|
||||||
hideFeedback();
|
|
||||||
if (nameInput) {
|
|
||||||
nameInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (action === "toggle") {
|
|
||||||
handleToggle(code, button);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
applyPayload();
|
|
||||||
if (editorSection && editorSection.dataset.defaultCode) {
|
|
||||||
defaultCurrencyCode = uppercaseCode(editorSection.dataset.defaultCode);
|
|
||||||
currencies = currencies.map((record) => {
|
|
||||||
return record
|
|
||||||
? {
|
|
||||||
...record,
|
|
||||||
is_default: record.code === defaultCurrencyCode,
|
|
||||||
}
|
|
||||||
: record;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
apiBase = normalizeApiBase(apiBase);
|
|
||||||
|
|
||||||
refreshUI();
|
|
||||||
|
|
||||||
if (form) {
|
|
||||||
form.addEventListener("submit", handleSubmit);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSelect) {
|
|
||||||
existingSelect.addEventListener("change", (event) => {
|
|
||||||
const selectedCode = uppercaseCode(event.target.value);
|
|
||||||
if (!selectedCode) {
|
|
||||||
hideFeedback();
|
|
||||||
resetFormState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currency = findCurrency(selectedCode);
|
|
||||||
if (currency) {
|
|
||||||
setFormForCurrency(currency);
|
|
||||||
hideFeedback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetButton) {
|
|
||||||
resetButton.addEventListener("click", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
hideFeedback();
|
|
||||||
resetFormState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeInput) {
|
|
||||||
codeInput.addEventListener("input", () => {
|
|
||||||
const value = uppercaseCode(codeInput.value).slice(0, 3);
|
|
||||||
codeInput.value = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableBody) {
|
|
||||||
tableBody.addEventListener("click", handleTableClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchCurrenciesFromApi();
|
|
||||||
});
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
(() => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const dataElement = document.getElementById("production-data");
|
|
||||||
let data = { scenarios: [], production: {}, unit_options: [] };
|
|
||||||
|
|
||||||
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
|
|
||||||
: {},
|
|
||||||
unit_options: Array.isArray(parsed.unit_options)
|
|
||||||
? parsed.unit_options
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} 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 unitSelect = document.getElementById("production-form-unit");
|
|
||||||
const unitSymbolInput = document.getElementById("production-form-unit-symbol");
|
|
||||||
|
|
||||||
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 formatMeasurement = (amount, symbol, name) => {
|
|
||||||
if (symbol) {
|
|
||||||
return `${formatAmount(amount)} ${symbol}`;
|
|
||||||
}
|
|
||||||
if (name) {
|
|
||||||
return `${formatAmount(amount)} ${name}`;
|
|
||||||
}
|
|
||||||
return formatAmount(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>${formatMeasurement(
|
|
||||||
record.amount,
|
|
||||||
record.unit_symbol,
|
|
||||||
record.unit_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 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 unitName = formData.get("unit_name");
|
|
||||||
const unitSymbol = formData.get("unit_symbol");
|
|
||||||
const payload = {
|
|
||||||
scenario_id: scenarioId ? Number(scenarioId) : null,
|
|
||||||
amount: Number(formData.get("amount")),
|
|
||||||
description: formData.get("description") || null,
|
|
||||||
unit_name: unitName ? String(unitName) : null,
|
|
||||||
unit_symbol: unitSymbol ? String(unitSymbol) : 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();
|
|
||||||
syncUnitSelection();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncUnitSelection = () => {
|
|
||||||
if (!unitSelect || !unitSymbolInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!unitSelect.value && unitSelect.options.length > 0) {
|
|
||||||
const firstOption = Array.from(unitSelect.options).find(
|
|
||||||
(option) => option.value
|
|
||||||
);
|
|
||||||
if (firstOption) {
|
|
||||||
firstOption.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selectedOption = unitSelect.options[unitSelect.selectedIndex];
|
|
||||||
unitSymbolInput.value = selectedOption
|
|
||||||
? selectedOption.getAttribute("data-symbol") || ""
|
|
||||||
: "";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (unitSelect) {
|
|
||||||
unitSelect.addEventListener("change", syncUnitSelection);
|
|
||||||
syncUnitSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterSelect && filterSelect.value) {
|
|
||||||
renderProductionRows(filterSelect.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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();
|
|
||||||
|
|
||||||
const feedback = document.getElementById("feedback");
|
|
||||||
if (feedback) {
|
|
||||||
feedback.textContent = `Scenario "${data.name}" created successfully.`;
|
|
||||||
feedback.classList.remove("hidden");
|
|
||||||
setTimeout(() => {
|
|
||||||
feedback.classList.add("hidden");
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
(function () {
|
|
||||||
const dataScript = document.getElementById("theme-settings-data");
|
|
||||||
const form = document.getElementById("theme-settings-form");
|
|
||||||
const feedbackEl = document.getElementById("theme-settings-feedback");
|
|
||||||
const resetBtn = document.getElementById("theme-settings-reset");
|
|
||||||
const panel = document.getElementById("theme-settings");
|
|
||||||
|
|
||||||
if (!dataScript || !form || !feedbackEl || !panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiUrl = panel.getAttribute("data-api");
|
|
||||||
if (!apiUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = JSON.parse(dataScript.textContent || "{}");
|
|
||||||
const currentValues = { ...(parsed.variables || {}) };
|
|
||||||
const defaultValues = parsed.defaults || {};
|
|
||||||
let envOverrides = { ...(parsed.envOverrides || {}) };
|
|
||||||
|
|
||||||
const previewElements = new Map();
|
|
||||||
const inputs = Array.from(form.querySelectorAll(".color-value-input"));
|
|
||||||
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
const key = input.name;
|
|
||||||
const field = input.closest(".color-form-field");
|
|
||||||
const preview = field ? field.querySelector(".color-preview") : null;
|
|
||||||
if (preview) {
|
|
||||||
previewElements.set(input, preview);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(envOverrides, key)) {
|
|
||||||
const overrideValue = envOverrides[key];
|
|
||||||
input.value = overrideValue;
|
|
||||||
input.disabled = true;
|
|
||||||
input.setAttribute("aria-disabled", "true");
|
|
||||||
input.dataset.envOverride = "true";
|
|
||||||
if (field) {
|
|
||||||
field.classList.add("is-env-override");
|
|
||||||
}
|
|
||||||
if (preview) {
|
|
||||||
preview.style.background = overrideValue;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
const previewEl = previewElements.get(input);
|
|
||||||
if (previewEl) {
|
|
||||||
previewEl.style.background = input.value || defaultValues[key] || "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setFeedback(message, type) {
|
|
||||||
feedbackEl.textContent = message;
|
|
||||||
feedbackEl.classList.remove("hidden", "success", "error");
|
|
||||||
if (type) {
|
|
||||||
feedbackEl.classList.add(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFeedback() {
|
|
||||||
feedbackEl.textContent = "";
|
|
||||||
feedbackEl.classList.add("hidden");
|
|
||||||
feedbackEl.classList.remove("success", "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRootVariables(values) {
|
|
||||||
if (!values) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const root = document.documentElement;
|
|
||||||
Object.entries(values).forEach(([key, value]) => {
|
|
||||||
if (typeof key === "string" && typeof value === "string") {
|
|
||||||
root.style.setProperty(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTo(source) {
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
const key = input.name;
|
|
||||||
if (input.disabled) {
|
|
||||||
const previewEl = previewElements.get(input);
|
|
||||||
const fallback = envOverrides[key] || currentValues[key];
|
|
||||||
if (previewEl && fallback) {
|
|
||||||
previewEl.style.background = fallback;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
||||||
input.value = source[key];
|
|
||||||
const previewEl = previewElements.get(input);
|
|
||||||
if (previewEl) {
|
|
||||||
previewEl.style.background = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize previews to current values after page load.
|
|
||||||
resetTo(currentValues);
|
|
||||||
|
|
||||||
resetBtn?.addEventListener("click", () => {
|
|
||||||
resetTo(defaultValues);
|
|
||||||
clearFeedback();
|
|
||||||
setFeedback("Reverted to default values. Submit to save.", "success");
|
|
||||||
});
|
|
||||||
|
|
||||||
form.addEventListener("submit", async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
clearFeedback();
|
|
||||||
|
|
||||||
const payload = {};
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
if (input.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload[input.name] = input.value.trim();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ variables: payload }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
let detail = "Unable to save theme settings.";
|
|
||||||
try {
|
|
||||||
const errorData = await response.json();
|
|
||||||
if (errorData?.detail) {
|
|
||||||
detail = Array.isArray(errorData.detail)
|
|
||||||
? errorData.detail.map((item) => item.msg || item).join("; ")
|
|
||||||
: errorData.detail;
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
// Ignore JSON parse errors and use default detail message.
|
|
||||||
}
|
|
||||||
setFeedback(detail, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const variables = data?.variables || {};
|
|
||||||
const responseOverrides = data?.env_overrides || {};
|
|
||||||
|
|
||||||
Object.assign(currentValues, variables);
|
|
||||||
envOverrides = { ...responseOverrides };
|
|
||||||
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
const key = input.name;
|
|
||||||
const field = input.closest(".color-form-field");
|
|
||||||
const previewEl = previewElements.get(input);
|
|
||||||
const isOverride = Object.prototype.hasOwnProperty.call(
|
|
||||||
envOverrides,
|
|
||||||
key,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isOverride) {
|
|
||||||
const overrideValue = envOverrides[key];
|
|
||||||
input.value = overrideValue;
|
|
||||||
if (!input.disabled) {
|
|
||||||
input.disabled = true;
|
|
||||||
input.setAttribute("aria-disabled", "true");
|
|
||||||
}
|
|
||||||
if (field) {
|
|
||||||
field.classList.add("is-env-override");
|
|
||||||
}
|
|
||||||
if (previewEl) {
|
|
||||||
previewEl.style.background = overrideValue;
|
|
||||||
}
|
|
||||||
} else if (input.disabled) {
|
|
||||||
input.disabled = false;
|
|
||||||
input.removeAttribute("aria-disabled");
|
|
||||||
if (field) {
|
|
||||||
field.classList.remove("is-env-override");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
previewEl &&
|
|
||||||
Object.prototype.hasOwnProperty.call(variables, key)
|
|
||||||
) {
|
|
||||||
previewEl.style.background = variables[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateRootVariables(variables);
|
|
||||||
resetTo(variables);
|
|
||||||
setFeedback("Theme colors updated successfully.", "success");
|
|
||||||
} catch (error) {
|
|
||||||
setFeedback("Network error: unable to save settings.", "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user