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:
@@ -195,7 +195,19 @@
|
||||
currentAbort = null;
|
||||
}
|
||||
var opts = { headers: headers };
|
||||
if (url.indexOf('/pos/api/') === 0 && url.indexOf('mode=') !== -1 || url.indexOf('/years') !== -1 || url.indexOf('/brands') !== -1 || url.indexOf('/models') !== -1 || url.indexOf('/engines') !== -1 || url.indexOf('/categories') !== -1 || url.indexOf('/groups') !== -1 || url.indexOf('/part-types') !== -1 || url.indexOf('/parts') !== -1 || url.indexOf('/search') !== -1) {
|
||||
var isCatalogNav = url.indexOf('/pos/api/') === 0 && (
|
||||
url.indexOf('mode=') !== -1 ||
|
||||
url.indexOf('/years') !== -1 ||
|
||||
url.indexOf('/brands') !== -1 ||
|
||||
url.indexOf('/models') !== -1 ||
|
||||
url.indexOf('/engines') !== -1 ||
|
||||
url.indexOf('/categories') !== -1 ||
|
||||
url.indexOf('/groups') !== -1 ||
|
||||
url.indexOf('/part-types') !== -1 ||
|
||||
url.indexOf('/parts') !== -1 ||
|
||||
url.indexOf('/search') !== -1
|
||||
);
|
||||
if (isCatalogNav) {
|
||||
currentAbort = new AbortController();
|
||||
opts.signal = currentAbort.signal;
|
||||
}
|
||||
@@ -233,7 +245,7 @@
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
return d.innerHTML.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ─── Breadcrumb ───
|
||||
@@ -290,9 +302,9 @@
|
||||
|
||||
function resetNav() {
|
||||
nav.level = 'brands';
|
||||
pushNavState();
|
||||
nav.brand = nav.model = nav.year = nav.engine = nav.category = nav.group = nav.partType = null;
|
||||
nav.nxGroup = nav.nxSubgroup = nav.nxPartType = null;
|
||||
pushNavState();
|
||||
}
|
||||
|
||||
function resetNavFrom(level) {
|
||||
@@ -927,9 +939,14 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -988,17 +1005,23 @@
|
||||
partsGrid.innerHTML = data.data.map(function (p) {
|
||||
// Stock badge — prefer tenant stock, then warehouse network, else fallback
|
||||
var stockBadge;
|
||||
if (p.local_stock > 0) {
|
||||
var isSupplier = p.source === 'supplier_catalog' || (typeof p.id_part === 'string' && p.id_part.indexOf('sc:') === 0);
|
||||
if (isSupplier) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none" style="background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
} else if (p.local_stock > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--local">En stock: ' + p.local_stock + '</span>';
|
||||
} else if (p.in_stock_network || p.bodega_count > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--bodega">' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '</span>';
|
||||
} else {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none">Sin stock</span>';
|
||||
}
|
||||
// Local inventory native badge
|
||||
var sourceBadge = p.source === 'local_inventory'
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
// Source badge for local inventory or supplier catalog
|
||||
var sourceBadge = '';
|
||||
if (p.source === 'local_inventory') {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
|
||||
var imgHtml = p.image_url
|
||||
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
|
||||
@@ -1039,10 +1062,15 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
// local-inventory item: info already visible on card
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -1185,6 +1213,73 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openSupplierDetail(supplierId) {
|
||||
detailBody.innerHTML = '<div class="loading is-visible"><div class="spinner"></div></div>';
|
||||
detailFooter.style.display = 'none';
|
||||
detailPanel.classList.add('is-open');
|
||||
detailOverlay.classList.add('is-visible');
|
||||
|
||||
apiFetch('/pos/api/supplier-catalog/items/' + supplierId).then(function (data) {
|
||||
if (!data || data.error) {
|
||||
detailBody.innerHTML = '<p style="color:var(--color-error);padding:var(--space-4);">Error al cargar detalle.</p>';
|
||||
return;
|
||||
}
|
||||
var p = data;
|
||||
var html = '';
|
||||
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-1);">' + esc(p.supplier_name) + ' > ' + esc(p.category || '') + '</div>';
|
||||
html += '<div class="detail-oem">' + esc(p.sku) + '</div>';
|
||||
html += '<div class="detail-name">' + esc((p.name || '').replace(/\\n/g, ' ')) + '</div>';
|
||||
if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>';
|
||||
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Interchanges
|
||||
if (p.interchanges && p.interchanges.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Intercambios OEM</div>';
|
||||
var seen = {};
|
||||
p.interchanges.forEach(function(ix) {
|
||||
var key = (ix.brand || '') + '|' + (ix.interchange_number || '');
|
||||
if (seen[key]) return;
|
||||
seen[key] = true;
|
||||
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);">' +
|
||||
'<span style="font-weight:600;">' + esc(ix.brand || '') + '</span>' +
|
||||
'<span style="color:var(--color-text-muted);font-family:monospace;">' + esc(ix.interchange_number || '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Compatibilities — deduplicate by (make, model, year, engine)
|
||||
if (p.compatibilities && p.compatibilities.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Vehiculos compatibles</div>';
|
||||
var seenCompat = {};
|
||||
var uniqCompat = [];
|
||||
p.compatibilities.forEach(function(c) {
|
||||
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
||||
if (seenCompat[key]) return;
|
||||
seenCompat[key] = true;
|
||||
uniqCompat.push(c);
|
||||
});
|
||||
var currentMake = '';
|
||||
uniqCompat.forEach(function(c) {
|
||||
if (c.make !== currentMake) {
|
||||
currentMake = c.make;
|
||||
html += '<div style="font-weight:600;margin-top:8px;">' + esc(c.make) + '</div>';
|
||||
}
|
||||
html += '<div style="padding-left:12px;color:var(--color-text-muted);font-size:var(--text-body-sm);">' +
|
||||
esc(c.model) + ' ' + c.year + ' ' + esc(c.engine || '') + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
detailBody.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailPanel.classList.remove('is-open');
|
||||
detailOverlay.classList.remove('is-visible');
|
||||
@@ -1398,17 +1493,22 @@
|
||||
}
|
||||
searchDropdown.innerHTML = data.data.map(function (r) {
|
||||
var isLocal = r.source === 'local_inventory' || (typeof r.id_part === 'string' && r.id_part.indexOf('inv:') === 0);
|
||||
var isSupplier = r.source === 'supplier_catalog' || (typeof r.id_part === 'string' && r.id_part.indexOf('sc:') === 0);
|
||||
var stockLabel = r.local_stock > 0
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
|
||||
: '';
|
||||
var localBadge = isLocal
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
var sourceBadge = '';
|
||||
if (isLocal) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
var oemNum = isLocal ? (r.oem_part_number || r.part_number || '') : (r.oem_part_number || '');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(r.name) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '">' +
|
||||
var cleanName = (r.name || '').replace(/\\n/g, ' ');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(cleanName) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '" data-source="' + (r.source || '') + '">' +
|
||||
'<div style="flex:1;">' +
|
||||
'<div class="search-result__oem">' + localBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(r.name) + '</div>' +
|
||||
'<div class="search-result__oem">' + sourceBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(cleanName) + '</div>' +
|
||||
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
|
||||
'</div>' +
|
||||
stockLabel +
|
||||
@@ -1420,6 +1520,7 @@
|
||||
el.addEventListener('click', function () {
|
||||
searchDropdown.classList.remove('is-visible');
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
var info = '💠 Stock Local\n\n' +
|
||||
'Parte: ' + (this.dataset.pn || 'N/A') + '\n' +
|
||||
@@ -1429,6 +1530,10 @@
|
||||
alert(info);
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user