Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- 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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -417,7 +417,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Info -->
|
||||
|
||||
<!-- CRM & Loyalty -->
|
||||
<div class="detail-section" id="crmSection">
|
||||
<div class="detail-section__title">CRM & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()">×</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()">×</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')">×</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>¿Ya que estás en eso…?</span>
|
||||
<button type="button" title="Ocultar" onclick="POS.hideCrossSell()" style="background:transparent;border:none;color:var(--color-text-muted);cursor:pointer;font-size:14px;">×</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()">×</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()">×</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
215
pos/templates/storefront.html
Normal file
215
pos/templates/storefront.html
Normal 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, '"'); }
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
1462
pos/templates/yonke.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user