diff --git a/pos/blueprints/config_bp.py b/pos/blueprints/config_bp.py
index 2d17b3d..922149e 100644
--- a/pos/blueprints/config_bp.py
+++ b/pos/blueprints/config_bp.py
@@ -126,6 +126,29 @@ def create_employee():
return jsonify({'id': emp_id, 'message': 'Employee created'}), 201
+@config_bp.route('/business', methods=['GET'])
+@require_auth()
+def get_business():
+ """Read-only tenant business info from tenant_config."""
+ conn = get_tenant_conn(g.tenant_id)
+ cur = conn.cursor()
+ cur.execute("SELECT key, value FROM tenant_config WHERE key LIKE 'tenant_%'")
+ cfg = {}
+ for row in cur.fetchall():
+ cfg[row[0]] = row[1]
+ cur.close()
+ conn.close()
+ return jsonify({
+ 'razon_social': cfg.get('tenant_razon_social', ''),
+ 'nombre': cfg.get('tenant_nombre', cfg.get('tenant_razon_social', '')),
+ 'rfc': cfg.get('tenant_rfc', ''),
+ 'regimen_fiscal': cfg.get('tenant_regimen_fiscal', ''),
+ 'direccion': cfg.get('tenant_direccion', ''),
+ 'telefono': cfg.get('tenant_telefono', ''),
+ 'email': cfg.get('tenant_email', ''),
+ })
+
+
@config_bp.route('/theme', methods=['GET'])
@require_auth()
def get_theme():
diff --git a/pos/static/js/config.js b/pos/static/js/config.js
index ea97695..1d686b6 100644
--- a/pos/static/js/config.js
+++ b/pos/static/js/config.js
@@ -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 '' + 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 {
- 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 = '
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) {
- 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 = '| 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) {
- 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
};
})();
diff --git a/pos/templates/config.html b/pos/templates/config.html
index 720b127..dcae0d5 100644
--- a/pos/templates/config.html
+++ b/pos/templates/config.html
@@ -1332,40 +1332,34 @@
-
-
-
+ Datos configurados durante el aprovisionamiento del tenant. Contacta soporte para cambios.
@@ -1385,10 +1379,10 @@
- 5 usuarios activos
-
@@ -1400,81 +1394,12 @@
Rol |
Sucursal |
Estado |
-
Último Acceso |
+
Desc. Max |
Acciones |
-
-
- |
-
- |
- admin@nexusautoparts.mx |
- Admin |
- Todas |
- Activo |
- Hoy, 14:32 |
- Editar |
-
-
- |
-
- |
- carlos@nexusautoparts.mx |
- Contador |
- Matriz |
- Activo |
- Hoy, 12:15 |
- Editar |
-
-
- |
-
- |
- laura@nexusautoparts.mx |
- Vendedor |
- Matriz |
- Activo |
- Hoy, 15:48 |
- Editar |
-
-
- |
-
- |
- jorge@nexusautoparts.mx |
- Vendedor |
- Sucursal Norte |
- Activo |
- Ayer, 18:20 |
- Editar |
-
-
- |
-
- |
- roberto@nexusautoparts.mx |
- Bodega |
- Matriz |
- Activo |
- Hoy, 09:30 |
- Editar |
-
+
+ | Cargando empleados... |
@@ -1568,50 +1493,8 @@
-
-
-
-
-
Matriz — Centro
-
- Principal
-
-
Av. Insurgentes Sur 1234, Col. Del Valle
-
3 empleados · 2 terminales POS
-
- Editar
-
-
-
-
-
-
-
-
Sucursal Norte
-
- Activa
-
-
Blvd. Ávila Camacho 456, Naucalpan
-
2 empleados · 1 terminal POS
-
- Editar
-
-
-
-
-
-
-
-
-
-
Agregar Sucursal
-
Configura una nueva ubicación
-
-
+
@@ -1823,6 +1706,176 @@
+
+
+
+
+
+
+
+
+
+