/* ========================================================================= Nexus Autoparts — Public Catalog (catalog-public.js) Vehicle hierarchy navigation: Brand > Model > Year > Engine > Category > Group > Parts No auth, no cart, no prices — public browsing only. ========================================================================= */ (function () { 'use strict'; // ── State ── var state = { level: 'brands', // brands | models | years | engines | categories | groups | part_types | parts | search brand: null, // {id, name} model: null, // {id, name} year: null, // {id, value} engine: null, // {id_mye, name, trim} // OEM mode (TecDoc) state — integer IDs category: null, // {id, name} group: null, // {id, name} partType: null, // {slug, name} ← 3rd subcategory level // Local mode (Nexpart) state — string slugs. Parallel to the OEM state // so toggle switching mid-nav doesn't trash either branch. nxGroup: null, // {slug, name} ← top-level Nexpart group nxSubgroup: null, // {slug, name} ← Nexpart subgroup nxPartType: null, // {slug, name} ← Nexpart part type region: 'north-america', mode: (localStorage.getItem('catalog_mode') === 'local' ? 'local' : 'oem'), page: 1, totalPages: 1, }; // ── Catalog mode toggle (OEM / Local) ── function updateModeToggleUI() { document.querySelectorAll('#modeToggle button').forEach(function (b) { b.classList.toggle('is-active', b.getAttribute('data-mode') === state.mode); }); } window.setCatalogMode = function (mode) { if (mode !== 'oem' && mode !== 'local') return; if (mode === state.mode) return; state.mode = mode; localStorage.setItem('catalog_mode', mode); updateModeToggleUI(); // Smart reset: if vehicle already picked, stay at categories in the new mode. var hasVehicle = !!(state.engine && state.engine.id_mye); // Clear category-and-below state from BOTH branches state.category = state.group = state.partType = null; state.nxGroup = state.nxSubgroup = state.nxPartType = null; state.page = 1; if (hasVehicle) { state.level = 'categories'; loadCategoriesForMode(); return; } // No vehicle — full reset back to brand selection state.brand = state.model = state.year = state.engine = null; state.level = 'brands'; loadBrands(); }; // ── Region selector (global) ── window.setRegion = function (region) { state.region = region; document.querySelectorAll('.region-btn').forEach(function (b) { b.classList.toggle('is-active', b.dataset.region === region); }); // Reload brands with new region state.brand = state.model = state.year = state.engine = null; state.category = state.group = state.partType = null; state.nxGroup = state.nxSubgroup = state.nxPartType = null; loadBrands(); }; var API = '/api/catalog'; var content = document.getElementById('content'); var breadcrumbEl = document.getElementById('breadcrumb'); var searchInput = document.getElementById('searchInput'); // Check URL for brand param var urlParams = new URLSearchParams(window.location.search); var initBrandId = urlParams.get('brand'); // ── Init ── updateModeToggleUI(); if (initBrandId) { // Load brands, find the one matching, then navigate fetch(API + '/brands?mode=' + state.mode) .then(function (r) { return r.json(); }) .then(function (brands) { var found = brands.find(function (b) { return b.id_brand == initBrandId; }); if (found) { state.brand = { id: found.id_brand, name: found.name_brand }; state.level = 'models'; loadModels(); } else { loadBrands(); } }) .catch(function () { loadBrands(); }); } else { loadBrands(); } // Enter on search // ── Smart search detector ── function detectQueryType(raw) { if (!raw) return 'keyword'; var q = raw.trim(); var compact = q.replace(/[\s\-]/g, '').toUpperCase(); if (/^[A-HJ-NPR-Z0-9]{17}$/.test(compact)) return 'vin'; if (/^[A-Z]{3}[-\s]?\d{3,4}$/.test(q.toUpperCase())) return 'plate'; var hasLowercase = /[a-z]/.test(q); if (hasLowercase) return 'keyword'; var tokens = q.split(/\s+/); var hasYear = tokens.some(function (t) { return /^(19|20)\d{2}$/.test(t); }); if (hasYear && tokens.length > 1) return 'keyword'; var qUpper = q.toUpperCase(); if (/^[A-Z0-9]{2,}[\-\/][A-Z0-9]{2,}([\-\/][A-Z0-9]+)*$/.test(qUpper) && compact.length >= 6) return 'part_number'; if (tokens.length >= 2 && tokens.every(function (t) { return /^[A-Z0-9]{1,}$/.test(t); }) && compact.length >= 6) return 'part_number'; if (/^[A-Z0-9]{8,}$/.test(compact) && /[A-Z]/.test(compact) && /\d/.test(compact)) return 'part_number'; return 'keyword'; } // Smart search hint var searchHint = document.createElement('div'); searchHint.style.cssText = 'display:none;padding:3px 10px;font-size:12px;color:var(--color-text-accent);background:var(--color-primary-muted);border:1px dashed var(--color-border-accent);border-radius:4px;margin-top:4px;'; searchInput.parentElement.after(searchHint); searchInput.addEventListener('input', function () { var q = this.value.trim(); if (q.length >= 3) { var type = detectQueryType(q); var hints = { vin: '🚗 VIN detectado', plate: '🔖 Placa detectada', part_number: '🔩 Numero de parte', keyword: null }; if (hints[type]) { searchHint.textContent = hints[type]; searchHint.style.display = ''; } else { searchHint.style.display = 'none'; } } else { searchHint.style.display = 'none'; } }); searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') doSearch(); }); // ── Theme toggle (global) ── window.toggleTheme = function () { var html = document.documentElement; var cur = html.getAttribute('data-theme'); var next = cur === 'industrial' ? 'modern' : 'industrial'; html.setAttribute('data-theme', next); localStorage.setItem('nexus-theme', next); }; // ── Search (global) ── window.doSearch = function () { var q = searchInput.value.trim(); if (!q || q.length < 2) return; state.level = 'search'; renderBreadcrumb(); content.innerHTML = '
Buscando...
'; fetch(API + '/search?q=' + encodeURIComponent(q)) .then(function (r) { return r.json(); }) .then(function (data) { renderSearchResults(data); }) .catch(function () { content.innerHTML = '
Error en la busqueda.
'; }); }; // ── Detail modal (global) ── window.openDetail = function (partId) { var modal = document.getElementById('detailModal'); var body = document.getElementById('detailBody'); body.innerHTML = '
Cargando detalle...
'; modal.classList.add('open'); fetch(API + '/part/' + partId) .then(function (r) { return r.json(); }) .then(function (d) { renderDetail(d, body); }) .catch(function () { body.innerHTML = '
Error cargando detalle.
'; }); }; window.closeDetail = function () { document.getElementById('detailModal').classList.remove('open'); }; // Close modal on backdrop click document.getElementById('detailModal').addEventListener('click', function (e) { if (e.target === this) closeDetail(); }); // ── Breadcrumb ── function renderBreadcrumb() { var parts = []; parts.push('Catalogo'); if (state.brand) { parts.push('/'); parts.push('' + esc(state.brand.name) + ''); } if (state.model) { parts.push('/'); parts.push('' + esc(state.model.name) + ''); } if (state.year) { parts.push('/'); parts.push('' + esc(String(state.year.value)) + ''); } if (state.engine) { parts.push('/'); var engineLabel = state.engine.name + (state.engine.trim ? ' (' + state.engine.trim + ')' : ''); parts.push('' + esc(engineLabel) + ''); } // Category / subgroup / part type — rendered from EITHER the Nexpart // branch (nxGroup/nxSubgroup/nxPartType) or the OEM branch. Only one // should be populated at any time after a navigation reset. if (state.nxGroup) { parts.push('/'); parts.push('' + esc(state.nxGroup.name) + ''); } else if (state.category) { parts.push('/'); parts.push('' + esc(state.category.name) + ''); } if (state.nxSubgroup) { parts.push('/'); if (state.nxPartType) { parts.push('' + esc(state.nxSubgroup.name) + ''); } else { parts.push('' + esc(state.nxSubgroup.name) + ''); } } else if (state.group) { parts.push('/'); if (state.partType) { parts.push('' + esc(state.group.name) + ''); } else { parts.push('' + esc(state.group.name) + ''); } } if (state.nxPartType) { parts.push('/'); parts.push('' + esc(state.nxPartType.name) + ''); } else if (state.partType) { parts.push('/'); parts.push('' + esc(state.partType.name) + ''); } if (state.level === 'search') { parts.push('/'); parts.push('Busqueda'); } breadcrumbEl.innerHTML = parts.join(''); } // Helper: clears every state key at-or-below the category level, for // BOTH the OEM branch and the Nexpart branch. Used whenever we navigate // backward to an ancestor and need a clean slate below. function clearCatSubtree() { state.category = state.group = state.partType = null; state.nxGroup = state.nxSubgroup = state.nxPartType = null; } // Global nav — jump to any ancestor in the breadcrumb window.catalogNav = function (level) { if (level === 'brands') { state.brand = state.model = state.year = state.engine = null; clearCatSubtree(); state.level = 'brands'; loadBrands(); } else if (level === 'models') { state.model = state.year = state.engine = null; clearCatSubtree(); state.level = 'models'; loadModels(); } else if (level === 'years') { state.year = state.engine = null; clearCatSubtree(); state.level = 'years'; loadYears(); } else if (level === 'engines') { state.engine = null; clearCatSubtree(); state.level = 'engines'; loadEngines(); } else if (level === 'categories') { clearCatSubtree(); state.level = 'categories'; loadCategoriesForMode(); // OEM branch back-nav } else if (level === 'groups') { state.group = state.partType = null; state.level = 'groups'; loadGroups(); } else if (level === 'part_types') { state.partType = null; state.level = 'part_types'; loadPartTypes(); // Nexpart branch back-nav } else if (level === 'nx_subgroups') { state.nxSubgroup = state.nxPartType = null; state.level = 'groups'; loadNexpartSubgroups(); } else if (level === 'nx_part_types') { state.nxPartType = null; state.level = 'part_types'; loadNexpartPartTypes(); } }; // ── Data loaders ── function loadBrands() { state.level = 'brands'; renderBreadcrumb(); content.innerHTML = '
Cargando marcas...
'; fetch(API + '/brands?region=' + (state.region || 'north-america') + '&mode=' + state.mode) .then(function (r) { return r.json(); }) .then(function (brands) { var html = '

Selecciona una Marca

'; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando marcas.
'; }); } window.selectBrand = function (id, name) { state.brand = { id: id, name: name }; state.level = 'models'; loadModels(); }; function loadModels() { renderBreadcrumb(); content.innerHTML = '
Cargando modelos...
'; fetch(API + '/models?brand_id=' + state.brand.id) .then(function (r) { return r.json(); }) .then(function (models) { var html = '

' + esc(state.brand.name) + ' — Modelos

'; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando modelos.
'; }); } window.selectModel = function (id, name) { state.model = { id: id, name: name }; state.level = 'years'; loadYears(); }; function loadYears() { renderBreadcrumb(); content.innerHTML = '
Cargando anos...
'; fetch(API + '/years?model_id=' + state.model.id) .then(function (r) { return r.json(); }) .then(function (years) { var html = '

' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' — Anos

'; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando anos.
'; }); } window.selectYear = function (id, value) { state.year = { id: id, value: value }; state.level = 'engines'; loadEngines(); }; function loadEngines() { renderBreadcrumb(); content.innerHTML = '
Cargando motores...
'; fetch(API + '/engines?model_id=' + state.model.id + '&year_id=' + state.year.id) .then(function (r) { return r.json(); }) .then(function (engines) { var html = '

' + esc(state.brand.name) + ' ' + esc(state.model.name) + ' ' + state.year.value + ' — Motor

'; html += ''; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando motores.
'; }); } window.selectEngine = function (id_mye, name, trim) { state.engine = { id_mye: id_mye, name: name, trim: trim }; state.level = 'categories'; loadCategoriesForMode(); }; // ── Mode dispatcher (OEM vs Nexpart Local) ── function loadCategoriesForMode() { if (state.mode === 'local') { loadNexpartCategories(); } else { loadCategories(); } } // ══════════════════════════════════════════════════════════════ // NEXPART (Local mode) parallel navigation // ══════════════════════════════════════════════════════════════ function loadNexpartCategories() { state.level = 'categories'; renderBreadcrumb(); content.innerHTML = '
Cargando categorias Local...
'; fetch(API + '/categories?mode=local&mye_id=' + state.engine.id_mye) .then(function (r) { return r.json(); }) .then(function (resp) { var cats = (resp && resp.data) || []; if (!cats.length) { content.innerHTML = '

Categorias (Local)

Ninguna parte de este vehiculo mapea al catalogo Local.
'; return; } var html = '

Categorias (Local · ' + cats.length + ')

'; html += ''; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando categorias Local.
'; }); } window.selectNxGroup = function (slug, name) { state.nxGroup = { slug: slug, name: name }; state.nxSubgroup = null; state.nxPartType = null; state.level = 'groups'; loadNexpartSubgroups(); }; function loadNexpartSubgroups() { state.level = 'groups'; renderBreadcrumb(); content.innerHTML = '
Cargando subcategorias...
'; var url = API + '/groups?mode=local&mye_id=' + state.engine.id_mye + '&category_slug=' + encodeURIComponent(state.nxGroup.slug); fetch(url) .then(function (r) { return r.json(); }) .then(function (resp) { var subs = (resp && resp.data) || []; if (!subs.length) { content.innerHTML = '

' + esc(state.nxGroup.name) + '

Sin subcategorias.
'; return; } var html = '

' + esc(state.nxGroup.name) + ' (' + subs.length + ' subcategorias)

'; html += ''; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando subcategorias.
'; }); } window.selectNxSubgroup = function (slug, name) { state.nxSubgroup = { slug: slug, name: name }; state.nxPartType = null; state.level = 'part_types'; loadNexpartPartTypes(); }; function loadNexpartPartTypes() { state.level = 'part_types'; renderBreadcrumb(); content.innerHTML = '
Cargando tipos de parte...
'; var url = API + '/part-types?mode=local&mye_id=' + state.engine.id_mye + '&group_slug=' + encodeURIComponent(state.nxGroup.slug) + '&subgroup_slug=' + encodeURIComponent(state.nxSubgroup.slug); fetch(url) .then(function (r) { return r.json(); }) .then(function (resp) { var pts = (resp && resp.data) || []; if (!pts.length) { content.innerHTML = '

' + esc(state.nxSubgroup.name) + '

Sin tipos de parte.
'; return; } // Single part type → auto-drill-down if (pts.length === 1) { state.nxPartType = { slug: pts[0].slug, name: pts[0].name }; state.level = 'parts'; state.page = 1; loadParts(); return; } var html = '

' + esc(state.nxSubgroup.name) + ' (' + pts.length + ' tipos)

'; html += ''; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando tipos de parte.
'; }); } window.selectNxPartType = function (slug, name) { state.nxPartType = { slug: slug, name: name }; state.level = 'parts'; state.page = 1; loadParts(); }; function loadCategories() { renderBreadcrumb(); content.innerHTML = '
Cargando categorias...
'; fetch(API + '/categories?mye_id=' + state.engine.id_mye) .then(function (r) { return r.json(); }) .then(function (cats) { if (!cats.length) { content.innerHTML = '

Categorias

No se encontraron categorias con partes para este vehiculo.
'; return; } var html = '

Categorias

'; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando categorias.
'; }); } window.selectCategory = function (id, name) { state.category = { id: id, name: name }; state.level = 'groups'; loadGroups(); }; function loadGroups() { renderBreadcrumb(); content.innerHTML = '
Cargando grupos...
'; fetch(API + '/groups?mye_id=' + state.engine.id_mye + '&category_id=' + state.category.id) .then(function (r) { return r.json(); }) .then(function (groups) { if (!groups.length) { content.innerHTML = '

' + esc(state.category.name) + '

No se encontraron sub-grupos.
'; return; } var html = '

' + esc(state.category.name) + '

'; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando grupos.
'; }); } window.selectGroup = function (id, name) { state.group = { id: id, name: name }; state.partType = null; state.level = 'part_types'; loadPartTypes(); }; // ── Part Types (3rd subcategory level — Nexpart-style) ── function loadPartTypes() { state.level = 'part_types'; renderBreadcrumb(); content.innerHTML = '
Cargando tipos de parte...
'; fetch(API + '/part-types?mye_id=' + state.engine.id_mye + '&group_id=' + state.group.id) .then(function (r) { return r.json(); }) .then(function (resp) { var types = resp.data || []; if (!types.length) { // No types available — fall through to all parts in the group. state.level = 'parts'; state.page = 1; loadParts(); return; } if (types.length === 1) { // Single type — auto-select and show parts directly. state.partType = { slug: types[0].slug, name: types[0].name }; state.level = 'parts'; state.page = 1; loadParts(); return; } var html = '

' + esc(state.group.name) + ' (' + types.length + ' tipos)

'; html += ''; content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando tipos de parte.
'; }); } window.selectPartType = function (slug, name) { state.partType = { slug: slug, name: name }; state.level = 'parts'; state.page = 1; loadParts(); }; function loadParts() { renderBreadcrumb(); content.innerHTML = '
Cargando partes...
'; // Build URL based on which navigation branch the user took. // Nexpart branch uses slug-based params; OEM branch uses integer ids. var url; if (state.nxGroup && state.nxSubgroup && state.nxPartType) { url = API + '/parts?mode=local' + '&mye_id=' + state.engine.id_mye + '&page=' + state.page + '&nexpart_group=' + encodeURIComponent(state.nxGroup.slug) + '&nexpart_subgroup=' + encodeURIComponent(state.nxSubgroup.slug) + '&nexpart_part_type=' + encodeURIComponent(state.nxPartType.slug); } else { var ptParam = state.partType ? '&part_type=' + encodeURIComponent(state.partType.slug) : ''; url = API + '/parts?mye_id=' + state.engine.id_mye + '&group_id=' + state.group.id + '&page=' + state.page + '&mode=' + state.mode + ptParam; } // The header title shows the deepest selected node, regardless of branch. var headerTitle = state.nxPartType ? state.nxPartType.name : state.nxSubgroup ? state.nxSubgroup.name : state.partType ? state.partType.name : state.group ? state.group.name : 'Partes'; fetch(url) .then(function (r) { return r.json(); }) .then(function (resp) { var parts = resp.data; var pag = resp.pagination; state.totalPages = pag.total_pages; var isLocal = (state.mode === 'local'); if (!parts.length) { content.innerHTML = '

' + esc(headerTitle) + '

No se encontraron partes.
'; return; } var html = '

' + esc(headerTitle) + ' (' + pag.total + ' partes)

'; html += '
'; parts.forEach(function (p) { var tierClass = ''; if (isLocal) { if (p.priority_tier === 1) tierClass = ' part-row--tier1'; else if (p.priority_tier === 2) tierClass = ' part-row--tier2'; } html += '
'; html += '
'; // Manufacturer badge (local mode only) if (isLocal && p.manufacturer) { var tierStar = p.priority_tier === 1 ? '' : ''; html += '
' + esc(p.manufacturer) + '' + tierStar + '
'; } // SKU line if (isLocal && p.part_number) { html += '
' + esc(p.part_number) + ' · OEM: ' + esc(p.oem_part_number) + '
'; } else { html += '
' + esc(p.oem_part_number) + '
'; } html += '
' + esc(p.name || '') + '
'; if (p.description) html += '
' + esc(p.description) + '
'; // Stock badge (local mode) if (isLocal) { if (p.in_stock_network) { html += '
En stock en ' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '
'; } else { html += '
Consultar disponibilidad
'; } } html += ''; html += '
'; if (p.image_url) { html += ''; } html += '
'; }); html += '
'; // Pagination if (pag.total_pages > 1) { html += ''; } content.innerHTML = html; }) .catch(function () { content.innerHTML = '
Error cargando partes.
'; }); } window.partsPage = function (p) { state.page = p; loadParts(); window.scrollTo({ top: 0, behavior: 'smooth' }); }; // ── Search results ── function renderSearchResults(results) { renderBreadcrumb(); if (!results.length) { content.innerHTML = '

Busqueda

No se encontraron resultados.
'; return; } var html = '

Resultados (' + results.length + ')

'; results.forEach(function (p) { html += '
'; html += '
'; html += '
' + esc(p.oem_part_number) + '
'; html += '
' + esc(p.name || '') + '
'; if (p.vehicle_info) html += '
' + esc(p.vehicle_info) + '
'; html += ''; html += '
'; if (p.image_url) { html += ''; } html += '
'; }); html += '
'; content.innerHTML = html; } // ── Part detail ── function renderDetail(d, body) { if (!d || !d.part) { body.innerHTML = '
Parte no encontrada.
'; return; } var p = d.part; var html = ''; html += '
' + esc(p.oem_part_number) + '
'; html += '
' + esc(p.name || '') + '
'; if (p.category_name) html += '
' + esc(p.category_name) + (p.group_name ? ' / ' + esc(p.group_name) : '') + '
'; if (p.description) html += '
' + esc(p.description) + '
'; if (p.image_url) { html += '
'; html += ''; html += '
'; } // Alternatives if (d.alternatives && d.alternatives.length) { html += '
'; html += '

Alternativas y Cross-References (' + d.alternatives.length + ')

'; html += ''; d.alternatives.forEach(function (a) { html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
NumeroFabricanteNombreTipo
' + esc(a.part_number || '') + '' + esc(a.manufacturer || '') + '' + esc(a.name || '-') + '' + esc(a.type === 'aftermarket' ? 'Aftermarket' : 'Cross-Ref') + '
'; } // Bodegas if (d.bodegas && d.bodegas.length) { html += '
'; html += '

Disponibilidad en Bodegas (' + d.bodegas.length + ')

'; html += ''; d.bodegas.forEach(function (b) { html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
BodegaStockUbicacion
' + esc(b.business_name || '') + '' + b.stock + '' + esc(b.location || '-') + '
'; } body.innerHTML = html; } // ── Helpers ── function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function escAttr(s) { return esc(s).replace(/'/g, "\\'").replace(/"/g, '"'); } })();