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:
@@ -53,6 +53,9 @@
|
||||
connectedDiv.style.display = 'block';
|
||||
document.getElementById('connectedNickname').textContent = cfg.meli_user_id || 'Usuario ML';
|
||||
document.getElementById('connectedSite').textContent = cfg.meli_site_id || 'MLM';
|
||||
if (!cfg.has_refresh_token) {
|
||||
statusDiv.innerHTML += '<div style="margin-top:8px;padding:10px;border-radius:6px;background:rgba(234,179,8,.12);color:#eab308;font-size:var(--text-caption);">⚠️ Esta conexión <strong>no tiene token de larga duración</strong> (refresh token) y expirará en ~6 horas. Habilite el scope <strong>offline_access</strong> en su aplicación de MercadoLibre Developers y vuelva a conectar para corregirlo.</div>';
|
||||
}
|
||||
} else {
|
||||
statusDiv.innerHTML = '<span class="meli-status meli-status--pending">● No conectado</span>';
|
||||
formDiv.style.display = 'block';
|
||||
@@ -133,6 +136,7 @@
|
||||
if (!res.ok) throw new Error('Failed to load listings');
|
||||
var data = await res.json();
|
||||
listingsData = data.items || [];
|
||||
listingsData.forEach(function(l) { l.external_status = normStatus(l.external_status); });
|
||||
renderListings();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
@@ -244,6 +248,7 @@
|
||||
if (!res.ok) throw new Error('Failed to load orders');
|
||||
var data = await res.json();
|
||||
ordersData = data.items || [];
|
||||
ordersData.forEach(function(o) { o.status = normStatus(o.status); });
|
||||
renderOrders();
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">' + renderEmptyState({
|
||||
@@ -304,6 +309,7 @@
|
||||
var res = await fetch(API + '/orders/' + id, { headers: headers() });
|
||||
var o = await res.json();
|
||||
if (!res.ok) throw new Error(o.error || 'Error');
|
||||
o.status = normStatus(o.status);
|
||||
|
||||
var itemsHtml = (o.items || []).map(function(it) {
|
||||
return '<tr><td>' + escapeHtml(it.title || '—') + '</td><td>' + it.quantity + '</td><td style="text-align:right">$' + (it.unit_price || 0).toFixed(2) + '</td></tr>';
|
||||
@@ -368,6 +374,11 @@
|
||||
|
||||
// ─── Utils ─────────────────────────────────────────────────────────────
|
||||
|
||||
// ML devuelve el estado de las preguntas en MAYÚSCULAS (UNANSWERED/ANSWERED/CLOSED)
|
||||
// mientras que nuestros filtros y literales usan minúsculas -> comparar
|
||||
// siempre en minúsculas para que las no respondidas aparezcan y se puedan responder.
|
||||
function normStatus(s) { return String(s == null ? '' : s).toLowerCase(); }
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
@@ -394,7 +405,11 @@
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('¡Conectado exitosamente con MercadoLibre!');
|
||||
if (data.warning === 'no_refresh_token') {
|
||||
alert('Conectado con MercadoLibre, pero con una advertencia:\n\nMercadoLibre NO devolvió un token de larga duración (refresh token), así que la conexión expirará en ~6 horas.\n\nPara corregirlo: entre a developers.mercadolibre.com → su aplicación → Editar → habilite el scope "offline_access" y vuelva a conectar.');
|
||||
} else {
|
||||
alert('¡Conectado exitosamente con MercadoLibre!');
|
||||
}
|
||||
window.history.replaceState({}, document.title, '/pos/marketplace-external');
|
||||
loadConfig();
|
||||
} else {
|
||||
@@ -415,9 +430,9 @@
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
var data = await res.json();
|
||||
var items = data.items || [];
|
||||
var active = items.filter(function(l) { return l.external_status === 'active'; }).length;
|
||||
var paused = items.filter(function(l) { return l.external_status === 'paused'; }).length;
|
||||
var closed = items.filter(function(l) { return l.external_status === 'closed'; }).length;
|
||||
var active = items.filter(function(l) { return normStatus(l.external_status) === 'active'; }).length;
|
||||
var paused = items.filter(function(l) { return normStatus(l.external_status) === 'paused'; }).length;
|
||||
var closed = items.filter(function(l) { return normStatus(l.external_status) === 'closed'; }).length;
|
||||
var total = items.length;
|
||||
|
||||
var html = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
||||
@@ -489,6 +504,7 @@
|
||||
if (!res.ok) throw new Error('Failed to load questions');
|
||||
var data = await res.json();
|
||||
questionsData = data.items || [];
|
||||
questionsData.forEach(function(q) { q.status = normStatus(q.status); });
|
||||
renderQuestions();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
@@ -506,14 +522,14 @@
|
||||
var search = document.getElementById('questionSearch').value.toLowerCase();
|
||||
|
||||
var filtered = questionsData.filter(function(q) {
|
||||
if (statusFilter && q.status !== statusFilter) return false;
|
||||
if (statusFilter && normStatus(q.status) !== normStatus(statusFilter)) return false;
|
||||
if (search && !((q.question_text || '').toLowerCase().includes(search)) && !((q.listing_title || '').toLowerCase().includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Stats bar
|
||||
var unanswered = questionsData.filter(function(q) { return q.status === 'unanswered'; }).length;
|
||||
var answered = questionsData.filter(function(q) { return q.status === 'answered'; }).length;
|
||||
var unanswered = questionsData.filter(function(q) { return normStatus(q.status) === 'unanswered'; }).length;
|
||||
var answered = questionsData.filter(function(q) { return normStatus(q.status) === 'answered'; }).length;
|
||||
var total = questionsData.length;
|
||||
var statsHtml = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-primary);">' + total + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Total preguntas</div></div>' +
|
||||
@@ -533,10 +549,10 @@
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(function(q) {
|
||||
var statusClass = 'meli-status--' + (q.status || 'pending');
|
||||
var statusLabel = q.status === 'unanswered' ? 'Sin responder' : (q.status === 'answered' ? 'Respondida' : (q.status || '—'));
|
||||
var statusClass = 'meli-status--' + (normStatus(q.status) || 'pending');
|
||||
var statusLabel = normStatus(q.status) === 'unanswered' ? 'Sin responder' : (normStatus(q.status) === 'answered' ? 'Respondida' : (q.status || '—'));
|
||||
var answerHtml = '';
|
||||
if (q.status === 'unanswered') {
|
||||
if (normStatus(q.status) === 'unanswered') {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);">' +
|
||||
'<textarea class="meli-title-input" id="qAnswer-' + q.id + '" rows="2" placeholder="Escribe tu respuesta..."></textarea>' +
|
||||
'<button class="btn btn--primary btn--xs" style="margin-top:6px;" onclick="submitAnswer(' + q.id + ')">Enviar respuesta</button>' +
|
||||
|
||||
Reference in New Issue
Block a user