feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts
Major features: - Pixel-Perfect glassmorphism design (landing + POS + public catalog) - OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types) - Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications) - Peer-to-peer inventory (multi-instance, LAN discovery) - WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations - Smart unified search (VIN/plate/part_number/keyword auto-detect) - Shop Supplies tab (vehicle-independent parts) - Chatbot AI fallback chain (5 models) + response cache - CSV inventory import tool + setup_instance.sh installer - Tablet-responsive CSS + sidebar toggle - Filters, export CSV, employee edit, business data save - Quotation system (WA→POS) with auto-print on confirmation - Live stats on landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,18 +9,63 @@
|
||||
|
||||
// ── State ──
|
||||
var state = {
|
||||
level: 'brands', // brands | models | years | engines | categories | groups | parts | search
|
||||
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;
|
||||
@@ -28,7 +73,9 @@
|
||||
b.classList.toggle('is-active', b.dataset.region === region);
|
||||
});
|
||||
// Reload brands with new region
|
||||
state.brand = state.model = state.year = state.engine = state.category = state.group = null;
|
||||
state.brand = state.model = state.year = state.engine = null;
|
||||
state.category = state.group = state.partType = null;
|
||||
state.nxGroup = state.nxSubgroup = state.nxPartType = null;
|
||||
loadBrands();
|
||||
};
|
||||
|
||||
@@ -42,9 +89,10 @@
|
||||
var initBrandId = urlParams.get('brand');
|
||||
|
||||
// ── Init ──
|
||||
updateModeToggleUI();
|
||||
if (initBrandId) {
|
||||
// Load brands, find the one matching, then navigate
|
||||
fetch(API + '/brands')
|
||||
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; });
|
||||
@@ -62,6 +110,42 @@
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
@@ -131,14 +215,41 @@
|
||||
var engineLabel = state.engine.name + (state.engine.trim ? ' (' + state.engine.trim + ')' : '');
|
||||
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'categories\')">' + esc(engineLabel) + '</a>');
|
||||
}
|
||||
if (state.category) {
|
||||
// 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('<span class="sep">/</span>');
|
||||
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'nx_subgroups\')">' + esc(state.nxGroup.name) + '</a>');
|
||||
} else if (state.category) {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'groups\')">' + esc(state.category.name) + '</a>');
|
||||
}
|
||||
if (state.group) {
|
||||
|
||||
if (state.nxSubgroup) {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
parts.push('<span>' + esc(state.group.name) + '</span>');
|
||||
if (state.nxPartType) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'nx_part_types\')">' + esc(state.nxSubgroup.name) + '</a>');
|
||||
} else {
|
||||
parts.push('<span>' + esc(state.nxSubgroup.name) + '</span>');
|
||||
}
|
||||
} else if (state.group) {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
if (state.partType) {
|
||||
parts.push('<a href="javascript:void(0)" onclick="catalogNav(\'part_types\')">' + esc(state.group.name) + '</a>');
|
||||
} else {
|
||||
parts.push('<span>' + esc(state.group.name) + '</span>');
|
||||
}
|
||||
}
|
||||
|
||||
if (state.nxPartType) {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
parts.push('<span>' + esc(state.nxPartType.name) + '</span>');
|
||||
} else if (state.partType) {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
parts.push('<span>' + esc(state.partType.name) + '</span>');
|
||||
}
|
||||
|
||||
if (state.level === 'search') {
|
||||
parts.push('<span class="sep">/</span>');
|
||||
parts.push('<span>Busqueda</span>');
|
||||
@@ -147,32 +258,58 @@
|
||||
breadcrumbEl.innerHTML = parts.join('');
|
||||
}
|
||||
|
||||
// Global nav
|
||||
// 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 = state.category = state.group = null;
|
||||
state.brand = state.model = state.year = state.engine = null;
|
||||
clearCatSubtree();
|
||||
state.level = 'brands';
|
||||
loadBrands();
|
||||
} else if (level === 'models') {
|
||||
state.model = state.year = state.engine = state.category = state.group = null;
|
||||
state.model = state.year = state.engine = null;
|
||||
clearCatSubtree();
|
||||
state.level = 'models';
|
||||
loadModels();
|
||||
} else if (level === 'years') {
|
||||
state.year = state.engine = state.category = state.group = null;
|
||||
state.year = state.engine = null;
|
||||
clearCatSubtree();
|
||||
state.level = 'years';
|
||||
loadYears();
|
||||
} else if (level === 'engines') {
|
||||
state.engine = state.category = state.group = null;
|
||||
state.engine = null;
|
||||
clearCatSubtree();
|
||||
state.level = 'engines';
|
||||
loadEngines();
|
||||
} else if (level === 'categories') {
|
||||
state.category = state.group = null;
|
||||
clearCatSubtree();
|
||||
state.level = 'categories';
|
||||
loadCategories();
|
||||
loadCategoriesForMode();
|
||||
// OEM branch back-nav
|
||||
} else if (level === 'groups') {
|
||||
state.group = null;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,7 +319,7 @@
|
||||
state.level = 'brands';
|
||||
renderBreadcrumb();
|
||||
content.innerHTML = '<div class="loading">Cargando marcas...</div>';
|
||||
fetch(API + '/brands?region=' + (state.region || 'north-america'))
|
||||
fetch(API + '/brands?region=' + (state.region || 'north-america') + '&mode=' + state.mode)
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (brands) {
|
||||
var html = '<h2>Selecciona una Marca</h2><div class="nav-grid">';
|
||||
@@ -274,7 +411,136 @@
|
||||
window.selectEngine = function (id_mye, name, trim) {
|
||||
state.engine = { id_mye: id_mye, name: name, trim: trim };
|
||||
state.level = 'categories';
|
||||
loadCategories();
|
||||
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 = '<div class="loading">Cargando categorias Local...</div>';
|
||||
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 = '<h2>Categorias (Local)</h2><div class="empty">Ninguna parte de este vehiculo mapea al catalogo Local.</div>';
|
||||
return;
|
||||
}
|
||||
var html = '<h2>Categorias <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(Local · ' + cats.length + ')</span></h2>';
|
||||
html += '<div class="nav-grid">';
|
||||
cats.forEach(function (c) {
|
||||
html += '<div class="nav-card" onclick="selectNxGroup(\'' + escAttr(c.slug) + '\',\'' + escAttr(c.name) + '\')">';
|
||||
html += '<span class="name">' + esc(c.name) + '</span>';
|
||||
html += '<span class="count">' + c.part_count + '</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
})
|
||||
.catch(function () { content.innerHTML = '<div class="empty">Error cargando categorias Local.</div>'; });
|
||||
}
|
||||
|
||||
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 = '<div class="loading">Cargando subcategorias...</div>';
|
||||
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 = '<h2>' + esc(state.nxGroup.name) + '</h2><div class="empty">Sin subcategorias.</div>';
|
||||
return;
|
||||
}
|
||||
var html = '<h2>' + esc(state.nxGroup.name) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + subs.length + ' subcategorias)</span></h2>';
|
||||
html += '<div class="nav-grid">';
|
||||
subs.forEach(function (s) {
|
||||
html += '<div class="nav-card" onclick="selectNxSubgroup(\'' + escAttr(s.slug) + '\',\'' + escAttr(s.name) + '\')">';
|
||||
html += '<span class="name">' + esc(s.name) + '</span>';
|
||||
html += '<span class="count">' + s.part_count + '</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
})
|
||||
.catch(function () { content.innerHTML = '<div class="empty">Error cargando subcategorias.</div>'; });
|
||||
}
|
||||
|
||||
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 = '<div class="loading">Cargando tipos de parte...</div>';
|
||||
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 = '<h2>' + esc(state.nxSubgroup.name) + '</h2><div class="empty">Sin tipos de parte.</div>';
|
||||
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 = '<h2>' + esc(state.nxSubgroup.name) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + pts.length + ' tipos)</span></h2>';
|
||||
html += '<div class="nav-grid">';
|
||||
pts.forEach(function (t) {
|
||||
var img = t.sample_image
|
||||
? '<img src="' + esc(t.sample_image) + '" alt="" style="width:24px;height:24px;object-fit:contain;margin-right:6px;vertical-align:middle;" onerror="this.style.display=\'none\'">'
|
||||
: '';
|
||||
html += '<div class="nav-card" onclick="selectNxPartType(\'' + escAttr(t.slug) + '\',\'' + escAttr(t.name) + '\')">';
|
||||
html += '<span class="name">' + img + esc(t.name) + '</span>';
|
||||
html += '<span class="count">' + t.variant_count + '</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
})
|
||||
.catch(function () { content.innerHTML = '<div class="empty">Error cargando tipos de parte.</div>'; });
|
||||
}
|
||||
|
||||
window.selectNxPartType = function (slug, name) {
|
||||
state.nxPartType = { slug: slug, name: name };
|
||||
state.level = 'parts';
|
||||
state.page = 1;
|
||||
loadParts();
|
||||
};
|
||||
|
||||
function loadCategories() {
|
||||
@@ -331,6 +597,52 @@
|
||||
|
||||
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 = '<div class="loading">Cargando tipos de parte...</div>';
|
||||
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 = '<h2>' + esc(state.group.name) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + types.length + ' tipos)</span></h2>';
|
||||
html += '<div class="nav-grid">';
|
||||
types.forEach(function (t) {
|
||||
var img = t.sample_image
|
||||
? '<img src="' + esc(t.sample_image) + '" alt="" style="width:24px;height:24px;object-fit:contain;margin-right:6px;vertical-align:middle;" onerror="this.style.display=\'none\'">'
|
||||
: '';
|
||||
html += '<div class="nav-card" onclick="selectPartType(\'' + escAttr(t.slug) + '\',\'' + escAttr(t.name) + '\')">';
|
||||
html += '<span class="name">' + img + esc(t.name) + '</span>';
|
||||
html += '<span class="count">' + t.variant_count + '</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
})
|
||||
.catch(function () { content.innerHTML = '<div class="empty">Error cargando tipos de parte.</div>'; });
|
||||
}
|
||||
|
||||
window.selectPartType = function (slug, name) {
|
||||
state.partType = { slug: slug, name: name };
|
||||
state.level = 'parts';
|
||||
state.page = 1;
|
||||
loadParts();
|
||||
@@ -339,27 +651,83 @@
|
||||
function loadParts() {
|
||||
renderBreadcrumb();
|
||||
content.innerHTML = '<div class="loading">Cargando partes...</div>';
|
||||
var url = API + '/parts?mye_id=' + state.engine.id_mye + '&group_id=' + state.group.id + '&page=' + state.page;
|
||||
|
||||
// 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 = '<h2>' + esc(state.group.name) + '</h2><div class="empty">No se encontraron partes.</div>';
|
||||
content.innerHTML = '<h2>' + esc(headerTitle) + '</h2><div class="empty">No se encontraron partes.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<h2>' + esc(state.group.name) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + pag.total + ' partes)</span></h2>';
|
||||
var html = '<h2>' + esc(headerTitle) + ' <span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">(' + pag.total + ' partes)</span></h2>';
|
||||
html += '<div class="parts-list">';
|
||||
parts.forEach(function (p) {
|
||||
html += '<div class="part-row">';
|
||||
var tierClass = '';
|
||||
if (isLocal) {
|
||||
if (p.priority_tier === 1) tierClass = ' part-row--tier1';
|
||||
else if (p.priority_tier === 2) tierClass = ' part-row--tier2';
|
||||
}
|
||||
|
||||
html += '<div class="part-row' + tierClass + '">';
|
||||
html += '<div>';
|
||||
html += '<div class="part-oem">' + esc(p.oem_part_number) + '</div>';
|
||||
|
||||
// Manufacturer badge (local mode only)
|
||||
if (isLocal && p.manufacturer) {
|
||||
var tierStar = p.priority_tier === 1 ? '<span class="manu-tier">★</span>' : '';
|
||||
html += '<div class="part-manu"><span class="manu-name">' + esc(p.manufacturer) + '</span>' + tierStar + '</div>';
|
||||
}
|
||||
|
||||
// SKU line
|
||||
if (isLocal && p.part_number) {
|
||||
html += '<div class="part-oem">' + esc(p.part_number) + '<span class="part-oem-sub"> · OEM: ' + esc(p.oem_part_number) + '</span></div>';
|
||||
} else {
|
||||
html += '<div class="part-oem">' + esc(p.oem_part_number) + '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="part-name">' + esc(p.name || '') + '</div>';
|
||||
if (p.description) html += '<div class="part-desc">' + esc(p.description) + '</div>';
|
||||
|
||||
// Stock badge (local mode)
|
||||
if (isLocal) {
|
||||
if (p.in_stock_network) {
|
||||
html += '<div class="part-stock part-stock--yes">En stock en ' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '</div>';
|
||||
} else {
|
||||
html += '<div class="part-stock part-stock--no">Consultar disponibilidad</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<button class="part-detail-btn" onclick="openDetail(' + p.id_part + ')">Ver detalle y alternativas</button>';
|
||||
html += '</div>';
|
||||
if (p.image_url) {
|
||||
|
||||
Reference in New Issue
Block a user