feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts

Major features:
- Pixel-Perfect glassmorphism design (landing + POS + public catalog)
- OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types)
- Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications)
- Peer-to-peer inventory (multi-instance, LAN discovery)
- WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations
- Smart unified search (VIN/plate/part_number/keyword auto-detect)
- Shop Supplies tab (vehicle-independent parts)
- Chatbot AI fallback chain (5 models) + response cache
- CSV inventory import tool + setup_instance.sh installer
- Tablet-responsive CSS + sidebar toggle
- Filters, export CSV, employee edit, business data save
- Quotation system (WA→POS) with auto-print on confirmation
- Live stats on landing page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 05:35:53 +00:00
parent 6b097614a0
commit e95f7cf684
54 changed files with 11226 additions and 1422 deletions

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contabilidad — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1732,6 +1733,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/accounting.js"></script>
<script src="/pos/static/js/sync-engine.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Catalogo — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
@@ -106,6 +107,41 @@
.header-actions { display: flex; align-items: center; gap: var(--space-3); }
/* ── Catalog mode toggle (OEM / Local) ── */
.mode-toggle {
display: inline-flex;
padding: 3px;
background: var(--glass-bg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px dashed var(--glass-border);
border-radius: var(--radius-md);
gap: 2px;
flex-shrink: 0;
}
.mode-toggle button {
background: transparent;
border: none;
color: var(--color-text-muted);
padding: 4px 12px;
border-radius: calc(var(--radius-md) - 3px);
font-family: var(--font-mono);
font-size: var(--text-caption);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
cursor: pointer;
transition: all 0.2s var(--ease-out);
}
.mode-toggle button:hover {
color: var(--color-text-accent);
}
.mode-toggle button.is-active {
background: var(--color-primary-muted);
color: var(--color-text-accent);
box-shadow: 0 0 12px var(--glow-color-soft);
}
/* Search bar */
.search-bar {
display: flex; align-items: center; gap: var(--space-2);
@@ -233,8 +269,39 @@
.part-card__body { padding: var(--space-3) var(--space-4); flex: 1; }
.part-card__oem { font-family: var(--font-mono, monospace); font-size: var(--text-caption); color: var(--color-primary); font-weight: var(--font-weight-semibold); margin-bottom: var(--space-1); }
.part-card__oem-sub { font-family: var(--font-mono, monospace); font-size: 10px; color: var(--color-text-muted); font-weight: var(--font-weight-regular); }
.part-card__name { font-size: var(--text-body-sm); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); line-height: 1.3; }
/* Local mode — manufacturer badge + priority tier */
.part-card__manu {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; margin-bottom: var(--space-1);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
font-size: 10px;
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
color: var(--color-text-secondary);
}
.part-card__manu .manu-tier {
color: var(--color-primary);
font-size: 11px;
}
.part-card--tier1 {
border-color: var(--color-border-accent);
box-shadow: 0 0 12px var(--glow-color-soft);
}
.part-card--tier1 .part-card__manu {
background: var(--color-primary-muted);
border-color: var(--color-border-accent);
color: var(--color-text-accent);
}
.part-card--tier2 .part-card__manu {
border-color: var(--color-border-strong);
}
.part-card__footer {
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--color-border);
@@ -592,6 +659,11 @@
<span class="breadcrumb__current">Catalogo</span>
</nav>
<div class="header-actions" style="position:relative;">
<div class="mode-toggle" id="modeToggle" title="Cambiar entre catalogo OEM (TecDoc), marcas locales y consumibles">
<button data-mode="oem" onclick="CatalogApp.setMode('oem')">OEM</button>
<button data-mode="local" onclick="CatalogApp.setMode('local')">Local</button>
<button data-mode="supplies" onclick="CatalogApp.setMode('supplies')" title="Aceites, quimicos, herramientas — sin vehiculo">Supplies</button>
</div>
<div class="search-bar" id="searchBar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" id="searchInput" placeholder="Buscar por numero de parte o nombre... (F1)" autocomplete="off" />
@@ -751,6 +823,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/kiosk.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/catalog.js"></script>
<script src="/pos/static/js/offline-banner.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Configuración — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1332,34 +1333,36 @@
<div class="form-grid">
<div class="form-group">
<label class="form-label">Razón Social</label>
<input class="form-input" id="biz-razon-social" type="text" value="" readonly />
<input class="form-input" id="biz-razon-social" type="text" value="" placeholder="Ej: Refacciones El Toro S.A. de C.V." />
</div>
<div class="form-group">
<label class="form-label">Nombre Comercial</label>
<input class="form-input" id="biz-nombre" type="text" value="" readonly />
<input class="form-input" id="biz-nombre" type="text" value="" placeholder="Ej: Refacciones El Toro" />
</div>
<div class="form-group">
<label class="form-label">RFC</label>
<input class="form-input" id="biz-rfc" type="text" value="" readonly />
<input class="form-input" id="biz-rfc" type="text" value="" placeholder="Ej: RET260101ABC" maxlength="13" style="text-transform:uppercase;" />
</div>
<div class="form-group">
<label class="form-label">Régimen Fiscal</label>
<input class="form-input" id="biz-regimen" type="text" value="" readonly />
<input class="form-input" id="biz-regimen" type="text" value="" placeholder="Ej: 601 - General de Ley" />
</div>
<div class="form-group form-group--full">
<label class="form-label">Dirección Fiscal</label>
<input class="form-input" id="biz-direccion" type="text" value="" readonly />
<input class="form-input" id="biz-direccion" type="text" value="" placeholder="Calle, Numero, Colonia, CP, Ciudad" />
</div>
<div class="form-group">
<label class="form-label">Teléfono</label>
<input class="form-input" id="biz-telefono" type="tel" value="" readonly />
<input class="form-input" id="biz-telefono" type="tel" value="" placeholder="Ej: 664-123-4567" />
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input class="form-input" id="biz-email" type="email" value="" readonly />
<input class="form-input" id="biz-email" type="email" value="" placeholder="Ej: contacto@refacciones.com" />
</div>
</div>
<p class="form-hint" style="margin-top: var(--space-3);">Datos configurados durante el aprovisionamiento del tenant. Contacta soporte para cambios.</p>
<div style="margin-top:var(--space-4);text-align:right;">
<button class="btn btn--primary" onclick="Config.saveBusiness()">Guardar datos de empresa</button>
</div>
</div>
</div>
@@ -1420,60 +1423,13 @@
</div>
</div>
<div class="device-grid">
<div class="device-card">
<div class="device-card__icon">
<svg viewBox="0 0 24 24"><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>
</div>
<div class="device-card__body">
<div class="device-card__name">Epson TM-T88VI</div>
<div class="device-card__detail">
<span class="badge badge--ok" style="padding: 0 4px; font-size: 0.625rem;">En línea</span>
Tickets de venta
</div>
<div class="device-card__detail">USB · 192.168.10.50</div>
<div class="device-card__detail">Predeterminada para POS</div>
<div class="device-card__actions">
<button class="btn btn--ghost btn--sm">Configurar</button>
<button class="btn btn--ghost btn--sm">Test</button>
</div>
</div>
</div>
<div class="device-card">
<div class="device-card__icon">
<svg viewBox="0 0 24 24"><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>
</div>
<div class="device-card__body">
<div class="device-card__name">Zebra GK420d</div>
<div class="device-card__detail">
<span class="badge badge--ok" style="padding: 0 4px; font-size: 0.625rem;">En línea</span>
Etiquetas de código de barras
</div>
<div class="device-card__detail">USB · 192.168.10.51</div>
<div class="device-card__detail">Predeterminada para inventario</div>
<div class="device-card__actions">
<button class="btn btn--ghost btn--sm">Configurar</button>
<button class="btn btn--ghost btn--sm">Test</button>
</div>
</div>
</div>
<div class="device-card">
<div class="device-card__icon" style="background: rgba(115,115,115,.12);">
<svg viewBox="0 0 24 24" style="stroke: var(--color-text-muted);"><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>
</div>
<div class="device-card__body">
<div class="device-card__name">HP LaserJet Pro M404</div>
<div class="device-card__detail">
<span class="badge badge--inactive" style="padding: 0 4px; font-size: 0.625rem;">Fuera de línea</span>
Facturas y reportes
</div>
<div class="device-card__detail">Red · 192.168.10.52</div>
<div class="device-card__actions">
<button class="btn btn--ghost btn--sm">Configurar</button>
<button class="btn btn--ghost btn--sm">Reintentar</button>
</div>
<div class="device-grid" id="printerGrid">
<div class="device-card" style="border-style:dashed;text-align:center;color:var(--color-text-muted);padding:var(--space-8);">
<div style="font-size:2rem;margin-bottom:var(--space-3);">🖨️</div>
<div>Sin impresoras configuradas</div>
<div style="font-size:var(--text-caption);margin-top:var(--space-2);">
La configuracion de impresoras se hace desde el navegador.<br>
Ve a <strong>chrome://devices</strong> o usa <strong>Ctrl+P</strong> para imprimir.
</div>
</div>
</div>
@@ -1595,41 +1551,41 @@
<div class="form-grid">
<div class="form-group">
<label class="form-label">Tasa IVA (%)</label>
<input class="form-input" type="number" value="16" />
<input class="form-input" id="tax-iva" type="number" value="16" step="1" min="0" max="100" />
</div>
<div class="form-group">
<label class="form-label">Tasa IEPS (%)</label>
<input class="form-input" type="number" value="0" />
<input class="form-input" id="tax-ieps" type="number" value="0" step="1" min="0" />
<span class="form-hint">Dejar en 0 si no aplica</span>
</div>
<div class="form-group">
<label class="form-label">Serie de Facturación</label>
<input class="form-input" type="text" value="FA" />
<input class="form-input" id="tax-serie" type="text" value="FA" maxlength="10" style="text-transform:uppercase;" />
</div>
<div class="form-group">
<label class="form-label">Folio Actual</label>
<input class="form-input" type="number" value="893" />
<input class="form-input" id="tax-folio" type="number" value="1" min="1" />
</div>
<div class="form-group">
<label class="form-label">Moneda Predeterminada</label>
<select class="form-select">
<option selected>MXN — Peso Mexicano</option>
<option>USD — Dólar Americano</option>
<select class="form-select" id="tax-moneda">
<option value="MXN">MXN — Peso Mexicano</option>
<option value="USD">USD — Dólar Americano</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Forma de Pago Default</label>
<select class="form-select">
<option selected>01 — Efectivo</option>
<option>03 — Transferencia Electrónica</option>
<option>04 — Tarjeta de Crédito</option>
<option>28 — Tarjeta de Débito</option>
<option>99 — Por Definir</option>
<select class="form-select" id="tax-forma-pago">
<option value="01">01 — Efectivo</option>
<option value="03">03 — Transferencia Electrónica</option>
<option value="04">04 — Tarjeta de Crédito</option>
<option value="28">28 — Tarjeta de Débito</option>
<option value="99">99 — Por Definir</option>
</select>
</div>
</div>
<div class="btn-group">
<button class="btn btn--primary btn--sm">Guardar Parámetros</button>
<button class="btn btn--primary btn--sm" onclick="Config.saveTaxParams()">Guardar Parámetros</button>
</div>
</div>
</div>
@@ -1926,6 +1882,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/kiosk.js"></script>
<script src="/pos/static/js/config.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Clientes</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1721,13 +1722,41 @@
<div class="page-header__subtitle">Directorio, crédito y historial de compras</div>
</div>
<div class="page-header__actions">
<button class="btn btn-ghost">
<button class="btn btn-ghost" onclick="openCustomerFilters(this)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 5h12M4 8h8M6 11h4"/></svg>
Filtros
</button>
<button class="btn btn-ghost">
<script>
function openCustomerFilters(btn) {
var table = document.querySelector('table');
if (!table) { showToast('Carga la lista de clientes primero', 'warn'); return; }
// Auto-detect columns: look at headers to find the right indexes
var ths = table.querySelectorAll('thead th');
var colMap = {};
ths.forEach(function(th, i) {
var t = th.textContent.trim().toLowerCase();
if (t.indexOf('tipo') !== -1 || t.indexOf('tier') !== -1) colMap.tipo = i;
if (t.indexOf('ciudad') !== -1 || t.indexOf('city') !== -1) colMap.ciudad = i;
if (t.indexOf('crédito') !== -1 || t.indexOf('credito') !== -1 || t.indexOf('credit') !== -1) colMap.credito = i;
if (t.indexOf('status') !== -1 || t.indexOf('estado') !== -1) colMap.status = i;
});
var filters = [];
if (colMap.tipo !== undefined) filters.push({label:'Tipo', column: colMap.tipo, values: getUniqueColumnValues(table, colMap.tipo)});
if (colMap.credito !== undefined) filters.push({label:'Crédito', column: colMap.credito, values: getUniqueColumnValues(table, colMap.credito)});
if (colMap.ciudad !== undefined) filters.push({label:'Ciudad', column: colMap.ciudad, values: getUniqueColumnValues(table, colMap.ciudad)});
if (colMap.status !== undefined) filters.push({label:'Estado', column: colMap.status, values: getUniqueColumnValues(table, colMap.status)});
if (filters.length === 0) {
// Fallback: use first 3 columns
for (var i = 1; i < Math.min(4, ths.length); i++) {
filters.push({label: ths[i].textContent.trim(), column: i, values: getUniqueColumnValues(table, i)});
}
}
toggleFilterPanel(btn, filters);
}
</script>
<button class="btn btn-ghost" onclick="exportVisibleTableCSV('clientes')">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 10v3a1 1 0 01-1 1H3a1 1 0 01-1-1v-3M8 1v9M4 6l4 4 4-4"/></svg>
Exportar
Exportar CSV
</button>
<button class="btn btn-primary" onclick="openNewCustomerModal()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="8" y1="2" x2="8" y2="14"/><line x1="2" y1="8" x2="14" y2="8"/></svg>
@@ -2149,6 +2178,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/customers.js"></script>
<script src="/pos/static/js/offline-banner.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Dashboard</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1687,6 +1688,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/dashboard.js"></script>
<script src="/pos/static/js/sync-engine.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagramas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
@@ -606,6 +607,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/kiosk.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/diagrams.js"></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flotillas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -964,7 +965,8 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/fleet.js"></script>
<script src="/pos/static/js/offline-banner.js"></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inventario — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1468,9 +1469,9 @@
<h1 class="page-header__title">Inventario</h1>
</div>
<div class="page-header__actions">
<button class="btn btn--ghost" onclick="alert('Exportar: próximamente')">
<button class="btn btn--ghost" onclick="exportVisibleTableCSV('inventario')">
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Exportar
Exportar CSV
</button>
<button class="btn btn--ghost" onclick="loadItems(1,'')">
<svg viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-.38-4.93"/></svg>
@@ -1585,14 +1586,24 @@
<option>OK</option><option>Bajo</option><option>Sobrestock</option>
</select>
<div class="toolbar__spacer"></div>
<button class="btn btn--ghost btn--sm">
<button class="btn btn--ghost btn--sm" onclick="openInventoryFilters(this)">
<svg viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
Filtros
</button>
<button class="btn btn--ghost btn--sm">
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Columnas
</button>
<script>
function openInventoryFilters(btn) {
var table = document.querySelector('table');
if (!table) { showToast('Carga el inventario primero', 'warn'); return; }
var brands = getUniqueColumnValues(table, 3); // brand column
var categories = getUniqueColumnValues(table, 4); // category column
var statuses = getUniqueColumnValues(table, 5); // stock status column
toggleFilterPanel(btn, [
{label: 'Marca', column: 3, values: brands},
{label: 'Categoría', column: 4, values: categories},
{label: 'Estado Stock', column: 5, values: statuses},
]);
}
</script>
<button class="btn btn--primary btn--sm" onclick="showCreateModal()">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Nuevo Producto
@@ -2097,6 +2108,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/inventory.js"></script>
<script src="/pos/static/js/offline-banner.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Facturación CFDI — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1516,15 +1517,43 @@
<h1 class="page-header__title">Facturación CFDI</h1>
</div>
<div class="page-header__actions">
<button class="btn btn--ghost">
<button class="btn btn--ghost" onclick="openInvoiceFilters(this)">
<svg viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
Filtros
</button>
<button class="btn btn--ghost" onclick="exportVisibleTableCSV('facturacion')">
<svg viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Exportar
Exportar CSV
</button>
<button class="btn btn--secondary" onclick="window.notaCreditoPlaceholder()">
<script>
function openInvoiceFilters(btn) {
var table = document.querySelector('table');
if (!table) { showToast('Carga las facturas primero', 'warn'); return; }
var ths = table.querySelectorAll('thead th');
var colMap = {};
ths.forEach(function(th, i) {
var t = th.textContent.trim().toLowerCase();
if (t.indexOf('status') !== -1 || t.indexOf('estado') !== -1) colMap.status = i;
if (t.indexOf('cliente') !== -1 || t.indexOf('receptor') !== -1) colMap.cliente = i;
if (t.indexOf('tipo') !== -1) colMap.tipo = i;
});
var filters = [];
if (colMap.status !== undefined) filters.push({label:'Estado', column: colMap.status, values: getUniqueColumnValues(table, colMap.status)});
if (colMap.tipo !== undefined) filters.push({label:'Tipo', column: colMap.tipo, values: getUniqueColumnValues(table, colMap.tipo)});
if (colMap.cliente !== undefined) filters.push({label:'Cliente', column: colMap.cliente, values: getUniqueColumnValues(table, colMap.cliente, 15)});
if (filters.length === 0) {
for (var i = 1; i < Math.min(3, ths.length); i++) {
filters.push({label: ths[i].textContent.trim(), column: i, values: getUniqueColumnValues(table, i)});
}
}
toggleFilterPanel(btn, filters);
}
</script>
<button class="btn btn--secondary" onclick="showToast('Nota de Crédito requiere integración SAT — disponible en siguiente actualización', 'info')">
<svg viewBox="0 0 24 24">
<path d="M9 14l-4-4 4-4"/>
<path d="M5 10h11a4 4 0 0 1 0 8h-1"/>
@@ -2359,6 +2388,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/invoicing.js"></script>
<script src="/pos/static/js/sync-engine.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Iniciar Sesión</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
<style>

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
<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" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1481,6 +1482,7 @@
================================================================ -->
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/kiosk.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/push.js"></script>
<script src="/pos/static/js/printer.js"></script>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="es" data-theme="industrial">
<head>
<script>/*pos_theme_early*/(function(){var t=localStorage.getItem("pos_theme")||"industrial";document.documentElement.setAttribute("data-theme",t);})()</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cotizaciones — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-body); background: var(--color-bg-base); color: var(--color-text-primary); min-height: 100vh; }
.page { max-width: 1200px; margin: 0 auto; padding: var(--space-6); margin-left: 240px; }
@media (max-width: 1023px) { .page { margin-left: 0; } }
.page-title { font-family: var(--font-heading); font-size: var(--text-h3); margin-bottom: var(--space-6); }
.quote-table { width: 100%; border-collapse: collapse; background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.quote-table th, .quote-table td { padding: var(--space-3) var(--space-4); text-align: left; border-bottom: 1px solid var(--glass-border); }
.quote-table th { background: var(--glass-bg-strong); font-family: var(--font-mono); font-size: var(--text-caption); text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-muted); }
.quote-table tbody tr { cursor: pointer; transition: background 0.15s; }
.quote-table tbody tr:hover { background: var(--glass-highlight); }
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; }
.badge--active { background: rgba(63,185,80,0.15); color: #3FB950; }
.badge--converted { background: rgba(0,212,255,0.15); color: #00D4FF; }
.badge--cancelled { background: rgba(248,81,73,0.15); color: #F85149; }
.badge--expired { background: rgba(130,130,130,0.2); color: #888; }
.badge--wa { background: rgba(37,211,102,0.15); color: #25D366; }
.badge--pos { background: var(--color-primary-muted); color: var(--color-text-accent); }
.modal-overlay { display:none; position:fixed; inset:0; z-index:1000; background:var(--overlay-backdrop); backdrop-filter:blur(4px); align-items:flex-start; justify-content:center; padding:var(--space-8) var(--space-4); overflow-y:auto; }
.modal-overlay.open { display:flex; }
.modal-content { background:var(--glass-bg-strong); backdrop-filter:blur(24px); border:1px solid var(--glass-border); border-radius:var(--radius-lg); max-width:650px; width:100%; padding:var(--space-6); position:relative; }
.modal-close { position:absolute; top:var(--space-3); right:var(--space-3); background:none; border:none; color:var(--color-text-muted); font-size:1.4rem; cursor:pointer; }
.detail-table { width:100%; border-collapse:collapse; margin:var(--space-4) 0; }
.detail-table th, .detail-table td { padding:var(--space-2) var(--space-3); text-align:left; border-bottom:1px solid var(--glass-border); font-size:var(--text-body-sm); }
.detail-table th { color:var(--color-text-muted); font-size:var(--text-caption); text-transform:uppercase; }
.empty { text-align:center; padding:var(--space-12); color:var(--color-text-muted); }
</style>
</head>
<body>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<div class="page">
<h1 class="page-title">Cotizaciones</h1>
<div id="quoteList">Cargando...</div>
</div>
<div class="modal-overlay" id="quoteModal">
<div class="modal-content">
<button class="modal-close" onclick="document.getElementById('quoteModal').classList.remove('open')">&times;</button>
<div id="quoteDetail">Cargando...</div>
</div>
</div>
<script>
(function() {
var token = localStorage.getItem('pos_token');
if (!token) { window.location.href = '/pos/login'; return; }
var API = '/pos/api';
function headers() { return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; }
function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function fmt(n) { return (n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function loadQuotes() {
fetch(API + '/quotations?per_page=50', { headers: headers() })
.then(function(r) { return r.json(); })
.then(function(d) {
var quotes = d.data || [];
if (!quotes.length) {
document.getElementById('quoteList').innerHTML = '<div class="empty"><h3>Sin cotizaciones</h3><p>Las cotizaciones creadas desde el POS (F4) o desde WhatsApp aparecen aqui.</p></div>';
return;
}
var html = '<table class="quote-table"><thead><tr>';
html += '<th>#</th><th>Origen</th><th>Cliente</th><th>Total</th><th>Estado</th><th>Fecha</th><th></th>';
html += '</tr></thead><tbody>';
quotes.forEach(function(q) {
var srcBadge = q.source === 'whatsapp'
? '<span class="badge badge--wa">📱 WA</span>'
: '<span class="badge badge--pos">🖥️ POS</span>';
var statusBadge = '<span class="badge badge--' + q.status + '">' + q.status + '</span>';
var client = q.customer_name || (q.wa_phone ? '📱 ' + q.wa_phone : 'Sin cliente');
var dateStr = q.created_at ? new Date(q.created_at).toLocaleDateString('es-MX') : '';
html += '<tr>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;"><strong>#' + q.id + '</strong></td>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;">' + srcBadge + '</td>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;">' + esc(client) + '</td>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;font-family:var(--font-mono);font-weight:700;">$' + fmt(q.total) + '</td>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;">' + statusBadge + '</td>';
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;color:var(--color-text-muted);">' + dateStr + '</td>';
html += '<td><button onclick="deleteQuote(' + q.id + ', event)" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:16px;padding:4px 8px;border-radius:4px;" onmouseover="this.style.color=\'#F85149\';this.style.background=\'rgba(248,81,73,0.1)\'" onmouseout="this.style.color=\'var(--color-text-muted)\';this.style.background=\'none\'">🗑️</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
document.getElementById('quoteList').innerHTML = html;
})
.catch(function() {
document.getElementById('quoteList').innerHTML = '<div class="empty">Error cargando cotizaciones</div>';
});
}
window.openQuote = function(id) {
var modal = document.getElementById('quoteModal');
modal.classList.add('open');
document.getElementById('quoteDetail').innerHTML = 'Cargando...';
fetch(API + '/quotations/' + id, { headers: headers() })
.then(function(r) { return r.json(); })
.then(function(q) {
if (q.error) { document.getElementById('quoteDetail').innerHTML = 'Error: ' + esc(q.error); return; }
var src = (q.notes || '').startsWith('WA:') ? 'WhatsApp' : 'POS';
var waPhone = src === 'WhatsApp' ? q.notes.replace('WA:', '') : null;
var html = '<h3 style="font-family:var(--font-heading);margin-bottom:var(--space-4);">Cotización #' + q.id + '</h3>';
html += '<div style="display:flex;gap:var(--space-6);margin-bottom:var(--space-4);font-size:var(--text-body-sm);">';
html += '<div><span style="color:var(--color-text-muted);">Origen:</span> ' + src + '</div>';
if (waPhone) html += '<div><span style="color:var(--color-text-muted);">WhatsApp:</span> +' + esc(waPhone) + '</div>';
if (q.customer_name) html += '<div><span style="color:var(--color-text-muted);">Cliente:</span> ' + esc(q.customer_name) + '</div>';
html += '<div><span style="color:var(--color-text-muted);">Estado:</span> <span class="badge badge--' + q.status + '">' + q.status + '</span></div>';
html += '<div><span style="color:var(--color-text-muted);">Vigencia:</span> ' + (q.valid_until || '—') + '</div>';
html += '</div>';
html += '<table class="detail-table"><thead><tr><th>#Parte</th><th>Nombre</th><th>Cant</th><th>Precio</th><th>Subtotal</th></tr></thead><tbody>';
(q.items || []).forEach(function(it) {
html += '<tr>';
html += '<td style="font-family:var(--font-mono);">' + esc(it.part_number) + '</td>';
html += '<td>' + esc(it.name) + '</td>';
html += '<td>' + it.quantity + '</td>';
html += '<td>$' + fmt(it.unit_price) + '</td>';
html += '<td style="font-weight:700;">$' + fmt(it.subtotal) + '</td>';
html += '</tr>';
});
html += '</tbody></table>';
html += '<div style="text-align:right;margin-top:var(--space-4);font-size:var(--text-body);">';
html += '<div>Subtotal: $' + fmt(q.subtotal) + '</div>';
html += '<div>IVA: $' + fmt(q.tax_total) + '</div>';
html += '<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-text-accent);">Total: $' + fmt(q.total) + '</div>';
html += '</div>';
html += '<div style="margin-top:var(--space-5);display:flex;gap:var(--space-3);justify-content:flex-end;">';
html += '<button class="btn btn--ghost" onclick="deleteQuote(' + q.id + ')" style="color:#F85149;">Eliminar</button>';
html += '<button class="btn btn--ghost" onclick="exportVisibleTableCSV(\'cotizacion_' + q.id + '\')">Exportar CSV</button>';
html += '<button class="btn btn--ghost" onclick="window.print()">Imprimir</button>';
html += '</div>';
document.getElementById('quoteDetail').innerHTML = html;
});
};
window.deleteQuote = function(id, event) {
if (event) event.stopPropagation();
if (!confirm('¿Eliminar cotización #' + id + '? Esta acción no se puede deshacer.')) return;
fetch(API + '/quotations/' + id, { method: 'DELETE', headers: headers() })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
document.getElementById('quoteModal').classList.remove('open');
loadQuotes();
if (typeof showToast === 'function') showToast('Cotización #' + id + ' eliminada', 'ok');
} else {
alert('Error: ' + (d.error || 'desconocido'));
}
});
};
// Close modal on outside click
document.getElementById('quoteModal').addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('open');
});
loadQuotes();
})();
</script>
</body>
</html>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reportes — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -1690,17 +1691,17 @@
<h1 class="content-header__title">Reportes</h1>
</div>
<div class="content-header__actions">
<button class="btn btn-ghost">
<button class="btn btn-ghost" onclick="exportReportCSV()">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" width="14" height="14">
<rect x="1" y="1" width="12" height="12" rx="1"/><line x1="4" y1="5" x2="10" y2="5"/><line x1="4" y1="8" x2="8" y2="8"/>
</svg>
Exportar Excel
Exportar Excel (CSV)
</button>
<button class="btn btn-primary">
<button class="btn btn-primary" onclick="window.print()">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" width="14" height="14">
<rect x="2" y="1" width="10" height="12" rx="1"/><line x1="5" y1="5" x2="9" y2="5"/><line x1="5" y1="7" x2="9" y2="7"/><line x1="5" y1="9" x2="7" y2="9"/>
</svg>
Exportar PDF
Exportar PDF (Imprimir)
</button>
</div>
</div>
@@ -1848,6 +1849,7 @@
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/app-init.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/reports.js"></script>
<script src="/pos/static/js/sync-engine.js"></script>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsApp — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -200,6 +201,24 @@
margin-top: 2px;
}
.conv-item__delete {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
color: var(--color-text-muted);
font-size: 16px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
padding: 2px 6px;
border-radius: 4px;
}
.conv-item:hover .conv-item__delete { opacity: 1; }
.conv-item__delete:hover { color: #F85149; background: rgba(248,81,73,0.1); }
.conv-item { position: relative; }
.conv-empty {
padding: var(--space-6);
text-align: center;
@@ -611,7 +630,8 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href='
<!-- Sidebar -->
<script src="/pos/static/js/i18n.js"></script>
<script src="/pos/static/js/whatsapp.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
<script src="/pos/static/js/pos-utils.js"></script>
<script src="/pos/static/js/sidebar.js"></script>
</body>
</html>