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

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