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

@@ -0,0 +1,497 @@
// ─── MercadoLibre Bulk Publish Modal ───────────────────────────────────
var meliPreviewData = {};
var meliCategoryAttrs = [];
var meliCategorySearchTimeout;
var meliCatItems = [];
var meliCatActiveIndex = -1;
window.openMeliPublishModal = function() {
if (selectedItems.size === 0) { showToast('Selecciona al menos un producto', 'warn'); return; }
document.getElementById('meliPublishModal').classList.add('is-open');
document.getElementById('meliPublishResult').innerHTML = '';
document.getElementById('meliCategoryId').value = '';
document.getElementById('meliCategorySearch').value = '';
document.getElementById('meliCategoryResults').innerHTML = '';
document.getElementById('meliAttrsSection').style.display = 'none';
document.getElementById('meliAttrsGrid').innerHTML = '';
meliCategoryAttrs = [];
refreshMeliPublishPreview();
};
window.closeMeliPublishModal = function() {
document.getElementById('meliPublishModal').classList.remove('is-open');
};
// ==== RESTAURADAS v4 (MELI + descuentos) ====
window.searchMeliCategories = function() {
var q = document.getElementById('meliCategorySearch').value.trim();
var resultsDiv = document.getElementById('meliCategoryResults');
if (q.length < 2) { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; return; }
clearTimeout(meliCategorySearchTimeout);
resultsDiv.innerHTML = '<div class="meli-cat-dropdown"><div class="meli-cat-loading">Buscando...</div></div>';
meliCategorySearchTimeout = setTimeout(function() {
fetch('/pos/api/marketplace-ext/categories?q=' + encodeURIComponent(q), { headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' } })
.then(function(r) { return r.json(); })
.then(function(data) {
var cats = data.categories || [];
meliCatItems = cats.slice(0, 10);
meliCatActiveIndex = -1;
if (!meliCatItems.length) { resultsDiv.innerHTML = '<div class="meli-cat-dropdown"><div class="meli-cat-empty">Sin resultados</div></div>'; return; }
var html = '<div class="meli-cat-dropdown">';
meliCatItems.forEach(function(c, idx) {
html += '<div class="meli-cat-item" data-idx="' + idx + '" onmouseenter="highlightMeliCat(' + idx + ')" onmousedown="selectMeliCategoryIdx(' + idx + ')">' +
'<span>' + esc(c.category_name || c.category_id) + '</span>' +
'<span class="cat-id">' + esc(c.category_id) + '</span>' +
'</div>';
});
html += '</div>';
resultsDiv.innerHTML = html;
})
.catch(function() { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; });
}, 300);
};
window.handleMeliCatKeydown = function(e) {
if (!meliCatItems.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
meliCatActiveIndex = Math.min(meliCatActiveIndex + 1, meliCatItems.length - 1);
highlightMeliCat(meliCatActiveIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
meliCatActiveIndex = Math.max(meliCatActiveIndex - 1, -1);
highlightMeliCat(meliCatActiveIndex);
} else if (e.key === 'Enter') {
e.preventDefault();
if (meliCatActiveIndex >= 0) selectMeliCategoryIdx(meliCatActiveIndex);
} else if (e.key === 'Escape') {
document.getElementById('meliCategoryResults').innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
}
};
/* Cerrar dropdown al hacer click fuera */
document.addEventListener('click', function(e) {
var field = document.getElementById('meliCategorySearch');
var results = document.getElementById('meliCategoryResults');
if (field && results && !field.contains(e.target) && !results.contains(e.target)) {
results.innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
}
});
window.onMeliShippingChange = function() {
var mode = document.getElementById('meliShippingMode').value;
var costField = document.getElementById('meliShippingCostField');
if (costField) costField.style.display = (mode === 'custom') ? 'block' : 'none';
};
// ─── MELI publish helpers ──────────────────────────────────────────────
// Contrato de `custom_data` esperado por validate_items / publish_items
// (pos/services/marketplace_external_service.py):
// {
// titles: { "<inventory_id>": "Título (<=60)" },
// prices: { "<inventory_id>": 123.45 },
// stocks: { "<inventory_id>": 3 },
// attributes: { "<inventory_id>": [{ id: "BRAND", value_name: "Nissan" }] },
// shipping_cost: 150 // solo se usa si shipping_mode === 'custom'
// }
// Las claves son el id de inventario como string (el backend consulta con str(inv_id)).
function _meliAttrValue(attr) {
// Los atributos con lista cerrada se pintan como <select> + input "Otra (escribir)";
// el resto como un input de texto libre.
var selEl = document.getElementById('meliAttrSel-' + attr.id);
if (selEl) {
if (selEl.value === '__other__') {
var otherEl = document.getElementById('meliAttrOther-' + attr.id);
return otherEl ? otherEl.value.trim() : '';
}
return (selEl.value || '').trim();
}
var el = document.getElementById('meliAttr-' + attr.id);
return el ? (el.value || '').trim() : '';
}
function _collectMeliCustomData() {
var ids = Array.from(selectedItems);
var customData = { titles: {}, prices: {}, stocks: {}, attributes: {} };
ids.forEach(function(id) {
var titleEl = document.getElementById('meliTitle-' + id);
var priceEl = document.getElementById('meliPrice-' + id);
var stockEl = document.getElementById('meliStock-' + id);
if (titleEl && titleEl.value.trim()) customData.titles[id] = titleEl.value.trim();
if (priceEl) {
var price = parseFloat(priceEl.value);
if (!isNaN(price)) customData.prices[id] = price;
}
if (stockEl) {
var stock = parseInt(stockEl.value, 10);
if (!isNaN(stock)) customData.stocks[id] = stock;
}
var attrs = [];
(meliCategoryAttrs || []).forEach(function(attr) {
var value = _meliAttrValue(attr);
if (attr.id && value) attrs.push({ id: attr.id, value_name: value });
});
if (attrs.length) customData.attributes[id] = attrs;
});
// El backend solo lo aplica con shipping_mode === 'custom'; si el campo no
// existe o está vacío simplemente se omite.
var shippingCostEl = document.getElementById('meliShippingCost');
var shippingCost = shippingCostEl ? parseFloat(shippingCostEl.value) : NaN;
if (!isNaN(shippingCost)) customData.shipping_cost = shippingCost;
return customData;
}
function _renderPublishResult(data, resultEl) {
if (!resultEl) return;
data = data || {};
var successList = data.success || [];
var failedList = data.failed || [];
var html = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + successList.length + ' publicado(s)</span> · <span style="color:var(--color-error);">❌ ' + failedList.length + ' fallo(s)</span></div>';
if (successList.length) {
html += '<ul style="margin:0 0 var(--space-2);padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
successList.forEach(function(s) {
var itemId = s.external_item_id || s.item_id;
var label = 'Item #' + esc(s.inventory_id);
if (itemId) {
label += ' → <a href="' + esc(s.permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + esc(itemId) + '</a>';
}
html += '<li>' + label + '</li>';
});
html += '</ul>';
}
if (failedList.length) {
html += '<ul style="margin:0;padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
failedList.forEach(function(f) {
html += '<li>Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + '</li>';
});
html += '</ul>';
}
resultEl.innerHTML = html;
if (successList.length > 0) {
selectedItems.clear();
updateSelectionUI();
if (inventoryVS) inventoryVS.refresh();
}
}
function _pollMeliAsync(taskId, resultEl, btn) {
var attempts = 0;
var maxAttempts = 60; // ~2 min (una consulta cada 2s)
var interval = setInterval(function() {
attempts++;
fetch('/pos/api/marketplace-ext/listings/async/' + encodeURIComponent(taskId), { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
clearInterval(interval);
if (btn) btn.disabled = false;
resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>';
return;
}
if (data.status === 'done') {
clearInterval(interval);
if (btn) btn.disabled = false;
_renderPublishResult(data.result || {}, resultEl);
setTimeout(function() { closeMeliPublishModal(); }, 3000);
} else if (attempts >= maxAttempts) {
clearInterval(interval);
if (btn) btn.disabled = false;
resultEl.innerHTML = '<span style="color:var(--color-error);">Timeout esperando resultado. Revisa la pestaña Publicaciones más tarde.</span>';
} else {
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Publicando en segundo plano... (' + (attempts * 2) + 's)</span>';
}
}).catch(function() {
clearInterval(interval);
if (btn) btn.disabled = false;
resultEl.innerHTML = '<span style="color:var(--color-error);">Error consultando progreso</span>';
});
}, 2000);
}
window.validateMeliPublish = function() {
var categoryId = document.getElementById('meliCategoryId').value.trim();
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = '<span style="color:var(--color-error);">Selecciona una categoría de MercadoLibre</span>'; return; }
var listingType = document.getElementById('meliListingType').value;
var shippingMode = document.getElementById('meliShippingMode').value;
var ids = Array.from(selectedItems);
var resultEl = document.getElementById('meliPublishResult');
var btn = document.getElementById('meliValidateBtn');
btn.disabled = true;
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Validando con MercadoLibre...</span>';
fetch('/pos/api/marketplace-ext/listings/validate', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({
inventory_ids: ids,
category_id: categoryId,
listing_type: listingType,
shipping_mode: shippingMode,
custom_data: _collectMeliCustomData()
})
}).then(function(r){ return r.json(); })
.then(function(data) {
btn.disabled = false;
if (data.error) { resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>'; return; }
var valid = (data.valid || []);
var invalid = (data.invalid || []);
var html = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + valid.length + ' válido(s)</span> · <span style="color:var(--color-error);">❌ ' + invalid.length + ' inválido(s)</span></div>';
// Show earnings estimate table for valid items
if (valid.length > 0) {
html += '<div style="margin:var(--space-3) 0;border:1px solid var(--color-border);border-radius:var(--radius-md);overflow:hidden;">';
html += '<div style="background:var(--color-surface-2);padding:var(--space-2) var(--space-3);font-size:var(--text-caption);font-weight:600;color:var(--color-text-primary);">💰 Estimado de ganancia (después de comisiones ML)</div>';
html += '<table style="width:100%;border-collapse:collapse;font-size:var(--text-caption);">';
html += '<tr style="background:var(--color-surface-1);"><th style="padding:6px 8px;text-align:left;">Item</th><th style="padding:6px 8px;text-align:right;">Precio</th><th style="padding:6px 8px;text-align:right;">Comisión</th><th style="padding:6px 8px;text-align:right;">Recibes</th></tr>';
valid.forEach(function(v) {
var price = v.price || 0;
var fee = v.fee_amount || 0;
var net = v.net_amount || 0;
var feePct = v.fee_pct || 0;
html += '<tr style="border-top:1px solid var(--color-border);">';
html += '<td style="padding:6px 8px;">#' + esc(v.inventory_id) + '</td>';
html += '<td style="padding:6px 8px;text-align:right;">$' + price.toFixed(2) + '</td>';
html += '<td style="padding:6px 8px;text-align:right;color:var(--color-error);">-$' + fee.toFixed(2) + ' (' + feePct.toFixed(1) + '%)</td>';
html += '<td style="padding:6px 8px;text-align:right;color:var(--color-success);font-weight:600;">$' + net.toFixed(2) + '</td>';
html += '</tr>';
});
html += '</table>';
if (valid.some(function(v){ return v.fee_source === 'approximation'; })) {
html += '<div style="padding:var(--space-2) var(--space-3);font-size:10px;color:var(--color-text-muted);background:var(--color-surface-0);">* Comisión estimada. El monto real puede variar según ML.</div>';
}
html += '</div>';
}
if (invalid.length) {
html += '<ul style="margin:0;padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
invalid.forEach(function(f) {
html += '<li>Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + '</li>';
});
html += '</ul>';
}
resultEl.innerHTML = html;
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(e.message) + '</span>'; });
};
window.executeMeliPublish = function() {
var categoryId = document.getElementById('meliCategoryId').value.trim();
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = '<span style="color:var(--color-error);">Selecciona una categoría de MercadoLibre</span>'; return; }
var listingType = document.getElementById('meliListingType').value;
var shippingMode = document.getElementById('meliShippingMode').value;
var ids = Array.from(selectedItems);
var resultEl = document.getElementById('meliPublishResult');
var btn = document.getElementById('meliPublishBtn');
btn.disabled = true;
var useAsync = ids.length > 3;
var endpoint = useAsync ? '/pos/api/marketplace-ext/listings/async' : '/pos/api/marketplace-ext/listings';
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">' + (useAsync ? 'Encolando ' : 'Publicando ') + ids.length + ' producto(s)...</span>';
fetch(endpoint, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({
inventory_ids: ids,
category_id: categoryId,
listing_type: listingType,
shipping_mode: shippingMode,
custom_data: _collectMeliCustomData()
})
}).then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { btn.disabled = false; resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>'; return; }
if (useAsync && data.task_id) {
_pollMeliAsync(data.task_id, resultEl, btn);
} else {
btn.disabled = false;
_renderPublishResult(data, resultEl);
if ((data.success || []).length > 0) {
setTimeout(function() { closeMeliPublishModal(); }, 2500);
}
}
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(e.message) + '</span>'; });
};
function refreshMeliPublishPreview() {
var container = document.getElementById('meliPublishItemsPreview');
var countEl = document.getElementById('meliPublishSelectedCount');
countEl.textContent = selectedItems.size + ' producto(s) seleccionado(s)';
container.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando verificaciones...</p>';
var ids = Array.from(selectedItems);
fetch('/pos/api/marketplace-ext/inventory-check', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ inventory_ids: ids })
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.items) { container.innerHTML = '<p style="color:var(--color-error);">Error cargando preview</p>'; return; }
var html = '';
data.items.forEach(function(it) {
if (!it.exists) {
html += '<div class="meli-preview-card" style="opacity:0.6;"><div style="color:var(--color-error);">Item #' + it.inventory_id + ' no encontrado</div></div>';
return;
}
meliPreviewData[it.inventory_id] = it;
var checks = '';
checks += '<span class="meli-check ' + (it.has_image ? 'ok' : 'fail') + '">' + (it.has_image ? '✅' : '❌') + ' Imagen</span>';
checks += '<span class="meli-check ' + (it.has_stock ? 'ok' : 'fail') + '">' + (it.has_stock ? '✅' : '❌') + ' Stock</span>';
checks += '<span class="meli-check ' + (it.has_price ? 'ok' : 'fail') + '">' + (it.has_price ? '✅' : '❌') + ' Precio</span>';
if (it.already_published) {
checks += '<span class="meli-check ok">✅ <a href="' + esc(it.existing_listing.permalink || '#') + '" target="_blank" style="color:var(--color-success);text-decoration:underline;">Ya publicado (' + esc(it.existing_listing.status) + ')</a></span>';
}
var imgSrc = it.image_url || '';
var imgHtml = imgSrc ? '<img src="' + esc(imgSrc) + '" alt="" onerror="this.style.display=\'none\'">' : '<div style="width:56px;height:56px;background:var(--color-surface-2);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--color-text-muted);">Sin img</div>';
if (!it.has_image) {
imgHtml = '<div class="meli-img-upload" onclick="document.getElementById(\'meliImgUpload-' + it.inventory_id + '\').click()">' +
'<div style="font-size:10px;">+ Subir</div>' +
'<input type="file" id="meliImgUpload-' + it.inventory_id + '" accept="image/*" onchange="handleMeliImageUpload(' + it.inventory_id + ', this)">' +
'</div>';
}
html += '<div class="meli-preview-card" id="meliCard-' + it.inventory_id + '">' +
imgHtml +
'<div>' +
'<input type="text" class="meli-title-input" id="meliTitle-' + it.inventory_id + '" value="' + esc(it.title) + '" maxlength="60" oninput="updateMeliTitleCount(' + it.inventory_id + ')">' +
'<div style="font-size:10px;color:var(--color-text-muted);text-align:right;" id="meliTitleCount-' + it.inventory_id + '">' + it.title.length + '/60</div>' +
'<div class="meli-checks-row">' + checks + '</div>' +
'</div>' +
'<div><label style="font-size:10px;color:var(--color-text-muted);">Precio</label><input type="number" class="meli-num-input" id="meliPrice-' + it.inventory_id + '" value="' + it.price + '" oninput="scheduleMeliFeeEstimates()"></div>' +
'<div><label style="font-size:10px;color:var(--color-text-muted);">Stock</label><input type="number" class="meli-num-input" id="meliStock-' + it.inventory_id + '" value="' + it.stock + '"></div>' +
'<div id="meliFee-' + it.inventory_id + '" style="font-size:10px;color:var(--color-text-muted);min-width:130px;line-height:1.35;"></div>' +
'</div>';
});
container.innerHTML = html;
scheduleMeliFeeEstimates();
}).catch(function() { container.innerHTML = '<p style="color:var(--color-error);">Error de red</p>'; });
}
window.selectMeliCategory = function(id, name) {
document.getElementById('meliCategoryId').value = id;
document.getElementById('meliCategorySearch').value = name;
document.getElementById('meliCategoryResults').innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
loadCategoryAttributes(id);
scheduleMeliFeeEstimates();
};
window.selectMeliCategoryIdx = function(idx) {
var c = meliCatItems[idx];
if (!c) return;
selectMeliCategory(c.category_id, c.category_name || c.category_id);
};
window.highlightMeliCat = function(idx) {
meliCatActiveIndex = idx;
var items = document.querySelectorAll('.meli-cat-item');
items.forEach(function(el, i) { el.classList.toggle('is-active', i === idx); });
};
window.loadCategoryAttributes = function(categoryId) {
var grid = document.getElementById('meliAttrsGrid');
var section = document.getElementById('meliAttrsSection');
grid.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando atributos...</p>';
section.style.display = 'block';
fetch('/pos/api/marketplace-ext/categories/' + encodeURIComponent(categoryId) + '/attributes', { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function(r){ return r.json(); })
.then(function(data) {
meliCategoryAttrs = data.attributes || [];
if (!meliCategoryAttrs.length) { grid.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">No hay atributos obligatorios adicionales.</p>'; return; }
var html = '';
meliCategoryAttrs.forEach(function(attr) {
var attrId = esc(attr.id);
var attrName = esc(attr.name || attr.id);
var inputHtml = '<input type="text" class="meli-title-input" id="meliAttr-' + attrId + '" placeholder="' + attrName + '">';
if (attr.values && attr.values.length) {
// Some ML attributes (like BRAND) have a closed list but the API still
// accepts free-text via value_name. Provide a select + "Other" fallback.
var selectId = 'meliAttrSel-' + attrId;
var otherId = 'meliAttrOther-' + attrId;
inputHtml = '<select class="meli-title-input" id="' + selectId + '" onchange="onMeliAttrSelectChange(\'' + attrId + '\')">' +
'<option value="">Selecciona ' + attrName + '</option>' +
attr.values.map(function(v) { return '<option value="' + esc(v.name) + '">' + esc(v.name) + '</option>'; }).join('') +
'<option value="__other__">Otra marca (escribir)...</option>' +
'</select>' +
'<input type="text" class="meli-title-input" id="' + otherId + '" placeholder="Escribe la ' + attrName + '" style="display:none;margin-top:6px;">';
}
html += '<div class="inv-field" id="meliAttrWrap-' + attrId + '"><label>' + attrName + (attr.tags && attr.tags.required ? ' *' : '') + '</label>' + inputHtml + '</div>';
});
grid.innerHTML = html;
}).catch(function() { grid.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-caption);">Error cargando atributos</p>'; });
};
window.updateMeliTitleCount = function(id) {
var el = document.getElementById('meliTitle-' + id);
var countEl = document.getElementById('meliTitleCount-' + id);
if (el && countEl) countEl.textContent = el.value.length + '/60';
};
window.handleMeliImageUpload = function(itemId, input) {
if (!input.files || !input.files[0]) return;
var file = input.files[0];
var formData = new FormData();
formData.append('file', file);
fetch('/pos/api/inventory/items/' + itemId + '/image', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken() },
body: formData
}).then(function(r){ return r.json(); })
.then(function(data) {
if (data.image_url) {
showToast('Imagen subida', 'success');
refreshMeliPublishPreview();
if (inventoryVS) inventoryVS.refresh();
} else {
showToast(data.error || 'Error subiendo imagen', 'error');
}
}).catch(function(){ showToast('Error de red', 'error'); });
};
// ─── Estimador de comisiones MeLi (por artículo) ───
var meliFeeTimeout;
function fmtMoney(n) {
return '$' + (parseFloat(n) || 0).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function updateMeliFeeEstimates() {
var categoryId = (document.getElementById('meliCategoryId') || {}).value;
var listingType = (document.getElementById('meliListingType') || {}).value || 'gold_special';
var ids = Object.keys(window.meliPreviewData || (typeof meliPreviewData !== 'undefined' ? meliPreviewData : {}));
if (!ids.length) return;
ids.forEach(function (id) {
var feeEl = document.getElementById('meliFee-' + id);
if (!feeEl) return;
if (!categoryId) { feeEl.innerHTML = ''; return; }
var priceEl = document.getElementById('meliPrice-' + id);
var price = parseFloat(priceEl && priceEl.value) || 0;
if (price <= 0) { feeEl.innerHTML = '<span style="color:var(--color-text-muted);">Ingresa un precio para estimar comisiones</span>'; return; }
feeEl.innerHTML = '<span style="color:var(--color-text-muted);">Calculando comisiones...</span>';
fetch('/pos/api/marketplace-ext/listing-prices?price=' + encodeURIComponent(price) + '&category_id=' + encodeURIComponent(categoryId), { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function (r) { return r.json().then(function (d) { return {ok: r.ok, data: d}; }); })
.then(function (res) {
if (!res.ok || res.data.error) { feeEl.innerHTML = '<span style="color:var(--color-warning);">No se pudo estimar: ' + esc(res.data.error || 'error') + '</span>'; return; }
var offers = res.data.listing_prices || [];
var offer = null;
offers.forEach(function (o) { if (o.listing_type_id === listingType) offer = o; });
if (!offer && offers.length) offer = offers[0];
if (!offer || !offer.sale_fee_details) { feeEl.innerHTML = ''; return; }
var fee = offer.sale_fee_details.gross_amount || 0;
var pct = offer.sale_fee_details.percentage_fee || 0;
var net = price - fee;
feeEl.innerHTML = '💰 Comisión MeLi: <strong style="color:var(--color-warning);">' + fmtMoney(fee) + ' (' + pct + '%)</strong>'
+ ' · Recibes: <strong style="color:' + (net > 0 ? 'var(--color-success)' : 'var(--color-error)') + ';">' + fmtMoney(net) + '</strong>'
+ ' <span style="opacity:.65;">(estimado, sin costo de envío)</span>';
})
.catch(function () { feeEl.innerHTML = ''; });
});
}
function scheduleMeliFeeEstimates() {
clearTimeout(meliFeeTimeout);
meliFeeTimeout = setTimeout(updateMeliFeeEstimates, 500);
}
window.scheduleMeliFeeEstimates = scheduleMeliFeeEstimates;
// =====================================================================
// EXPOSE GLOBALS (for onclick handlers in HTML)