- 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%
300 lines
12 KiB
JavaScript
300 lines
12 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
const API = '/pos/api/supplier-catalog';
|
|
const VEHICLE_API = '/pos/api/inventory/vehicles';
|
|
const token = localStorage.getItem('pos_token') || '';
|
|
|
|
let state = {
|
|
q: '',
|
|
category: '',
|
|
make: '',
|
|
model: '',
|
|
year: '',
|
|
engine: '',
|
|
myeId: null,
|
|
page: 1,
|
|
perPage: 30,
|
|
totalPages: 1,
|
|
categories: [],
|
|
items: []
|
|
};
|
|
|
|
function headers() {
|
|
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
let scAbort = null;
|
|
let scSeq = 0;
|
|
|
|
async function apiFetch(url) {
|
|
if (scAbort) {
|
|
scAbort.abort();
|
|
scAbort = null;
|
|
}
|
|
const ctrl = new AbortController();
|
|
scAbort = ctrl;
|
|
try {
|
|
const resp = await fetch(url, { headers: headers(), signal: ctrl.signal });
|
|
if (resp.status === 401) { window.location.href = '/pos/login'; return null; }
|
|
if (!resp.ok) { console.error('API error', url, resp.status); return null; }
|
|
return resp.json();
|
|
} catch (e) {
|
|
if (e.name === 'AbortError') return null;
|
|
console.error('API error', url, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function apiFetchSeq(url) {
|
|
const mySeq = ++scSeq;
|
|
const data = await apiFetch(url);
|
|
if (!data || scSeq !== mySeq) return null;
|
|
return data;
|
|
}
|
|
|
|
// ─── Categories ─────────────────────────────────────────────
|
|
async function loadCategories() {
|
|
const data = await apiFetch(API + '/categories');
|
|
if (!data) return;
|
|
state.categories = data.categories || [];
|
|
renderCategories();
|
|
}
|
|
|
|
function renderCategories() {
|
|
const el = document.getElementById('categoriesGrid');
|
|
if (!el) return;
|
|
let html = '<div class="sc-cat-card' + (state.category === '' ? ' active' : '') + '" onclick="selectCategory(\'\')">' +
|
|
'<div>Todas</div><div class="count">' + state.categories.reduce((a,c)=>a+c.count,0) + ' items</div></div>';
|
|
state.categories.forEach(function(c) {
|
|
html += '<div class="sc-cat-card' + (state.category === c.name ? ' active' : '') + '" onclick="selectCategory(\'' + escapeHtml(c.name) + '\')">' +
|
|
'<div>' + escapeHtml(c.name) + '</div><div class="count">' + c.count + ' items</div></div>';
|
|
});
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
window.selectCategory = function(name) {
|
|
state.category = name;
|
|
state.page = 1;
|
|
renderCategories();
|
|
doSearch();
|
|
};
|
|
|
|
// ─── Vehicle filters ────────────────────────────────────────
|
|
async function loadMakes() {
|
|
const data = await apiFetch(VEHICLE_API + '/makes');
|
|
if (!data) return;
|
|
const sel = document.getElementById('filterMake');
|
|
sel.innerHTML = '<option value="">Marca vehiculo</option>';
|
|
(data.data || []).forEach(function(m) {
|
|
sel.innerHTML += '<option value="' + escapeHtml(m.name_brand) + '">' + escapeHtml(m.name_brand) + '</option>';
|
|
});
|
|
}
|
|
|
|
window.onMakeChange = async function() {
|
|
const sel = document.getElementById('filterMake');
|
|
state.make = sel.value;
|
|
state.model = ''; state.year = ''; state.engine = ''; state.myeId = null;
|
|
document.getElementById('filterModel').disabled = true;
|
|
document.getElementById('filterYear').disabled = true;
|
|
document.getElementById('filterEngine').disabled = true;
|
|
if (!state.make) { doSearch(); return; }
|
|
|
|
const makes = await apiFetchSeq(VEHICLE_API + '/makes');
|
|
if (!makes) return;
|
|
const brand = (makes.data || []).find(function(m) { return m.name_brand === state.make; });
|
|
if (!brand) { doSearch(); return; }
|
|
|
|
const models = await apiFetchSeq(VEHICLE_API + '/models?brand_id=' + brand.id_brand);
|
|
if (!models) return;
|
|
const msel = document.getElementById('filterModel');
|
|
msel.innerHTML = '<option value="">Modelo</option>';
|
|
(models.data || []).forEach(function(m) {
|
|
msel.innerHTML += '<option value="' + m.id_model + '">' + escapeHtml(m.name_model) + '</option>';
|
|
});
|
|
msel.disabled = false;
|
|
doSearch();
|
|
};
|
|
|
|
window.onModelChange = async function() {
|
|
const sel = document.getElementById('filterModel');
|
|
const modelId = sel.value;
|
|
state.model = modelId ? sel.options[sel.selectedIndex].text : '';
|
|
state.year = ''; state.engine = ''; state.myeId = null;
|
|
document.getElementById('filterYear').disabled = true;
|
|
document.getElementById('filterEngine').disabled = true;
|
|
if (!modelId) { doSearch(); return; }
|
|
|
|
const years = await apiFetchSeq(VEHICLE_API + '/years?model_id=' + modelId);
|
|
if (!years) return;
|
|
const ysel = document.getElementById('filterYear');
|
|
ysel.innerHTML = '<option value="">Año</option>';
|
|
(years.data || []).forEach(function(y) {
|
|
ysel.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
|
|
});
|
|
ysel.disabled = false;
|
|
doSearch();
|
|
};
|
|
|
|
window.onYearChange = async function() {
|
|
const sel = document.getElementById('filterYear');
|
|
const yearId = sel.value;
|
|
const modelId = document.getElementById('filterModel').value;
|
|
state.year = yearId ? sel.options[sel.selectedIndex].text : '';
|
|
state.engine = ''; state.myeId = null;
|
|
document.getElementById('filterEngine').disabled = true;
|
|
if (!yearId || !modelId) { doSearch(); return; }
|
|
|
|
const engines = await apiFetchSeq(VEHICLE_API + '/engines?model_id=' + modelId + '&year_id=' + yearId);
|
|
if (!engines) return;
|
|
const esel = document.getElementById('filterEngine');
|
|
esel.innerHTML = '<option value="">Motorizacion</option>';
|
|
(engines.data || []).forEach(function(e) {
|
|
const label = escapeHtml(e.name_engine) + (e.trim_level ? ' (' + escapeHtml(e.trim_level) + ')' : '');
|
|
esel.innerHTML += '<option value="' + e.id_mye + '">' + label + '</option>';
|
|
});
|
|
esel.disabled = false;
|
|
doSearch();
|
|
};
|
|
|
|
// ─── Search ─────────────────────────────────────────────────
|
|
window.doSearch = async function() {
|
|
state.q = document.getElementById('searchInput').value.trim();
|
|
const engineSel = document.getElementById('filterEngine');
|
|
state.myeId = engineSel.value || null;
|
|
|
|
let url = API + '/search?page=' + state.page + '&per_page=' + state.perPage;
|
|
if (state.q) url += '&q=' + encodeURIComponent(state.q);
|
|
if (state.category) url += '&category=' + encodeURIComponent(state.category);
|
|
if (state.myeId) {
|
|
url += '&mye_id=' + state.myeId;
|
|
} else {
|
|
if (state.make) url += '&make=' + encodeURIComponent(state.make);
|
|
if (state.model) url += '&model=' + encodeURIComponent(state.model);
|
|
if (state.year) url += '&year=' + encodeURIComponent(state.year);
|
|
}
|
|
|
|
const data = await apiFetch(url);
|
|
if (!data) return;
|
|
state.items = data.data || [];
|
|
state.totalPages = (data.pagination || {}).total_pages || 1;
|
|
renderItems();
|
|
renderPagination();
|
|
};
|
|
|
|
window.clearFilters = function() {
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('filterMake').value = '';
|
|
document.getElementById('filterModel').innerHTML = '<option value="">Modelo</option>'; document.getElementById('filterModel').disabled = true;
|
|
document.getElementById('filterYear').innerHTML = '<option value="">Año</option>'; document.getElementById('filterYear').disabled = true;
|
|
document.getElementById('filterEngine').innerHTML = '<option value="">Motorizacion</option>'; document.getElementById('filterEngine').disabled = true;
|
|
state.q = ''; state.category = ''; state.make = ''; state.model = ''; state.year = ''; state.engine = ''; state.myeId = null; state.page = 1;
|
|
renderCategories();
|
|
doSearch();
|
|
};
|
|
|
|
// ─── Render results ─────────────────────────────────────────
|
|
function renderItems() {
|
|
const el = document.getElementById('partsGrid');
|
|
if (!el) return;
|
|
if (!state.items.length) {
|
|
el.innerHTML = '<div class="sc-empty" style="grid-column:1/-1;"><div style="font-size:48px;margin-bottom:var(--space-4);">🔍</div><h3>Sin resultados</h3><p>Intenta con otros filtros o terminos de busqueda.</p></div>';
|
|
return;
|
|
}
|
|
el.innerHTML = state.items.map(function(it) {
|
|
return '<div class="sc-card" onclick="openDetail(' + it.id + ')">' +
|
|
'<div class="sc-card__sku">' + escapeHtml(it.sku) + '</div>' +
|
|
'<div class="sc-card__name">' + escapeHtml(it.name) + '</div>' +
|
|
'<div class="sc-card__meta">' +
|
|
'<span class="sc-card__badge">' + escapeHtml(it.category || 'SIN CATEGORIA') + '</span>' +
|
|
' <span>' + escapeHtml(it.supplier_name) + '</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function renderPagination() {
|
|
const el = document.getElementById('pagination');
|
|
if (!el) return;
|
|
if (state.totalPages <= 1) { el.innerHTML = ''; return; }
|
|
let html = '<button ' + (state.page <= 1 ? 'disabled' : '') + ' onclick="goPage(' + (state.page - 1) + ')">Anterior</button>';
|
|
html += '<span>Pagina ' + state.page + ' de ' + state.totalPages + '</span>';
|
|
html += '<button ' + (state.page >= state.totalPages ? 'disabled' : '') + ' onclick="goPage(' + (state.page + 1) + ')">Siguiente</button>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
window.goPage = function(p) {
|
|
state.page = p;
|
|
doSearch();
|
|
};
|
|
|
|
// ─── Detail modal ───────────────────────────────────────────
|
|
window.openDetail = async function(id) {
|
|
const data = await apiFetch(API + '/items/' + id);
|
|
if (!data) return;
|
|
document.getElementById('modalTitle').textContent = escapeHtml(data.sku);
|
|
let html = '';
|
|
html += '<div><strong style="font-size:var(--text-h6);">' + escapeHtml(data.name) + '</strong></div>';
|
|
html += '<div class="sc-modal__section"><h4>Informacion</h4>' +
|
|
'<p>Proveedor: ' + escapeHtml(data.supplier_name) + '<br>Categoria: ' + escapeHtml(data.category || 'N/A') + '</p></div>';
|
|
|
|
if (data.interchanges && data.interchanges.length) {
|
|
html += '<div class="sc-modal__section"><h4>Intercambios</h4><div class="sc-interchange-list">' +
|
|
data.interchanges.map(function(ix) {
|
|
return '<span class="sc-interchange-chip">' + escapeHtml(ix.brand) + ' — ' + escapeHtml(ix.part_number) + '</span>';
|
|
}).join('') + '</div></div>';
|
|
}
|
|
|
|
if (data.compatibilities && data.compatibilities.length) {
|
|
var seenCompat = {};
|
|
var uniqCompat = data.compatibilities.filter(function(c) {
|
|
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
|
if (seenCompat[key]) return false;
|
|
seenCompat[key] = true;
|
|
return true;
|
|
});
|
|
html += '<div class="sc-modal__section"><h4>Vehiculos compatibles (' + uniqCompat.length + ')</h4>' +
|
|
'<div class="sc-compat-grid">' +
|
|
uniqCompat.slice(0, 50).map(function(c) {
|
|
return '<div class="sc-compat-item">' +
|
|
'<strong>' + escapeHtml(c.make || '') + ' ' + escapeHtml(c.model || '') + '</strong><br>' +
|
|
(c.year || '') + ' ' + escapeHtml(c.engine || '') +
|
|
'</div>';
|
|
}).join('') +
|
|
(uniqCompat.length > 50 ? '<div style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">... y ' + (uniqCompat.length - 50) + ' mas</div>' : '') +
|
|
'</div></div>';
|
|
}
|
|
|
|
document.getElementById('modalBody').innerHTML = html;
|
|
document.getElementById('detailModal').classList.add('open');
|
|
};
|
|
|
|
window.closeModal = function() {
|
|
document.getElementById('detailModal').classList.remove('open');
|
|
};
|
|
|
|
// ─── Utils ──────────────────────────────────────────────────
|
|
function escapeHtml(s) {
|
|
if (s == null) return '';
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ─── Init ───────────────────────────────────────────────────
|
|
function init() {
|
|
if (!token) { window.location.href = '/pos/login'; return; }
|
|
loadCategories();
|
|
loadMakes();
|
|
doSearch().then(function() {
|
|
var params = new URLSearchParams(window.location.search);
|
|
var id = params.get('id');
|
|
if (id) { openDetail(parseInt(id)); }
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|