feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
This commit is contained in:
@@ -188,10 +188,10 @@
|
||||
<!-- Tabs -->
|
||||
<div class="tabs-row" role="tablist">
|
||||
<button class="tab-btn is-active" role="tab" aria-selected="true" onclick="switchTab('cxc')">
|
||||
Ctas. por Cobrar <span class="tab-btn__badge">23</span>
|
||||
Ctas. por Cobrar <span class="tab-btn__badge" id="badge-cxc">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="switchTab('cxp')">
|
||||
Ctas. por Pagar <span class="tab-btn__badge--alert tab-btn__badge">3</span>
|
||||
Ctas. por Pagar <span class="tab-btn__badge--alert tab-btn__badge" id="badge-cxp">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="switchTab('balance')">
|
||||
Balance General
|
||||
@@ -498,5 +498,31 @@
|
||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||
|
||||
<script src="/pos/static/js/chat.js" defer></script>
|
||||
|
||||
<script>
|
||||
// Load accounting stats for tab badges
|
||||
async function loadAccountingStats() {
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
try {
|
||||
const res = await fetch('/pos/api/accounting/stats', {
|
||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const map = {
|
||||
'badge-cxc': data.cuentas_cobrar,
|
||||
'badge-cxp': data.cuentas_pagar
|
||||
};
|
||||
Object.entries(map).forEach(function([id, val]) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = val || 0;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load accounting stats:', e);
|
||||
}
|
||||
}
|
||||
window.loadAccountingStats = loadAccountingStats;
|
||||
loadAccountingStats();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -420,6 +420,10 @@
|
||||
<div class="credit-metric__label">Utilizado</div>
|
||||
<div class="credit-metric__value used" id="detailCreditUsed">$31,500</div>
|
||||
</div>
|
||||
<div class="credit-metric">
|
||||
<div class="credit-metric__label">Descuento Max</div>
|
||||
<div class="credit-metric__value" id="detailMaxDiscount">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credit-progress">
|
||||
<div class="credit-progress__labels">
|
||||
@@ -611,7 +615,7 @@
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn-edit" onclick="if(typeof Customers!=='undefined') Customers.editCurrent();">Editar Cliente</button>
|
||||
<button class="btn-sale" onclick="window.location.href='/pos/';">Nueva Venta</button>
|
||||
<button class="btn-sale" onclick="window.location.href='/pos/sale';">Nueva Venta</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
||||
<meta name="theme-color" content="#F5A623" />
|
||||
|
||||
<link rel="stylesheet" href="/pos/static/css/inventory.css">
|
||||
<link rel="stylesheet" href="/pos/static/css/inventory.css?v=4">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -306,10 +306,18 @@
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
<button class="btn btn--ghost btn--sm" onclick="showTierDiscountModal()">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="6" x2="12" y2="12"/><line x1="16.24" y1="16.24" x2="12" y2="12"/></svg>
|
||||
<span id="tierDiscountBadge">Taller -15% · Mayoreo -25%</span>
|
||||
</button>
|
||||
<button class="btn btn--primary btn--sm" onclick="showCreateModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Nuevo Producto
|
||||
</button>
|
||||
<button class="btn btn--sm btn--meli" id="btnPublishML" style="display:none;" onclick="openMeliPublishModal()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||||
Publicar en ML <span id="meliSelectedCountBadge" style="background:#2D3277;color:#FFE600;border-radius:10px;padding:0 6px;font-size:11px;margin-left:4px;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
@@ -317,6 +325,7 @@
|
||||
<table class="data-table" id="stockTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px;"><input type="checkbox" id="selectAllItems" onclick="toggleSelectAllItems()" title="Seleccionar todos" /></th>
|
||||
<th style="font-size:var(--text-caption);color:var(--color-text-muted);">ID</th>
|
||||
<th>Barcode</th>
|
||||
<th>No. Parte</th>
|
||||
@@ -324,9 +333,9 @@
|
||||
<th>Marca</th>
|
||||
<th style="text-align:right">Stock</th>
|
||||
<th style="text-align:right">Costo</th>
|
||||
<th style="text-align:right">Precio 1</th>
|
||||
<th style="text-align:right">Precio 2</th>
|
||||
<th style="text-align:right">Precio 3</th>
|
||||
<th style="text-align:right">Mostrador</th>
|
||||
<th style="text-align:right">Taller</th>
|
||||
<th style="text-align:right">Mayoreo</th>
|
||||
<th>Ubicación</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
@@ -689,9 +698,7 @@
|
||||
<div class="inv-field"><label>Marca</label><input type="text" id="newBrand" placeholder="Marca" /></div>
|
||||
<div class="inv-field"><label>Barcode</label><input type="text" id="newBarcode" placeholder="Auto-generado si vacío" /></div>
|
||||
<div class="inv-field"><label>Costo</label><input type="number" id="newCost" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 1</label><input type="number" id="newPrice1" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 2</label><input type="number" id="newPrice2" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio 3</label><input type="number" id="newPrice3" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Precio Mostrador</label><input type="number" id="newPrice1" step="0.01" placeholder="0.00" /></div>
|
||||
<div class="inv-field"><label>Stock Mínimo</label><input type="number" id="newMinStock" placeholder="0" /></div>
|
||||
<div class="inv-field"><label>Stock Inicial</label><input type="number" id="newInitialStock" placeholder="0" /></div>
|
||||
<div class="inv-field"><label>Ubicación</label><input type="text" id="newLocation" placeholder="Ej: A-12-3" /></div>
|
||||
@@ -809,6 +816,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tier Discounts Modal -->
|
||||
<div class="inv-modal-overlay" id="tierDiscountModal">
|
||||
<div class="inv-modal">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Descuentos por Tipo de Cliente</h3>
|
||||
<button class="inv-modal__close" onclick="closeTierDiscountModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<p style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-4);">Estos descuentos se aplican automáticamente a todos los productos al calcular precios de Taller y Mayoreo.</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="inv-field"><label>Descuento Taller (%)</label><input type="number" id="tierDisc2" step="0.1" min="0" max="100" placeholder="15" /></div>
|
||||
<div class="inv-field"><label>Descuento Mayoreo (%)</label><input type="number" id="tierDisc3" step="0.1" min="0" max="100" placeholder="25" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeTierDiscountModal()">Cancelar</button>
|
||||
<button class="btn btn--primary" onclick="saveTierDiscounts()">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ Publicar en MercadoLibre Modal ══════════ -->
|
||||
<div class="inv-modal-overlay" id="meliPublishModal">
|
||||
<div class="inv-modal inv-modal--wide">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Publicar en MercadoLibre</h3>
|
||||
<button class="inv-modal__close" onclick="closeMeliPublishModal()">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body">
|
||||
<div id="meliPublishSelectedCount" style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-3);">0 productos seleccionados</div>
|
||||
<div id="meliPublishItemsPreview" style="max-height:200px;overflow-y:auto;border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);">
|
||||
<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Selecciona productos del inventario para ver el preview.</p>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:var(--space-4);">
|
||||
<div class="inv-field">
|
||||
<label>Categoría ML *</label>
|
||||
<div style="position:relative;">
|
||||
<input type="text" id="meliCategorySearch" placeholder="Buscar categoría..." oninput="searchMeliCategories()" onkeydown="handleMeliCatKeydown(event)" autocomplete="off" />
|
||||
<div id="meliCategoryResults"></div>
|
||||
</div>
|
||||
<input type="hidden" id="meliCategoryId" />
|
||||
</div>
|
||||
<div class="inv-field">
|
||||
<label>Tipo de Publicación</label>
|
||||
<select id="meliListingType">
|
||||
<option value="gold_special">Gold Special</option>
|
||||
<option value="gold_pro">Gold Pro</option>
|
||||
<option value="bronze">Bronce (gratis)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inv-field">
|
||||
<label>Modo de Envío</label>
|
||||
<select id="meliShippingMode">
|
||||
<option value="me2" selected>MercadoEnvíos (me2)</option>
|
||||
</select>
|
||||
<small style="color:var(--color-text-muted);font-size:var(--text-caption);">Tu cuenta requiere ME2 obligatoriamente.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="meliPublishResult" style="min-height:1.5em;"></div>
|
||||
</div>
|
||||
<div class="inv-modal__footer">
|
||||
<button class="btn btn--ghost" onclick="closeMeliPublishModal()">Cancelar</button>
|
||||
<button class="btn btn--meli" id="meliPublishBtn" onclick="executeMeliPublish()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> Publicar</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>
|
||||
@@ -821,7 +895,7 @@
|
||||
<script src="/pos/static/js/pos-utils.js" defer></script>
|
||||
<script src="/pos/static/js/sidebar.js" defer></script>
|
||||
<script src="/pos/static/js/virtual-scroll.js" defer></script>
|
||||
<script src="/pos/static/js/inventory.js?v=5" defer></script>
|
||||
<script src="/pos/static/js/inventory.js?v=10" 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>
|
||||
|
||||
@@ -301,16 +301,16 @@
|
||||
<!-- Tabs Row -->
|
||||
<div class="tabs-row" role="tablist" aria-label="Módulos de Facturación">
|
||||
<button class="tab-btn is-active" role="tab" aria-selected="true" aria-controls="panel-facturas" id="tab-facturas" onclick="switchTab('facturas')">
|
||||
Facturas <span class="tab-btn__badge">247</span>
|
||||
Facturas <span class="tab-btn__badge" id="badge-facturas">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-notas" id="tab-notas" onclick="switchTab('notas')">
|
||||
Notas de Crédito <span class="tab-btn__badge">8</span>
|
||||
Notas de Crédito <span class="tab-btn__badge" id="badge-notas-credito">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-complementos" id="tab-complementos" onclick="switchTab('complementos')">
|
||||
Complementos de Pago <span class="tab-btn__badge">12</span>
|
||||
Complementos de Pago <span class="tab-btn__badge" id="badge-complementos">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-cancelaciones" id="tab-cancelaciones" onclick="switchTab('cancelaciones')">
|
||||
Cancelaciones <span class="tab-btn__badge tab-btn__badge--warn">6</span>
|
||||
Cancelaciones <span class="tab-btn__badge tab-btn__badge--warn" id="badge-cancelaciones">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-config" id="tab-config" onclick="switchTab('config')">
|
||||
Configuración CFDI
|
||||
@@ -1060,5 +1060,33 @@
|
||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||
|
||||
<script src="/pos/static/js/chat.js" defer></script>
|
||||
|
||||
<script>
|
||||
// Load invoicing stats for tab badges
|
||||
async function loadInvoicingStats() {
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
try {
|
||||
const res = await fetch('/pos/api/invoicing/stats', {
|
||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const map = {
|
||||
'badge-facturas': data.facturas,
|
||||
'badge-notas-credito': data.notas_credito,
|
||||
'badge-complementos': data.complementos,
|
||||
'badge-cancelaciones': data.cancelaciones
|
||||
};
|
||||
Object.entries(map).forEach(function([id, val]) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = val || 0;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load invoicing stats:', e);
|
||||
}
|
||||
}
|
||||
window.loadInvoicingStats = loadInvoicingStats;
|
||||
loadInvoicingStats();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -291,6 +291,13 @@
|
||||
btnLogin.disabled = !canLogin;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
LOGIN BUTTON CLICK
|
||||
------------------------------------------------------------------ */
|
||||
btnLogin.addEventListener('click', function() {
|
||||
triggerLogin();
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
TRIGGER LOGIN (demo)
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
@@ -57,10 +57,11 @@
|
||||
<h2 style="margin-bottom:var(--space-4);font-family:var(--font-heading);">Mi Inventario</h2>
|
||||
<div class="upload-box">
|
||||
<label>Cargar inventario via CSV</label>
|
||||
<textarea id="csvText" placeholder="part_number,stock,price AB-123,5,150.50 CD-456,12,89.00"></textarea>
|
||||
<textarea id="csvText" placeholder="part_number,stock,price,name AB-123,5,150.50,Filtro de aceite CD-456,12,89.00,Balata delantera"></textarea>
|
||||
<div class="hint">
|
||||
Columnas requeridas: <code>part_number, stock, price</code>.
|
||||
Opcionales: <code>min_order, warehouse_location, currency</code>.
|
||||
Opcionales: <code>name, min_order, warehouse_location, currency</code>.
|
||||
<br>Si la parte no existe en el catálogo, se crea automáticamente como <em>seller listing</em>.
|
||||
</div>
|
||||
<button style="margin-top:var(--space-3);padding:var(--space-3) var(--space-6);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-md);font-weight:bold;cursor:pointer;" onclick="uploadCSV()">Subir CSV</button>
|
||||
<div id="uploadResult" style="margin-top:var(--space-3);font-size:var(--text-body-sm);"></div>
|
||||
@@ -205,8 +206,18 @@
|
||||
var priceStr = p.min_price === p.max_price
|
||||
? '$' + fmt(p.min_price)
|
||||
: '$' + fmt(p.min_price) + ' – $' + fmt(p.max_price);
|
||||
return '<div class="part-card" onclick="openPartDetail(' + p.id_part + ')">' +
|
||||
'<div class="part-card__oem">' + esc(p.oem_part_number) + '</div>' +
|
||||
var isOem = p.listing_type === 'oem';
|
||||
var badge = isOem
|
||||
? '<span style="background:#3FB95020;color:#3FB950;padding:2px 6px;border-radius:4px;font-size:var(--text-caption);">Catálogo</span>'
|
||||
: '<span style="background:#F5A62320;color:#F5A623;padding:2px 6px;border-radius:4px;font-size:var(--text-caption);">Listing</span>';
|
||||
var onclick = isOem
|
||||
? 'openPartDetail(' + p.id + ')'
|
||||
: 'openListingDetail(' + p.id + ')';
|
||||
return '<div class="part-card" onclick="' + onclick + '">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-1);">' +
|
||||
'<div class="part-card__oem">' + esc(p.part_number) + '</div>' +
|
||||
badge +
|
||||
'</div>' +
|
||||
'<div class="part-card__name">' + esc(p.name) + '</div>' +
|
||||
'<div class="part-card__meta">' +
|
||||
'<span class="price-range">' + priceStr + '</span>' +
|
||||
@@ -248,7 +259,7 @@
|
||||
'</div>' +
|
||||
'<div style="text-align:right;">' +
|
||||
'<div class="price-range">$' + fmt(b.price) + '</div>' +
|
||||
'<button style="margin-top:var(--space-1);padding:var(--space-1) var(--space-3);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-sm);font-size:var(--text-caption);cursor:pointer;" onclick="createOrderFor(' + partId + ', ' + b.id_bodega + ', \'' + esc(b.name).replace(/\'/g, '\\\'') + '\', ' + (b.price || 0) + ')">Ordenar</button>' +
|
||||
'<button style="margin-top:var(--space-1);padding:var(--space-1) var(--space-3);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-sm);font-size:var(--text-caption);cursor:pointer;" onclick="createOrderFor(' + partId + ', null, ' + b.id_bodega + ', \'' + esc(b.name).replace(/\'/g, '\\\'') + '\', ' + (b.price || 0) + ')">Ordenar</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
@@ -257,7 +268,38 @@
|
||||
});
|
||||
};
|
||||
|
||||
window.createOrderFor = function (partId, bodegaId, bodegaName, price) {
|
||||
window.openListingDetail = function (wiId) {
|
||||
var modal = document.getElementById('partModal');
|
||||
modal.classList.add('open');
|
||||
document.getElementById('partModalBody').innerHTML = 'Cargando bodegas...';
|
||||
|
||||
apiFetch('/inventory/listing/' + wiId).then(function (resp) {
|
||||
if (!resp || !resp.data) return;
|
||||
var bodegas = resp.data;
|
||||
if (bodegas.length === 0) {
|
||||
document.getElementById('partModalBody').innerHTML = '<div class="empty-state">Ninguna bodega tiene esta parte en stock.</div>';
|
||||
return;
|
||||
}
|
||||
var html = '<p style="color:var(--color-text-muted);margin-bottom:var(--space-3);">Elige una bodega para ordenar:</p>';
|
||||
html += '<div style="display:flex;flex-direction:column;gap:var(--space-2);">';
|
||||
bodegas.forEach(function (b) {
|
||||
html += '<div style="padding:var(--space-3);border:1px solid var(--glass-border);border-radius:var(--radius-md);display:flex;justify-content:space-between;align-items:center;">' +
|
||||
'<div>' +
|
||||
'<strong>' + esc(b.name) + '</strong>' +
|
||||
'<div style="color:var(--color-text-muted);font-size:var(--text-caption);">' + esc(b.city || '') + ' · ' + esc(b.stock_hint) + '</div>' +
|
||||
'</div>' +
|
||||
'<div style="text-align:right;">' +
|
||||
'<div class="price-range">$' + fmt(b.price) + '</div>' +
|
||||
'<button style="margin-top:var(--space-1);padding:var(--space-1) var(--space-3);background:var(--gradient-accent);color:var(--btn-primary-text);border:none;border-radius:var(--radius-sm);font-size:var(--text-caption);cursor:pointer;" onclick="createOrderFor(null, ' + wiId + ', ' + b.id_bodega + ', \'' + esc(b.name).replace(/\'/g, '\\\'') + '\', ' + (b.price || 0) + ')">Ordenar</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
document.getElementById('partModalBody').innerHTML = html;
|
||||
});
|
||||
};
|
||||
|
||||
window.createOrderFor = function (partId, wiId, bodegaId, bodegaName, price) {
|
||||
var qty = prompt('Cantidad a ordenar para "' + bodegaName + '":', '1');
|
||||
if (!qty) return;
|
||||
qty = parseInt(qty);
|
||||
@@ -265,12 +307,22 @@
|
||||
|
||||
var notes = prompt('Notas para la bodega (opcional):', '') || '';
|
||||
|
||||
var item = { quantity: qty, unit_price: price };
|
||||
if (partId) {
|
||||
item.part_id = partId;
|
||||
} else if (wiId) {
|
||||
item.wi_id = wiId;
|
||||
} else {
|
||||
alert('Error: se requiere part_id o wi_id');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PO draft
|
||||
apiFetch('/orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
bodega_id: bodegaId,
|
||||
items: [{ part_id: partId, quantity: qty, unit_price: price }],
|
||||
items: [item],
|
||||
delivery_method: 'pickup',
|
||||
buyer_notes: notes,
|
||||
}),
|
||||
@@ -482,6 +534,10 @@
|
||||
r.updated + ' actualizados';
|
||||
if (r.skipped > 0) msg += ', ' + r.skipped + ' omitidos';
|
||||
msg += '</span>';
|
||||
if (r.oem_count !== undefined || r.seller_count !== undefined) {
|
||||
msg += '<div style="margin-top:var(--space-2);font-size:var(--text-caption);color:var(--color-text-muted);">' +
|
||||
(r.oem_count || 0) + ' catálogo OEM · ' + (r.seller_count || 0) + ' seller listings</div>';
|
||||
}
|
||||
if (r.errors && r.errors.length) {
|
||||
msg += '<div style="margin-top:var(--space-2);color:var(--color-text-muted);font-size:var(--text-caption);">';
|
||||
r.errors.slice(0, 5).forEach(function (e) {
|
||||
|
||||
322
pos/templates/marketplace_external.html
Normal file
322
pos/templates/marketplace_external.html
Normal file
@@ -0,0 +1,322 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" data-theme="industrial">
|
||||
<head>
|
||||
<script>(function(){var t=localStorage.getItem("pos_theme")||"industrial";document.documentElement.setAttribute("data-theme",t);})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MercadoLibre — Nexus Autoparts POS</title>
|
||||
<link rel="stylesheet" href="/pos/static/css/chat.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/common.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/inventory.css" />
|
||||
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
||||
<meta name="theme-color" content="#F5A623" />
|
||||
<style>
|
||||
.meli-status { display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:20px;font-size:12px;font-weight:600; }
|
||||
.meli-status--active { background:#d4edda;color:#155724; }
|
||||
.meli-status--paused { background:#fff3cd;color:#856404; }
|
||||
.meli-status--closed { background:#f8d7da;color:#721c24; }
|
||||
.meli-status--pending { background:#e2e3e5;color:#383d41; }
|
||||
.meli-card { background:var(--color-surface-1);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-4); }
|
||||
.meli-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:var(--space-4); }
|
||||
.meli-connect-btn { display:inline-flex;align-items:center;gap:8px;padding:10px 20px;background:#FFE600;color:#2D3277;border:none;border-radius:var(--radius-md);font-weight:700;cursor:pointer; }
|
||||
.meli-connect-btn:hover { filter:brightness(0.95); }
|
||||
.meli-config-row { display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4); }
|
||||
.meli-config-row label { display:block;font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:4px; }
|
||||
.meli-config-row input, .meli-config-row select { padding:8px 12px;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface-0);color:var(--color-text-primary); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- =========================================================================
|
||||
THEME SWITCHER BAR
|
||||
========================================================================= -->
|
||||
|
||||
<header class="theme-bar" role="banner">
|
||||
<div class="theme-bar__left">
|
||||
<div class="theme-bar__store">
|
||||
<span class="theme-bar__dot"></span>
|
||||
Nexus Autoparts
|
||||
</div>
|
||||
<div class="theme-bar__sep"></div>
|
||||
<span class="theme-bar__label">MercadoLibre — Integración</span>
|
||||
</div>
|
||||
<div class="theme-bar__right">
|
||||
<span class="theme-bar__label">Tema:</span>
|
||||
<button class="theme-btn theme-btn--industrial is-active" data-theme-target="industrial" onclick="setTheme('industrial')">
|
||||
<span class="theme-btn__swatch"></span>
|
||||
Industrial
|
||||
</button>
|
||||
<button class="theme-btn theme-btn--modern" data-theme-target="modern" onclick="setTheme('modern')">
|
||||
<span class="theme-btn__swatch"></span>
|
||||
Moderno
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- =========================================================================
|
||||
APP SHELL
|
||||
========================================================================= -->
|
||||
|
||||
<div class="app-shell">
|
||||
|
||||
<!-- -----------------------------------------------------------------------
|
||||
SIDEBAR NAVIGATION
|
||||
----------------------------------------------------------------------- -->
|
||||
|
||||
<aside class="sidebar" role="navigation" aria-label="Navegación principal">
|
||||
<div class="sidebar__brand">
|
||||
<div class="brand-logo">NA</div>
|
||||
<div class="brand-name">
|
||||
<span class="brand-name__primary">Nexus</span>
|
||||
<span class="brand-name__sub">Autoparts POS</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="sidebar__nav">
|
||||
<div class="nav-section-label">Principal</div>
|
||||
<a class="nav-item" href="/pos/dashboard">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/sale">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
<span>POS</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/catalog">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
|
||||
</svg>
|
||||
<span>Catálogo</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/inventory">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
<span>Inventario</span>
|
||||
</a>
|
||||
<div class="nav-section-label">Gestión</div>
|
||||
<a class="nav-item" href="/pos/customers">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<span>Clientes</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/marketplace">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
||||
</svg>
|
||||
<span>Marketplace B2B</span>
|
||||
</a>
|
||||
<a class="nav-item is-active" href="/pos/marketplace-external" aria-current="page">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
<span>MercadoLibre</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/invoicing">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
<span>Facturación</span>
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/config">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>
|
||||
</svg>
|
||||
<span>Configuración</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar__footer">
|
||||
<div class="sidebar__user-avatar" id="sidebarAvatar">U</div>
|
||||
<div class="sidebar__user-info">
|
||||
<div class="sidebar__user-name" id="sidebarName">Usuario</div>
|
||||
<div class="sidebar__user-role" id="sidebarRole">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- -----------------------------------------------------------------------
|
||||
MAIN CONTENT
|
||||
----------------------------------------------------------------------- -->
|
||||
|
||||
<main class="main" role="main">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div class="page-header__title-group">
|
||||
<span class="page-header__eyebrow">Marketplace</span>
|
||||
<h1 class="page-header__title">MercadoLibre</h1>
|
||||
</div>
|
||||
<div class="page-header__actions">
|
||||
<a class="btn btn--ghost" href="/pos/dashboard">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn is-active" role="tab" aria-selected="true" aria-controls="panel-config" onclick="switchTab('config')">
|
||||
Configuración
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-listings" onclick="switchTab('listings')">
|
||||
Publicaciones
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-orders" onclick="switchTab('orders')">
|
||||
Órdenes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panels -->
|
||||
<div class="tab-panels" id="tab-panels">
|
||||
|
||||
<!-- ══════════ TAB: Configuración ══════════ -->
|
||||
<div class="tab-panel is-active" id="panel-config" role="tabpanel">
|
||||
<div class="meli-card" style="max-width:600px;">
|
||||
<h3 style="margin:0 0 var(--space-4);font-family:var(--font-heading);">Conexión con MercadoLibre</h3>
|
||||
<div id="configStatus">
|
||||
<p>Cargando estado...</p>
|
||||
</div>
|
||||
<div id="configForm" style="display:none;margin-top:var(--space-4);">
|
||||
<p style="margin-bottom:var(--space-3);font-size:var(--text-body-sm);color:var(--color-text-secondary);">
|
||||
Para conectar, necesitas una <strong>aplicación de MercadoLibre</strong>.
|
||||
Ve a <a href="https://developers.mercadolibre.com.mx" target="_blank">developers.mercadolibre.com.mx</a>
|
||||
y crea una app. Luego pega los datos aquí:
|
||||
</p>
|
||||
<div class="meli-config-row">
|
||||
<div>
|
||||
<label>Client ID</label>
|
||||
<input type="text" id="cfgClientId" placeholder="Tu App ID" style="width:200px;" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Client Secret</label>
|
||||
<input type="password" id="cfgClientSecret" placeholder="Tu Secret Key" style="width:200px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="meli-config-row">
|
||||
<div>
|
||||
<label>Categoría Default</label>
|
||||
<input type="text" id="cfgCategory" placeholder="MLM1747" style="width:150px;" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Modo de Envío</label>
|
||||
<select id="cfgShipping">
|
||||
<option value="me2">MercadoEnvíos (me2)</option>
|
||||
<option value="custom">Propio (custom)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button class="meli-connect-btn" onclick="startOAuth()">🔗 Conectar con MercadoLibre</button>
|
||||
</div>
|
||||
<div id="configConnected" style="display:none;margin-top:var(--space-4);">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:var(--space-3);">
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:#FFE600;display:flex;align-items:center;justify-content:center;font-weight:800;color:#2D3277;">ML</div>
|
||||
<div>
|
||||
<div style="font-weight:700;" id="connectedNickname">Usuario ML</div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);" id="connectedSite">MLM</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--danger btn--sm" onclick="disconnectMeli()">Desconectar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ TAB: Publicaciones ══════════ -->
|
||||
<div class="tab-panel" id="panel-listings" role="tabpanel">
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="listingSearch" placeholder="Buscar publicación..." oninput="filterListings()" />
|
||||
</div>
|
||||
<select class="select-filter" id="listingStatusFilter" onchange="filterListings()">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="active">Activas</option>
|
||||
<option value="paused">Pausadas</option>
|
||||
<option value="closed">Cerradas</option>
|
||||
</select>
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary" onclick="loadListings()">🔄 Actualizar</button>
|
||||
</div>
|
||||
<div id="listingsContainer" class="meli-grid"></div>
|
||||
<div id="listingsPagination" class="table-footer" style="margin-top:var(--space-4);"></div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ TAB: Órdenes ══════════ -->
|
||||
<div class="tab-panel" id="panel-orders" role="tabpanel">
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="orderSearch" placeholder="Buscar orden..." oninput="filterOrders()" />
|
||||
</div>
|
||||
<select class="select-filter" id="orderStatusFilter" onchange="filterOrders()">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="pending">Pendientes</option>
|
||||
<option value="confirmed">Confirmadas</option>
|
||||
<option value="packed">Empacadas</option>
|
||||
<option value="shipped">Enviadas</option>
|
||||
<option value="delivered">Entregadas</option>
|
||||
<option value="cancelled">Canceladas</option>
|
||||
</select>
|
||||
<div class="toolbar__spacer"></div>
|
||||
<button class="btn btn--primary" onclick="loadOrders()">🔄 Actualizar</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Orden ML</th>
|
||||
<th>Comprador</th>
|
||||
<th style="text-align:right">Total</th>
|
||||
<th>Estado Nexus</th>
|
||||
<th>Fecha</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTableBody"></tbody>
|
||||
</table>
|
||||
<div id="ordersPagination" class="table-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-panels -->
|
||||
|
||||
</main>
|
||||
|
||||
</div><!-- /app-shell -->
|
||||
|
||||
<!-- ══════════ Order Detail Modal ══════════ -->
|
||||
<div class="inv-modal-overlay" id="orderModal">
|
||||
<div class="inv-modal inv-modal--wide">
|
||||
<div class="inv-modal__header">
|
||||
<h3>Detalle de Orden ML</h3>
|
||||
<button class="inv-modal__close" onclick="closeModal('orderModal')">×</button>
|
||||
</div>
|
||||
<div class="inv-modal__body" id="orderModalBody">cargando...</div>
|
||||
<div class="inv-modal__footer" id="orderModalFooter"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js" defer></script>
|
||||
<script src="/pos/static/js/app-init.js" defer></script>
|
||||
<script src="/pos/static/js/sidebar.js" defer></script>
|
||||
<script src="/pos/static/js/marketplace_external.js?v=3" defer></script>
|
||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -32,6 +32,10 @@
|
||||
<span id="statusClock"></span>
|
||||
</div>
|
||||
<div class="status-bar__right">
|
||||
<a href="/pos/dashboard" class="btn btn--ghost btn--sm" id="backToSystemBtn" style="margin-right:8px;display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Sistema
|
||||
</a>
|
||||
<div class="status-bar__user" aria-label="Usuario activo">
|
||||
<div class="status-bar__user-avatar" aria-hidden="true">--</div>
|
||||
<span id="employeeName">Empleado</span>
|
||||
@@ -203,7 +207,7 @@
|
||||
<button class="btn-secondary-action" onclick="POS.saveQuotation()" title="Cotizacion (F4)">Cotizar</button>
|
||||
<button class="btn-secondary-action" onclick="POS.showLastSale()" title="Ultima venta (F5)">Ult.Venta</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" title="Cancelar (Esc)">Cancelar</button>
|
||||
<button class="btn-secondary-action danger" id="btnCancelSale" onclick="POS.openCancelModal()" title="Cancelar (Esc)">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -233,14 +237,14 @@
|
||||
<span class="fkey-key">F6</span><span class="fkey-label">Cajon</span>
|
||||
</div>
|
||||
<div class="fkey-sep"></div>
|
||||
<div class="fkey" title="Cantidad +/-">
|
||||
<div class="fkey" onclick="POS.changeQuantity()" title="Cantidad +/-">
|
||||
<span class="fkey-key">+/-</span><span class="fkey-label">Cantidad</span>
|
||||
</div>
|
||||
<div class="fkey" title="Descuento">
|
||||
<div class="fkey" onclick="POS.applyDiscount()" title="Descuento">
|
||||
<span class="fkey-key">*</span><span class="fkey-label">Descuento</span>
|
||||
</div>
|
||||
<div class="fkey-sep"></div>
|
||||
<div class="fkey" id="fkeyEsc" title="Cancelar">
|
||||
<div class="fkey" id="fkeyEsc" onclick="POS.openCancelModal()" title="Cancelar">
|
||||
<span class="fkey-key">Esc</span><span class="fkey-label">Cancelar</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -563,7 +567,7 @@
|
||||
<script src="/pos/static/js/app-init.js" 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=4" defer></script>
|
||||
<script src="/pos/static/js/pos.js?v=5" defer></script>
|
||||
|
||||
<script>
|
||||
// Cancel sale button wiring
|
||||
@@ -577,11 +581,7 @@
|
||||
closeCancelModal();
|
||||
// Clear cart via POS module
|
||||
if (typeof POS !== 'undefined') {
|
||||
// Clear each item
|
||||
const tbody = document.getElementById('cartBody');
|
||||
while (tbody && tbody.firstChild) {
|
||||
POS.removeFromCart(0);
|
||||
}
|
||||
POS.clearCart();
|
||||
POS.clearCustomer();
|
||||
}
|
||||
});
|
||||
@@ -614,6 +614,20 @@
|
||||
}
|
||||
updateClock();
|
||||
setInterval(updateClock, 30000);
|
||||
|
||||
// Show "Back to System" button when NOT in kiosk mode
|
||||
(function checkKiosk() {
|
||||
var btn = document.getElementById('backToSystemBtn');
|
||||
if (!btn) return;
|
||||
function showIfNotKiosk() {
|
||||
try {
|
||||
var isKiosk = window.isKioskEnabled && window.isKioskEnabled();
|
||||
btn.style.display = isKiosk ? 'none' : 'inline-flex';
|
||||
} catch (e) { btn.style.display = 'inline-flex'; }
|
||||
}
|
||||
showIfNotKiosk();
|
||||
setInterval(showIfNotKiosk, 2000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="/pos/static/js/chat.js" defer></script>
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;font-family:var(--font-mono);font-weight:700;">$' + fmt(q.total) + '</td>';
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;">' + statusBadge + '</td>';
|
||||
html += '<td onclick="openQuote(' + q.id + ')" style="cursor:pointer;color:var(--color-text-muted);">' + dateStr + '</td>';
|
||||
html += '<td><button onclick="deleteQuote(' + q.id + ', event)" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:16px;padding:4px 8px;border-radius:4px;" onmouseover="this.style.color=\'#F85149\';this.style.background=\'rgba(248,81,73,0.1)\'" onmouseout="this.style.color=\'var(--color-text-muted)\';this.style.background=\'none\'">🗑️</button></td>';
|
||||
html += '<td><button onclick="deleteQuote(' + q.id + ', event)" style="background:var(--color-bg-overlay);border:1.5px solid var(--color-border);color:var(--color-text-muted);cursor:pointer;font-size:13px;padding:6px 10px;border-radius:var(--radius-md);display:inline-flex;align-items:center;gap:4px;transition:var(--transition-fast);" onmouseover="this.style.color=\'var(--color-error)\';this.style.borderColor=\'var(--color-error)\';this.style.background=\'rgba(248,81,73,0.08)\'" onmouseout="this.style.color=\'var(--color-text-muted)\';this.style.borderColor=\'var(--color-border)\';this.style.background=\'var(--color-bg-overlay)\'">';
|
||||
html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||
html += 'Eliminar</button></td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
@@ -114,15 +116,33 @@
|
||||
html += '<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-text-accent);">Total: $' + fmt(q.total) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:var(--space-5);display:flex;gap:var(--space-3);justify-content:flex-end;flex-wrap:wrap;">';
|
||||
html += '<div style="margin-top:var(--space-5);">';
|
||||
// Primary actions
|
||||
if (q.status === 'active') {
|
||||
html += '<button class="btn btn--ghost" onclick="editQuote(' + q.id + ')" style="color:#4f46e5;">Editar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="convertQuote(' + q.id + ')" style="color:#059669;">Convertir a venta</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="shareQuote(' + q.id + ')">Compartir link</button>';
|
||||
html += '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3);">';
|
||||
html += '<button class="btn btn--primary" onclick="convertQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>';
|
||||
html += 'Convertir a Venta</button>';
|
||||
html += '<button class="btn btn--secondary" onclick="editQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
||||
html += 'Editar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="shareQuote(' + q.id + ')" style="padding:10px 20px;font-size:var(--text-body);display:inline-flex;align-items:center;gap:8px;border:1.5px solid var(--color-border);">';
|
||||
html += '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>';
|
||||
html += 'Compartir</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<button class="btn btn--ghost" onclick="deleteQuote(' + q.id + ')" style="color:#F85149;">Eliminar</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="exportVisibleTableCSV(\'cotizacion_' + q.id + '\')">Exportar CSV</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="window.print()">Imprimir</button>';
|
||||
// Secondary actions
|
||||
html += '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;justify-content:flex-end;">';
|
||||
html += '<button class="btn btn--ghost" onclick="window.print()" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>';
|
||||
html += 'Imprimir</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="exportVisibleTableCSV(\'cotizacion_' + q.id + '\')" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="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>';
|
||||
html += 'Exportar CSV</button>';
|
||||
html += '<button class="btn btn--ghost" onclick="deleteQuote(' + q.id + ')" style="padding:8px 16px;font-size:var(--text-body-sm);display:inline-flex;align-items:center;gap:6px;color:var(--color-error);">';
|
||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
||||
html += 'Eliminar</button>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
document.getElementById('quoteDetail').innerHTML = html;
|
||||
@@ -142,69 +162,83 @@
|
||||
} else {
|
||||
alert('Error: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(err) { alert('Error de red al eliminar: ' + err.message); });
|
||||
};
|
||||
|
||||
window.editQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_edit_quote_id', id);
|
||||
localStorage.setItem('pos_edit_quote_customer_id', q.customer_id || '');
|
||||
localStorage.setItem('pos_edit_quote_notes', q.notes || '');
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos';
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_edit_quote_id', id);
|
||||
localStorage.setItem('pos_edit_quote_customer_id', q.customer_id || '');
|
||||
localStorage.setItem('pos_edit_quote_notes', q.notes || '');
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos/sale';
|
||||
})
|
||||
.catch(function(err) { alert('Error al cargar para editar: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en editQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
window.convertQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_convert_quote_id', id);
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos';
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id, { headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(q) {
|
||||
if (!q.items) { alert('Error cargando cotización'); return; }
|
||||
var cartItems = q.items.map(function(it) {
|
||||
return {
|
||||
inventory_id: it.inventory_id,
|
||||
part_number: it.part_number,
|
||||
name: it.name,
|
||||
quantity: it.quantity,
|
||||
unit_price: it.unit_price,
|
||||
discount_pct: it.discount_pct,
|
||||
tax_rate: it.tax_rate
|
||||
};
|
||||
});
|
||||
localStorage.setItem('pos_convert_quote_id', id);
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||
window.location.href = '/pos/sale';
|
||||
})
|
||||
.catch(function(err) { alert('Error al cargar para convertir: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en convertQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
window.shareQuote = function(id) {
|
||||
fetch(API + '/quotations/' + id + '/share', { method: 'POST', headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.url) {
|
||||
navigator.clipboard.writeText(d.url).then(function() {
|
||||
alert('Link copiado al portapapeles:\n' + d.url);
|
||||
}).catch(function() {
|
||||
prompt('Copia este link:', d.url);
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
});
|
||||
try {
|
||||
fetch(API + '/quotations/' + id + '/share', { method: 'POST', headers: headers() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.url) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(d.url).then(function() {
|
||||
alert('Link copiado al portapapeles:\n' + d.url);
|
||||
}).catch(function() {
|
||||
prompt('Copia este link:', d.url);
|
||||
});
|
||||
} else {
|
||||
prompt('Copia este link:', d.url);
|
||||
}
|
||||
} else {
|
||||
alert('Error del servidor: ' + (d.error || 'desconocido'));
|
||||
}
|
||||
})
|
||||
.catch(function(err) { alert('Error de red al compartir: ' + err.message); });
|
||||
} catch (e) { alert('Excepcion en shareQuote: ' + e.message); }
|
||||
};
|
||||
|
||||
// Close modal on outside click
|
||||
|
||||
@@ -131,7 +131,7 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href='
|
||||
|
||||
<!-- Sidebar -->
|
||||
<script src="/pos/static/js/i18n.js" defer></script>
|
||||
<script src="/pos/static/js/whatsapp2.js" defer></script>
|
||||
<script src="/pos/static/js/whatsapp2.js?v=5" defer></script>
|
||||
<script src="/pos/static/js/pos-utils.js" defer></script>
|
||||
<script src="/pos/static/js/sidebar.js" defer></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user