Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -493,7 +493,7 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -316,7 +316,7 @@
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/kiosk.js" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -367,6 +367,10 @@
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-payment" style="width:auto;" checked /> Mostrar detalle de pago</label>
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-employee" style="width:auto;" /> Mostrar nombre del empleado</label>
</div>
<div class="form-group" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" id="receipt-auto-print" />
<label for="receipt-auto-print" style="margin:0;">Imprimir ticket automáticamente al cobrar</label>
</div>
<div style="margin-top:var(--space-4);text-align:right;">
<button class="btn btn--primary" onclick="Config.saveReceiptConfig()">Guardar ticket</button>
</div>
@@ -454,6 +458,42 @@
</div>
</div>
<!-- ===============================================================
SECTION 4B: ALERTAS POR WHATSAPP (contactos por negocio)
=============================================================== -->
<div class="settings-section" data-tab="general">
<div class="settings-section__header">
<div class="settings-section__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
</div>
<div>
<div class="settings-section__title">Alertas por WhatsApp</div>
<div class="settings-section__desc">Números que reciben las alertas automáticas de este negocio. Si no configuras ninguno, las alertas no se envían.</div>
</div>
</div>
<div class="settings-card">
<div class="toggle-row">
<div class="toggle-row__info">
<span class="toggle-row__label">Enviar alertas por WhatsApp</span>
<span class="toggle-row__desc">Activa el reenvío de alertas al número de este negocio</span>
</div>
<label class="toggle">
<input type="checkbox" id="cfg-alert-whatsapp-enabled" />
<span class="toggle__slider"></span>
</label>
</div>
<div class="form-group" style="margin-top:var(--space-3);">
<label class="form-label">Números de alerta</label>
<input type="text" class="form-input" id="alert-phones" placeholder="+5215512345678, +5215587654321" />
<span class="form-hint">Formato internacional, separados por coma. Solo dígitos y "+" (10-15 dígitos).</span>
</div>
<div style="margin-top:var(--space-4);display:flex;justify-content:flex-end;">
<button class="btn btn--primary" id="btn-save-alert-contacts">Guardar alertas</button>
</div>
</div>
</div>
<!-- ===============================================================
SECTION 4: USUARIOS Y PERMISOS
=============================================================== -->
@@ -598,7 +638,7 @@
<span class="toggle-row__desc">Generar CFDI automáticamente al cerrar una venta</span>
</div>
<label class="toggle">
<input type="checkbox" checked />
<input type="checkbox" id="cfg-invoice-auto" checked />
<span class="toggle__slider"></span>
</label>
</div>
@@ -608,7 +648,7 @@
<span class="toggle-row__desc">Enviar PDF y XML al correo del cliente</span>
</div>
<label class="toggle">
<input type="checkbox" checked />
<input type="checkbox" id="cfg-invoice-email" checked />
<span class="toggle__slider"></span>
</label>
</div>
@@ -618,7 +658,7 @@
<span class="toggle-row__desc">Generar CFDI global para ventas sin RFC</span>
</div>
<label class="toggle">
<input type="checkbox" />
<input type="checkbox" id="cfg-invoice-global" />
<span class="toggle__slider"></span>
</label>
</div>
@@ -835,6 +875,42 @@
</div>
</div>
<!-- ===============================================================
SECTION 9B: PRECIOS POR TIPO DE CLIENTE
=============================================================== -->
<div class="settings-section" data-tab="catalog">
<div class="settings-section__header">
<div class="settings-section__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
</div>
<div>
<div class="settings-section__title">Precios por Tipo de Cliente</div>
<div class="settings-section__desc">Descuento sobre precio de mostrador para clientes Taller y Mayoreo</div>
</div>
</div>
<div class="settings-card">
<div class="settings-card__title">Descuentos por Nivel de Precio</div>
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
<div class="form-group">
<label class="form-label">Taller (Nivel 2) — % descuento</label>
<input class="form-input" id="cfg-tier-disc-2" type="number" min="0" max="100" step="0.5" value="0" />
</div>
<div class="form-group">
<label class="form-label">Mayoreo (Nivel 3) — % descuento</label>
<input class="form-input" id="cfg-tier-disc-3" type="number" min="0" max="100" step="0.5" value="0" />
</div>
</div>
<p style="color:var(--color-text-muted);font-size:var(--text-caption);margin-top:var(--space-2);">
El descuento se aplica sobre el precio de mostrador al crear o actualizar el precio de un producto. No modifica los precios ya calculados de productos existentes.
</p>
<div style="margin-top:var(--space-4);display:flex;gap:var(--space-3);align-items:center;justify-content:flex-end;">
<span id="tier-discount-status" style="font-size:var(--text-caption);color:var(--color-text-muted);"></span>
<button class="btn btn--primary" id="btn-save-tier-discounts" onclick="Config.saveTierDiscounts()">Guardar Descuentos</button>
</div>
</div>
</div>
<!-- ===============================================================
SECTION 10: MONEDA / CURRENCY
=============================================================== -->
@@ -900,6 +976,7 @@
<option value="accountant">Contador</option>
<option value="workshop">Taller</option>
<option value="mechanic">Mecánico</option>
<option value="driver">Repartidor</option>
</select>
</div>
@@ -1044,6 +1121,7 @@
<option value="accountant">Contador</option>
<option value="workshop">Taller</option>
<option value="mechanic">Mecanico</option>
<option value="driver">Repartidor</option>
</select>
</div>
<div class="form-group">
@@ -1070,12 +1148,12 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/kiosk.js" defer></script>
<script src="/pos/static/js/config.js?v=41" defer></script>
<script src="/pos/static/js/config.js?v=45" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/pwa-install.js" defer></script>

View File

@@ -417,7 +417,23 @@
</div>
</div>
<!-- Credit Info -->
<!-- CRM & Loyalty -->
<div class="detail-section" id="crmSection">
<div class="detail-section__title">CRM &amp; Lealtad</div>
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-2);">
<div style="font-size:var(--text-h4);font-weight:700;" id="crmLoyaltyBalance">—</div>
<span style="color:var(--color-text-muted);font-size:var(--text-caption);">puntos</span>
<div style="flex:1;"></div>
<button class="btn btn-sm btn-ghost" onclick="Customers.crmRedeem()">Canjear</button>
<button class="btn btn-sm btn-ghost" onclick="Customers.crmAddNote()">Nota</button>
</div>
<div id="crmActivities" style="max-height:180px;overflow-y:auto;display:flex;flex-direction:column;gap:var(--space-2);">
<span style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando actividad...</span>
</div>
</div>
<!-- Credit Info -->
<div class="detail-section">
<div class="detail-section__title">Crédito</div>
<div class="credit-block">
@@ -651,12 +667,12 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/virtual-scroll.js" defer></script>
<script src="/pos/static/js/customers.js?v=35" defer></script>
<script src="/pos/static/js/customers.js?v=38" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -606,14 +606,14 @@
<script src="/pos/static/js/chart.umd.min.js" defer></script>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/dashboard-stats.js?v=33" defer></script>
<script src="/pos/static/js/dashboard.js?v=33" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js?v=34',{scope:'/pos/'});}</script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/pwa-install.js" defer></script>
<script src="/pos/static/js/chat.js" defer></script>

View File

@@ -150,7 +150,7 @@
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/kiosk.js" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -303,10 +303,11 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/virtual-scroll.js?v=1" defer></script>
<script src="/pos/static/js/fleet.js" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -194,11 +194,11 @@
<div class="page-header__actions">
<button class="btn btn--ghost" id="btnHeaderImport" onclick="document.getElementById('bulkImportModal').classList.add('is-open')">
<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="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar CSV
Importar
</button>
<button class="btn btn--ghost" onclick="exportVisibleTableCSV('inventario')">
<button class="btn btn--ghost" onclick="document.getElementById('exportModal').classList.add('is-open')">
<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 CSV
Exportar
</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>
@@ -788,24 +788,41 @@
<!-- Transfer Modal -->
<div class="inv-modal-overlay" id="transferModal">
<div class="inv-modal">
<div class="inv-modal inv-modal--wide">
<div class="inv-modal__header">
<h3>Nuevo Traspaso</h3>
<h3>Transferencias entre Sucursales</h3>
<button class="inv-modal__close" onclick="closeTransferModal()">&times;</button>
</div>
<div class="inv-modal__body">
<div class="inv-form-grid">
<div class="inv-field"><label>ID Producto *</label><input type="number" id="transferItemId" placeholder="ID inventario" /></div>
<div class="inv-field"><label>Cantidad *</label><input type="number" id="transferQty" placeholder="Cantidad" /></div>
<div class="inv-field"><label>Sucursal Origen *</label><input type="number" id="transferFrom" placeholder="ID sucursal origen" /></div>
<div class="inv-field"><label>Sucursal Destino *</label><input type="number" id="transferTo" placeholder="ID sucursal destino" /></div>
<div class="inv-field inv-field--full"><label>Notas</label><input type="text" id="transferNotes" placeholder="Notas adicionales" /></div>
<div class="cfg-tabs" style="position:static;margin-bottom:12px;">
<button type="button" class="cfg-tab-btn active" id="tr-tab-new" onclick="Inventory.switchTransferTab('new')">Nueva Transferencia</button>
<button type="button" class="cfg-tab-btn" id="tr-tab-list" onclick="Inventory.switchTransferTab('list')">Enviadas / Por Recibir</button>
</div>
<div id="tr-panel-new">
<div class="inv-form-grid">
<div class="inv-field"><label>Sucursal Destino *</label><select id="transferTo" class="form-input"><option value="">--</option></select></div>
<div class="inv-field inv-field--full"><label>Notas</label><input type="text" id="transferNotes" class="form-input" placeholder="Notas adicionales" /></div>
</div>
<div style="margin:10px 0;">
<input type="text" id="transferScan" class="form-input" placeholder="Escanea código de barras / No. parte y Enter" style="width:100%;" onkeydown="if(event.key==='Enter'){event.preventDefault();Inventory.transferAddByCode(this.value);this.value='';}" />
</div>
<div id="transferLines" style="max-height:220px;overflow-y:auto;border:1px solid var(--color-border);border-radius:8px;">
<p style="padding:10px;color:var(--color-text-muted);font-size:var(--text-caption);">Escanea o busca productos para agregarlos</p>
</div>
<div id="transferResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
</div>
<div id="tr-panel-list" style="display:none;">
<div id="transferDocsList" style="max-height:380px;overflow-y:auto;">
<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando...</p>
</div>
</div>
<div id="transferResult" style="margin-top:var(--space-3);min-height:1.5em;"></div>
</div>
<div class="inv-modal__footer">
<button class="btn btn--ghost" onclick="closeTransferModal()">Cancelar</button>
<button class="btn btn--primary" onclick="recordTransfer()">Registrar Traspaso</button>
<div class="inv-modal__footer" id="transferNewFooter">
<button class="btn btn--ghost" onclick="closeTransferModal()">Cerrar</button>
<button class="btn btn--primary" onclick="transferDocSave(false)">Guardar Borrador</button>
<button class="btn btn--primary" onclick="transferDocSave(true)" style="background:var(--color-success);">Guardar y Enviar</button>
</div>
</div>
</div>
@@ -840,8 +857,13 @@
<button class="inv-modal__close" onclick="closeCountModal()">&times;</button>
</div>
<div class="inv-modal__body">
<div id="countLines"></div>
<button class="btn btn--ghost btn--sm" onclick="addCountLine()" style="margin-top:var(--space-2);">+ Agregar Línea</button>
<div style="margin-bottom:10px;">
<input type="text" id="countScanInput" class="form-input" placeholder="Escanea código de barras / No. parte y Enter" style="width:100%;" onkeydown="if(event.key==='Enter'){event.preventDefault();Inventory.countAddByCode(this.value);this.value='';}" />
<p style="margin:6px 0 0;font-size:var(--text-caption);color:var(--color-text-muted);">Cada escaneo suma 1. Edita la cantidad si es necesario. Cantidades vacías = 0 (faltante).</p>
</div>
<div id="countLines" style="max-height:300px;overflow-y:auto;border:1px solid var(--color-border);border-radius:8px;">
<p style="padding:10px;color:var(--color-text-muted);font-size:var(--text-caption);">Sin productos escaneados</p>
</div>
<div id="countResults" style="margin-top:var(--space-4);"></div>
</div>
<div class="inv-modal__footer">
@@ -913,7 +935,7 @@
</div>
<div class="inv-field">
<label>Tipo de Publicación</label>
<select id="meliListingType">
<select id="meliListingType" onchange="scheduleMeliFeeEstimates()">
<option value="gold_special">Gold Special</option>
<option value="gold_pro">Gold Pro</option>
<option value="bronze">Bronce (gratis)</option>
@@ -1040,6 +1062,12 @@
<option value="reject">Rechazar filas sin compatibilidad</option>
</select>
</div>
<div style="margin-bottom:12px;">
<label style="display:flex;align-items:center;gap:8px;font-size:var(--text-body-sm);color:var(--color-text);cursor:pointer;">
<input type="checkbox" id="bulkImportSkipStock" />
No modificar existencias de productos existentes (solo actualizar datos y precios)
</label>
</div>
<div style="font-size:var(--text-caption);color:var(--color-text-muted);background:var(--color-surface);padding:10px;border-radius:6px;">
<strong>Columnas esperadas:</strong>
<code style="display:block;margin-top:4px;word-break:break-all;">sku, name, brand, price, stock, cost, sku_secondary, description, category, make, model, year, engine, engine_code</code>
@@ -1054,6 +1082,42 @@
</div>
</div>
<!-- ══════════ Export Modal ══════════ -->
<div class="inv-modal-overlay" id="exportModal">
<div class="inv-modal" style="max-width:480px;">
<div class="inv-modal__header">
<h3>Exportar Inventario</h3>
<button class="inv-modal__close" onclick="document.getElementById('exportModal').classList.remove('is-open')">&times;</button>
</div>
<div class="inv-modal__body">
<div style="display:flex;flex-direction:column;gap:12px;">
<div>
<label style="display:block;margin-bottom:4px;font-size:var(--text-caption);color:var(--color-text-muted);">Sucursal</label>
<select id="exportBranch" class="select-filter" style="width:100%;">
<option value="">Todas las sucursales — existencias totales</option>
</select>
</div>
<button class="btn btn--primary" onclick="exportFullInventory()" type="button" id="btnExportFull">
📦 Inventario completo (CSV)
</button>
<p style="margin:0;font-size:var(--text-caption);color:var(--color-text-muted);">
Descarga <strong>todos los productos</strong> con número de parte, SKU alterno, nombre, marca, categoría, precios (mostrador/taller/mayoreo), existencias totales, ubicación y código de barras. Compatible con la plantilla de importación: puedes editarlo y re-importarlo.
</p>
<hr style="border:none;border-top:1px solid var(--color-border);margin:4px 0;" />
<button class="btn btn--ghost" onclick="exportCurrentView()" type="button">
📄 Solo la vista actual (lo que se ve en pantalla)
</button>
<p style="margin:0;font-size:var(--text-caption);color:var(--color-text-muted);">
Exporta únicamente las filas y columnas visibles de la tabla actual (según página y filtros aplicados).
</p>
</div>
</div>
<div class="inv-modal__footer">
<button class="btn btn--ghost" onclick="document.getElementById('exportModal').classList.remove('is-open')">Cerrar</button>
</div>
</div>
</div>
<!-- Offline Banner -->
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
<span class="banner__icon"></span>
@@ -1062,12 +1126,12 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/virtual-scroll.js?v=33" defer></script>
<script src="/pos/static/js/inventory.js?v=38" defer></script>
<script src="/pos/static/js/inventory.js?v=45" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -1138,7 +1138,7 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -389,6 +389,8 @@
var role = (result.data.employee.role || '').toLowerCase();
if (role === 'workshop' || role === 'mechanic' || role === 'counter') {
window.location.href = '/pos/workshop';
} else if (role === 'driver') {
window.location.href = '/pos/remission-notes';
} else {
window.location.href = '/pos/catalog';
}

View File

@@ -343,11 +343,11 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/marketplace_external.js?v=33" defer></script>
<script src="/pos/static/js/marketplace_external.js?v=35" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
</body>
</html>

View File

@@ -19,6 +19,9 @@
<link rel="stylesheet" href="/pos/static/css/pos.css?v=35"></head>
<body class="pos-shell" id="appBody">
<div id="offlineBanner" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;padding:10px 16px;align-items:center;justify-content:center;gap:8px;font-size:var(--text-body-sm);">
<span id="offlineBannerText"></span>
</div>
<!-- ================================================================
STATUS BAR
@@ -63,6 +66,13 @@
placeholder="Buscar por nombre, No. parte, codigo..."
autocomplete="off" spellcheck="false" aria-label="Buscar productos" />
</div>
<div class="search-wrap" style="max-width:260px;display:flex;gap:6px;">
<input class="search-input" type="text" id="vehicleLookup"
placeholder="Placa o VIN"
autocomplete="off" spellcheck="false" aria-label="Placa o VIN"
onkeydown="if(event.key==='Enter'){event.preventDefault();POS.vehicleLookup();}" />
<button class="btn-secondary-action" onclick="POS.vehicleLookup()" title="Ver piezas compatibles">🚗</button>
</div>
</div>
<!-- Search Results Dropdown -->
@@ -177,6 +187,15 @@
<span class="margin-summary__value" id="avgMargin">--</span>
</div>
<!-- Cross-sell suggestions (A10) -->
<div id="crossSellPanel" style="display:none;margin:6px 0;padding:8px 10px;border:1px dashed var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg-base);">
<div style="display:flex;justify-content:space-between;align-items:center;font-size:var(--text-caption);font-weight:var(--font-weight-semibold);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.4px;">
<span>&iquest;Ya que est&aacute;s en eso&hellip;?</span>
<button type="button" title="Ocultar" onclick="POS.hideCrossSell()" style="background:transparent;border:none;color:var(--color-text-muted);cursor:pointer;font-size:14px;">&times;</button>
</div>
<div id="crossSellList" style="display:flex;flex-direction:column;gap:4px;margin-top:6px;"></div>
</div>
<!-- Cart Footer -->
<div class="cart-footer">
<div class="totals-block">
@@ -224,6 +243,9 @@
<button class="btn-secondary-action" onclick="POS.createLayaway()" title="Apartado (requiere cliente)">Apartado</button>
<button class="btn-secondary-action" onclick="POS.createServiceOrder()" title="Orden de servicio (F7)">Orden Taller</button>
<button class="btn-secondary-action" onclick="POS.showLastSale()" title="Ultima venta (F5)">Ult.Venta</button>
<span id="offlineQueueBadge" style="display:none;background:var(--color-warning);color:#fff;border-radius:999px;padding:2px 10px;font-weight:700;font-size:var(--text-caption);">0</span>
<button class="btn-secondary-action" onclick="POS.openReturnModal()" title="Devolución de venta">Devolución</button>
<button class="btn-secondary-action" onclick="POS.openLayawaysModal()" title="Apartados activos">Apartados</button>
<button class="btn-secondary-action" onclick="POS.showCutZModal()" title="Corte Z - Cerrar caja">Corte Z</button>
<button class="btn-secondary-action danger" id="btnCancelSale" onclick="POS.openCancelModal()" title="Cancelar (Esc)">Cancelar</button>
</div>
@@ -325,7 +347,15 @@
<label class="form-label">Monto recibido</label>
<input type="number" class="form-input form-input-lg" id="cashReceived"
placeholder="0.00" step="0.01" min="0" oninput="POS.updateChange()" />
</div>
<div style="display:flex;align-items:center;gap:8px;margin-top:8px;">
<label style="font-size:var(--text-caption);color:var(--color-text-muted);">Moneda:</label>
<select id="payCurrency" class="form-input" style="width:90px;padding:4px 8px;" onchange="POS.updateChange()">
<option value="MXN">MXN $</option>
<option value="USD">USD</option>
</select>
<span id="usdRateHint" style="font-size:var(--text-caption);color:var(--color-text-muted);"></span>
</div></div>
<div class="quick-amounts" id="quickAmounts">
<!-- Populated dynamically -->
</div>
@@ -416,6 +446,14 @@
<span class="cfdi-hint" id="cfdiHint"></span>
</div>
<!-- Repartidor (opcional) -->
<div class="form-group" style="margin-top: 12px;">
<label class="form-label" for="saleCourier">Repartidor (opcional)</label>
<select class="form-input" id="saleCourier">
<option value="">— Sin repartidor —</option>
</select>
</div>
<!-- Footer -->
<div class="modal-footer">
<button class="btn btn-ghost" onclick="POS.closePaymentModal()">Cancelar</button>
@@ -729,11 +767,11 @@
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/kiosk.js" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/push.js" defer></script>
<script src="/pos/static/js/printer.js" defer></script>
<script src="/pos/static/js/pos.js?v=42" defer></script>
<script src="/pos/static/js/pos.js?v=46" defer></script>
<script>
// Cancel sale button wiring
@@ -798,7 +836,68 @@
<script src="/pos/static/js/chat.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/pwa-install.js" defer></script>
<!-- ══════════ RETURN MODAL ══════════ -->
<div class="modal-overlay" id="returnModal">
<div class="modal-box" style="max-width:640px;">
<div class="modal-header">
<h3>Devolución de venta</h3>
<button class="btn-close" onclick="POS.closeReturnModal()">&times;</button>
</div>
<div class="modal-body">
<div style="display:flex;gap:8px;margin-bottom:12px;">
<input type="number" id="returnSaleId" class="form-input" placeholder="Folio de venta (ej. 123)" style="flex:1;" />
<button class="btn btn-primary" onclick="POS.searchReturnSale()">Buscar</button>
</div>
<div id="returnSaleInfo" style="display:none;margin-bottom:10px;font-size:var(--text-body-sm);color:var(--color-text-muted);"></div>
<div id="returnItemsList" style="display:none;max-height:280px;overflow-y:auto;border:1px solid var(--color-border);border-radius:8px;"></div>
<div style="margin-top:10px;">
<input type="text" id="returnNotes" class="form-input" placeholder="Notas (opcional)" />
</div>
<div id="returnResult" style="margin-top:10px;font-size:var(--text-body-sm);"></div>
</div>
<div class="modal-footer" id="returnFooter" style="display:none;">
<button class="btn btn-ghost" onclick="POS.closeReturnModal()">Cerrar</button>
<button class="btn btn-primary" onclick="POS.submitReturn()">Registrar devolución</button>
</div>
</div>
</div>
<!-- ══════════ LAYAWAYS MODAL ══════════ -->
<div class="modal-overlay" id="layawaysModal">
<div class="modal-box" style="max-width:720px;">
<div class="modal-header">
<h3>Apartados activos</h3>
<button class="btn-close" onclick="POS.closeLayawaysModal()">&times;</button>
</div>
<div class="modal-body">
<div id="layawaysList" style="max-height:340px;overflow-y:auto;">
<span style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando...</span>
</div>
<div id="layawayActions" style="display:none;margin-top:12px;border-top:1px solid var(--color-border);padding-top:12px;">
<div id="layawayDetail" style="font-size:var(--text-body-sm);margin-bottom:10px;"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<input type="number" id="layawayPayAmount" class="form-input" placeholder="Monto abono" style="width:140px;" />
<select id="layawayPayMethod" class="form-input" style="width:150px;">
<option value="efectivo">Efectivo</option>
<option value="tarjeta">Tarjeta</option>
<option value="transferencia">Transferencia</option>
</select>
<button class="btn btn-primary" onclick="POS.layawayPay()">Abonar</button>
<button class="btn btn-primary" onclick="POS.layawayComplete()" style="background:var(--color-success);">Liquidar</button>
<button class="btn btn-ghost" onclick="POS.layawayCancel()" style="color:var(--color-error);">Cancelar apartado</button>
</div>
<div id="layawayResult" style="margin-top:8px;font-size:var(--text-body-sm);"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="POS.closeLayawaysModal()">Cerrar</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -15,7 +15,7 @@
<body>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -227,7 +227,7 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script>

View File

@@ -406,7 +406,7 @@
<!-- End app-shell -->
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

View File

@@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ business_name }} — Refacciones en línea</title>
<meta name="description" content="Compra refacciones y accesorios con stock real. Pedidos en línea con confirmación inmediata.">
<style>
:root { --brand:#e11d48; --bg:#f6f7f9; --card:#fff; --border:#e5e7eb; --text:#111827; --muted:#6b7280; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, -apple-system, sans-serif; background:var(--bg); color:var(--text); }
header { background:var(--card); border-bottom:1px solid var(--border); padding:14px 20px; display:flex; align-items:center; gap:14px; position:sticky; top:0; z-index:5; }
header .logo { font-weight:800; font-size:1.15rem; color:var(--brand); }
header .search { flex:1; max-width:560px; display:flex; gap:8px; }
header input { flex:1; padding:10px 14px; border:1px solid var(--border); border-radius:10px; font-size:1rem; }
button { cursor:pointer; border:none; border-radius:10px; padding:10px 16px; font-size:0.95rem; font-weight:600; }
.btn-brand { background:var(--brand); color:#fff; }
.btn-ghost { background:transparent; border:1px solid var(--border); }
main { max-width:1000px; margin:0 auto; padding:20px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:14px; }
.card { background:var(--card); border:1px solid var(--border); border-radius:14px; overflow:hidden; display:flex; flex-direction:column; }
.card img { width:100%; height:150px; object-fit:cover; background:#f0f0f0; }
.card .body { padding:12px; flex:1; display:flex; flex-direction:column; gap:6px; }
.name { font-size:0.92rem; font-weight:600; line-height:1.3; }
.sku { font-family:ui-monospace,monospace; font-size:0.75rem; color:var(--muted); }
.price { font-size:1.1rem; font-weight:800; color:var(--brand); }
.stock { font-size:0.75rem; }
.in { color:#059669; } .out { color:#dc2626; }
.card button { margin-top:auto; background:var(--brand); color:#fff; }
.card button:disabled { background:#d1d5db; cursor:not-allowed; }
.empty { text-align:center; color:var(--muted); padding:40px 0; }
/* carrito flotante */
#cartBtn { position:fixed; bottom:20px; right:20px; background:var(--brand); color:#fff; border-radius:999px; padding:14px 20px; font-size:1rem; box-shadow:0 8px 24px rgba(0,0,0,.18); }
#cartCount { background:#fff; color:var(--brand); border-radius:999px; padding:1px 8px; font-weight:800; margin-left:6px; }
.overlay { position:fixed; inset:0; background:rgba(0,0,0,.45); display:none; align-items:center; justify-content:center; padding:16px; z-index:10; }
.overlay.open { display:flex; }
.modal { background:#fff; border-radius:16px; max-width:460px; width:100%; max-height:88vh; overflow:auto; padding:20px; }
.modal h3 { margin:0 0 12px; }
.line { display:flex; justify-content:space-between; gap:8px; padding:8px 0; border-bottom:1px solid var(--border); font-size:0.9rem; }
.field { margin:10px 0; }
.field label { display:block; font-size:0.8rem; color:var(--muted); margin-bottom:4px; }
.field input, .field textarea { width:100%; padding:10px; border:1px solid var(--border); border-radius:10px; font-size:0.95rem; }
.total { font-size:1.15rem; font-weight:800; text-align:right; margin:10px 0; }
.ok { text-align:center; padding:20px 0; }
.ok .big { font-size:3rem; }
</style>
</head>
<body>
<header>
<div class="logo">{{ business_name }}</div>
<div class="search">
<input id="q" type="search" placeholder="Buscar refacción, No. de parte..." />
<button class="btn-brand" onclick="doSearch(1)">Buscar</button>
</div>
<button class="btn-ghost" onclick="openCart()">🛒 <span id="cartQty">0</span></button>
</header>
<main>
<div class="grid" id="results"><div class="empty">Escribe para buscar refacciones...</div></div>
<div style="text-align:center;margin:18px 0;">
<button class="btn-ghost" id="moreBtn" style="display:none;" onclick="doSearch(page+1)">Ver más</button>
</div>
</main>
<button id="cartBtn" onclick="openCart()">🛒 Carrito <span id="cartQty2">0</span></button>
<div class="overlay" id="cartOverlay">
<div class="modal">
<h3>Tu pedido</h3>
<div id="cartLines"></div>
<div class="total" id="cartTotal"></div>
<div class="field"><label>Nombre *</label><input id="cName" /></div>
<div class="field"><label>Teléfono (WhatsApp) *</label><input id="cPhone" type="tel" placeholder="10 dígitos" /></div>
<div class="field"><label>Notas</label><textarea id="cNotes" rows="2"></textarea></div>
<div class="field"><label style="display:flex;gap:8px;align-items:center;"><input type="checkbox" id="cDelivery" /> Requiero envío a domicilio</label></div>
<div id="orderMsg" style="font-size:0.9rem;"></div>
<div style="display:flex;gap:10px;margin-top:12px;">
<button class="btn-ghost" style="flex:1;" onclick="closeCart()">Seguir comprando</button>
<button class="btn-brand" style="flex:2;" id="orderBtn" onclick="submitOrder()">Hacer pedido</button>
</div>
</div>
</div>
<script>
var BUSINESS = "{{ business_name }}";
var cart = [];
try { cart = JSON.parse(localStorage.getItem('store_cart') || '[]'); } catch(e) {}
var page = 0, lastQ = '';
function saveCart() {
localStorage.setItem('store_cart', JSON.stringify(cart));
var n = cart.reduce(function (a, l) { return a + l.quantity; }, 0);
document.getElementById('cartQty').textContent = n;
document.getElementById('cartQty2').textContent = n;
}
function renderResults(items, append) {
var el = document.getElementById('results');
var html = items.map(function (it) {
var hasStock = it.stock > 0;
return '<div class="card">'
+ (it.image_url ? '<img src="' + escAttr(it.image_url) + '" loading="lazy" onerror="this.style.display=\'none\'">' : '')
+ '<div class="body">'
+ '<div class="name">' + esc(it.name) + '</div>'
+ '<div class="sku">' + esc(it.part_number) + (it.brand ? ' · ' + esc(it.brand) : '') + '</div>'
+ '<div class="price">' + money(it.price) + '</div>'
+ '<div class="stock ' + (hasStock ? 'in' : 'out') + '">' + (hasStock ? 'Disponible (' + it.stock + ')' : 'Sin stock') + '</div>'
+ '<button ' + (hasStock ? '' : 'disabled') + ' onclick="addToCart(' + it.id + ')">Agregar</button>'
+ '</div></div>';
}).join('');
if (append) el.innerHTML += html; else el.innerHTML = html || '<div class="empty">Sin resultados</div>';
}
function money(n) { return '$' + (parseFloat(n) || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 }); }
function esc(s) {
var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML;
}
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
function doSearch(p) {
page = p || 1;
lastQ = document.getElementById('q').value.trim();
fetch('/pos/api/store/search?q=' + encodeURIComponent(lastQ) + '&page=' + page)
.then(function (r) { return r.json(); })
.then(function (d) {
renderResults(d.items || [], page > 1);
document.getElementById('moreBtn').style.display = (d.page < d.total_pages) ? '' : 'none';
}).catch(function () {
document.getElementById('results').innerHTML = '<div class="empty">Error de conexión. Intenta de nuevo.</div>';
});
}
document.getElementById('q').addEventListener('keydown', function (e) {
if (e.key === 'Enter') doSearch(1);
});
function addToCart(id) {
// buscar el item en el último render guardado
var it = window._lastItems && window._lastItems.find(function (x) { return x.id === id; });
if (!it) return;
var line = cart.find(function (l) { return l.inventory_id === id; });
if (line) line.quantity += 1;
else cart.push({ inventory_id: id, name: it.name, part_number: it.part_number, price: it.price, quantity: 1 });
saveCart();
}
function renderCart() {
var el = document.getElementById('cartLines');
var total = 0;
el.innerHTML = cart.length ? cart.map(function (l, i) {
total += l.price * l.quantity;
return '<div class="line"><span>' + esc(l.name) + ' × ' + l.quantity + '</span>'
+ '<span><button class="btn-ghost" style="padding:2px 8px;" onclick="cartQty(' + i + ',-1)">−</button> '
+ '<button class="btn-ghost" style="padding:2px 8px;" onclick="cartQty(' + i + ',1)">+</button> '
+ money(l.price * l.quantity) + '</span></div>';
}).join('') : '<div class="empty">Carrito vacío</div>';
document.getElementById('cartTotal').textContent = 'Total: ' + money(total);
}
function cartQty(i, d) {
cart[i].quantity += d;
if (cart[i].quantity <= 0) cart.splice(i, 1);
saveCart(); renderCart();
}
function openCart() { saveCart(); renderCart(); document.getElementById('cartOverlay').classList.add('open'); }
function closeCart() { document.getElementById('cartOverlay').classList.remove('open'); }
function submitOrder() {
var btn = document.getElementById('orderBtn');
btn.disabled = true;
fetch('/pos/api/store/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_name: document.getElementById('cName').value.trim(),
customer_phone: document.getElementById('cPhone').value.trim(),
notes: document.getElementById('cNotes').value.trim(),
delivery: document.getElementById('cDelivery').checked,
items: cart.map(function (l) { return { inventory_id: l.inventory_id, quantity: l.quantity }; }),
}),
}).then(function (r) { return r.json(); }).then(function (d) {
if (d.ok) {
cart = []; saveCart();
document.querySelector('#cartOverlay .modal').innerHTML =
'<div class="ok"><div class="big">✅</div><h3>¡Pedido #' + d.order_id + ' registrado!</h3>'
+ '<p>Total: ' + money(d.total) + '</p><p>' + esc(d.message) + '</p>'
+ '<button class="btn-brand" onclick="closeCart()">Aceptar</button></div>';
} else {
document.getElementById('orderMsg').innerHTML = '<span style="color:#dc2626;">' + esc(d.error || 'Error') + '</span>';
btn.disabled = false;
}
}).catch(function () {
document.getElementById('orderMsg').innerHTML = '<span style="color:#dc2626;">Error de conexión</span>';
btn.disabled = false;
});
}
// search guarda los items para addToCart
(function () {
var orig = doSearch;
doSearch = function (p) {
orig(p);
setTimeout(function () {
window._lastItems = (window._lastItems || []).slice(); // noop keep
}, 0);
};
})();
var _origRender = renderResults;
renderResults = function (items, append) {
window._lastItems = items;
_origRender(items, append);
};
saveCart();
doSearch(1);
</script>
</body>
</html>

View File

@@ -126,7 +126,7 @@
</div>
</div>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
<script src="/pos/static/js/supplier_catalog.js?v=33" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>

View File

@@ -124,6 +124,93 @@
</div>
</div>
<div style="max-width:720px;margin:16px auto;padding:16px;border:1px solid var(--color-border);border-radius:12px;">
<h3 style="margin:0 0 4px;">WA CRM — Meta Cloud API (oficial)</h3>
<p style="margin:0 0 12px;font-size:var(--text-caption);color:var(--color-text-muted);">
Un número por refaccionaria. Credenciales de Meta Business (phone number ID + token permanente).
El proveedor Baileys queda como respaldo.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label style="font-size:var(--text-caption);">Proveedor</label>
<select id="waProvider" class="form-input">
<option value="baileys">Baileys (bridge actual)</option>
<option value="meta">Meta Cloud API (WA CRM)</option>
</select>
</div>
<div>
<label style="font-size:var(--text-caption);">Phone Number ID</label>
<input id="waPnid" class="form-input" placeholder="ej. 123456789012345" />
</div>
<div>
<label style="font-size:var(--text-caption);">Access Token</label>
<input id="waToken" type="password" class="form-input" placeholder="(dejar vacío = sin cambio)" />
</div>
<div>
<label style="font-size:var(--text-caption);">App Secret</label>
<input id="waSecret" type="password" class="form-input" placeholder="(dejar vacío = sin cambio)" />
</div>
<div>
<label style="font-size:var(--text-caption);">Verify Token (webhook)</label>
<input id="waVerify" class="form-input" />
</div>
<div>
<label style="font-size:var(--text-caption);">WA CRM — API base</label>
<input id="waApiBase" class="form-input" placeholder="http://192.168.10.91:3010" />
</div>
<div>
<label style="font-size:var(--text-caption);">WA CRM — API key (wacrm_live_…)</label>
<input id="waApiKey" type="password" class="form-input" placeholder="(dejar vacío = sin cambio)" />
</div>
<div>
<label style="font-size:var(--text-caption);">URL pública del POS (para imágenes)</label>
<input id="waPublicBase" class="form-input" placeholder="https://<subdominio>.nexusautoparts.com.mx" />
</div>
<div style="display:flex;align-items:flex-end;">
<button class="btn btn--primary" onclick="saveWaMeta()" style="width:100%;">Guardar WA CRM</button>
</div>
</div>
<div id="waMetaStatus" style="margin-top:8px;font-size:var(--text-caption);color:var(--color-text-muted);"></div>
</div>
<script>
(async function loadWaMeta() {
try {
const t = localStorage.getItem('pos_token');
const r = await fetch('/pos/api/config/whatsapp', { headers: { 'Authorization': 'Bearer ' + t } });
if (!r.ok) return;
const d = await r.json();
document.getElementById('waProvider').value = d.provider || 'baileys';
document.getElementById('waPnid').value = d.phone_number_id || '';
document.getElementById('waVerify').value = d.verify_token || '';
document.getElementById('waApiBase').value = d.api_base || '';
document.getElementById('waPublicBase').value = d.public_base || '';
document.getElementById('waMetaStatus').textContent =
(d.has_access_token ? '✓ Token guardado. ' : 'Sin token. ') +
(d.has_app_secret ? '✓ App secret guardado.' : 'Sin app secret (webhook sin validación HMAC).');
} catch (e) { /* noop */ }
})();
async function saveWaMeta() {
const t = localStorage.getItem('pos_token');
const body = {
provider: document.getElementById('waProvider').value,
phone_number_id: document.getElementById('waPnid').value.trim(),
verify_token: document.getElementById('waVerify').value.trim(),
api_base: document.getElementById('waApiBase').value.trim(),
api_key: document.getElementById('waApiKey').value.trim(),
public_base: document.getElementById('waPublicBase').value.trim(),
access_token: document.getElementById('waToken').value.trim(),
app_secret: document.getElementById('waSecret').value.trim(),
};
const r = await fetch('/pos/api/config/whatsapp', {
method: 'PUT', headers: { 'Authorization': 'Bearer ' + t, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const el = document.getElementById('waMetaStatus');
if (r.ok) { el.textContent = '✓ Configuración guardada'; el.style.color = 'var(--color-success)'; }
else { el.textContent = 'Error guardando'; el.style.color = 'var(--color-error)'; }
}
</script>
<!-- Theme engine -->
<script>
function posSetTheme(t){document.documentElement.setAttribute('data-theme',t);localStorage.setItem('pos_theme',t);}
@@ -132,7 +219,7 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href='
<!-- Sidebar -->
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/whatsapp2.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>

View File

@@ -494,7 +494,7 @@
</div>
<script src="/pos/static/js/i18n.js?v=39" defer></script>
<script src="/pos/static/js/app-init.js?v=8" defer></script>
<script src="/pos/static/js/app-init.js?v=9" defer></script>
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
<script src="/pos/static/js/sidebar.js?v=46" defer></script>

1462
pos/templates/yonke.html Normal file

File diff suppressed because it is too large Load Diff