feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
});
|
||||
if (tab === 'listings') loadListings();
|
||||
if (tab === 'orders') loadOrders();
|
||||
if (tab === 'questions') loadQuestions();
|
||||
};
|
||||
|
||||
function closeModal(id) {
|
||||
@@ -81,7 +82,7 @@
|
||||
localStorage.setItem('meli_shipping', shipping);
|
||||
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri);
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&scope=read+write+offline_access';
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
@@ -148,16 +149,18 @@
|
||||
var statusClass = 'meli-status--' + (l.external_status || 'pending');
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + '</div>'
|
||||
+ '<a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;color:var(--color-primary);text-decoration:none;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + ' ↗</a>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + (l.external_status || '—') + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: ' + escapeHtml(l.external_item_id || '—')
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: <a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + escapeHtml(l.external_item_id || '—') + '</a>'
|
||||
+ '</div>'
|
||||
+ '<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);">'
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="syncListing(' + l.id + ')">Sync</button>'
|
||||
+ (l.external_status === 'active' ? '<button class="btn btn--ghost btn--xs" onclick="pauseListing(' + l.id + ')">Pausar</button>' : '<button class="btn btn--ghost btn--xs" onclick="activateListing(' + l.id + ')">Activar</button>')
|
||||
+ '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>'
|
||||
+ (l.external_status === 'closed' || !l.is_active
|
||||
? '<button class="btn btn--danger btn--xs" onclick="deleteListingPermanently(' + l.id + ')">Eliminar</button>'
|
||||
: '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>')
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
@@ -202,6 +205,14 @@
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.deleteListingPermanently = async function(id) {
|
||||
if (!confirm('¿Eliminar permanentemente esta publicación del listado local? Esta acción no se puede deshacer.')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/permanent', { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error al eliminar'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Orders ────────────────────────────────────────────────────────────
|
||||
|
||||
var ordersData = [];
|
||||
@@ -451,11 +462,144 @@
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ─── Questions ─────────────────────────────────────────────────────────
|
||||
|
||||
var questionsData = [];
|
||||
|
||||
window.loadQuestions = async function() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
container.innerHTML = '<div class="skeleton-grid">' + Array(6).fill('<div class="skeleton skeleton--card"></div>').join('') + '</div>';
|
||||
try {
|
||||
var res = await fetch(API + '/questions', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load questions');
|
||||
var data = await res.json();
|
||||
questionsData = data.items || [];
|
||||
renderQuestions();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: 'No hay preguntas de compradores pendientes. Sincroniza con MercadoLibre para obtenerlas.',
|
||||
action: '<button class="btn btn--meli btn--sm" onclick="syncQuestions()">Sincronizar con ML</button>'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function renderQuestions() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
var statusFilter = document.getElementById('questionStatusFilter').value;
|
||||
var search = document.getElementById('questionSearch').value.toLowerCase();
|
||||
|
||||
var filtered = questionsData.filter(function(q) {
|
||||
if (statusFilter && q.status !== 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 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>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-error);">' + unanswered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Sin responder</div></div>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-success);">' + answered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Respondidas</div></div>' +
|
||||
'</div>';
|
||||
document.getElementById('questionsStatsBar').innerHTML = statsHtml;
|
||||
|
||||
if (!filtered.length) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: statusFilter ? 'No hay preguntas con el filtro seleccionado.' : 'No hay preguntas sincronizadas.',
|
||||
action: ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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 answerHtml = '';
|
||||
if (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>' +
|
||||
'</div>';
|
||||
} else if (q.answer_text) {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);padding:var(--space-2);background:var(--color-surface-0);border-radius:var(--radius-sm);font-size:var(--text-caption);color:var(--color-text-secondary);">' +
|
||||
'<strong>Respuesta:</strong> ' + escapeHtml(q.answer_text) +
|
||||
'</div>';
|
||||
}
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(q.listing_title || 'Artículo sin título') + '</div>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + statusLabel + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'Comprador: ' + escapeHtml(q.buyer_nickname || '—') + ' · ' + (q.question_date ? new Date(q.question_date).toLocaleString('es-MX') : '—')
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-body-sm);color:var(--color-text-primary);margin-bottom:var(--space-2);">'
|
||||
+ '<strong>Pregunta:</strong> ' + escapeHtml(q.question_text)
|
||||
+ '</div>'
|
||||
+ answerHtml
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterQuestions = renderQuestions;
|
||||
|
||||
window.syncQuestions = async function() {
|
||||
var btn = document.querySelector('#panel-questions .btn--primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Sincronizando...'; }
|
||||
try {
|
||||
var res = await fetch(API + '/questions/sync', { method: 'POST', headers: headers() });
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Sincronizadas ' + (data.synced || 0) + ' preguntas', 'ok', { title: 'Sincronización' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al sincronizar', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔄 Actualizar'; }
|
||||
}
|
||||
};
|
||||
|
||||
window.submitAnswer = async function(questionId) {
|
||||
var textarea = document.getElementById('qAnswer-' + questionId);
|
||||
if (!textarea) return;
|
||||
var text = textarea.value.trim();
|
||||
if (!text) {
|
||||
showToast('Escribe una respuesta antes de enviar', 'error', { title: 'Respuesta vacía' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var res = await fetch(API + '/questions/' + questionId + '/answer', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Respuesta enviada correctamente', 'ok', { title: 'Pregunta respondida' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al enviar respuesta', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === 'function') {
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Configuración ML', href: '/pos/marketplace-external', icon: '⚙️' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Publicaciones ML', href: '/pos/marketplace-external#listings', icon: '📦' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Órdenes ML', href: '/pos/marketplace-external#orders', icon: '🛒' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Preguntas ML', href: '/pos/marketplace-external#questions', icon: '❓' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
Reference in New Issue
Block a user