- Add catalog module to POS config endpoints, sidebar filter, config UI - Add catalog toggle to Instance Manager tenant modules modal
819 lines
33 KiB
JavaScript
819 lines
33 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 escapeHtml(text) {
|
|
if (!text) return '';
|
|
return String(text).replace(/&/g, '&').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('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" onclick="Config.editEmployee(' + emp.id + ')">Editar</button></td>'
|
|
+ '</tr>';
|
|
});
|
|
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 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 = '<p style="color:var(--color-text-muted);font-size:var(--text-body-sm);">No hay marcas disponibles.</p>';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
allBrands.forEach(function(b) {
|
|
var checked = allowed.indexOf(b) !== -1 ? 'checked' : '';
|
|
html += '<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;font-size:var(--text-body-sm);color:var(--color-text-primary);padding:var(--space-1);">' +
|
|
'<input type="checkbox" value="' + escapeHtml(b) + '" data-brand-checkbox ' + checked + ' style="width:16px;height:16px;cursor:pointer;">' +
|
|
escapeHtml(b) + '</label>';
|
|
});
|
|
container.innerHTML = html;
|
|
} catch (e) {
|
|
console.error('Config.loadAllowedBrands:', e);
|
|
if (container) container.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-body-sm);">Error al cargar marcas.</p>';
|
|
}
|
|
}
|
|
|
|
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');
|
|
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;
|
|
localStorage.setItem('pos_modules', JSON.stringify(data));
|
|
} catch (e) {
|
|
console.error('Config.loadModules:', e);
|
|
}
|
|
}
|
|
|
|
async function saveModules() {
|
|
var btn = event.target;
|
|
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
|
try {
|
|
var data = {
|
|
whatsapp: document.getElementById('cfg-module-whatsapp').checked,
|
|
marketplace: document.getElementById('cfg-module-marketplace').checked,
|
|
meli: document.getElementById('cfg-module-meli').checked,
|
|
catalog: document.getElementById('cfg-module-catalog').checked,
|
|
};
|
|
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));
|
|
toast('Módulos actualizados');
|
|
} catch (e) {
|
|
toast(e.message, 'error');
|
|
} finally {
|
|
if (btn) { btn.disabled = false; btn.textContent = 'Guardar módulos'; }
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 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();
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load real data in parallel
|
|
loadBranches();
|
|
loadEmployees();
|
|
loadBusiness();
|
|
loadCurrency();
|
|
loadVehicleCompatSource();
|
|
loadAllowedBrands();
|
|
loadModules();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
return {
|
|
init, setTheme, selectThemeOption, loadAllowedBrands, saveAllowedBrands,
|
|
loadBranches, loadEmployees, saveBranch, saveEmployee, editEmployee,
|
|
loadBusiness, saveBusiness, saveTaxParams,
|
|
loadCurrency, saveCurrency,
|
|
loadModules, saveModules,
|
|
openModal, closeModal
|
|
};
|
|
// 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: "📊" });
|
|
}
|
|
|
|
})();
|