// /home/Autopartes/pos/static/js/config.js // Config module: branches, employees, theme, business data const Config = (() => { const API = '/pos/api/config'; const user = window.POS_USER || {}; const canDeleteBranch = (user.role === 'owner' || user.role === 'admin'); // 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 escapeHtml(text) { if (!text) return ''; return String(text).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } 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('pos_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', counter: 'Mostrador', warehouse: 'Almacenista', accountant: 'Contador', workshop: 'Taller', mechanic: 'Mecanico' }; var ROLE_BADGE = { owner: 'badge--owner', admin: 'badge--blue', cashier: 'badge--green', counter: 'badge--gray', warehouse: 'badge--yellow', accountant: 'badge--purple', workshop: 'badge--orange', mechanic: 'badge--teal' }; 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 ? '' + (b.is_main ? 'Principal' : 'Activa') + '' : 'Inactiva'; html += '
' + '
' + '' + '
' + '
' + '
' + escHtml(b.name) + '
' + '
' + statusBadge + (b.rfc ? ' · RFC: ' + escHtml(b.rfc) : '') + (b.cp ? ' · CP: ' + escHtml(b.cp) : '') + '
' + (b.address ? '
' + escHtml(b.address) + '
' : '') + (b.phone ? '
' + escHtml(b.phone) + '
' : '') + '
' + '
' + '' + (canDeleteBranch && b.is_active && !b.is_main ? '' : '') + '
'; }); // "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); }); } function openBranchModal(branch) { document.getElementById('branch-modal-title').textContent = branch ? 'Editar Sucursal' : 'Nueva Sucursal'; document.getElementById('branch-id').value = branch ? branch.id : ''; document.getElementById('branch-name').value = branch ? branch.name : ''; document.getElementById('branch-rfc').value = branch ? (branch.rfc || '') : ''; document.getElementById('branch-razon').value = branch ? (branch.razon_social || '') : ''; document.getElementById('branch-regimen').value = branch ? (branch.regimen_fiscal || '') : ''; document.getElementById('branch-cp').value = branch ? (branch.cp || '') : ''; document.getElementById('branch-direccion-fiscal').value = branch ? (branch.direccion_fiscal || '') : ''; document.getElementById('branch-serie').value = branch ? (branch.serie_cfdi || '') : ''; document.getElementById('branch-folio-inicio').value = branch ? (branch.folio_inicio || '') : ''; document.getElementById('branch-folio-actual').value = branch ? (branch.folio_actual || '') : ''; document.getElementById('branch-email').value = branch ? (branch.email || '') : ''; document.getElementById('branch-address').value = branch ? (branch.address || '') : ''; document.getElementById('branch-phone').value = branch ? (branch.phone || '') : ''; document.getElementById('branch-main').checked = branch ? !!branch.is_main : false; openModal('modal-branch'); } function editBranch(branchId) { var b = _branches.find(function(x) { return x.id === branchId; }); if (!b) { toast('Sucursal no encontrada', 'error'); return; } openBranchModal(b); } async function deleteBranch(branchId) { var b = _branches.find(function(x) { return x.id === branchId; }); if (!b) { toast('Sucursal no encontrada', 'error'); return; } if (b.is_main) { toast('No se puede eliminar la sucursal principal', 'error'); return; } if (!confirm('¿Eliminar la sucursal "' + b.name + '"? Se marcará como inactiva.')) return; try { var res = await fetch(API + '/branches/' + branchId, { method: 'DELETE', headers: headers() }); var json = await res.json().catch(function() { return {}; }); if (!res.ok) throw new Error(json.error || res.statusText); toast('Sucursal eliminada'); loadBranches(); } catch (e) { toast(e.message || 'Error al eliminar sucursal', 'error'); } } async function saveBranch(data) { var branchId = document.getElementById('branch-id').value; var url = API + '/branches' + (branchId ? '/' + branchId : ''); var res = await fetch(url, { method: branchId ? 'PUT' : '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) + '%' + '' + '' + (emp.role !== 'owner' ? ' ' : '') + '' + ''; }); tbody.innerHTML = html; return employees; } async function saveEmployee(data) { // Check if we're editing (modal has editId) or creating var modal = document.getElementById('modal-employee'); 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 deleteEmployee(empId, name) { if (!confirm('¿Eliminar al empleado "' + name + '"? Esta accion lo desactiva.')) return; try { var res = await fetch(API + '/employees/' + empId, { method: 'DELETE', headers: headers() }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Error ' + res.status }; }); throw new Error(err.error || 'Error al eliminar'); } toast('Empleado eliminado', 'ok'); loadEmployees(); } catch (e) { toast(e.message, 'error'); } } 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 employee modal with existing data for editing setVal('emp-name', emp.name); setVal('emp-email', emp.email || ''); setVal('emp-phone', emp.phone || ''); var roleSelect = document.getElementById('emp-role'); if (roleSelect) roleSelect.value = emp.role || 'cashier'; var branchSelect = document.getElementById('emp-branch'); if (branchSelect) branchSelect.value = emp.branch_id || ''; setVal('emp-discount', emp.max_discount_pct || ''); setVal('emp-pin', ''); // Don't pre-fill PIN for security // Store the ID so saveEmployee knows it's an update var modal = document.getElementById('modal-employee'); if (modal) { modal.dataset.editId = empId; var title = modal.querySelector('.modal-title, h3'); if (title) title.textContent = 'Editar Empleado'; } openModal('modal-employee'); } 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'); } } // ------------------------------------------------------------------------- // Receipt / ticket customization // ------------------------------------------------------------------------- let _receiptLogo = ''; async function loadReceiptConfig() { try { var res = await fetch(API + '/receipt', { headers: headers() }); if (!res.ok) return; var d = await res.json(); _receiptLogo = d.logo || ''; setVal('receipt-store-name', d.store_name); setVal('receipt-tagline', d.tagline); setVal('receipt-rfc', d.rfc); setVal('receipt-address', d.address); setVal('receipt-phone', d.phone); setVal('receipt-paper-width', d.paper_width || '80'); setVal('receipt-thanks', d.thanks_message); setVal('receipt-footer', d.footer); setChecked('receipt-show-logo', d.show_logo); setChecked('receipt-show-rfc', d.show_rfc); setChecked('receipt-show-address', d.show_address); setChecked('receipt-show-phone', d.show_phone); setChecked('receipt-show-iva', d.show_iva_breakdown); setChecked('receipt-show-payment', d.show_payment_details); setChecked('receipt-show-employee', d.show_employee); renderReceiptLogoThumb(); } catch (e) { console.error('Config.loadReceiptConfig:', e); } } function setChecked(id, v) { var el = document.getElementById(id); if (el) el.checked = !!v; } function getChecked(id) { var el = document.getElementById(id); return el ? el.checked : false; } function renderReceiptLogoThumb() { var thumb = document.getElementById('receipt-logo-thumb'); var removeBtn = document.getElementById('receipt-logo-remove'); if (!thumb) return; if (_receiptLogo) { thumb.innerHTML = 'Logo ticket'; if (removeBtn) removeBtn.style.display = ''; } else { thumb.innerHTML = 'Sin logo'; if (removeBtn) removeBtn.style.display = 'none'; } } function handleReceiptLogo(input) { var file = input && input.files ? input.files[0] : null; if (!file) return; if (!file.type.match(/^image\/(png|jpeg|jpg|webp)$/)) { toast('Solo se permiten imágenes PNG, JPG o WebP', 'error'); input.value = ''; return; } var reader = new FileReader(); reader.onload = function(e) { var img = new Image(); img.onload = function() { var maxWidth = 300; var scale = Math.min(1, maxWidth / img.width); var w = Math.round(img.width * scale); var h = Math.round(img.height * scale); var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; var ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h); _receiptLogo = canvas.toDataURL('image/jpeg', 0.85); renderReceiptLogoThumb(); toast('Logo cargado. Guarda los cambios para aplicarlo.', 'ok'); }; img.src = e.target.result; }; reader.readAsDataURL(file); input.value = ''; } function removeReceiptLogo() { _receiptLogo = ''; renderReceiptLogoThumb(); } async function saveReceiptConfig() { if (!checkAuth()) return; var data = { logo: _receiptLogo, store_name: getVal('receipt-store-name'), tagline: getVal('receipt-tagline'), rfc: getVal('receipt-rfc'), address: getVal('receipt-address'), phone: getVal('receipt-phone'), paper_width: getVal('receipt-paper-width') || '80', thanks_message: getVal('receipt-thanks'), footer: getVal('receipt-footer'), show_logo: getChecked('receipt-show-logo'), show_rfc: getChecked('receipt-show-rfc'), show_address: getChecked('receipt-show-address'), show_phone: getChecked('receipt-show-phone'), show_iva_breakdown: getChecked('receipt-show-iva'), show_payment_details: getChecked('receipt-show-payment'), show_employee: getChecked('receipt-show-employee'), }; try { var res = await fetch(API + '/receipt', { 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('Configuración de ticket guardada', 'ok'); } catch (e) { toast(e.message, 'error'); } } async function saveAll() { if (!checkAuth()) return; var btn = document.getElementById('btn-save-all'); if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } try { await saveBusiness(); await saveTaxParams(); await saveCurrency(); await saveVehicleCompatSource(); await saveAllowedBrands(); await saveModules(); await saveSalesSettings(); await saveReceiptConfig(); toast('Configuración guardada', 'ok'); } catch (e) { toast(e.message, 'error'); } finally { if (btn) { btn.disabled = false; btn.textContent = 'Guardar Cambios'; } } } // ------------------------------------------------------------------------- // 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, rfc: document.getElementById('branch-rfc').value.trim() || null, razon_social: document.getElementById('branch-razon').value.trim() || null, regimen_fiscal: document.getElementById('branch-regimen').value.trim() || null, cp: document.getElementById('branch-cp').value.trim() || null, direccion_fiscal: document.getElementById('branch-direccion-fiscal').value.trim() || null, serie_cfdi: document.getElementById('branch-serie').value.trim() || null, folio_inicio: document.getElementById('branch-folio-inicio').value ? parseInt(document.getElementById('branch-folio-inicio').value, 10) : null, folio_actual: document.getElementById('branch-folio-actual').value ? parseInt(document.getElementById('branch-folio-actual').value, 10) : null, email: document.getElementById('branch-email').value.trim() || null, address: document.getElementById('branch-address').value.trim() || null, phone: document.getElementById('branch-phone').value.trim() || null, is_main: document.getElementById('branch-main').checked, }); toast('Sucursal guardada'); closeModal('modal-branch'); // Reset form document.getElementById('branch-id').value = ''; document.getElementById('branch-name').value = ''; document.getElementById('branch-rfc').value = ''; document.getElementById('branch-razon').value = ''; document.getElementById('branch-regimen').value = ''; document.getElementById('branch-cp').value = ''; document.getElementById('branch-direccion-fiscal').value = ''; document.getElementById('branch-serie').value = ''; document.getElementById('branch-folio-inicio').value = ''; document.getElementById('branch-folio-actual').value = ''; document.getElementById('branch-email').value = ''; document.getElementById('branch-address').value = ''; document.getElementById('branch-phone').value = ''; document.getElementById('branch-main').checked = false; 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() { var modal = document.getElementById('modal-employee'); if (modal) { delete modal.dataset.editId; var title = modal.querySelector('.modal-title, h3'); if (title) title.textContent = 'Nuevo Empleado'; } 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...'; var isEdit = !!document.getElementById('modal-employee').dataset.editId; 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(isEdit ? 'Empleado actualizado' : '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'; } } } // ------------------------------------------------------------------------- // Vehicle Compatibility Source // ------------------------------------------------------------------------- async function loadVehicleCompatSource() { try { var res = await fetch(API + '/vehicle-compat-source', { headers: headers() }); if (!res.ok) return; var d = await res.json(); var sel = document.getElementById('cfg-compat-source'); if (sel) sel.value = d.source || 'both'; } catch (e) { console.error('Config.loadVehicleCompatSource:', e); } } async function saveVehicleCompatSource() { var sel = document.getElementById('cfg-compat-source'); var btn = document.getElementById('btn-save-compat-source'); if (!sel) return; if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } try { var res = await fetch(API + '/vehicle-compat-source', { method: 'PUT', headers: headers(), body: JSON.stringify({ source: sel.value }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: res.statusText }; }); throw new Error(err.error || 'Save failed'); } toast('Fuente de compatibilidad actualizada'); } catch (e) { toast(e.message, 'error'); } finally { if (btn) { btn.disabled = false; btn.textContent = 'Guardar'; } } } // ------------------------------------------------------------------------- // Allowed Part Brands // ------------------------------------------------------------------------- async function loadAllowedBrands() { var container = document.getElementById('allowed-brands-container'); if (!container) return; try { var res = await fetch(API + '/available-brands', { headers: headers() }); if (!res.ok) throw new Error('Failed to load brands'); var d = await res.json(); var allBrands = d.brands || []; var res2 = await fetch(API + '/allowed-brands', { headers: headers() }); if (!res2.ok) throw new Error('Failed to load allowed brands'); var d2 = await res2.json(); var allowed = d2.brands || []; if (!allBrands.length) { container.innerHTML = '

No hay marcas disponibles.

'; return; } var html = ''; allBrands.forEach(function(b) { var checked = allowed.indexOf(b) !== -1 ? 'checked' : ''; html += ''; }); container.innerHTML = html; } catch (e) { console.error('Config.loadAllowedBrands:', e); if (container) container.innerHTML = '

Error al cargar marcas.

'; } } async function saveAllowedBrands() { var btn = document.getElementById('btn-save-allowed-brands'); if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; } try { var checked = []; document.querySelectorAll('[data-brand-checkbox]').forEach(function(cb) { if (cb.checked) checked.push(cb.value); }); var res = await fetch(API + '/allowed-brands', { method: 'PUT', headers: headers(), body: JSON.stringify({ brands: checked }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: res.statusText }; }); throw new Error(err.error || 'Save failed'); } toast('Marcas permitidas actualizadas'); } catch (e) { toast(e.message, 'error'); } finally { if (btn) { btn.disabled = false; btn.textContent = 'Guardar Marcas'; } } } // ------------------------------------------------------------------------- // Modules / Integrations // ------------------------------------------------------------------------- async function loadModules() { try { var res = await fetch(API + '/modules', { headers: headers() }); if (!res.ok) return; var data = await res.json(); var cbWa = document.getElementById('cfg-module-whatsapp'); var cbMp = document.getElementById('cfg-module-marketplace'); var cbMeli = document.getElementById('cfg-module-meli'); var cbCat = document.getElementById('cfg-module-catalog'); var cbWork = document.getElementById('cfg-module-workshop'); if (cbWa) cbWa.checked = data.whatsapp !== false; if (cbMp) cbMp.checked = data.marketplace !== false; if (cbMeli) cbMeli.checked = data.meli !== false; if (cbCat) cbCat.checked = data.catalog !== false; if (cbWork) cbWork.checked = data.workshop !== false; localStorage.setItem('pos_modules', JSON.stringify(data)); } catch (e) { console.error('Config.loadModules:', e); } try { var res2 = await fetch(API + '/counter-remission', { headers: headers() }); if (!res2.ok) return; var d2 = await res2.json(); var cbCr = document.getElementById('cfg-module-counter-remission'); if (cbCr) cbCr.checked = d2.enabled === true; } catch (e) { console.error('Config.loadCounterRemission:', e); } } async function saveModules() { var cbWa = document.getElementById('cfg-module-whatsapp'); var cbMp = document.getElementById('cfg-module-marketplace'); var cbMeli = document.getElementById('cfg-module-meli'); var cbCat = document.getElementById('cfg-module-catalog'); var cbWork = document.getElementById('cfg-module-workshop'); var cbCr = document.getElementById('cfg-module-counter-remission'); if (!cbWa && !cbMp && !cbMeli && !cbCat && !cbWork && !cbCr) return; try { var data = { whatsapp: cbWa ? cbWa.checked : true, marketplace: cbMp ? cbMp.checked : true, meli: cbMeli ? cbMeli.checked : true, catalog: cbCat ? cbCat.checked : true, workshop: cbWork ? cbWork.checked : true, }; var res = await fetch(API + '/modules', { 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 || 'Save failed'); } localStorage.setItem('pos_modules', JSON.stringify(data)); if (cbCr) { var res2 = await fetch(API + '/counter-remission', { method: 'PUT', headers: headers(), body: JSON.stringify({ enabled: cbCr.checked }) }); if (!res2.ok) { var err2 = await res2.json().catch(function() { return { error: res2.statusText }; }); throw new Error(err2.error || 'Save failed'); } } toast('Módulos actualizados'); } catch (e) { toast(e.message, 'error'); } } async function loadSalesSettings() { try { var res = await fetch(API + '/sales-settings', { headers: headers() }); if (!res.ok) return; var data = await res.json(); var cbZero = document.getElementById('cfg-allow-zero-price'); if (cbZero) cbZero.checked = data.allow_zero_price_sales !== false; var cbNeg = document.getElementById('cfg-allow-negative-stock'); if (cbNeg) cbNeg.checked = data.allow_negative_stock === true; } catch (e) { console.error('Config.loadSalesSettings:', e); } } async function saveSalesSettings() { var cbZero = document.getElementById('cfg-allow-zero-price'); var cbNeg = document.getElementById('cfg-allow-negative-stock'); if (!cbZero && !cbNeg) return; try { var body = {}; if (cbZero) body.allow_zero_price_sales = cbZero.checked; if (cbNeg) body.allow_negative_stock = cbNeg.checked; var res = await fetch(API + '/sales-settings', { method: 'PUT', headers: headers(), body: JSON.stringify(body) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: res.statusText }; }); throw new Error(err.error || 'Save failed'); } } catch (e) { toast(e.message, 'error'); throw e; } } // ------------------------------------------------------------------------- // Tab navigation // ------------------------------------------------------------------------- function switchTab(tab) { document.querySelectorAll('.cfg-tab-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.tab === tab); }); document.querySelectorAll('.settings-section[data-tab]').forEach(function(sec) { var isActive = sec.dataset.tab === tab; sec.classList.toggle('active', isActive); sec.style.display = isActive ? '' : 'none'; }); try { localStorage.setItem('pos_config_tab', tab); } catch(e) {} } // ------------------------------------------------------------------------- // Role permissions editor // ------------------------------------------------------------------------- var _rolePermissions = {}; var _availablePermissions = []; var _workshopPermissions = {}; var _workshopSchema = { statuses: [], actions: [] }; var _currentPermTab = 'modules'; async function loadRolePermissions() { try { var res = await fetch(API + '/role-permissions', { headers: headers() }); if (!res.ok) return; var data = await res.json(); _rolePermissions = data.roles || {}; _availablePermissions = data.available || []; renderRolePermissions(); } catch (e) { console.error('Config.loadRolePermissions:', e); } } function renderRolePermissions() { var container = document.getElementById('role-permissions-container'); var roleSel = document.getElementById('cfg-perm-role'); if (!container || !roleSel) return; var role = roleSel.value; if (!role) { container.innerHTML = '

Selecciona un rol para ver y editar sus permisos.

'; return; } var current = _rolePermissions[role] || []; var html = '
'; _availablePermissions.forEach(function(group) { html += '
'; html += '

' + escapeHtml(group.module) + '

'; group.permissions.forEach(function(p) { var checked = current.indexOf(p.key) !== -1 ? 'checked' : ''; html += ''; }); html += '
'; }); html += '
'; container.innerHTML = html; } async function saveRolePermissions() { var roleSel = document.getElementById('cfg-perm-role'); var status = document.getElementById('role-permissions-status'); if (!roleSel || !roleSel.value) { if (status) status.textContent = 'Selecciona un rol'; return; } var role = roleSel.value; var selected = []; document.querySelectorAll('#role-permissions-container input[data-perm-key]').forEach(function(cb) { if (cb.checked) selected.push(cb.dataset.permKey); }); var payload = { roles: {} }; payload.roles[role] = selected; try { var res = await fetch(API + '/role-permissions', { method: 'PUT', headers: headers(), body: JSON.stringify(payload) }); var data = await res.json().catch(function() { return { error: res.statusText }; }); if (!res.ok) throw new Error(data.error || 'Error al guardar'); _rolePermissions[role] = selected; if (status) status.textContent = 'Permisos guardados y aplicados a empleados existentes'; setTimeout(function() { if (status) status.textContent = ''; }, 4000); } catch (e) { if (status) status.textContent = e.message; toast(e.message, 'error'); } } // Workshop-specific permissions editor // ------------------------------------------------------------------------- async function loadWorkshopPermissions() { try { var res = await fetch(API + '/role-permissions/workshop', { headers: headers() }); if (!res.ok) return; var data = await res.json(); _workshopPermissions = data.roles || {}; _workshopSchema = { statuses: data.statuses || [], actions: data.actions || [] }; renderWorkshopPermissions(); } catch (e) { console.error('Config.loadWorkshopPermissions:', e); } } function renderWorkshopPermissions() { var container = document.getElementById('workshop-permissions-container'); var roleSel = document.getElementById('cfg-perm-role'); if (!container || !roleSel) return; var role = roleSel.value; if (!role) { container.innerHTML = '

Selecciona un rol para ver y editar sus permisos de Taller.

'; return; } var cfg = _workshopPermissions[role] || { statuses: [], actions: [] }; var visibleStatuses = cfg.statuses || []; var visibleActions = cfg.actions || []; var html = '
'; // Statuses html += '
'; html += '

Estatus visibles en Taller

'; (_workshopSchema.statuses || []).forEach(function(s) { var checked = visibleStatuses.indexOf(s.key) !== -1 ? 'checked' : ''; html += ''; }); html += '
'; // Actions html += '
'; html += '

Acciones permitidas en Taller

'; (_workshopSchema.actions || []).forEach(function(a) { var checked = visibleActions.indexOf(a.key) !== -1 ? 'checked' : ''; html += ''; }); html += '
'; html += '
'; container.innerHTML = html; } async function saveWorkshopPermissions() { var roleSel = document.getElementById('cfg-perm-role'); var status = document.getElementById('workshop-permissions-status'); if (!roleSel || !roleSel.value) { if (status) status.textContent = 'Selecciona un rol'; return; } var role = roleSel.value; var statuses = []; var actions = []; document.querySelectorAll('#workshop-permissions-container input[data-ws-status]').forEach(function(cb) { if (cb.checked) statuses.push(cb.dataset.wsStatus); }); document.querySelectorAll('#workshop-permissions-container input[data-ws-action]').forEach(function(cb) { if (cb.checked) actions.push(cb.dataset.wsAction); }); var payload = { roles: {} }; payload.roles[role] = { statuses: statuses, actions: actions }; try { var res = await fetch(API + '/role-permissions/workshop', { method: 'PUT', headers: headers(), body: JSON.stringify(payload) }); var data = await res.json().catch(function() { return { error: res.statusText }; }); if (!res.ok) throw new Error(data.error || 'Error al guardar'); _workshopPermissions[role] = { statuses: statuses, actions: actions }; if (status) status.textContent = 'Permisos de Taller guardados'; setTimeout(function() { if (status) status.textContent = ''; }, 4000); } catch (e) { if (status) status.textContent = e.message; toast(e.message, 'error'); } } function switchPermTab(tab) { _currentPermTab = tab; document.querySelectorAll('#perm-panel-modules, #perm-panel-workshop').forEach(function(el) { el.style.display = 'none'; el.classList.remove('active'); }); document.querySelectorAll('#tab-perm-modules, #tab-perm-workshop').forEach(function(el) { el.classList.remove('active'); }); document.getElementById('perm-panel-' + tab).style.display = 'block'; document.getElementById('perm-panel-' + tab).classList.add('active'); document.getElementById('tab-perm-' + tab).classList.add('active'); } // ------------------------------------------------------------------------- // Init // ------------------------------------------------------------------------- function init() { if (!checkAuth()) return; // Restore theme try { var saved = localStorage.getItem('pos_theme'); if (saved === 'industrial' || saved === 'modern') { setTheme(saved); } } catch(e) {} // Start clock updateClock(); setInterval(updateClock, 30000); // Bind UI events bindEvents(); // Global save button var btnSaveAll = document.getElementById('btn-save-all'); if (btnSaveAll) btnSaveAll.addEventListener('click', saveAll); // Vehicle compat source save button var btnCompat = document.getElementById('btn-save-compat-source'); if (btnCompat) { btnCompat.addEventListener('click', saveVehicleCompatSource); } // Allowed brands save button var btnBrands = document.getElementById('btn-save-allowed-brands'); if (btnBrands) { btnBrands.addEventListener('click', saveAllowedBrands); } // 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'); } }); } // Show permissions tab only for owner/admin var isAdmin = user.role === 'owner' || user.role === 'admin'; var permTabBtn = document.querySelector('.cfg-tab-btn--permissions'); if (permTabBtn) permTabBtn.style.display = isAdmin ? '' : 'none'; // Load real data in parallel loadBranches(); loadEmployees(); loadBusiness(); loadCurrency(); loadVehicleCompatSource(); loadAllowedBrands(); loadModules(); loadSalesSettings(); loadReceiptConfig(); if (isAdmin) { loadRolePermissions(); loadWorkshopPermissions(); } // Activate default or stored tab var defaultTab = 'general'; try { var storedTab = localStorage.getItem('pos_config_tab'); if (storedTab) defaultTab = storedTab; } catch(e) {} switchTab(defaultTab); } document.addEventListener('DOMContentLoaded', init); // Register Cmd+K items if (typeof registerCmdKItem === "function") { registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" }); registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" }); registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" }); registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" }); } return { init, setTheme, selectThemeOption, loadAllowedBrands, saveAllowedBrands, loadBranches, loadEmployees, saveBranch, saveEmployee, editEmployee, deleteEmployee, deleteBranch, loadBusiness, saveBusiness, saveTaxParams, saveAll, loadCurrency, saveCurrency, loadVehicleCompatSource, saveVehicleCompatSource, loadModules, saveModules, loadReceiptConfig, saveReceiptConfig, handleReceiptLogo, removeReceiptLogo, openModal, closeModal, openBranchModal, editBranch, switchTab, loadRolePermissions, renderRolePermissions, saveRolePermissions, loadWorkshopPermissions, renderWorkshopPermissions, saveWorkshopPermissions, switchPermTab }; })();