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