feat(pos): add multi-language i18n (#37) and multi-currency USD/MXN (#38)

- i18n.js with 130+ translation keys for es/en, loaded in all 11 templates
- sidebar.js uses t() for all nav labels, adds MX/US language toggle
- app-init.js role labels use i18n
- currency.py service with convert() and format_currency()
- config.py adds DEFAULT_CURRENCY and EXCHANGE_RATE_USD_MXN settings
- config_bp.py adds GET/PUT /pos/api/config/currency endpoints
- config.html adds currency/exchange-rate section (Section 8)
- config.js adds loadCurrency/saveCurrency with localStorage sync
- pos.js fmt() reads pos_currency from localStorage for USD/MXN display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:19:18 +00:00
parent e00dce7d5a
commit c1d0638b45
15 changed files with 554 additions and 24 deletions

View File

@@ -416,6 +416,68 @@ const Config = (() => {
});
}
// -------------------------------------------------------------------------
// Currency
// -------------------------------------------------------------------------
async function loadCurrency() {
try {
var res = await fetch(API + '/currency', { headers: headers() });
if (!res.ok) return;
var d = await res.json();
var selCurrency = document.getElementById('cfg-currency');
var inpRate = document.getElementById('cfg-exchange-rate');
if (selCurrency) selCurrency.value = d.currency || 'MXN';
if (inpRate) inpRate.value = d.exchange_rate || 17.5;
// Store in localStorage for POS fmt() to pick up
localStorage.setItem('pos_currency', d.currency || 'MXN');
localStorage.setItem('pos_exchange_rate', d.exchange_rate || 17.5);
} catch (e) {
console.error('Config.loadCurrency:', e);
}
}
async function saveCurrency() {
var selCurrency = document.getElementById('cfg-currency');
var inpRate = document.getElementById('cfg-exchange-rate');
var statusEl = document.getElementById('currency-status');
var btn = document.getElementById('btn-save-currency');
if (!selCurrency || !inpRate) return;
var currency = selCurrency.value;
var rate = parseFloat(inpRate.value);
if (!rate || rate <= 0) {
toast('Tipo de cambio invalido', 'error');
return;
}
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
try {
var res = await fetch(API + '/currency', {
method: 'PUT',
headers: headers(),
body: JSON.stringify({ currency: currency, exchange_rate: rate })
});
if (!res.ok) {
var err = await res.json().catch(function() { return { error: res.statusText }; });
throw new Error(err.error || 'Save failed');
}
localStorage.setItem('pos_currency', currency);
localStorage.setItem('pos_exchange_rate', rate);
toast('Moneda actualizada');
if (statusEl) statusEl.textContent = currency + ' — TC: ' + rate;
} catch (e) {
toast(e.message, 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = 'Guardar Moneda';
}
}
}
// -------------------------------------------------------------------------
// Init
// -------------------------------------------------------------------------
@@ -441,6 +503,7 @@ const Config = (() => {
loadBranches();
loadEmployees();
loadBusiness();
loadCurrency();
}
document.addEventListener('DOMContentLoaded', init);
@@ -448,6 +511,7 @@ const Config = (() => {
return {
init, setTheme, selectThemeOption,
loadBranches, loadEmployees, saveBranch, saveEmployee,
loadCurrency, saveCurrency,
openModal, closeModal
};
})();