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:
Lucy
2026-04-01 07:06:34 +00:00
parent 380698258a
commit ccd3962458
25 changed files with 7153 additions and 129 deletions

View File

@@ -188,7 +188,7 @@
===================================================================== */
.sidebar {
width: 220px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;

View File

@@ -169,7 +169,7 @@
========================================================================= */
.sidebar {
width: 220px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;

View File

@@ -169,7 +169,7 @@
========================================================================= */
.sidebar {
width: 220px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;

View File

@@ -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);

View File

@@ -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;">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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"&gt;
&lt;cfdi:Emisor Rfc="NAP960714JK3"
Nombre="Nexus Autoparts SA de CV"
RegimenFiscal="601"/&gt;
&lt;cfdi:Receptor Rfc="TMR8402156HJ"
Nombre="Taller Mecánico Rodríguez"
UsoCFDI="G03" DomicilioFiscalReceptor="64000"
RegimenFiscalReceptor="612"/&gt;
&lt;cfdi:Conceptos&gt;
&lt;cfdi:Concepto ClaveProdServ="25174800"
Cantidad="4" ClaveUnidad="H87"
Descripcion="Balatas delanteras TRW"
ValorUnitario="485.00" Importe="1940.00"/&gt;
&lt;/cfdi:Conceptos&gt;
&lt;/cfdi:Comprobante&gt;</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>

View File

@@ -169,7 +169,7 @@
========================================================================= */
.sidebar {
width: 220px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;

View File

@@ -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

View File

@@ -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
------------------------------------------------------------------ */

View File

@@ -165,7 +165,7 @@
========================================================================= */
.sidebar {
width: 220px;
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;