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 - - - -
-
AM
- Adrián Morales -
- - admin@nexusautoparts.mx - Admin - Todas - Activo - Hoy, 14:32 - - - - -
-
CM
- Carlos Mendoza -
- - carlos@nexusautoparts.mx - Contador - Matriz - Activo - Hoy, 12:15 - - - - -
-
LR
- Laura Ríos -
- - laura@nexusautoparts.mx - Vendedor - Matriz - Activo - Hoy, 15:48 - - - - -
-
JP
- Jorge Pérez -
- - jorge@nexusautoparts.mx - Vendedor - Sucursal Norte - Activo - Ayer, 18:20 - - - - -
-
RG
- Roberto García -
- - roberto@nexusautoparts.mx - Bodega - Matriz - Activo - Hoy, 09:30 - - + + Cargando empleados...
@@ -1568,50 +1493,8 @@
-
-
-
- -
-
-
Matriz — Centro
-
- Principal -
-
Av. Insurgentes Sur 1234, Col. Del Valle
-
3 empleados · 2 terminales POS
-
- -
-
-
- -
-
- -
-
-
Sucursal Norte
-
- Activa -
-
Blvd. Ávila Camacho 456, Naucalpan
-
2 empleados · 1 terminal POS
-
- -
-
-
- -
-
- -
-
-
Agregar Sucursal
-
Configura una nueva ubicación
-
-
+
+
Cargando sucursales...
@@ -1823,6 +1706,176 @@ + + + + + + + + + +