feat(design): add 16 new components + update 5 pages (ronda 2)
Bloqueantes POS: modal-pago, ticket-termico, banner-cliente, fkeys-footer, columnas-costo-margen Componentes nuevos: calculadora-cambio, panel-deslizante, badge-cfdi, arbol-colapsable, tarjeta-metrica, grafica-barras, selector-periodo, etiqueta-codigo-barras Estados: estado-vacio, banner-offline, modal-confirmacion Páginas actualizadas: facturación, contabilidad, dashboard, configuración, reportes
This commit is contained in:
@@ -188,7 +188,7 @@
|
||||
===================================================================== */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
========================================================================== */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-right: 1px solid var(--color-border);
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2573,6 +2573,131 @@
|
||||
|
||||
</div><!-- /app-shell -->
|
||||
|
||||
<!-- =========================================================================
|
||||
MODAL: DETALLE CFDI + XML PREVIEW
|
||||
========================================================================= -->
|
||||
<div class="modal-overlay" id="modalDetalleOverlay" style="display:none; position:fixed; inset:0; z-index:var(--z-modal); background:var(--overlay-backdrop); display:none; align-items:center; justify-content:center;">
|
||||
<div class="modal-card" style="background:var(--color-bg-elevated); border:1px solid var(--color-border); border-radius:var(--radius-lg); width:720px; max-width:95vw; max-height:90vh; overflow:auto; box-shadow:var(--shadow-xl);">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:var(--space-5) var(--space-6); border-bottom:1px solid var(--color-border);">
|
||||
<div>
|
||||
<div style="font-family:var(--font-heading); font-size:var(--text-h5); font-weight:var(--heading-weight-primary); color:var(--color-text-primary);">Detalle de Factura</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); margin-top:2px;">NAP-001289 — Timbrada</div>
|
||||
</div>
|
||||
<button onclick="document.getElementById('modalDetalleOverlay').style.display='none'" style="background:none; border:none; color:var(--color-text-muted); font-size:1.4rem; cursor:pointer; padding:var(--space-2);">✕</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-5) var(--space-6);">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-4); margin-bottom:var(--space-6);">
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Emisor</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">Nexus Autoparts SA de CV</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">NAP960714JK3</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Receptor</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">Taller Mecánico Rodríguez</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-body-sm); color:var(--color-text-secondary);">TMR8402156HJ</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">UUID</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-caption); color:var(--color-text-accent); word-break:break-all;">6ba7b810-9dad-11d1-80b4-00c04fd430c8</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-1);">Total</div>
|
||||
<div style="font-family:var(--font-mono); font-size:var(--text-h4); font-weight:var(--font-weight-bold); color:var(--color-text-primary);">$4,002.00</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- XML Preview -->
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;"><?xml version="1.0" encoding="UTF-8"?>
|
||||
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
|
||||
Version="4.0" Serie="A" Folio="001289"
|
||||
Fecha="2026-03-31T14:22:00" FormaPago="01"
|
||||
SubTotal="3450.00" Moneda="MXN"
|
||||
Total="4002.00" TipoDeComprobante="I"
|
||||
MetodoPago="PUE" LugarExpedicion="64000">
|
||||
<cfdi:Emisor Rfc="NAP960714JK3"
|
||||
Nombre="Nexus Autoparts SA de CV"
|
||||
RegimenFiscal="601"/>
|
||||
<cfdi:Receptor Rfc="TMR8402156HJ"
|
||||
Nombre="Taller Mecánico Rodríguez"
|
||||
UsoCFDI="G03" DomicilioFiscalReceptor="64000"
|
||||
RegimenFiscalReceptor="612"/>
|
||||
<cfdi:Conceptos>
|
||||
<cfdi:Concepto ClaveProdServ="25174800"
|
||||
Cantidad="4" ClaveUnidad="H87"
|
||||
Descripcion="Balatas delanteras TRW"
|
||||
ValorUnitario="485.00" Importe="1940.00"/>
|
||||
</cfdi:Conceptos>
|
||||
</cfdi:Comprobante></pre>
|
||||
</div>
|
||||
<div style="display:flex; gap:var(--space-3); justify-content:flex-end; padding:var(--space-4) var(--space-6); border-top:1px solid var(--color-border);">
|
||||
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Descargar XML</button>
|
||||
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Descargar PDF</button>
|
||||
<button style="padding:var(--space-2) var(--space-5); border:1px solid var(--btn-danger-bg); border-radius:var(--radius-md); background:var(--btn-danger-bg); color:var(--btn-danger-text); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold);" onclick="document.getElementById('modalDetalleOverlay').style.display='none'; document.getElementById('modalCancelOverlay').style.display='flex';">Cancelar CFDI</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================================================================
|
||||
MODAL: CANCELACIÓN CFDI — Selector Motivo SAT
|
||||
========================================================================= -->
|
||||
<div class="modal-overlay" id="modalCancelOverlay" style="display:none; position:fixed; inset:0; z-index:var(--z-modal); background:var(--overlay-backdrop); display:none; align-items:center; justify-content:center;">
|
||||
<div class="modal-card" style="background:var(--color-bg-elevated); border:1px solid var(--color-border); border-radius:var(--radius-lg); width:520px; max-width:95vw; box-shadow:var(--shadow-xl);">
|
||||
<div style="display:flex; align-items:center; gap:var(--space-3); padding:var(--space-5) var(--space-6); border-bottom:1px solid var(--color-border);">
|
||||
<div style="width:40px; height:40px; border-radius:var(--radius-full); background:rgba(239,68,68,0.12); display:flex; align-items:center; justify-content:center; flex-shrink:0;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-error)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-family:var(--font-heading); font-size:var(--text-h5); font-weight:var(--heading-weight-primary); color:var(--color-text-primary);">Cancelar CFDI</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">NAP-001289 — $4,002.00 MXN</div>
|
||||
</div>
|
||||
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="background:none; border:none; color:var(--color-text-muted); font-size:1.4rem; cursor:pointer; padding:var(--space-2); margin-left:auto;">✕</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-5) var(--space-6);">
|
||||
<div style="font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold); color:var(--color-text-primary); margin-bottom:var(--space-3);">Motivo de cancelación (SAT)</div>
|
||||
<div style="display:flex; flex-direction:column; gap:var(--space-2);">
|
||||
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
|
||||
<input type="radio" name="motivo-sat" value="01" style="margin-top:3px; accent-color:var(--color-primary);" checked>
|
||||
<div>
|
||||
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">01 — Comprobante emitido con errores con relación</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">Se sustituirá por otro CFDI. Requiere UUID de reemplazo.</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
|
||||
<input type="radio" name="motivo-sat" value="02" style="margin-top:3px; accent-color:var(--color-primary);">
|
||||
<div>
|
||||
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">02 — Comprobante emitido con errores sin relación</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">No se sustituirá por otro CFDI.</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
|
||||
<input type="radio" name="motivo-sat" value="03" style="margin-top:3px; accent-color:var(--color-primary);">
|
||||
<div>
|
||||
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">03 — No se llevó a cabo la operación</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">La operación que amparó el CFDI no se realizó.</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border:1px solid var(--color-border); border-radius:var(--radius-md); cursor:pointer; transition:var(--transition-fast);" onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='var(--color-border)'">
|
||||
<input type="radio" name="motivo-sat" value="04" style="margin-top:3px; accent-color:var(--color-primary);">
|
||||
<div>
|
||||
<div style="font-weight:var(--font-weight-semibold); font-size:var(--text-body-sm);">04 — Operación nominativa relacionada en una factura global</div>
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted);">Cancelar cuando la operación se desglosó en una factura global.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<!-- UUID de reemplazo (solo para motivo 01) -->
|
||||
<div style="margin-top:var(--space-4);">
|
||||
<label style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); display:block; margin-bottom:var(--space-1);">UUID de CFDI sustituto (motivo 01)</label>
|
||||
<input type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" style="width:100%; padding:var(--space-2) var(--space-3); border:1px solid var(--color-border); border-radius:var(--radius-md); background:var(--color-surface-2); color:var(--color-text-primary); font-family:var(--font-mono); font-size:var(--text-body-sm);">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:var(--space-3); justify-content:flex-end; padding:var(--space-4) var(--space-6); border-top:1px solid var(--color-border);">
|
||||
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="padding:var(--space-2) var(--space-5); border:1px solid var(--color-border); border-radius:var(--radius-md); background:transparent; color:var(--color-text-primary); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm);">Cancelar</button>
|
||||
<button onclick="document.getElementById('modalCancelOverlay').style.display='none'" style="padding:var(--space-2) var(--space-5); border:1px solid var(--btn-danger-bg); border-radius:var(--radius-md); background:var(--btn-danger-bg); color:var(--btn-danger-text); cursor:pointer; font-family:var(--font-body); font-size:var(--text-body-sm); font-weight:var(--font-weight-semibold);">Solicitar Cancelación SAT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================================================================
|
||||
JAVASCRIPT
|
||||
========================================================================= -->
|
||||
@@ -2633,6 +2758,28 @@
|
||||
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
MODAL HELPERS
|
||||
------------------------------------------------------------------------- */
|
||||
// Wire all XML buttons to open the detail modal
|
||||
document.querySelectorAll('.btn--ghost').forEach(function(btn) {
|
||||
if (btn.textContent.trim() === 'XML') {
|
||||
btn.addEventListener('click', function() {
|
||||
document.getElementById('modalDetalleOverlay').style.display = 'flex';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals on overlay click
|
||||
['modalDetalleOverlay', 'modalCancelOverlay'].forEach(function(id) {
|
||||
var overlay = document.getElementById(id);
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) overlay.style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -890,92 +890,8 @@
|
||||
<!-- 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="users-grid" role="radiogroup" aria-label="Usuarios disponibles">
|
||||
<!-- Empleados cargados dinámicamente desde el tenant -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1071,12 +987,61 @@
|
||||
maxPinLength: 6,
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
EMPLEADOS DEL TENANT (dinámico — en producción viene de la API)
|
||||
------------------------------------------------------------------ */
|
||||
const tenantEmployees = [
|
||||
{ id: 'JR', initials: 'JR', name: 'J. Ramírez', fullName: 'Jorge Ramírez', role: 'Vendedor' },
|
||||
{ id: 'ML', initials: 'ML', name: 'M. López', fullName: 'María López', role: 'Cajero' },
|
||||
{ id: 'AP', initials: 'AP', name: 'A. Peña', fullName: 'Alejandro Peña', role: 'Almacén' },
|
||||
{ id: 'SC', initials: 'SC', name: 'S. Cruz', fullName: 'Sara Cruz', role: 'Supervisor' },
|
||||
{ id: 'HG', initials: 'HG', name: 'H. García', fullName: 'Hugo García', role: 'Gerente' },
|
||||
{ id: 'RT', initials: 'RT', name: 'R. Torres', fullName: 'Roberto Torres', role: 'Vendedor' },
|
||||
];
|
||||
|
||||
function renderEmployees(employees) {
|
||||
const grid = document.getElementById('users-grid');
|
||||
grid.innerHTML = employees.map(emp => `
|
||||
<button
|
||||
class="user-avatar-btn"
|
||||
data-user="${emp.id}"
|
||||
data-name="${emp.name}"
|
||||
data-role="${emp.role}"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
aria-label="${emp.fullName}, ${emp.role}"
|
||||
>
|
||||
<div class="user-initials" aria-hidden="true">${emp.initials}</div>
|
||||
<div class="user-name">${emp.name}</div>
|
||||
<div class="user-role">${emp.role}</div>
|
||||
</button>
|
||||
`).join('');
|
||||
bindUserButtons();
|
||||
}
|
||||
|
||||
function bindUserButtons() {
|
||||
document.querySelectorAll('.user-avatar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.user-avatar-btn').forEach(b => {
|
||||
b.classList.remove('selected');
|
||||
b.setAttribute('aria-checked', 'false');
|
||||
});
|
||||
btn.classList.add('selected');
|
||||
btn.setAttribute('aria-checked', 'true');
|
||||
state.selectedUser = btn.dataset.user;
|
||||
state.pin = [];
|
||||
enablePinPad();
|
||||
updatePinDisplay();
|
||||
updateLoginButton();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
DOM REFS
|
||||
------------------------------------------------------------------ */
|
||||
const html = document.documentElement;
|
||||
const themeBtns = document.querySelectorAll('.theme-btn');
|
||||
const userBtns = document.querySelectorAll('.user-avatar-btn');
|
||||
const pinDisplay = document.getElementById('pin-display');
|
||||
const pinPlaceholder = document.getElementById('pin-placeholder');
|
||||
const pinDots = document.querySelectorAll('.pin-dot');
|
||||
@@ -1105,28 +1070,9 @@
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
USER SELECTION
|
||||
RENDER EMPLOYEES ON INIT
|
||||
------------------------------------------------------------------ */
|
||||
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();
|
||||
});
|
||||
});
|
||||
renderEmployees(tenantEmployees);
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
PIN PAD LOGIC
|
||||
|
||||
@@ -804,6 +804,83 @@
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Columnas Costo/Margen (solo visibles con permiso Admin/Owner) */
|
||||
.cart-item__cost,
|
||||
.cart-item__margin {
|
||||
display: none;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-caption);
|
||||
text-align: right;
|
||||
min-width: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-item__cost {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-item__margin {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.cart-item__margin.margin-high { color: var(--color-success); }
|
||||
.cart-item__margin.margin-mid { color: var(--color-warning); }
|
||||
.cart-item__margin.margin-low { color: var(--color-error); }
|
||||
|
||||
/* When cost columns are visible */
|
||||
body.show-cost-columns .cart-item__cost,
|
||||
body.show-cost-columns .cart-item__margin {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Margin footer summary */
|
||||
.margin-summary {
|
||||
display: none;
|
||||
padding: var(--space-2) var(--space-5);
|
||||
border-top: 1px dashed var(--color-border);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
body.show-cost-columns .margin-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.margin-summary__value {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* Toggle button for cost columns */
|
||||
.cost-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
font-family: var(--font-body);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.cost-toggle:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
|
||||
.cost-toggle.active {
|
||||
background: var(--color-primary-muted);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
|
||||
/* Cart footer (totals + payment) */
|
||||
.cart-footer {
|
||||
flex-shrink: 0;
|
||||
@@ -1423,6 +1500,7 @@
|
||||
<div class="cart-header">
|
||||
<div class="cart-header__top">
|
||||
<div class="cart-header__sale-id" id="saleId">Venta #1247</div>
|
||||
<button class="cost-toggle" id="costToggle" title="Mostrar costo/margen (Admin)">💲 C/M</button>
|
||||
<span class="cart-header__status">Activa</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
@@ -1448,6 +1526,12 @@
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
|
||||
<!-- Margin Summary (visible when cost columns are on) -->
|
||||
<div class="margin-summary" id="marginSummary">
|
||||
<span>Margen promedio ponderado:</span>
|
||||
<span class="margin-summary__value" id="avgMargin">--</span>
|
||||
</div>
|
||||
|
||||
<!-- Cart Footer -->
|
||||
<div class="cart-footer">
|
||||
|
||||
@@ -1608,20 +1692,20 @@
|
||||
DATA — Product catalogue
|
||||
------------------------------------------------------------------ */
|
||||
const PRODUCTS = [
|
||||
{ id: 1, name: 'Balatas Delanteras', oem: 'TRW-GDB1246', price: 485.00, stock: 12, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 2, name: 'Disco de Freno Ventilado',oem: 'BREMBO-09A388X',price: 1290.00, stock: 4, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 3, name: 'Cilindro de Rueda', oem: 'LPR-4502', price: 320.00, stock: 7, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 4, name: 'Bujía Iridium NGK', oem: 'NGK-ILKAR7L11', price: 185.00, stock: 48, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 5, name: 'Empaque de Culata', oem: 'VICTOR-R10154', price: 890.00, stock: 2, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 6, name: 'Correa de Distribución', oem: 'GATES-T236', price: 650.00, stock: 9, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 7, name: 'Filtro de Aire Fram', oem: 'FRAM-CA10755', price: 195.00, stock: 23, category: 'filtros', cat_label: 'Filtros' },
|
||||
{ id: 8, name: 'Filtro de Aceite Bosch', oem: 'BOSCH-0986AF10',price: 145.00, stock: 31, category: 'filtros', cat_label: 'Filtros' },
|
||||
{ id: 9, name: 'Aceite Mobil 5W-30 1L', oem: 'MOBIL-5W30-1L', price: 210.00, stock: 64, category: 'aceites', cat_label: 'Aceites' },
|
||||
{ id: 10, name: 'Aceite Pennzoil 10W-40', oem: 'PZ-10W40-1L', price: 175.00, stock: 50, category: 'aceites', cat_label: 'Aceites' },
|
||||
{ id: 11, name: 'Amortiguador Delantero', oem: 'KYB-339123', price: 1450.00, stock: 3, category: 'suspension', cat_label: 'Suspensión'},
|
||||
{ id: 12, name: 'Rótula de Dirección', oem: 'MOOG-K9648', price: 560.00, stock: 6, category: 'suspension', cat_label: 'Suspensión'},
|
||||
{ id: 13, name: 'Bobina de Encendido', oem: 'DELPHI-GN10570',price: 780.00, stock: 5, category: 'electrico', cat_label: 'Eléctrico' },
|
||||
{ id: 14, name: 'Sensor de Oxígeno Denso', oem: 'DENSO-234-4127',price: 920.00, stock: 4, category: 'electrico', cat_label: 'Eléctrico' },
|
||||
{ id: 1, name: 'Balatas Delanteras', oem: 'TRW-GDB1246', price: 485.00, cost: 290.00, stock: 12, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 2, name: 'Disco de Freno Ventilado',oem: 'BREMBO-09A388X',price: 1290.00, cost: 820.00, stock: 4, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 3, name: 'Cilindro de Rueda', oem: 'LPR-4502', price: 320.00, cost: 195.00, stock: 7, category: 'frenos', cat_label: 'Frenos' },
|
||||
{ id: 4, name: 'Bujía Iridium NGK', oem: 'NGK-ILKAR7L11', price: 185.00, cost: 98.00, stock: 48, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 5, name: 'Empaque de Culata', oem: 'VICTOR-R10154', price: 890.00, cost: 580.00, stock: 2, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 6, name: 'Correa de Distribución', oem: 'GATES-T236', price: 650.00, cost: 390.00, stock: 9, category: 'motor', cat_label: 'Motor' },
|
||||
{ id: 7, name: 'Filtro de Aire Fram', oem: 'FRAM-CA10755', price: 195.00, cost: 85.00, stock: 23, category: 'filtros', cat_label: 'Filtros' },
|
||||
{ id: 8, name: 'Filtro de Aceite Bosch', oem: 'BOSCH-0986AF10',price: 145.00, cost: 68.00, stock: 31, category: 'filtros', cat_label: 'Filtros' },
|
||||
{ id: 9, name: 'Aceite Mobil 5W-30 1L', oem: 'MOBIL-5W30-1L', price: 210.00, cost: 155.00, stock: 64, category: 'aceites', cat_label: 'Aceites' },
|
||||
{ id: 10, name: 'Aceite Pennzoil 10W-40', oem: 'PZ-10W40-1L', price: 175.00, cost: 120.00, stock: 50, category: 'aceites', cat_label: 'Aceites' },
|
||||
{ id: 11, name: 'Amortiguador Delantero', oem: 'KYB-339123', price: 1450.00, cost: 950.00, stock: 3, category: 'suspension', cat_label: 'Suspensión'},
|
||||
{ id: 12, name: 'Rótula de Dirección', oem: 'MOOG-K9648', price: 560.00, cost: 320.00, stock: 6, category: 'suspension', cat_label: 'Suspensión'},
|
||||
{ id: 13, name: 'Bobina de Encendido', oem: 'DELPHI-GN10570',price: 780.00, cost: 480.00, stock: 5, category: 'electrico', cat_label: 'Eléctrico' },
|
||||
{ id: 14, name: 'Sensor de Oxígeno Denso', oem: 'DENSO-234-4127',price: 920.00, cost: 610.00, stock: 4, category: 'electrico', cat_label: 'Eléctrico' },
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
@@ -1857,6 +1941,8 @@
|
||||
const p = getProduct(item.productId);
|
||||
if (!p) return '';
|
||||
const lineTotal = p.price * item.qty;
|
||||
const margin = p.cost ? ((p.price - p.cost) / p.price * 100) : 0;
|
||||
const marginClass = margin > 30 ? 'margin-high' : margin > 15 ? 'margin-mid' : 'margin-low';
|
||||
return `
|
||||
<div class="cart-item" role="listitem" data-product-id="${p.id}">
|
||||
<div class="cart-item__qty-ctrl">
|
||||
@@ -1868,6 +1954,8 @@
|
||||
<div class="cart-item__name" title="${p.name}">${p.name}</div>
|
||||
<div class="cart-item__unit">${fmt(p.price)} c/u</div>
|
||||
</div>
|
||||
<div class="cart-item__cost">${p.cost ? fmt(p.cost) : '—'}</div>
|
||||
<div class="cart-item__margin ${marginClass}">${margin.toFixed(1)}%</div>
|
||||
<div class="cart-item__total">${fmt(lineTotal)}</div>
|
||||
<button class="cart-item__remove" data-id="${p.id}" aria-label="Eliminar ${p.name} del carrito">✕</button>
|
||||
</div>`;
|
||||
@@ -1890,8 +1978,41 @@
|
||||
|
||||
// disable cobrar if cart is empty
|
||||
$btnCobrar.disabled = state.cart.length === 0;
|
||||
|
||||
// update weighted average margin
|
||||
updateAvgMargin();
|
||||
}
|
||||
|
||||
function updateAvgMargin() {
|
||||
const $avg = document.getElementById('avgMargin');
|
||||
if (!$avg) return;
|
||||
let totalRevenue = 0, totalCost = 0;
|
||||
state.cart.forEach(item => {
|
||||
const p = getProduct(item.productId);
|
||||
if (p && p.cost) {
|
||||
totalRevenue += p.price * item.qty;
|
||||
totalCost += p.cost * item.qty;
|
||||
}
|
||||
});
|
||||
if (totalRevenue > 0) {
|
||||
const avg = ((totalRevenue - totalCost) / totalRevenue * 100);
|
||||
$avg.textContent = avg.toFixed(1) + '%';
|
||||
$avg.className = 'margin-summary__value ' +
|
||||
(avg > 30 ? 'margin-high' : avg > 15 ? 'margin-mid' : 'margin-low');
|
||||
$avg.style.color = avg > 30 ? 'var(--color-success)' : avg > 15 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||
} else {
|
||||
$avg.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
COST/MARGIN TOGGLE (Admin/Owner only)
|
||||
------------------------------------------------------------------ */
|
||||
document.getElementById('costToggle').addEventListener('click', function() {
|
||||
document.body.classList.toggle('show-cost-columns');
|
||||
this.classList.toggle('active');
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
DISCOUNT INPUT
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user