Files
Autoparts-DB/pos/templates/pos.html
consultoria-as 380698258a feat: agregar design system completo con 2 temas (Industrial + Moderno)
- Login: PIN pad con seleccion de usuario + auth real via API
- Catalogo: grid de productos con sidebar nav y filtros
- POS: layout split con numpad y area de venta
- tokens.css: sistema completo de CSS variables (colores, tipografia, espaciado)
- 2 temas: Industrial Robusto (dark/amber) y Tecnico Moderno (light/orange)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 06:22:46 +00:00

2098 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Punto de Venta</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<style>
/* =====================================================================
BASE RESET & LAYOUT SHELL
===================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: var(--font-body);
font-size: var(--text-body);
font-weight: var(--font-weight-regular);
background-color: var(--color-bg-base);
color: var(--color-text-primary);
transition: background-color var(--duration-normal) var(--ease-in-out),
color var(--duration-normal) var(--ease-in-out);
display: flex;
flex-direction: column;
}
/* Dot-grid for modern theme (applied to body shell) */
[data-theme="modern"] body,
[data-theme="modern"].pos-shell {
background-color: var(--color-bg-base);
background-image: radial-gradient(
circle,
var(--dot-grid-color) 1px,
transparent 1px
);
background-size: var(--dot-grid-size) var(--dot-grid-size);
}
/* =====================================================================
STATUS BAR (very top)
===================================================================== */
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
padding: 0 var(--space-5);
background-color: var(--color-surface-3);
border-bottom: 1px solid var(--color-border);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--color-text-muted);
flex-shrink: 0;
z-index: var(--z-sticky);
}
[data-theme="industrial"] .status-bar {
background-color: #111111;
border-bottom-color: var(--color-primary-muted);
}
.status-bar__store {
display: flex;
align-items: center;
gap: var(--space-3);
color: var(--color-text-accent);
font-family: var(--font-heading);
font-weight: var(--heading-weight-primary);
font-size: 0.8rem;
letter-spacing: var(--tracking-widest);
}
.status-bar__store-dot {
width: 7px;
height: 7px;
background-color: var(--color-success);
border-radius: var(--radius-full);
box-shadow: 0 0 6px var(--color-success);
animation: pulse-dot 2.5s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-bar__center {
display: flex;
align-items: center;
gap: var(--space-6);
}
.status-bar__right {
display: flex;
align-items: center;
gap: var(--space-4);
}
.status-bar__user {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-text-secondary);
}
.status-bar__user-avatar {
width: 20px;
height: 20px;
border-radius: var(--radius-full);
background-color: var(--color-primary);
color: var(--color-text-inverse);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: var(--font-weight-bold);
}
/* Theme switcher inside status bar */
.theme-switcher {
display: flex;
align-items: center;
gap: var(--space-2);
background-color: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
padding: 2px;
}
.theme-btn {
display: flex;
align-items: center;
gap: var(--space-1);
padding: 3px var(--space-3);
border: none;
border-radius: var(--radius-full);
font-family: var(--font-body);
font-size: 0.68rem;
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
cursor: pointer;
transition: var(--transition-fast);
background: transparent;
color: var(--color-text-muted);
}
.theme-btn.active {
background-color: var(--color-primary);
color: var(--color-text-inverse);
box-shadow: var(--shadow-sm);
}
[data-theme="industrial"] .theme-btn.active {
color: #000;
}
/* =====================================================================
MAIN CONTENT — split layout
===================================================================== */
.pos-shell {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.pos-main {
display: flex;
flex: 1;
overflow: hidden;
gap: 0;
}
/* =====================================================================
LEFT PANEL — Product Browser (60%)
===================================================================== */
.panel-products {
display: flex;
flex-direction: column;
width: 60%;
min-width: 0;
border-right: 1px solid var(--color-border);
background-color: var(--color-bg-base);
overflow: hidden;
}
[data-theme="modern"] .panel-products {
background-color: transparent;
}
/* Search bar row */
.search-row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
background-color: var(--color-surface-1);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.search-wrap {
position: relative;
flex: 1;
}
.search-icon {
position: absolute;
left: var(--space-4);
top: 50%;
transform: translateY(-50%);
color: var(--color-text-muted);
pointer-events: none;
}
.search-input {
width: 100%;
height: 48px;
padding: 0 var(--space-4) 0 44px;
background-color: var(--color-bg-overlay);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-md);
font-family: var(--font-body);
font-size: var(--text-body-lg);
font-weight: var(--font-weight-regular);
color: var(--color-text-primary);
outline: none;
transition: var(--transition-fast);
}
.search-input::placeholder {
color: var(--color-text-muted);
}
.search-input:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
background-color: var(--color-bg-overlay);
}
[data-theme="modern"] .search-input {
border-radius: var(--radius-lg);
background-color: #fff;
}
.btn-scan {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--btn-secondary-bg);
border: 1.5px solid var(--btn-secondary-border);
border-radius: var(--radius-md);
color: var(--color-primary);
cursor: pointer;
transition: var(--transition-fast);
flex-shrink: 0;
}
.btn-scan:hover {
background-color: var(--btn-secondary-bg-hover);
box-shadow: var(--shadow-accent);
}
[data-theme="industrial"] .btn-scan {
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
[data-theme="modern"] .btn-scan {
border-radius: var(--radius-lg);
}
/* Quick category buttons */
.categories-row {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
border-bottom: 1px solid var(--color-border);
overflow-x: auto;
flex-shrink: 0;
scrollbar-width: none;
}
.categories-row::-webkit-scrollbar { display: none; }
.category-label {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--color-text-muted);
white-space: nowrap;
margin-right: var(--space-1);
}
.cat-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: transparent;
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
cursor: pointer;
white-space: nowrap;
transition: var(--transition-fast);
letter-spacing: var(--tracking-snug);
}
.cat-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background-color: var(--color-primary-muted);
}
.cat-btn.active {
border-color: var(--color-primary);
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
[data-theme="industrial"] .cat-btn.active { color: #000; }
[data-theme="modern"] .cat-btn {
border-radius: var(--radius-full);
}
[data-theme="industrial"] .cat-btn {
clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
}
/* Product grid */
.product-grid-wrap {
flex: 1;
overflow-y: auto;
padding: var(--space-4) var(--space-5);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.product-grid-wrap::-webkit-scrollbar { width: 6px; }
.product-grid-wrap::-webkit-scrollbar-track { background: var(--scrollbar-track); }
.product-grid-wrap::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: var(--radius-full);
}
.product-grid-wrap::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-3);
}
@media (max-width: 1100px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Product card */
.product-card {
position: relative;
display: flex;
flex-direction: column;
padding: var(--space-4);
background-color: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
overflow: hidden;
}
.product-card:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.product-card:hover .product-card__add {
opacity: 1;
transform: scale(1);
}
[data-theme="industrial"] .product-card {
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
background-color: var(--color-surface-1);
border-color: #2a2a2a;
}
[data-theme="industrial"] .product-card:hover {
border-color: var(--color-primary);
box-shadow: 0 0 0 1px var(--color-primary), var(--shadow-md);
}
[data-theme="modern"] .product-card {
border-radius: var(--radius-lg);
background-color: #fff;
box-shadow: var(--shadow-sm);
}
[data-theme="modern"] .product-card:hover {
box-shadow: var(--shadow-lg);
}
.product-card__category {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
color: var(--color-primary);
margin-bottom: var(--space-1);
}
.product-card__name {
font-family: var(--font-heading);
font-size: var(--text-body);
font-weight: var(--heading-weight-primary);
line-height: var(--leading-h5);
color: var(--color-text-primary);
margin-bottom: var(--space-1);
letter-spacing: var(--heading-tracking-h5);
}
[data-theme="industrial"] .product-card__name {
font-size: 1.05rem;
letter-spacing: 0.01em;
}
.product-card__oem {
font-family: var(--font-mono);
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-bottom: var(--space-3);
}
.product-card__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: auto;
gap: var(--space-2);
}
.product-card__price {
font-family: var(--font-mono);
font-size: 1.1rem;
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
line-height: 1;
}
.product-card__stock {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-top: 2px;
}
.product-card__stock.low { color: var(--color-warning); }
.product-card__stock.out { color: var(--color-error); }
.product-card__add {
width: 32px;
height: 32px;
border: none;
border-radius: var(--radius-sm);
background-color: var(--color-primary);
color: var(--color-text-inverse);
font-size: 1.2rem;
font-weight: var(--font-weight-bold);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.75;
transform: scale(0.95);
transition: var(--transition-fast);
flex-shrink: 0;
}
.product-card__add:hover {
background-color: var(--color-primary-hover);
opacity: 1;
transform: scale(1.08) !important;
}
[data-theme="industrial"] .product-card__add { color: #000; }
[data-theme="modern"] .product-card__add { border-radius: var(--radius-md); }
/* Add flash animation */
@keyframes card-flash {
0% { background-color: var(--color-primary-muted); }
100% { background-color: transparent; }
}
.product-card.added {
animation: card-flash 0.4s ease forwards;
}
/* =====================================================================
RIGHT PANEL — Cart / Ticket (40%)
===================================================================== */
.panel-cart {
display: flex;
flex-direction: column;
width: 40%;
min-width: 320px;
background-color: var(--color-surface-1);
overflow: hidden;
position: relative;
}
[data-theme="modern"] .panel-cart {
background-color: #fff;
box-shadow: -4px 0 20px rgba(26, 26, 46, 0.06);
}
[data-theme="industrial"] .panel-cart {
background-color: #141414;
border-left: 1px solid #2a2a2a;
}
/* Cart header */
.cart-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
background-color: var(--color-surface-2);
}
[data-theme="industrial"] .cart-header {
background-color: #1a1a1a;
border-bottom-color: var(--color-primary-muted);
}
.cart-header__top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
.cart-header__sale-id {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-primary);
color: var(--color-text-primary);
letter-spacing: var(--heading-tracking-h5);
display: flex;
align-items: center;
gap: var(--space-2);
}
.cart-header__sale-id::before {
content: '';
display: block;
width: 4px;
height: 20px;
background-color: var(--color-primary);
border-radius: 2px;
flex-shrink: 0;
}
[data-theme="industrial"] .cart-header__sale-id {
font-size: 1.35rem;
letter-spacing: 0.03em;
}
.cart-header__status {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--color-success);
background-color: rgba(34, 197, 94, 0.12);
padding: 3px var(--space-3);
border-radius: var(--radius-full);
border: 1px solid rgba(34, 197, 94, 0.25);
}
/* Customer row */
.customer-row {
display: flex;
align-items: center;
gap: var(--space-3);
}
.customer-icon {
width: 34px;
height: 34px;
border-radius: var(--radius-full);
background-color: var(--color-primary-muted);
border: 1.5px solid var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary);
flex-shrink: 0;
}
.customer-info {
flex: 1;
min-width: 0;
}
.customer-info__name {
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.customer-info__label {
font-size: var(--text-caption);
color: var(--color-text-muted);
}
.btn-change-customer {
padding: var(--space-1) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
background: transparent;
font-family: var(--font-body);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
cursor: pointer;
transition: var(--transition-fast);
white-space: nowrap;
}
.btn-change-customer:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Cart items list */
.cart-items {
flex: 1;
overflow-y: auto;
padding: var(--space-3) var(--space-4);
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.cart-items::-webkit-scrollbar { width: 4px; }
.cart-items::-webkit-scrollbar-track { background: transparent; }
.cart-items::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: var(--radius-full);
}
.cart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
color: var(--color-text-muted);
font-size: var(--text-body-sm);
gap: var(--space-2);
opacity: 0.6;
}
.cart-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-2);
border-bottom: 1px solid var(--color-border);
transition: var(--transition-fast);
animation: item-slide-in 0.2s ease;
}
@keyframes item-slide-in {
from { opacity: 0; transform: translateX(10px); }
to { opacity: 1; transform: translateX(0); }
}
.cart-item:hover {
background-color: var(--color-primary-muted);
border-radius: var(--radius-sm);
}
.cart-item:last-child { border-bottom: none; }
.cart-item__qty-ctrl {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
.qty-btn {
width: 26px;
height: 26px;
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
background-color: var(--color-surface-3);
color: var(--color-text-primary);
font-size: 0.9rem;
font-weight: var(--font-weight-bold);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
line-height: 1;
}
.qty-btn:hover {
border-color: var(--color-primary);
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
[data-theme="industrial"] .qty-btn:hover { color: #000; }
.qty-display {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
min-width: 24px;
text-align: center;
}
.cart-item__info {
flex: 1;
min-width: 0;
}
.cart-item__name {
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cart-item__unit {
font-family: var(--font-mono);
font-size: var(--text-caption);
color: var(--color-text-muted);
}
.cart-item__total {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
text-align: right;
min-width: 72px;
}
.cart-item__remove {
width: 24px;
height: 24px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-muted);
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
flex-shrink: 0;
line-height: 1;
}
.cart-item__remove:hover {
background-color: rgba(239, 68, 68, 0.15);
color: var(--color-error);
}
/* Cart footer (totals + payment) */
.cart-footer {
flex-shrink: 0;
border-top: 1px solid var(--color-border);
background-color: var(--color-surface-2);
}
[data-theme="industrial"] .cart-footer {
background-color: #1a1a1a;
border-top-color: var(--color-primary-muted);
}
.totals-block {
padding: var(--space-3) var(--space-5) var(--space-2);
}
.totals-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-1) 0;
}
.totals-row__label {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.totals-row__value {
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.totals-row--total {
margin-top: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--color-border);
}
.totals-row--total .totals-row__label {
font-family: var(--font-heading);
font-size: var(--text-body-lg);
font-weight: var(--heading-weight-primary);
color: var(--color-text-primary);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}
.totals-row--total .totals-row__value {
font-family: var(--font-mono);
font-size: 1.5rem;
font-weight: var(--font-weight-bold);
color: var(--color-primary);
}
/* Discount row */
.discount-row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-5) var(--space-3);
}
.discount-label {
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
white-space: nowrap;
}
.discount-input {
flex: 1;
height: 30px;
padding: 0 var(--space-3);
background-color: var(--color-bg-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: var(--text-body-sm);
color: var(--color-text-primary);
outline: none;
transition: var(--transition-fast);
}
.discount-input:focus {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus);
}
[data-theme="modern"] .discount-input {
border-radius: var(--radius-md);
}
.discount-input::placeholder { color: var(--color-text-muted); }
/* Payment method buttons */
.payment-methods {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-2);
padding: 0 var(--space-5) var(--space-3);
}
.pay-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
padding: var(--space-2) var(--space-2);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
font-family: var(--font-body);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--color-text-muted);
cursor: pointer;
transition: var(--transition-fast);
}
.pay-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background-color: var(--color-primary-muted);
}
.pay-btn.selected {
border-color: var(--color-primary);
background-color: var(--color-primary-muted);
color: var(--color-primary);
box-shadow: var(--shadow-accent);
}
.pay-btn__icon {
font-size: 1.1rem;
}
[data-theme="modern"] .pay-btn {
border-radius: var(--radius-md);
}
[data-theme="industrial"] .pay-btn {
clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
}
/* Main CTA button */
.btn-cobrar {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-3);
width: calc(100% - var(--space-10));
margin: 0 var(--space-5) var(--space-3);
height: 56px;
background-color: var(--btn-primary-bg);
border: none;
border-radius: var(--radius-md);
font-family: var(--font-heading);
font-size: 1.2rem;
font-weight: var(--heading-weight-primary);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
color: var(--btn-primary-text);
cursor: pointer;
transition: var(--transition-fast);
box-shadow: var(--shadow-md);
}
.btn-cobrar:hover {
background-color: var(--btn-primary-bg-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-cobrar:active {
background-color: var(--btn-primary-bg-active);
transform: translateY(0);
}
.btn-cobrar:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
[data-theme="industrial"] .btn-cobrar {
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 0 100%);
}
[data-theme="modern"] .btn-cobrar {
border-radius: var(--radius-lg);
}
/* Secondary actions row */
.secondary-actions {
display: flex;
gap: var(--space-2);
padding: 0 var(--space-5) var(--space-4);
}
.btn-secondary-action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
height: 34px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
font-family: var(--font-body);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--color-text-muted);
cursor: pointer;
transition: var(--transition-fast);
}
.btn-secondary-action:hover {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background-color: var(--color-surface-3);
}
.btn-secondary-action.danger:hover {
border-color: var(--color-error);
color: var(--color-error);
background-color: rgba(239, 68, 68, 0.08);
}
[data-theme="modern"] .btn-secondary-action {
border-radius: var(--radius-md);
}
/* =====================================================================
NUMPAD OVERLAY
===================================================================== */
.numpad-overlay {
position: fixed;
inset: 0;
background-color: var(--overlay-backdrop);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: var(--z-modal);
opacity: 0;
pointer-events: none;
transition: opacity var(--duration-normal) var(--ease-in-out);
}
.numpad-overlay.visible {
opacity: 1;
pointer-events: all;
}
.numpad-panel {
width: 360px;
background-color: var(--color-surface-2);
border: 1px solid var(--color-border-strong);
border-bottom: none;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
padding: var(--space-5);
transform: translateY(100%);
transition: transform var(--duration-normal) var(--ease-out);
}
.numpad-overlay.visible .numpad-panel {
transform: translateY(0);
}
[data-theme="industrial"] .numpad-panel {
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background-color: #1e1e1e;
border-color: var(--color-primary-muted);
}
.numpad-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.numpad-title {
font-family: var(--font-heading);
font-size: var(--text-h6);
font-weight: var(--heading-weight-primary);
text-transform: uppercase;
letter-spacing: var(--tracking-widest);
color: var(--color-text-primary);
}
.numpad-close {
width: 28px;
height: 28px;
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
background: transparent;
color: var(--color-text-muted);
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-fast);
}
.numpad-close:hover {
border-color: var(--color-error);
color: var(--color-error);
}
.numpad-display {
background-color: var(--color-bg-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: var(--space-3) var(--space-4);
font-family: var(--font-mono);
font-size: 1.8rem;
font-weight: var(--font-weight-bold);
color: var(--color-primary);
text-align: right;
margin-bottom: var(--space-4);
min-height: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
letter-spacing: var(--tracking-wide);
}
.numpad-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-2);
}
.numpad-key {
height: 52px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-surface-3);
font-family: var(--font-mono);
font-size: 1.1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
cursor: pointer;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.numpad-key:hover {
background-color: var(--color-primary-muted);
border-color: var(--color-primary);
color: var(--color-primary);
}
.numpad-key:active {
transform: scale(0.95);
}
.numpad-key.key-0 { grid-column: span 2; }
.numpad-key.key-clear {
background-color: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: var(--color-error);
}
.numpad-key.key-enter {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-text-inverse);
font-family: var(--font-heading);
font-size: 0.85rem;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
[data-theme="industrial"] .numpad-key.key-enter { color: #000; }
.numpad-key.key-enter:hover {
background-color: var(--color-primary-hover);
}
[data-theme="modern"] .numpad-key {
border-radius: var(--radius-md);
}
/* =====================================================================
TOAST NOTIFICATION
===================================================================== */
.toast-container {
position: fixed;
bottom: var(--space-6);
left: 50%;
transform: translateX(-50%);
z-index: var(--z-toast);
display: flex;
flex-direction: column;
gap: var(--space-2);
pointer-events: none;
}
.toast {
padding: var(--space-3) var(--space-5);
background-color: var(--color-surface-3);
border: 1px solid var(--color-border);
border-left: 3px solid var(--color-primary);
border-radius: var(--radius-md);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
box-shadow: var(--shadow-lg);
white-space: nowrap;
animation: toast-in 0.25s ease, toast-out 0.3s ease 1.7s forwards;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-6px); }
}
/* =====================================================================
RESPONSIVE — tablet/small desktop
===================================================================== */
@media (max-width: 900px) {
.pos-main {
flex-direction: column;
}
.panel-products,
.panel-cart {
width: 100%;
min-width: 0;
}
.panel-products {
border-right: none;
border-bottom: 1px solid var(--color-border);
max-height: 55%;
}
.panel-cart {
max-height: 45%;
}
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
.status-bar__center { display: none; }
}
@media (max-width: 600px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
.payment-methods {
grid-template-columns: repeat(3, 1fr);
}
.status-bar { padding: 0 var(--space-3); }
.search-row { padding: var(--space-3); }
.categories-row { padding: var(--space-2) var(--space-3); }
}
/* =====================================================================
UTILITY
===================================================================== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
</head>
<body class="pos-shell themed-scrollbar" id="appBody">
<!-- ================================================================
STATUS BAR
================================================================ -->
<header class="status-bar" role="banner">
<div class="status-bar__store">
<span class="status-bar__store-dot"></span>
Nexus Autoparts — Suc. Centro
</div>
<div class="status-bar__center">
<span id="statusClock">Mar 31, 2026 — 10:42 AM</span>
<span>Terminal #3</span>
</div>
<div class="status-bar__right">
<!-- Theme Switcher -->
<div class="theme-switcher" role="group" aria-label="Cambiar tema">
<button class="theme-btn active" id="btnIndustrial" data-theme-val="industrial" title="Tema Industrial">
⬡ Industrial
</button>
<button class="theme-btn" id="btnModern" data-theme-val="modern" title="Tema Moderno">
○ Moderno
</button>
</div>
<div class="status-bar__user" aria-label="Usuario activo">
<div class="status-bar__user-avatar" aria-hidden="true">HR</div>
<span>Hugo Reyes</span>
</div>
</div>
</header>
<!-- ================================================================
MAIN POS LAYOUT
================================================================ -->
<main class="pos-main" role="main">
<!-- ============================================================
LEFT — PRODUCT BROWSER
============================================================ -->
<section class="panel-products" aria-label="Catálogo de productos">
<!-- Search Bar -->
<div class="search-row">
<div class="search-wrap">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
class="search-input"
type="search"
id="searchInput"
placeholder="Buscar por nombre, OEM#, SKU..."
autocomplete="off"
spellcheck="false"
aria-label="Buscar productos"
/>
</div>
<button class="btn-scan" id="btnScan" title="Escanear código de barras" aria-label="Escanear código">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<path d="M3 5v2M3 19v-2M5 3h2M19 3h-2M21 5v2M21 19v-2M19 21h-2M5 21h2"/>
<line x1="7" y1="8" x2="7" y2="16"/><line x1="10" y1="8" x2="10" y2="16"/>
<line x1="13" y1="8" x2="13" y2="16"/><line x1="17" y1="8" x2="17" y2="16"/>
</svg>
</button>
</div>
<!-- Category Buttons -->
<div class="categories-row" role="toolbar" aria-label="Categorías de productos">
<span class="category-label">Filtrar:</span>
<button class="cat-btn active" data-category="all">Todos</button>
<button class="cat-btn" data-category="frenos">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"/>
<circle cx="12" cy="12" r="4"/>
</svg>
Frenos
</button>
<button class="cat-btn" data-category="motor">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="2" y="7" width="20" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="6" y1="7" x2="6" y2="17" stroke="currentColor" stroke-width="1.5"/>
<line x1="12" y1="7" x2="12" y2="17" stroke="currentColor" stroke-width="1.5"/>
<line x1="18" y1="7" x2="18" y2="17" stroke="currentColor" stroke-width="1.5"/>
</svg>
Motor
</button>
<button class="cat-btn" data-category="filtros">Filtros</button>
<button class="cat-btn" data-category="aceites">Aceites</button>
<button class="cat-btn" data-category="suspension">Suspensión</button>
<button class="cat-btn" data-category="electrico">Eléctrico</button>
</div>
<!-- Product Grid -->
<div class="product-grid-wrap" id="productGridWrap">
<div class="product-grid" id="productGrid" role="list" aria-label="Productos disponibles">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- ============================================================
RIGHT — CART / TICKET
============================================================ -->
<aside class="panel-cart" aria-label="Carrito de venta">
<!-- Cart Header -->
<div class="cart-header">
<div class="cart-header__top">
<div class="cart-header__sale-id" id="saleId">Venta #1247</div>
<span class="cart-header__status">Activa</span>
</div>
<div class="customer-row">
<div class="customer-icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<div class="customer-info">
<div class="customer-info__name" id="customerName">Cliente General</div>
<div class="customer-info__label">Sin RFC registrado</div>
</div>
<button class="btn-change-customer" id="btnChangeCustomer" aria-label="Cambiar cliente">
Cambiar
</button>
</div>
</div>
<!-- Cart Items -->
<div class="cart-items themed-scrollbar" id="cartItems" role="list" aria-label="Artículos en carrito">
<!-- Populated by JS -->
</div>
<!-- Cart Footer -->
<div class="cart-footer">
<!-- Totals -->
<div class="totals-block">
<div class="totals-row">
<span class="totals-row__label">Subtotal</span>
<span class="totals-row__value" id="subtotalDisplay">$0.00</span>
</div>
<div class="totals-row">
<span class="totals-row__label">IVA (16%)</span>
<span class="totals-row__value" id="ivaDisplay">$0.00</span>
</div>
<div class="totals-row totals-row--total">
<span class="totals-row__label">Total</span>
<span class="totals-row__value" id="totalDisplay">$0.00</span>
</div>
</div>
<!-- Discount -->
<div class="discount-row">
<span class="discount-label">Desc.</span>
<input
type="number"
class="discount-input"
id="discountInput"
placeholder="0.00"
min="0"
step="0.01"
aria-label="Descuento en pesos"
/>
<span class="discount-label">MXN</span>
</div>
<!-- Payment Methods -->
<div class="payment-methods" role="radiogroup" aria-label="Forma de pago">
<button class="pay-btn selected" data-method="efectivo" aria-pressed="true">
<span class="pay-btn__icon">💵</span>
<span>Efectivo</span>
</button>
<button class="pay-btn" data-method="tarjeta" aria-pressed="false">
<span class="pay-btn__icon">💳</span>
<span>Tarjeta</span>
</button>
<button class="pay-btn" data-method="transferencia" aria-pressed="false">
<span class="pay-btn__icon">📲</span>
<span>Transfer.</span>
</button>
</div>
<!-- COBRAR Button -->
<button class="btn-cobrar" id="btnCobrar" aria-label="Procesar cobro">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<line x1="12" y1="1" x2="12" y2="23"/>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
<span id="cobrarLabel">COBRAR $0.00</span>
</button>
<!-- Secondary Actions -->
<div class="secondary-actions" role="toolbar" aria-label="Acciones secundarias">
<button class="btn-secondary-action" id="btnGuardar" title="Guardar venta">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Guardar
</button>
<button class="btn-secondary-action" id="btnImprimir" title="Imprimir ticket">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<polyline points="6 9 6 2 18 2 18 9"/>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
<rect x="6" y="14" width="12" height="8"/>
</svg>
Imprimir
</button>
<button class="btn-secondary-action danger" id="btnCancelar" title="Cancelar venta">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
Cancelar
</button>
<button class="btn-secondary-action" id="btnNumpad" title="Abrir teclado numérico">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="4" height="4" rx="1"/>
<rect x="10" y="3" width="4" height="4" rx="1"/>
<rect x="17" y="3" width="4" height="4" rx="1"/>
<rect x="3" y="10" width="4" height="4" rx="1"/>
<rect x="10" y="10" width="4" height="4" rx="1"/>
<rect x="17" y="10" width="4" height="4" rx="1"/>
<rect x="3" y="17" width="4" height="4" rx="1"/>
<rect x="10" y="17" width="4" height="4" rx="1"/>
<rect x="17" y="17" width="4" height="4" rx="1"/>
</svg>
Teclado
</button>
</div>
</div><!-- /.cart-footer -->
</aside>
</main>
<!-- ================================================================
NUMPAD OVERLAY
================================================================ -->
<div class="numpad-overlay" id="numpadOverlay" role="dialog" aria-modal="true"
aria-label="Teclado numérico" aria-hidden="true">
<div class="numpad-panel">
<div class="numpad-header">
<span class="numpad-title">Cantidad / Importe</span>
<button class="numpad-close" id="numpadClose" aria-label="Cerrar teclado"></button>
</div>
<div class="numpad-display" id="numpadDisplay" aria-live="polite">0</div>
<div class="numpad-grid" role="group" aria-label="Teclas numéricas">
<button class="numpad-key" data-key="7">7</button>
<button class="numpad-key" data-key="8">8</button>
<button class="numpad-key" data-key="9">9</button>
<button class="numpad-key key-clear" data-key="C">C</button>
<button class="numpad-key" data-key="4">4</button>
<button class="numpad-key" data-key="5">5</button>
<button class="numpad-key" data-key="6">6</button>
<button class="numpad-key" data-key="back"></button>
<button class="numpad-key" data-key="1">1</button>
<button class="numpad-key" data-key="2">2</button>
<button class="numpad-key" data-key="3">3</button>
<button class="numpad-key key-enter" data-key="ENTER">OK</button>
<button class="numpad-key key-0" data-key="0">0</button>
<button class="numpad-key" data-key=".">.</button>
</div>
</div>
</div>
<!-- ================================================================
TOAST CONTAINER
================================================================ -->
<div class="toast-container" id="toastContainer" aria-live="assertive" aria-atomic="true"></div>
<!-- ================================================================
JAVASCRIPT
================================================================ -->
<script>
'use strict';
/* ------------------------------------------------------------------
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' },
];
/* ------------------------------------------------------------------
STATE
------------------------------------------------------------------ */
const state = {
theme: 'industrial',
cart: [], // { productId, qty }
selectedMethod: 'efectivo',
discount: 0,
activeCategory: 'all',
searchQuery: '',
numpadValue: '0',
saleNumber: 1247,
};
// Seed cart with 3 demo items
state.cart = [
{ productId: 4, qty: 4 },
{ productId: 7, qty: 2 },
{ productId: 11, qty: 1 },
];
/* ------------------------------------------------------------------
DOM REFS
------------------------------------------------------------------ */
const $html = document.documentElement;
const $body = document.getElementById('appBody');
const $btnIndustrial = document.getElementById('btnIndustrial');
const $btnModern = document.getElementById('btnModern');
const $searchInput = document.getElementById('searchInput');
const $productGrid = document.getElementById('productGrid');
const $cartItems = document.getElementById('cartItems');
const $subtotal = document.getElementById('subtotalDisplay');
const $iva = document.getElementById('ivaDisplay');
const $total = document.getElementById('totalDisplay');
const $cobrarLabel = document.getElementById('cobrarLabel');
const $btnCobrar = document.getElementById('btnCobrar');
const $discountInput = document.getElementById('discountInput');
const $numpadOverlay = document.getElementById('numpadOverlay');
const $numpadDisplay = document.getElementById('numpadDisplay');
const $numpadClose = document.getElementById('numpadClose');
const $statusClock = document.getElementById('statusClock');
const $toastContainer = document.getElementById('toastContainer');
/* ------------------------------------------------------------------
FORMATTING HELPERS
------------------------------------------------------------------ */
const fmt = (n) =>
new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(n);
/* ------------------------------------------------------------------
THEME SWITCHING
------------------------------------------------------------------ */
function setTheme(theme) {
state.theme = theme;
$html.setAttribute('data-theme', theme);
// dot-grid class on body for modern theme
$body.classList.toggle('bg-dot-grid', theme === 'modern');
// update buttons
$btnIndustrial.classList.toggle('active', theme === 'industrial');
$btnModern.classList.toggle('active', theme === 'modern');
// update aria-pressed
$btnIndustrial.setAttribute('aria-pressed', theme === 'industrial');
$btnModern.setAttribute('aria-pressed', theme === 'modern');
showToast(theme === 'industrial' ? 'Tema Industrial activado' : 'Tema Moderno activado');
}
$btnIndustrial.addEventListener('click', () => setTheme('industrial'));
$btnModern.addEventListener('click', () => setTheme('modern'));
/* ------------------------------------------------------------------
CLOCK
------------------------------------------------------------------ */
function updateClock() {
const now = new Date();
const opts = { year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: true };
$statusClock.textContent = now.toLocaleString('es-MX', opts);
}
updateClock();
setInterval(updateClock, 30_000);
/* ------------------------------------------------------------------
PRODUCT GRID RENDERING
------------------------------------------------------------------ */
function stockClass(stock) {
if (stock === 0) return 'out';
if (stock <= 3) return 'low';
return '';
}
function stockText(stock) {
if (stock === 0) return 'Sin stock';
if (stock <= 3) return `¡Solo ${stock}!`;
return `${stock} en stock`;
}
function renderProducts() {
const q = state.searchQuery.toLowerCase().trim();
const cat = state.activeCategory;
const filtered = PRODUCTS.filter(p => {
const matchCat = cat === 'all' || p.category === cat;
const matchSearch = !q ||
p.name.toLowerCase().includes(q) ||
p.oem.toLowerCase().includes(q);
return matchCat && matchSearch;
});
$productGrid.innerHTML = '';
if (filtered.length === 0) {
$productGrid.innerHTML = `
<div style="grid-column: 1/-1; padding: 2rem; text-align: center;
color: var(--color-text-muted); font-size: var(--text-body-sm);">
No se encontraron productos
</div>`;
return;
}
filtered.forEach(p => {
const card = document.createElement('div');
card.className = 'product-card';
card.setAttribute('role', 'listitem');
card.dataset.productId = p.id;
const sc = stockClass(p.stock);
card.innerHTML = `
<div class="product-card__category">${p.cat_label}</div>
<div class="product-card__name">${p.name}</div>
<div class="product-card__oem">OEM# ${p.oem}</div>
<div class="product-card__footer">
<div>
<div class="product-card__price">${fmt(p.price)}</div>
<div class="product-card__stock ${sc}">${stockText(p.stock)}</div>
</div>
<button
class="product-card__add"
data-product-id="${p.id}"
aria-label="Agregar ${p.name} al carrito"
${p.stock === 0 ? 'disabled' : ''}
>+</button>
</div>`;
card.querySelector('.product-card__add').addEventListener('click', e => {
e.stopPropagation();
addToCart(p.id);
card.classList.remove('added');
// force reflow
void card.offsetWidth;
card.classList.add('added');
});
card.addEventListener('click', () => addToCart(p.id));
$productGrid.appendChild(card);
});
}
/* ------------------------------------------------------------------
CART LOGIC
------------------------------------------------------------------ */
function getProduct(id) {
return PRODUCTS.find(p => p.id === id);
}
function addToCart(productId) {
const product = getProduct(productId);
if (!product || product.stock === 0) return;
const existing = state.cart.find(i => i.productId === productId);
if (existing) {
existing.qty = Math.min(existing.qty + 1, product.stock);
} else {
state.cart.push({ productId, qty: 1 });
}
renderCart();
showToast(`${product.name} agregado`);
}
function removeFromCart(productId) {
state.cart = state.cart.filter(i => i.productId !== productId);
renderCart();
}
function updateQty(productId, delta) {
const item = state.cart.find(i => i.productId === productId);
const product = getProduct(productId);
if (!item || !product) return;
item.qty += delta;
if (item.qty <= 0) {
removeFromCart(productId);
} else {
item.qty = Math.min(item.qty, product.stock);
renderCart();
}
}
function calcTotals() {
const subtotal = state.cart.reduce((acc, item) => {
const p = getProduct(item.productId);
return acc + (p ? p.price * item.qty : 0);
}, 0);
const discount = Math.min(state.discount, subtotal);
const subtotalNet = subtotal - discount;
const iva = subtotalNet * 0.16;
const total = subtotalNet + iva;
return { subtotal, discount, subtotalNet, iva, total };
}
function renderCart() {
const { subtotal, iva, total } = calcTotals();
// render items
if (state.cart.length === 0) {
$cartItems.innerHTML = `
<div class="cart-empty" role="status">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" opacity="0.4" aria-hidden="true">
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
</svg>
<span>Carrito vacío — agrega productos</span>
</div>`;
} else {
$cartItems.innerHTML = state.cart.map(item => {
const p = getProduct(item.productId);
if (!p) return '';
const lineTotal = p.price * item.qty;
return `
<div class="cart-item" role="listitem" data-product-id="${p.id}">
<div class="cart-item__qty-ctrl">
<button class="qty-btn qty-minus" data-id="${p.id}" aria-label="Quitar uno de ${p.name}"></button>
<span class="qty-display" aria-label="Cantidad: ${item.qty}">${item.qty}</span>
<button class="qty-btn qty-plus" data-id="${p.id}" aria-label="Agregar uno más de ${p.name}">+</button>
</div>
<div class="cart-item__info">
<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__total">${fmt(lineTotal)}</div>
<button class="cart-item__remove" data-id="${p.id}" aria-label="Eliminar ${p.name} del carrito">✕</button>
</div>`;
}).join('');
}
// wire cart item events
$cartItems.querySelectorAll('.qty-minus').forEach(btn =>
btn.addEventListener('click', () => updateQty(+btn.dataset.id, -1)));
$cartItems.querySelectorAll('.qty-plus').forEach(btn =>
btn.addEventListener('click', () => updateQty(+btn.dataset.id, +1)));
$cartItems.querySelectorAll('.cart-item__remove').forEach(btn =>
btn.addEventListener('click', () => removeFromCart(+btn.dataset.id)));
// update totals display
$subtotal.textContent = fmt(subtotal);
$iva.textContent = fmt(iva);
$total.textContent = fmt(total);
$cobrarLabel.textContent = `COBRAR ${fmt(total)}`;
// disable cobrar if cart is empty
$btnCobrar.disabled = state.cart.length === 0;
}
/* ------------------------------------------------------------------
DISCOUNT INPUT
------------------------------------------------------------------ */
$discountInput.addEventListener('input', () => {
state.discount = parseFloat($discountInput.value) || 0;
renderCart();
});
/* ------------------------------------------------------------------
CATEGORY FILTERS
------------------------------------------------------------------ */
document.querySelectorAll('.cat-btn').forEach(btn => {
btn.addEventListener('click', () => {
state.activeCategory = btn.dataset.category;
document.querySelectorAll('.cat-btn').forEach(b => {
b.classList.remove('active');
b.removeAttribute('aria-current');
});
btn.classList.add('active');
btn.setAttribute('aria-current', 'true');
renderProducts();
});
});
/* ------------------------------------------------------------------
SEARCH
------------------------------------------------------------------ */
$searchInput.addEventListener('input', () => {
state.searchQuery = $searchInput.value;
renderProducts();
});
/* ------------------------------------------------------------------
PAYMENT METHODS
------------------------------------------------------------------ */
document.querySelectorAll('.pay-btn').forEach(btn => {
btn.addEventListener('click', () => {
state.selectedMethod = btn.dataset.method;
document.querySelectorAll('.pay-btn').forEach(b => {
b.classList.remove('selected');
b.setAttribute('aria-pressed', 'false');
});
btn.classList.add('selected');
btn.setAttribute('aria-pressed', 'true');
});
});
/* ------------------------------------------------------------------
COBRAR
------------------------------------------------------------------ */
$btnCobrar.addEventListener('click', () => {
const { total } = calcTotals();
if (state.cart.length === 0) return;
const method = {
efectivo: 'Efectivo',
tarjeta: 'Tarjeta',
transferencia: 'Transferencia',
}[state.selectedMethod];
showToast(`✓ Venta #${state.saleNumber} completada — ${method} ${fmt(total)}`);
// Simulate new sale
setTimeout(() => {
state.saleNumber++;
state.cart = [];
state.discount = 0;
$discountInput.value = '';
document.getElementById('saleId').textContent = `Venta #${state.saleNumber}`;
renderCart();
}, 800);
});
/* ------------------------------------------------------------------
SECONDARY ACTIONS
------------------------------------------------------------------ */
document.getElementById('btnGuardar').addEventListener('click', () => {
showToast('Venta guardada como pendiente');
});
document.getElementById('btnImprimir').addEventListener('click', () => {
showToast('Enviando a impresora...');
});
document.getElementById('btnCancelar').addEventListener('click', () => {
if (state.cart.length === 0) return;
if (confirm('¿Cancelar la venta actual? Se perderán los artículos en el carrito.')) {
state.cart = [];
state.discount = 0;
$discountInput.value = '';
renderCart();
showToast('Venta cancelada');
}
});
document.getElementById('btnScan').addEventListener('click', () => {
showToast('Listo para escanear...');
$searchInput.focus();
});
document.getElementById('btnChangeCustomer').addEventListener('click', () => {
const name = prompt('Nombre del cliente:', 'Cliente General');
if (name && name.trim()) {
document.getElementById('customerName').textContent = name.trim();
showToast(`Cliente: ${name.trim()}`);
}
});
/* ------------------------------------------------------------------
NUMPAD OVERLAY
------------------------------------------------------------------ */
function openNumpad() {
state.numpadValue = '0';
$numpadDisplay.textContent = '0';
$numpadOverlay.classList.add('visible');
$numpadOverlay.setAttribute('aria-hidden', 'false');
}
function closeNumpad() {
$numpadOverlay.classList.remove('visible');
$numpadOverlay.setAttribute('aria-hidden', 'true');
}
document.getElementById('btnNumpad').addEventListener('click', openNumpad);
$numpadClose.addEventListener('click', closeNumpad);
$numpadOverlay.addEventListener('click', e => {
if (e.target === $numpadOverlay) closeNumpad();
});
document.querySelectorAll('.numpad-key').forEach(key => {
key.addEventListener('click', () => {
const k = key.dataset.key;
if (k === 'C') {
state.numpadValue = '0';
} else if (k === 'back') {
state.numpadValue = state.numpadValue.length > 1
? state.numpadValue.slice(0, -1)
: '0';
} else if (k === 'ENTER') {
const v = parseFloat(state.numpadValue);
if (!isNaN(v)) {
$discountInput.value = v.toFixed(2);
state.discount = v;
renderCart();
showToast(`Descuento aplicado: ${fmt(v)}`);
}
closeNumpad();
return;
} else if (k === '.') {
if (!state.numpadValue.includes('.')) {
state.numpadValue += '.';
}
return;
} else {
state.numpadValue = state.numpadValue === '0' ? k : state.numpadValue + k;
}
// max 8 chars
if (state.numpadValue.replace('.', '').length > 8) return;
$numpadDisplay.textContent = state.numpadValue;
});
});
/* ------------------------------------------------------------------
TOAST
------------------------------------------------------------------ */
function showToast(msg) {
const el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
$toastContainer.appendChild(el);
setTimeout(() => el.remove(), 2100);
}
/* ------------------------------------------------------------------
KEYBOARD SHORTCUTS
------------------------------------------------------------------ */
document.addEventListener('keydown', e => {
// Escape — close numpad
if (e.key === 'Escape' && $numpadOverlay.classList.contains('visible')) {
closeNumpad();
return;
}
// F2 — focus search
if (e.key === 'F2') {
e.preventDefault();
$searchInput.focus();
$searchInput.select();
}
// F5 — toggle theme
if (e.key === 'F5') {
e.preventDefault();
setTheme(state.theme === 'industrial' ? 'modern' : 'industrial');
}
});
/* ------------------------------------------------------------------
INITIAL RENDER
------------------------------------------------------------------ */
renderProducts();
renderCart();
setTheme('industrial');
</script>
</body>
</html>