feat(pos): configuracion funcional — sucursales, empleados, tema
Replace hardcoded demo data in the config page with real API calls. Branches and employees now load from /pos/api/config/* endpoints, with create modals for both. Business data tab reads tenant_config (read-only). Theme selector was already working and is preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
// /home/Autopartes/pos/static/js/config.js
|
||||
// Config module: branches, employees, theme, system settings
|
||||
// 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') || '';
|
||||
}
|
||||
@@ -20,6 +23,30 @@ const Config = (() => {
|
||||
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
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -57,56 +84,338 @@ const Config = (() => {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// API calls using existing config_bp endpoints
|
||||
// Utility: initials from name
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadBranches() {
|
||||
try {
|
||||
const res = await fetch(`${API}/branches`, { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load branches');
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
console.error('Config.loadBranches:', e);
|
||||
return [];
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
async function loadEmployees() {
|
||||
// -------------------------------------------------------------------------
|
||||
// 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 {
|
||||
const res = await fetch(`${API}/employees`, { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load employees');
|
||||
return await res.json();
|
||||
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.loadEmployees:', e);
|
||||
return [];
|
||||
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) {
|
||||
const res = await fetch(`${API}/branches`, {
|
||||
var res = await fetch(API + '/branches', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||
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) {
|
||||
const res = await fetch(`${API}/employees`, {
|
||||
var res = await fetch(API + '/employees', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||
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';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -125,15 +434,20 @@ const Config = (() => {
|
||||
updateClock();
|
||||
setInterval(updateClock, 30000);
|
||||
|
||||
// Load initial data
|
||||
// Bind UI events
|
||||
bindEvents();
|
||||
|
||||
// Load real data in parallel
|
||||
loadBranches();
|
||||
loadEmployees();
|
||||
loadBusiness();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return {
|
||||
init, setTheme, selectThemeOption,
|
||||
loadBranches, loadEmployees, saveBranch, saveEmployee
|
||||
loadBranches, loadEmployees, saveBranch, saveEmployee,
|
||||
openModal, closeModal
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user