fix(pos): cargar empleados reales en login en vez de datos demo

- Nuevo endpoint GET /pos/api/auth/employees/<tenant_id>
- Login carga empleados dinamicamente del tenant
- Si hay 1 solo empleado, se auto-selecciona
- Removidos 6 usuarios hardcodeados del design system

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 08:46:25 +00:00
parent 56fed52253
commit 7810005a57
2 changed files with 96 additions and 107 deletions

View File

@@ -157,6 +157,38 @@ def login_pin():
})
@auth_bp.route('/employees/<int:tenant_id>', methods=['GET'])
def list_login_employees(tenant_id):
"""Public endpoint: list employees for the login screen (names + roles only, no sensitive data)."""
try:
conn = get_tenant_conn(tenant_id)
except ValueError:
return jsonify({'error': 'Tenant not found'}), 404
cur = conn.cursor()
cur.execute("""
SELECT id, name, role FROM employees
WHERE is_active = true AND pin IS NOT NULL
ORDER BY name
""")
employees = []
for row in cur.fetchall():
name = row[1]
parts = name.split()
initials = ''.join([p[0].upper() for p in parts[:2]]) if parts else '?'
role_labels = {'owner': 'Dueño', 'admin': 'Administrador', 'cashier': 'Cajero', 'warehouse': 'Almacén', 'accountant': 'Contador'}
employees.append({
'id': row[0],
'name': name,
'initials': initials,
'role': row[2],
'role_label': role_labels.get(row[2], row[2])
})
cur.close()
conn.close()
return jsonify({'data': employees})
@auth_bp.route('/me', methods=['GET'])
def auth_me():
"""Get current employee info from token."""

View File

@@ -890,92 +890,9 @@
<!-- USER SELECTION -->
<section class="users-section" aria-labelledby="users-label">
<div class="section-label" id="users-label">Seleccionar usuario</div>
<div class="users-grid" role="radiogroup" aria-label="Usuarios disponibles">
<button
class="user-avatar-btn"
data-user="JR"
data-name="J. Ramírez"
data-role="Vendedor"
role="radio"
aria-checked="false"
aria-label="Jorge Ramírez, Vendedor"
>
<div class="user-initials" aria-hidden="true">JR</div>
<div class="user-name">J. Ramírez</div>
<div class="user-role">Vendedor</div>
</button>
<button
class="user-avatar-btn"
data-user="ML"
data-name="M. López"
data-role="Cajero"
role="radio"
aria-checked="false"
aria-label="María López, Cajero"
>
<div class="user-initials" aria-hidden="true">ML</div>
<div class="user-name">M. López</div>
<div class="user-role">Cajero</div>
</button>
<button
class="user-avatar-btn"
data-user="AP"
data-name="A. Peña"
data-role="Almacén"
role="radio"
aria-checked="false"
aria-label="Alejandro Peña, Almacén"
>
<div class="user-initials" aria-hidden="true">AP</div>
<div class="user-name">A. Peña</div>
<div class="user-role">Almacén</div>
</button>
<button
class="user-avatar-btn"
data-user="SC"
data-name="S. Cruz"
data-role="Supervisor"
role="radio"
aria-checked="false"
aria-label="Sara Cruz, Supervisor"
>
<div class="user-initials" aria-hidden="true">SC</div>
<div class="user-name">S. Cruz</div>
<div class="user-role">Supervisor</div>
</button>
<button
class="user-avatar-btn"
data-user="HG"
data-name="H. García"
data-role="Gerente"
role="radio"
aria-checked="false"
aria-label="Hugo García, Gerente"
>
<div class="user-initials" aria-hidden="true">HG</div>
<div class="user-name">H. García</div>
<div class="user-role">Gerente</div>
</button>
<button
class="user-avatar-btn"
data-user="RT"
data-name="R. Torres"
data-role="Vendedor"
role="radio"
aria-checked="false"
aria-label="Roberto Torres, Vendedor"
>
<div class="user-initials" aria-hidden="true">RT</div>
<div class="user-name">R. Torres</div>
<div class="user-role">Vendedor</div>
</button>
<div class="users-grid" id="usersGrid" role="radiogroup" aria-label="Usuarios disponibles">
<!-- Employees loaded dynamically from API -->
<div style="text-align:center;padding:var(--space-4);color:var(--color-text-muted);">Cargando empleados...</div>
</div>
</section>
@@ -1105,28 +1022,8 @@
});
/* ------------------------------------------------------------------
USER SELECTION
USER SELECTION — handled dynamically in loadEmployees()
------------------------------------------------------------------ */
userBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Deselect all
userBtns.forEach(b => {
b.classList.remove('selected');
b.setAttribute('aria-checked', 'false');
});
// Select clicked
btn.classList.add('selected');
btn.setAttribute('aria-checked', 'true');
state.selectedUser = btn.dataset.user;
state.pin = [];
// Enable PIN pad
enablePinPad();
updatePinDisplay();
updateLoginButton();
});
});
/* ------------------------------------------------------------------
PIN PAD LOGIC
@@ -1373,6 +1270,64 @@
}
});
/* ------------------------------------------------------------------
LOAD REAL EMPLOYEES FROM API
------------------------------------------------------------------ */
function loadEmployees() {
if (!tenantId) {
document.getElementById('usersGrid').innerHTML = '<div style="text-align:center;padding:var(--space-4);color:var(--color-error);">No se especificó tenant. Agrega ?tenant=ID a la URL.</div>';
return;
}
fetch('/pos/api/auth/employees/' + tenantId)
.then(function(r) { return r.json(); })
.then(function(data) {
var grid = document.getElementById('usersGrid');
var employees = data.data || [];
if (!employees.length) {
grid.innerHTML = '<div style="text-align:center;padding:var(--space-4);color:var(--color-text-muted);">No hay empleados registrados.</div>';
return;
}
grid.innerHTML = '';
employees.forEach(function(emp) {
var btn = document.createElement('button');
btn.className = 'user-avatar-btn';
btn.setAttribute('data-user', emp.initials);
btn.setAttribute('data-name', emp.name);
btn.setAttribute('data-role', emp.role_label);
btn.setAttribute('role', 'radio');
btn.setAttribute('aria-checked', 'false');
btn.setAttribute('aria-label', emp.name + ', ' + emp.role_label);
btn.innerHTML = '<div class="user-initials" aria-hidden="true">' + emp.initials + '</div>'
+ '<div class="user-name">' + emp.name + '</div>'
+ '<div class="user-role">' + emp.role_label + '</div>';
btn.addEventListener('click', function() {
// Deselect all
grid.querySelectorAll('.user-avatar-btn').forEach(function(b) {
b.classList.remove('selected');
b.setAttribute('aria-checked', 'false');
});
// Select this one
btn.classList.add('selected');
btn.setAttribute('aria-checked', 'true');
state.selectedUser = emp.initials;
state.pin = [];
enablePinPad();
updatePinDisplay();
updateLoginButton();
});
grid.appendChild(btn);
});
// If only 1 employee, auto-select
if (employees.length === 1) {
grid.querySelector('.user-avatar-btn').click();
}
})
.catch(function() {
document.getElementById('usersGrid').innerHTML = '<div style="text-align:center;padding:var(--space-4);color:var(--color-error);">Error al cargar empleados.</div>';
});
}
/* ------------------------------------------------------------------
INIT
------------------------------------------------------------------ */
@@ -1380,6 +1335,8 @@
disablePinPad();
updatePinDisplay();
updateLoginButton();
// Load real employees
loadEmployees();
</script>