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 = ` ${formatCurrencyAmount(record.amount, record.currency_code)} ${record.description || "—"} `; 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 = ` ${formatCurrencyAmount(record.amount, record.currency_code)} ${record.description || "—"} `; 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); } });