// /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 '' + escHtml(label) + ''; } 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 = '
Error al cargar sucursales
'; 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 ? '' + (idx === 0 ? 'Principal' : 'Activa') + '' : 'Inactiva'; html += '
' + '
' + '' + '
' + '
' + '
' + escHtml(b.name) + '
' + '
' + statusBadge + '
' + (b.address ? '
' + escHtml(b.address) + '
' : '') + (b.phone ? '
' + escHtml(b.phone) + '
' : '') + '
'; }); // "Agregar Sucursal" card html += '
' + '
' + '' + '
' + '
' + '
Agregar Sucursal
' + '
Configura una nueva ubicacion
' + '
'; grid.innerHTML = html; } function populateBranchSelectors() { var sel = document.getElementById('emp-branch'); if (!sel) return; // Keep the first option "-- Todas --" sel.innerHTML = ''; _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 = 'Error al cargar empleados'; 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 = 'Sin empleados registrados'; return employees; } var html = ''; employees.forEach(function(emp) { var ini = initials(emp.name); var statusBadge = emp.is_active ? 'Activo' : 'Inactivo'; html += '' + '
' + '
' + ini + '
' + '' + escHtml(emp.name) + '' + '
' + '' + escHtml(emp.email || '-') + '' + '' + roleBadge(emp.role) + '' + '' + escHtml(emp.branch_name || 'Todas') + '' + '' + statusBadge + '' + '' + (emp.max_discount_pct || 0) + '%' + '' + ''; }); tbody.innerHTML = html; return employees; } async function saveEmployee(data) { // Check if we're editing (modal has editId) or creating var modal = document.getElementById('employee-modal'); var editId = modal ? modal.dataset.editId : null; var url = API + '/employees'; var method = 'POST'; if (editId) { url = API + '/employees/' + editId; method = 'PUT'; // Clear the edit marker so next use is a fresh create delete modal.dataset.editId; } var res = await fetch(url, { method: method, 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 || ''; } function getVal(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; } async function editEmployee(empId) { if (!checkAuth()) return; // Find the employee in the loaded data by re-fetching try { var res = await fetch(API + '/employees', { headers: headers() }); if (!res.ok) throw new Error('Failed to load employees'); var json = await res.json(); var emp = (json.data || []).find(function(e) { return e.id === empId; }); if (!emp) { toast('Empleado no encontrado', 'error'); return; } // Pre-fill the "new employee" modal with existing data for editing setVal('new-emp-name', emp.name); setVal('new-emp-email', emp.email || ''); var roleSelect = document.getElementById('new-emp-role'); if (roleSelect) roleSelect.value = emp.role || 'cashier'; var branchSelect = document.getElementById('new-emp-branch'); if (branchSelect) branchSelect.value = emp.branch_id || ''; setVal('new-emp-discount', emp.max_discount_pct || ''); setVal('new-emp-pin', ''); // Don't pre-fill PIN for security // Store the ID so saveEmployee knows it's an update var modal = document.getElementById('employee-modal'); if (modal) { modal.dataset.editId = empId; var title = modal.querySelector('.modal-title, h3'); if (title) title.textContent = 'Editar Empleado'; } openModal('employee-modal'); } catch (e) { toast('Error: ' + e.message, 'error'); } } async function saveTaxParams() { if (!checkAuth()) return; var data = { tax_iva: getVal('tax-iva') || '16', tax_ieps: getVal('tax-ieps') || '0', invoice_serie: getVal('tax-serie') || 'FA', invoice_folio: getVal('tax-folio') || '1', default_currency: document.getElementById('tax-moneda') ? document.getElementById('tax-moneda').value : 'MXN', default_payment_method: document.getElementById('tax-forma-pago') ? document.getElementById('tax-forma-pago').value : '01', }; try { // Use the business PUT endpoint with tax_ prefixed keys var res = await fetch(API + '/business', { method: 'PUT', headers: headers(), body: JSON.stringify(data), }); if (!res.ok) throw new Error('Error al guardar'); toast('ParĂ¡metros de impuestos guardados', 'ok'); } catch (e) { toast(e.message, 'error'); } } async function saveBusiness() { if (!checkAuth()) return; var data = { razon_social: getVal('biz-razon-social'), nombre: getVal('biz-nombre'), rfc: getVal('biz-rfc'), regimen_fiscal: getVal('biz-regimen'), direccion: getVal('biz-direccion'), telefono: getVal('biz-telefono'), email: getVal('biz-email'), }; try { var res = await fetch(API + '/business', { method: 'PUT', 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 || 'Error al guardar'); } toast('Datos de empresa guardados', 'ok'); } catch (e) { toast(e.message, 'error'); } } // ------------------------------------------------------------------------- // 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(); // Kiosk mode toggle var kioskToggle = document.getElementById('cfg-kiosk-mode'); if (kioskToggle && window.NexusKiosk) { kioskToggle.checked = window.NexusKiosk.isEnabled(); kioskToggle.addEventListener('change', function () { if (this.checked) { window.NexusKiosk.enable(); toast('Modo Kiosko activado'); } else { window.NexusKiosk.disable(); toast('Modo Kiosko desactivado'); } }); } // Load real data in parallel loadBranches(); loadEmployees(); loadBusiness(); loadCurrency(); } document.addEventListener('DOMContentLoaded', init); return { init, setTheme, selectThemeOption, loadBranches, loadEmployees, saveBranch, saveEmployee, editEmployee, loadBusiness, saveBusiness, saveTaxParams, loadCurrency, saveCurrency, openModal, closeModal }; })();