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:
@@ -126,6 +126,29 @@ def create_employee():
|
|||||||
return jsonify({'id': emp_id, 'message': 'Employee created'}), 201
|
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'])
|
@config_bp.route('/theme', methods=['GET'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def get_theme():
|
def get_theme():
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// /home/Autopartes/pos/static/js/config.js
|
// /home/Autopartes/pos/static/js/config.js
|
||||||
// Config module: branches, employees, theme, system settings
|
// Config module: branches, employees, theme, business data
|
||||||
|
|
||||||
const Config = (() => {
|
const Config = (() => {
|
||||||
const API = '/pos/api/config';
|
const API = '/pos/api/config';
|
||||||
|
|
||||||
|
// Cache for branches (used by employee modal selector)
|
||||||
|
let _branches = [];
|
||||||
|
|
||||||
function token() {
|
function token() {
|
||||||
return localStorage.getItem('pos_token') || '';
|
return localStorage.getItem('pos_token') || '';
|
||||||
}
|
}
|
||||||
@@ -20,6 +23,30 @@ const Config = (() => {
|
|||||||
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
|
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
|
// Theme switcher
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -57,56 +84,338 @@ const Config = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// API calls using existing config_bp endpoints
|
// Utility: initials from name
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
async function loadBranches() {
|
function initials(name) {
|
||||||
try {
|
if (!name) return '??';
|
||||||
const res = await fetch(`${API}/branches`, { headers: headers() });
|
var parts = name.trim().split(/\s+/);
|
||||||
if (!res.ok) throw new Error('Failed to load branches');
|
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||||
return await res.json();
|
return parts[0].substring(0, 2).toUpperCase();
|
||||||
} catch (e) {
|
|
||||||
console.error('Config.loadBranches:', e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
const res = await fetch(`${API}/employees`, { headers: headers() });
|
var res = await fetch(API + '/branches', { headers: headers() });
|
||||||
if (!res.ok) throw new Error('Failed to load employees');
|
if (!res.ok) throw new Error('Failed');
|
||||||
return await res.json();
|
var json = await res.json();
|
||||||
|
_branches = json.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Config.loadEmployees:', e);
|
console.error('Config.loadBranches:', e);
|
||||||
return [];
|
_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) {
|
async function saveBranch(data) {
|
||||||
const res = await fetch(`${API}/branches`, {
|
var res = await fetch(API + '/branches', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
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');
|
throw new Error(err.error || 'Save failed');
|
||||||
}
|
}
|
||||||
return res.json();
|
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) {
|
async function saveEmployee(data) {
|
||||||
const res = await fetch(`${API}/employees`, {
|
var res = await fetch(API + '/employees', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
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');
|
throw new Error(err.error || 'Save failed');
|
||||||
}
|
}
|
||||||
return res.json();
|
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
|
// Init
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -125,15 +434,20 @@ const Config = (() => {
|
|||||||
updateClock();
|
updateClock();
|
||||||
setInterval(updateClock, 30000);
|
setInterval(updateClock, 30000);
|
||||||
|
|
||||||
// Load initial data
|
// Bind UI events
|
||||||
|
bindEvents();
|
||||||
|
|
||||||
|
// Load real data in parallel
|
||||||
loadBranches();
|
loadBranches();
|
||||||
loadEmployees();
|
loadEmployees();
|
||||||
|
loadBusiness();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init, setTheme, selectThemeOption,
|
init, setTheme, selectThemeOption,
|
||||||
loadBranches, loadEmployees, saveBranch, saveEmployee
|
loadBranches, loadEmployees, saveBranch, saveEmployee,
|
||||||
|
openModal, closeModal
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1332,40 +1332,34 @@
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Razón Social</label>
|
<label class="form-label">Razón Social</label>
|
||||||
<input class="form-input" type="text" value="Nexus Autoparts S.A. de C.V." />
|
<input class="form-input" id="biz-razon-social" type="text" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Nombre Comercial</label>
|
<label class="form-label">Nombre Comercial</label>
|
||||||
<input class="form-input" type="text" value="Nexus Autoparts" />
|
<input class="form-input" id="biz-nombre" type="text" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">RFC</label>
|
<label class="form-label">RFC</label>
|
||||||
<input class="form-input" type="text" value="NAU260101ABC" />
|
<input class="form-input" id="biz-rfc" type="text" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Régimen Fiscal</label>
|
<label class="form-label">Régimen Fiscal</label>
|
||||||
<select class="form-select">
|
<input class="form-input" id="biz-regimen" type="text" value="" readonly />
|
||||||
<option selected>601 - General de Ley Personas Morales</option>
|
|
||||||
<option>603 - Personas Morales con Fines no Lucrativos</option>
|
|
||||||
<option>612 - Personas Físicas con Actividades Empresariales</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-group--full">
|
<div class="form-group form-group--full">
|
||||||
<label class="form-label">Dirección Fiscal</label>
|
<label class="form-label">Dirección Fiscal</label>
|
||||||
<input class="form-input" type="text" value="Av. Insurgentes Sur 1234, Col. Del Valle, Benito Juárez, CDMX, C.P. 03100" />
|
<input class="form-input" id="biz-direccion" type="text" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Teléfono</label>
|
<label class="form-label">Teléfono</label>
|
||||||
<input class="form-input" type="tel" value="(55) 1234-5678" />
|
<input class="form-input" id="biz-telefono" type="tel" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Email</label>
|
<label class="form-label">Email</label>
|
||||||
<input class="form-input" type="email" value="contacto@nexusautoparts.mx" />
|
<input class="form-input" id="biz-email" type="email" value="" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<p class="form-hint" style="margin-top: var(--space-3);">Datos configurados durante el aprovisionamiento del tenant. Contacta soporte para cambios.</p>
|
||||||
<button class="btn btn--primary btn--sm">Guardar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1385,10 +1379,10 @@
|
|||||||
|
|
||||||
<div class="settings-card" style="padding: 0;">
|
<div class="settings-card" style="padding: 0;">
|
||||||
<div style="padding: var(--space-4) var(--space-5); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--color-border);">
|
<div style="padding: var(--space-4) var(--space-5); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--color-border);">
|
||||||
<span style="font-weight: var(--font-weight-semibold); color: var(--color-text-primary);">5 usuarios activos</span>
|
<span id="employees-count" style="font-weight: var(--font-weight-semibold); color: var(--color-text-primary);">Cargando...</span>
|
||||||
<button class="btn btn--primary btn--sm">
|
<button class="btn btn--primary btn--sm" id="btn-new-employee">
|
||||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
Nuevo Usuario
|
Nuevo Empleado
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper" style="border: none; border-radius: 0; box-shadow: none;">
|
<div class="table-wrapper" style="border: none; border-radius: 0; box-shadow: none;">
|
||||||
@@ -1400,81 +1394,12 @@
|
|||||||
<th>Rol</th>
|
<th>Rol</th>
|
||||||
<th>Sucursal</th>
|
<th>Sucursal</th>
|
||||||
<th>Estado</th>
|
<th>Estado</th>
|
||||||
<th>Último Acceso</th>
|
<th>Desc. Max</th>
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="employees-tbody">
|
||||||
<tr>
|
<tr><td colspan="7" style="text-align:center; padding: var(--space-5);">Cargando empleados...</td></tr>
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<div class="user-cell__avatar">AM</div>
|
|
||||||
<span class="user-cell__name">Adrián Morales</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>admin@nexusautoparts.mx</td>
|
|
||||||
<td><span class="badge badge--admin">Admin</span></td>
|
|
||||||
<td>Todas</td>
|
|
||||||
<td><span class="badge badge--ok">Activo</span></td>
|
|
||||||
<td>Hoy, 14:32</td>
|
|
||||||
<td><button class="btn btn--ghost btn--sm">Editar</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<div class="user-cell__avatar">CM</div>
|
|
||||||
<span class="user-cell__name">Carlos Mendoza</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>carlos@nexusautoparts.mx</td>
|
|
||||||
<td><span class="badge badge--pending">Contador</span></td>
|
|
||||||
<td>Matriz</td>
|
|
||||||
<td><span class="badge badge--ok">Activo</span></td>
|
|
||||||
<td>Hoy, 12:15</td>
|
|
||||||
<td><button class="btn btn--ghost btn--sm">Editar</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<div class="user-cell__avatar">LR</div>
|
|
||||||
<span class="user-cell__name">Laura Ríos</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>laura@nexusautoparts.mx</td>
|
|
||||||
<td><span class="badge badge--ok">Vendedor</span></td>
|
|
||||||
<td>Matriz</td>
|
|
||||||
<td><span class="badge badge--ok">Activo</span></td>
|
|
||||||
<td>Hoy, 15:48</td>
|
|
||||||
<td><button class="btn btn--ghost btn--sm">Editar</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<div class="user-cell__avatar">JP</div>
|
|
||||||
<span class="user-cell__name">Jorge Pérez</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>jorge@nexusautoparts.mx</td>
|
|
||||||
<td><span class="badge badge--ok">Vendedor</span></td>
|
|
||||||
<td>Sucursal Norte</td>
|
|
||||||
<td><span class="badge badge--ok">Activo</span></td>
|
|
||||||
<td>Ayer, 18:20</td>
|
|
||||||
<td><button class="btn btn--ghost btn--sm">Editar</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<div class="user-cell__avatar">RG</div>
|
|
||||||
<span class="user-cell__name">Roberto García</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>roberto@nexusautoparts.mx</td>
|
|
||||||
<td><span class="badge badge--ok">Bodega</span></td>
|
|
||||||
<td>Matriz</td>
|
|
||||||
<td><span class="badge badge--ok">Activo</span></td>
|
|
||||||
<td>Hoy, 09:30</td>
|
|
||||||
<td><button class="btn btn--ghost btn--sm">Editar</button></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -1568,50 +1493,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="device-grid">
|
<div class="device-grid" id="branches-grid">
|
||||||
<div class="device-card">
|
<div class="device-card"><div class="device-card__body" style="text-align:center;">Cargando sucursales...</div></div>
|
||||||
<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">Matriz — Centro</div>
|
|
||||||
<div class="device-card__detail">
|
|
||||||
<span class="badge badge--ok" style="padding: 0 4px; font-size: 0.625rem;">Principal</span>
|
|
||||||
</div>
|
|
||||||
<div class="device-card__detail">Av. Insurgentes Sur 1234, Col. Del Valle</div>
|
|
||||||
<div class="device-card__detail">3 empleados · 2 terminales POS</div>
|
|
||||||
<div class="device-card__actions">
|
|
||||||
<button class="btn btn--ghost btn--sm">Editar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">Sucursal Norte</div>
|
|
||||||
<div class="device-card__detail">
|
|
||||||
<span class="badge badge--ok" style="padding: 0 4px; font-size: 0.625rem;">Activa</span>
|
|
||||||
</div>
|
|
||||||
<div class="device-card__detail">Blvd. Ávila Camacho 456, Naucalpan</div>
|
|
||||||
<div class="device-card__detail">2 empleados · 1 terminal POS</div>
|
|
||||||
<div class="device-card__actions">
|
|
||||||
<button class="btn btn--ghost btn--sm">Editar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="device-card" style="border-style: dashed; cursor: pointer;">
|
|
||||||
<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 ubicación</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1823,6 +1706,176 @@
|
|||||||
</main>
|
</main>
|
||||||
</div><!-- /app-shell -->
|
</div><!-- /app-shell -->
|
||||||
|
|
||||||
|
<!-- =====================================================================
|
||||||
|
MODALS
|
||||||
|
===================================================================== -->
|
||||||
|
|
||||||
|
<!-- Modal: Nueva Sucursal -->
|
||||||
|
<div class="cfg-modal-overlay" id="modal-branch" style="display:none;">
|
||||||
|
<div class="cfg-modal">
|
||||||
|
<div class="cfg-modal__header">
|
||||||
|
<h3 class="cfg-modal__title">Nueva Sucursal</h3>
|
||||||
|
<button class="cfg-modal__close" onclick="Config.closeModal('modal-branch')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="cfg-modal__body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Nombre</label>
|
||||||
|
<input class="form-input" id="branch-name" type="text" placeholder="Ej. Sucursal Norte" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Direccion</label>
|
||||||
|
<input class="form-input" id="branch-address" type="text" placeholder="Calle, Colonia, Ciudad" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Telefono</label>
|
||||||
|
<input class="form-input" id="branch-phone" type="tel" placeholder="(55) 1234-5678" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cfg-modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="Config.closeModal('modal-branch')">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" id="btn-save-branch">Guardar Sucursal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Nuevo Empleado -->
|
||||||
|
<div class="cfg-modal-overlay" id="modal-employee" style="display:none;">
|
||||||
|
<div class="cfg-modal">
|
||||||
|
<div class="cfg-modal__header">
|
||||||
|
<h3 class="cfg-modal__title">Nuevo Empleado</h3>
|
||||||
|
<button class="cfg-modal__close" onclick="Config.closeModal('modal-employee')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="cfg-modal__body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nombre Completo *</label>
|
||||||
|
<input class="form-input" id="emp-name" type="text" placeholder="Nombre y apellido" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input class="form-input" id="emp-email" type="email" placeholder="correo@empresa.mx" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Telefono</label>
|
||||||
|
<input class="form-input" id="emp-phone" type="tel" placeholder="(55) 1234-5678" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Rol *</label>
|
||||||
|
<select class="form-select" id="emp-role">
|
||||||
|
<option value="">-- Seleccionar --</option>
|
||||||
|
<option value="admin">Administrador</option>
|
||||||
|
<option value="cashier">Cajero</option>
|
||||||
|
<option value="warehouse">Almacenista</option>
|
||||||
|
<option value="accountant">Contador</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">PIN (4 digitos) *</label>
|
||||||
|
<input class="form-input" id="emp-pin" type="password" maxlength="4" pattern="\d{4}" placeholder="0000" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Sucursal</label>
|
||||||
|
<select class="form-select" id="emp-branch">
|
||||||
|
<option value="">-- Todas --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Descuento Max. (%)</label>
|
||||||
|
<input class="form-input" id="emp-discount" type="number" min="0" max="100" value="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cfg-modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="Config.closeModal('modal-employee')">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" id="btn-save-employee">Guardar Empleado</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Modal styles */
|
||||||
|
.cfg-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: fadeIn var(--duration-fast) var(--ease-in-out);
|
||||||
|
}
|
||||||
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
|
||||||
|
.cfg-modal {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
width: 520px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
.cfg-modal__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.cfg-modal__title {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: var(--heading-weight-secondary);
|
||||||
|
font-size: var(--text-h5);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.cfg-modal__close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.cfg-modal__close:hover { color: var(--color-text-primary); }
|
||||||
|
.cfg-modal__body { padding: var(--space-5); }
|
||||||
|
.cfg-modal__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra badge colors for roles */
|
||||||
|
.badge--owner { background: rgba(245,166,35,0.15); color: var(--color-primary); }
|
||||||
|
.badge--blue { background: rgba(99,102,241,0.15); color: #818cf8; }
|
||||||
|
.badge--green { background: rgba(34,197,94,0.15); color: var(--color-success); }
|
||||||
|
.badge--yellow { background: rgba(234,179,8,0.15); color: #eab308; }
|
||||||
|
.badge--purple { background: rgba(168,85,247,0.15); color: #a855f7; }
|
||||||
|
|
||||||
|
/* Toast notification */
|
||||||
|
.cfg-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--space-5);
|
||||||
|
right: var(--space-5);
|
||||||
|
z-index: 10000;
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
animation: slideUp var(--duration-normal) var(--ease-in-out);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
.cfg-toast--ok { background: var(--color-success); }
|
||||||
|
.cfg-toast--error { background: var(--color-error, #ef4444); }
|
||||||
|
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
</style>
|
||||||
|
|
||||||
<script src="/pos/static/js/app-init.js"></script>
|
<script src="/pos/static/js/app-init.js"></script>
|
||||||
<script src="/pos/static/js/sidebar.js"></script>
|
<script src="/pos/static/js/sidebar.js"></script>
|
||||||
<script src="/pos/static/js/config.js"></script>
|
<script src="/pos/static/js/config.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user