feat: Add currency and unit support across models, routes, and templates; enhance UI for consumption, costs, and production
This commit is contained in:
@@ -2,6 +2,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const dataElement = document.getElementById("costs-payload");
|
||||
let capexByScenario = {};
|
||||
let opexByScenario = {};
|
||||
let currencyOptions = [];
|
||||
|
||||
if (dataElement) {
|
||||
try {
|
||||
@@ -13,6 +14,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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);
|
||||
@@ -34,6 +38,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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");
|
||||
|
||||
const showFeedback = (element, message, type = "success") => {
|
||||
if (!element) {
|
||||
@@ -58,9 +64,44 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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 ||
|
||||
@@ -86,7 +127,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
capexRecords.forEach((record) => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${formatAmount(record.amount)}</td>
|
||||
<td>${formatCurrencyAmount(record.amount, record.currency_code)}</td>
|
||||
<td>${record.description || "—"}</td>
|
||||
`;
|
||||
capexTableBody.appendChild(row);
|
||||
@@ -100,15 +141,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
opexRecords.forEach((record) => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${formatAmount(record.amount)}</td>
|
||||
<td>${formatCurrencyAmount(record.amount, record.currency_code)}</td>
|
||||
<td>${record.description || "—"}</td>
|
||||
`;
|
||||
opexTableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
capexTotal.textContent = formatAmount(sumAmount(capexRecords));
|
||||
opexTotal.textContent = formatAmount(sumAmount(opexRecords));
|
||||
capexTotal.textContent = describeTotal(capexRecords);
|
||||
opexTotal.textContent = describeTotal(opexRecords);
|
||||
};
|
||||
|
||||
const toggleCostView = (show) => {
|
||||
@@ -153,6 +194,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -173,10 +226,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
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) {
|
||||
@@ -184,6 +239,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.currency_code) {
|
||||
showFeedback(feedbackEl, "Choose a currency before submitting.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
@@ -206,6 +266,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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) {
|
||||
@@ -221,12 +282,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
};
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user