Files
Autoparts-DB/pos/static/js/config.js
consultoria-as c1d0638b45 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>
2026-04-04 08:19:18 +00:00

518 lines
20 KiB
JavaScript

// /home/Autopartes/pos/static/js/config.js
// Config module: branches, employees, theme, business data
const Config = (() => {
const API = '/pos/api/config';
// Cache for branches (used by employee modal selector)
let _branches = [];
function token() {
return localStorage.getItem('pos_token') || '';
}
function checkAuth() {
if (!token()) {
window.location.href = '/pos/login';
return false;
}
return true;
}
function headers() {
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
}
// -------------------------------------------------------------------------
// Toast notifications
// -------------------------------------------------------------------------
function toast(msg, type) {
var t = document.createElement('div');
t.className = 'cfg-toast cfg-toast--' + (type || 'ok');
t.textContent = msg;
document.body.appendChild(t);
setTimeout(function() { t.remove(); }, 3000);
}
// -------------------------------------------------------------------------
// Modal helpers
// -------------------------------------------------------------------------
function openModal(id) {
var el = document.getElementById(id);
if (el) el.style.display = 'flex';
}
function closeModal(id) {
var el = document.getElementById(id);
if (el) el.style.display = 'none';
}
// -------------------------------------------------------------------------
// Theme switcher
// -------------------------------------------------------------------------
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('nexus-theme', theme); } catch(e) {}
document.querySelectorAll('.theme-btn').forEach(function(btn) {
btn.classList.toggle('is-active', btn.dataset.themeTarget === theme);
});
document.querySelectorAll('.theme-option').forEach(function(opt) {
opt.classList.remove('is-selected');
});
var idx = theme === 'industrial' ? 0 : 1;
var opts = document.querySelectorAll('.theme-option');
if (opts[idx]) opts[idx].classList.add('is-selected');
}
window.setTheme = setTheme;
function selectThemeOption(theme) {
setTheme(theme);
}
window.selectThemeOption = selectThemeOption;
// -------------------------------------------------------------------------
// Live clock
// -------------------------------------------------------------------------
function updateClock() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var el = document.getElementById('live-clock');
if (el) el.textContent = hh + ':' + mm;
}
// -------------------------------------------------------------------------
// Utility: initials from name
// -------------------------------------------------------------------------
function initials(name) {
if (!name) return '??';
var parts = name.trim().split(/\s+/);
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
return parts[0].substring(0, 2).toUpperCase();
}
// -------------------------------------------------------------------------
// Role display helpers
// -------------------------------------------------------------------------
var ROLE_LABELS = {
owner: 'Dueno',
admin: 'Admin',
cashier: 'Cajero',
warehouse: 'Almacenista',
accountant: 'Contador'
};
var ROLE_BADGE = {
owner: 'badge--owner',
admin: 'badge--blue',
cashier: 'badge--green',
warehouse: 'badge--yellow',
accountant: 'badge--purple'
};
function roleBadge(role) {
var cls = ROLE_BADGE[role] || 'badge--ok';
var label = ROLE_LABELS[role] || role;
return '<span class="badge ' + cls + '">' + escHtml(label) + '</span>';
}
function escHtml(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// -------------------------------------------------------------------------
// Branches
// -------------------------------------------------------------------------
async function loadBranches() {
var grid = document.getElementById('branches-grid');
if (!grid) return [];
try {
var res = await fetch(API + '/branches', { headers: headers() });
if (!res.ok) throw new Error('Failed');
var json = await res.json();
_branches = json.data || [];
} catch (e) {
console.error('Config.loadBranches:', e);
_branches = [];
grid.innerHTML = '<div class="device-card"><div class="device-card__body" style="text-align:center;color:var(--color-error,red);">Error al cargar sucursales</div></div>';
return _branches;
}
renderBranches();
populateBranchSelectors();
return _branches;
}
function renderBranches() {
var grid = document.getElementById('branches-grid');
if (!grid) return;
var html = '';
_branches.forEach(function(b, idx) {
var statusBadge = b.is_active
? '<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">' + (idx === 0 ? 'Principal' : 'Activa') + '</span>'
: '<span class="badge badge--inactive" style="padding:0 4px;font-size:0.625rem;">Inactiva</span>';
html += '<div class="device-card">'
+ '<div class="device-card__icon">'
+ '<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>'
+ '</div>'
+ '<div class="device-card__body">'
+ '<div class="device-card__name">' + escHtml(b.name) + '</div>'
+ '<div class="device-card__detail">' + statusBadge + '</div>'
+ (b.address ? '<div class="device-card__detail">' + escHtml(b.address) + '</div>' : '')
+ (b.phone ? '<div class="device-card__detail">' + escHtml(b.phone) + '</div>' : '')
+ '</div></div>';
});
// "Agregar Sucursal" card
html += '<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openModal(\'modal-branch\')">'
+ '<div class="device-card__icon" style="background:transparent;border:2px dashed var(--color-border);">'
+ '<svg viewBox="0 0 24 24" style="stroke:var(--color-text-muted);"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
+ '</div>'
+ '<div class="device-card__body">'
+ '<div class="device-card__name" style="color:var(--color-text-muted);">Agregar Sucursal</div>'
+ '<div class="device-card__detail">Configura una nueva ubicacion</div>'
+ '</div></div>';
grid.innerHTML = html;
}
function populateBranchSelectors() {
var sel = document.getElementById('emp-branch');
if (!sel) return;
// Keep the first option "-- Todas --"
sel.innerHTML = '<option value="">-- Todas --</option>';
_branches.forEach(function(b) {
if (!b.is_active) return;
var opt = document.createElement('option');
opt.value = b.id;
opt.textContent = b.name;
sel.appendChild(opt);
});
}
async function saveBranch(data) {
var res = await fetch(API + '/branches', {
method: 'POST',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) {
var err = await res.json().catch(function() { return { error: res.statusText }; });
throw new Error(err.error || 'Save failed');
}
return res.json();
}
// -------------------------------------------------------------------------
// Employees
// -------------------------------------------------------------------------
async function loadEmployees() {
var tbody = document.getElementById('employees-tbody');
var countEl = document.getElementById('employees-count');
if (!tbody) return [];
var employees = [];
try {
var res = await fetch(API + '/employees', { headers: headers() });
if (!res.ok) throw new Error('Failed');
var json = await res.json();
employees = json.data || [];
} catch (e) {
console.error('Config.loadEmployees:', e);
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:var(--space-5);color:var(--color-error,red);">Error al cargar empleados</td></tr>';
return [];
}
var activeCount = employees.filter(function(e) { return e.is_active; }).length;
if (countEl) countEl.textContent = activeCount + ' empleado' + (activeCount !== 1 ? 's' : '') + ' activo' + (activeCount !== 1 ? 's' : '');
if (employees.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:var(--space-5);color:var(--color-text-muted);">Sin empleados registrados</td></tr>';
return employees;
}
var html = '';
employees.forEach(function(emp) {
var ini = initials(emp.name);
var statusBadge = emp.is_active
? '<span class="badge badge--ok">Activo</span>'
: '<span class="badge badge--inactive">Inactivo</span>';
html += '<tr>'
+ '<td><div class="user-cell">'
+ '<div class="user-cell__avatar">' + ini + '</div>'
+ '<span class="user-cell__name">' + escHtml(emp.name) + '</span>'
+ '</div></td>'
+ '<td>' + escHtml(emp.email || '-') + '</td>'
+ '<td>' + roleBadge(emp.role) + '</td>'
+ '<td>' + escHtml(emp.branch_name || 'Todas') + '</td>'
+ '<td>' + statusBadge + '</td>'
+ '<td>' + (emp.max_discount_pct || 0) + '%</td>'
+ '<td><button class="btn btn--ghost btn--sm" disabled>Editar</button></td>'
+ '</tr>';
});
tbody.innerHTML = html;
return employees;
}
async function saveEmployee(data) {
var res = await fetch(API + '/employees', {
method: 'POST',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) {
var err = await res.json().catch(function() { return { error: res.statusText }; });
throw new Error(err.error || 'Save failed');
}
return res.json();
}
// -------------------------------------------------------------------------
// Business data (read-only)
// -------------------------------------------------------------------------
async function loadBusiness() {
try {
var res = await fetch(API + '/business', { headers: headers() });
if (!res.ok) return;
var d = await res.json();
setVal('biz-razon-social', d.razon_social);
setVal('biz-nombre', d.nombre);
setVal('biz-rfc', d.rfc);
setVal('biz-regimen', d.regimen_fiscal);
setVal('biz-direccion', d.direccion);
setVal('biz-telefono', d.telefono);
setVal('biz-email', d.email);
} catch (e) {
console.error('Config.loadBusiness:', e);
}
}
function setVal(id, v) {
var el = document.getElementById(id);
if (el) el.value = v || '';
}
// -------------------------------------------------------------------------
// Event bindings
// -------------------------------------------------------------------------
function bindEvents() {
// New Branch modal
var btnNewBranch = document.querySelector('#branches-grid');
// The "Agregar Sucursal" card is rendered dynamically, handled via onclick
// Save Branch
var btnSaveBranch = document.getElementById('btn-save-branch');
if (btnSaveBranch) {
btnSaveBranch.addEventListener('click', async function() {
var name = document.getElementById('branch-name').value.trim();
if (!name) { toast('Nombre de sucursal requerido', 'error'); return; }
btnSaveBranch.disabled = true;
btnSaveBranch.textContent = 'Guardando...';
try {
await saveBranch({
name: name,
address: document.getElementById('branch-address').value.trim(),
phone: document.getElementById('branch-phone').value.trim()
});
toast('Sucursal creada');
closeModal('modal-branch');
document.getElementById('branch-name').value = '';
document.getElementById('branch-address').value = '';
document.getElementById('branch-phone').value = '';
await loadBranches();
} catch (e) {
toast(e.message, 'error');
} finally {
btnSaveBranch.disabled = false;
btnSaveBranch.textContent = 'Guardar Sucursal';
}
});
}
// New Employee modal
var btnNewEmp = document.getElementById('btn-new-employee');
if (btnNewEmp) {
btnNewEmp.addEventListener('click', function() {
openModal('modal-employee');
});
}
// Save Employee
var btnSaveEmp = document.getElementById('btn-save-employee');
if (btnSaveEmp) {
btnSaveEmp.addEventListener('click', async function() {
var name = document.getElementById('emp-name').value.trim();
var role = document.getElementById('emp-role').value;
var pin = document.getElementById('emp-pin').value.trim();
if (!name) { toast('Nombre requerido', 'error'); return; }
if (!role) { toast('Selecciona un rol', 'error'); return; }
if (!pin || pin.length !== 4 || !/^\d{4}$/.test(pin)) {
toast('PIN debe ser 4 digitos', 'error');
return;
}
var branchId = document.getElementById('emp-branch').value;
btnSaveEmp.disabled = true;
btnSaveEmp.textContent = 'Guardando...';
try {
await saveEmployee({
name: name,
email: document.getElementById('emp-email').value.trim() || null,
phone: document.getElementById('emp-phone').value.trim() || null,
role: role,
pin: pin,
branch_id: branchId ? parseInt(branchId, 10) : null,
max_discount_pct: parseFloat(document.getElementById('emp-discount').value) || 0
});
toast('Empleado creado');
closeModal('modal-employee');
// Reset form
document.getElementById('emp-name').value = '';
document.getElementById('emp-email').value = '';
document.getElementById('emp-phone').value = '';
document.getElementById('emp-role').value = '';
document.getElementById('emp-pin').value = '';
document.getElementById('emp-branch').value = '';
document.getElementById('emp-discount').value = '0';
await loadEmployees();
} catch (e) {
toast(e.message, 'error');
} finally {
btnSaveEmp.disabled = false;
btnSaveEmp.textContent = 'Guardar Empleado';
}
});
}
// Close modals on overlay click
document.querySelectorAll('.cfg-modal-overlay').forEach(function(overlay) {
overlay.addEventListener('click', function(ev) {
if (ev.target === overlay) {
overlay.style.display = 'none';
}
});
});
// Close modals on Escape key
document.addEventListener('keydown', function(ev) {
if (ev.key === 'Escape') {
document.querySelectorAll('.cfg-modal-overlay').forEach(function(o) {
o.style.display = 'none';
});
}
});
}
// -------------------------------------------------------------------------
// 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
// -------------------------------------------------------------------------
function init() {
if (!checkAuth()) return;
// Restore theme
try {
var saved = localStorage.getItem('nexus-theme');
if (saved === 'industrial' || saved === 'modern') {
setTheme(saved);
}
} catch(e) {}
// Start clock
updateClock();
setInterval(updateClock, 30000);
// Bind UI events
bindEvents();
// Load real data in parallel
loadBranches();
loadEmployees();
loadBusiness();
loadCurrency();
}
document.addEventListener('DOMContentLoaded', init);
return {
init, setTheme, selectThemeOption,
loadBranches, loadEmployees, saveBranch, saveEmployee,
loadCurrency, saveCurrency,
openModal, closeModal
};
})();