Remueve codigos de generacion, numeros romanos, tipos de carroceria. Deduplica por display_name. Toyota: 236 → 73 modelos limpios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
981 lines
43 KiB
JavaScript
981 lines
43 KiB
JavaScript
// /home/Autopartes/pos/static/js/catalog.js
|
|
// Catalog UI: TecDoc vehicle navigation with local stock enrichment, cart, detail panel.
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var API = '/pos/api/catalog';
|
|
var token = localStorage.getItem('pos_token');
|
|
if (!token) { window.location.href = '/pos/login'; return; }
|
|
|
|
var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
|
|
|
// ─── DOM refs ───
|
|
var breadcrumb = document.getElementById('breadcrumb');
|
|
var searchInput = document.getElementById('searchInput');
|
|
var searchDropdown = document.getElementById('searchDropdown');
|
|
var levelTitle = document.getElementById('levelTitle');
|
|
var levelFilter = document.getElementById('levelFilter');
|
|
var loading = document.getElementById('loading');
|
|
var emptyState = document.getElementById('emptyState');
|
|
var emptyTitle = document.getElementById('emptyTitle');
|
|
var emptySubtitle = document.getElementById('emptySubtitle');
|
|
var navGrid = document.getElementById('navGrid');
|
|
var partsGrid = document.getElementById('partsGrid');
|
|
var paginationNav = document.getElementById('pagination');
|
|
var pageBody = document.getElementById('pageBody');
|
|
// Detail panel
|
|
var detailPanel = document.getElementById('detailPanel');
|
|
var detailOverlay = document.getElementById('detailOverlay');
|
|
var detailBody = document.getElementById('detailBody');
|
|
var detailFooter = document.getElementById('detailFooter');
|
|
var detailClose = document.getElementById('detailClose');
|
|
var qtyMinus = document.getElementById('qtyMinus');
|
|
var qtyPlus = document.getElementById('qtyPlus');
|
|
var qtyDisplay = document.getElementById('qtyDisplay');
|
|
var addToCartBtn = document.getElementById('addToCartBtn');
|
|
// Cart
|
|
var cartSidebar = document.getElementById('cartSidebar');
|
|
var cartOverlay = document.getElementById('cartOverlay');
|
|
var cartItemsEl = document.getElementById('cartItems');
|
|
var cartEmptyEl = document.getElementById('cartEmpty');
|
|
var cartSubtotalEl= document.getElementById('cartSubtotal');
|
|
var cartTaxEl = document.getElementById('cartTax');
|
|
var cartTotalEl = document.getElementById('cartTotal');
|
|
var cartBadge = document.getElementById('cartBadge');
|
|
var checkoutBtn = document.getElementById('checkoutBtn');
|
|
var cartFab = document.getElementById('cartFab');
|
|
var cartCloseBtn = document.getElementById('cartCloseBtn');
|
|
|
|
// ─── Navigation State ───
|
|
var nav = {
|
|
level: 'brands', // brands|models|years|engines|categories|groups|parts
|
|
brand: null, // {id, name}
|
|
model: null, // {id, name}
|
|
year: null, // {id, year}
|
|
engine: null, // {id_mye, name}
|
|
category: null, // {id, name}
|
|
group: null, // {id, name}
|
|
};
|
|
|
|
var currentPage = 1;
|
|
var currentDetailPart = null;
|
|
var detailQty = 1;
|
|
var isOffline = false;
|
|
|
|
// ─── Cart State ───
|
|
var cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]');
|
|
|
|
// ─── API helper ───
|
|
function apiFetch(url) {
|
|
return fetch(url, { headers: headers })
|
|
.then(function (resp) {
|
|
if (resp.status === 401) {
|
|
localStorage.removeItem('pos_token');
|
|
window.location.href = '/pos/login';
|
|
return null;
|
|
}
|
|
return resp.json();
|
|
})
|
|
.catch(function (e) {
|
|
console.error('API error:', e);
|
|
return null;
|
|
});
|
|
}
|
|
|
|
// ─── UI helpers ───
|
|
function showLoading() { loading.classList.add('is-visible'); navGrid.innerHTML = ''; partsGrid.style.display = 'none'; partsGrid.innerHTML = ''; emptyState.classList.remove('is-visible'); paginationNav.innerHTML = ''; }
|
|
function hideLoading() { loading.classList.remove('is-visible'); }
|
|
|
|
function showEmpty(title, subtitle) {
|
|
emptyTitle.textContent = title;
|
|
emptySubtitle.textContent = subtitle || '';
|
|
emptyState.classList.add('is-visible');
|
|
navGrid.innerHTML = '';
|
|
partsGrid.style.display = 'none';
|
|
}
|
|
|
|
function fmt(n) { return (parseFloat(n) || 0).toFixed(2); }
|
|
|
|
function esc(s) {
|
|
if (!s) return '';
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
// ─── Breadcrumb ───
|
|
function updateBreadcrumb() {
|
|
var parts = [];
|
|
parts.push({ label: 'Catalogo', action: 'loadBrands' });
|
|
|
|
if (nav.brand) parts.push({ label: nav.brand.name, action: 'loadModels' });
|
|
if (nav.model) parts.push({ label: nav.model.name, action: 'loadYears' });
|
|
if (nav.year) parts.push({ label: String(nav.year.year), action: 'loadEngines' });
|
|
if (nav.engine) parts.push({ label: nav.engine.name, action: 'loadCategories' });
|
|
if (nav.category) parts.push({ label: nav.category.name, action: 'loadGroups' });
|
|
if (nav.group) parts.push({ label: nav.group.name, action: null });
|
|
|
|
var html = '';
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (i > 0) html += '<span class="breadcrumb__sep" aria-hidden="true">/</span>';
|
|
if (i < parts.length - 1 && parts[i].action) {
|
|
html += '<a class="breadcrumb__link" data-bc-action="' + parts[i].action + '">' + esc(parts[i].label) + '</a>';
|
|
} else {
|
|
html += '<span class="breadcrumb__current">' + esc(parts[i].label) + '</span>';
|
|
}
|
|
}
|
|
breadcrumb.innerHTML = html;
|
|
|
|
// Wire breadcrumb clicks
|
|
breadcrumb.querySelectorAll('[data-bc-action]').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
var action = this.dataset.bcAction;
|
|
if (action === 'loadBrands') { resetNav(); loadBrands(); }
|
|
else if (action === 'loadModels') { resetNavFrom('models'); loadModels(); }
|
|
else if (action === 'loadYears') { resetNavFrom('years'); loadYears(); }
|
|
else if (action === 'loadEngines') { resetNavFrom('engines'); loadEngines(); }
|
|
else if (action === 'loadCategories') { resetNavFrom('categories'); loadCategories(); }
|
|
else if (action === 'loadGroups') { resetNavFrom('groups'); loadGroups(); }
|
|
});
|
|
});
|
|
}
|
|
|
|
function resetNav() {
|
|
nav.level = 'brands';
|
|
nav.brand = nav.model = nav.year = nav.engine = nav.category = nav.group = null;
|
|
}
|
|
|
|
function resetNavFrom(level) {
|
|
var levels = ['brands', 'models', 'years', 'engines', 'categories', 'groups', 'parts'];
|
|
var idx = levels.indexOf(level);
|
|
if (idx <= 0) { resetNav(); return; }
|
|
nav.level = level;
|
|
var keys = [null, 'model', 'year', 'engine', 'category', 'group', null];
|
|
for (var i = idx; i < keys.length; i++) {
|
|
if (keys[i]) nav[keys[i]] = null;
|
|
}
|
|
}
|
|
|
|
// ─── Level filter ───
|
|
function setupLevelFilter(show) {
|
|
if (!show) { levelFilter.style.display = 'none'; levelFilter.value = ''; return; }
|
|
levelFilter.style.display = '';
|
|
levelFilter.value = '';
|
|
levelFilter.oninput = function () {
|
|
var q = this.value.toLowerCase();
|
|
var cards = navGrid.querySelectorAll('.nav-card');
|
|
cards.forEach(function (card) {
|
|
var text = card.textContent.toLowerCase();
|
|
card.style.display = text.indexOf(q) >= 0 ? '' : 'none';
|
|
});
|
|
};
|
|
}
|
|
|
|
// ─── LEVEL LOADERS ───
|
|
|
|
function loadBrands() {
|
|
nav.level = 'brands';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = 'Selecciona una marca';
|
|
setupLevelFilter(true);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/brands').then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) {
|
|
if (!data) {
|
|
enterOfflineMode();
|
|
return;
|
|
}
|
|
showEmpty('Sin marcas', 'El catalogo no tiene marcas con partes disponibles.');
|
|
return;
|
|
}
|
|
navGrid.className = 'nav-grid';
|
|
navGrid.innerHTML = data.data.map(function (b) {
|
|
return '<div class="nav-card" role="listitem" data-brand-id="' + b.id_brand + '" data-name="' + esc(b.name_brand) + '">' +
|
|
'<div class="nav-card__name">' + esc(b.name_brand) + '</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.brand = { id: parseInt(this.dataset.brandId), name: this.dataset.name };
|
|
loadModels();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadModels() {
|
|
nav.level = 'models';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = 'Modelos de ' + nav.brand.name;
|
|
setupLevelFilter(true);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/models?brand_id=' + nav.brand.id).then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin modelos', 'No hay modelos con partes para ' + nav.brand.name); return; }
|
|
navGrid.className = 'nav-grid';
|
|
navGrid.innerHTML = data.data.map(function (m) {
|
|
return '<div class="nav-card" role="listitem" data-model-id="' + m.id_model + '" data-name="' + esc(m.display_name || m.name_model) + '">' +
|
|
'<div class="nav-card__name">' + esc(m.display_name || m.name_model) + '</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.model = { id: parseInt(this.dataset.modelId), name: this.dataset.name };
|
|
loadYears();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadYears() {
|
|
nav.level = 'years';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = nav.brand.name + ' ' + nav.model.name + ' — Anios';
|
|
setupLevelFilter(false);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/years?model_id=' + nav.model.id).then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin anios', 'No hay anios con partes para este modelo.'); return; }
|
|
navGrid.className = 'nav-grid nav-grid--years';
|
|
navGrid.innerHTML = data.data.map(function (y) {
|
|
return '<div class="nav-card nav-card--year" role="listitem" data-year-id="' + y.id_year + '" data-year="' + y.year_car + '">' +
|
|
'<div class="nav-card__name">' + y.year_car + '</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.year = { id: parseInt(this.dataset.yearId), year: parseInt(this.dataset.year) };
|
|
loadEngines();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadEngines() {
|
|
nav.level = 'engines';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = nav.brand.name + ' ' + nav.model.name + ' ' + nav.year.year + ' — Motor';
|
|
setupLevelFilter(false);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/engines?model_id=' + nav.model.id + '&year_id=' + nav.year.id).then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin motores', 'No hay configuraciones de motor para esta combinacion.'); return; }
|
|
|
|
// If only one engine, auto-select it
|
|
if (data.data.length === 1) {
|
|
var e = data.data[0];
|
|
nav.engine = { id_mye: e.id_mye, name: e.name_engine + (e.trim_level ? ' ' + e.trim_level : '') };
|
|
loadCategories();
|
|
return;
|
|
}
|
|
|
|
navGrid.className = 'nav-grid';
|
|
navGrid.innerHTML = data.data.map(function (e) {
|
|
var label = e.name_engine + (e.trim_level ? ' — ' + e.trim_level : '');
|
|
return '<div class="nav-card" role="listitem" data-mye-id="' + e.id_mye + '" data-name="' + esc(label) + '">' +
|
|
'<div class="nav-card__name">' + esc(e.name_engine) + '</div>' +
|
|
(e.trim_level ? '<div class="nav-card__sub">' + esc(e.trim_level) + '</div>' : '') +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.engine = { id_mye: parseInt(this.dataset.myeId), name: this.dataset.name };
|
|
loadCategories();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadCategories() {
|
|
nav.level = 'categories';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = 'Categorias de partes';
|
|
setupLevelFilter(true);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/categories?mye_id=' + nav.engine.id_mye).then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin categorias', 'No hay partes catalogadas para este vehiculo.'); return; }
|
|
navGrid.className = 'nav-grid';
|
|
navGrid.innerHTML = data.data.map(function (c) {
|
|
return '<div class="nav-card" role="listitem" data-cat-id="' + c.id_part_category + '" data-name="' + esc(c.name) + '">' +
|
|
'<div class="nav-card__name">' + esc(c.name) + '</div>' +
|
|
'<div class="nav-card__count">' + c.part_count + ' partes</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.category = { id: parseInt(this.dataset.catId), name: this.dataset.name };
|
|
loadGroups();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadGroups() {
|
|
nav.level = 'groups';
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = nav.category.name;
|
|
setupLevelFilter(true);
|
|
showLoading();
|
|
|
|
apiFetch(API + '/groups?mye_id=' + nav.engine.id_mye + '&category_id=' + nav.category.id).then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin subcategorias', 'No hay subcategorias para ' + nav.category.name); return; }
|
|
navGrid.className = 'nav-grid';
|
|
navGrid.innerHTML = data.data.map(function (g) {
|
|
return '<div class="nav-card" role="listitem" data-group-id="' + g.id_part_group + '" data-name="' + esc(g.name) + '">' +
|
|
'<div class="nav-card__name">' + esc(g.name) + '</div>' +
|
|
'<div class="nav-card__count">' + g.part_count + ' partes</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
nav.group = { id: parseInt(this.dataset.groupId), name: this.dataset.name };
|
|
loadParts(1);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadParts(page) {
|
|
nav.level = 'parts';
|
|
currentPage = page || 1;
|
|
updateBreadcrumb();
|
|
levelTitle.textContent = nav.group.name;
|
|
setupLevelFilter(false);
|
|
showLoading();
|
|
navGrid.innerHTML = '';
|
|
|
|
apiFetch(API + '/parts?mye_id=' + nav.engine.id_mye + '&group_id=' + nav.group.id + '&page=' + currentPage + '&per_page=30').then(function (data) {
|
|
hideLoading();
|
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin partes', 'No hay partes en este grupo.'); return; }
|
|
|
|
partsGrid.style.display = '';
|
|
partsGrid.innerHTML = data.data.map(function (p) {
|
|
var stockBadge;
|
|
if (p.local_stock > 0) {
|
|
stockBadge = '<span class="stock-badge stock-badge--local">En stock: ' + p.local_stock + '</span>';
|
|
} else if (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>';
|
|
}
|
|
|
|
var imgHtml = p.image_url
|
|
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '">'
|
|
: '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>';
|
|
|
|
return '<article class="part-card" role="listitem" data-part-id="' + p.id_part + '">' +
|
|
'<div class="part-card__image">' + imgHtml + '</div>' +
|
|
'<div class="part-card__body">' +
|
|
'<div class="part-card__oem">' + esc(p.oem_part_number) + '</div>' +
|
|
'<div class="part-card__name">' + esc(p.name) + '</div>' +
|
|
'</div>' +
|
|
'<div class="part-card__footer">' +
|
|
(p.local_price ? '<span class="part-card__price">$' + fmt(p.local_price) + '</span>' : '<span class="part-card__price" style="color:var(--color-text-muted);">Sin precio</span>') +
|
|
stockBadge +
|
|
'</div>' +
|
|
'</article>';
|
|
}).join('');
|
|
|
|
// Wire part card clicks → open detail panel
|
|
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
openPartDetail(parseInt(this.dataset.partId));
|
|
});
|
|
});
|
|
|
|
// Pagination
|
|
if (data.pagination) renderPagination(data.pagination);
|
|
});
|
|
}
|
|
|
|
// ─── PAGINATION ───
|
|
function renderPagination(pg) {
|
|
if (!pg || pg.total_pages <= 1) { paginationNav.innerHTML = ''; return; }
|
|
var html = '';
|
|
|
|
if (pg.page <= 1) {
|
|
html += '<button class="page-item page-item--wide is-disabled" disabled>Anterior</button>';
|
|
} else {
|
|
html += '<button class="page-item page-item--wide" data-page="' + (pg.page - 1) + '">Anterior</button>';
|
|
}
|
|
|
|
var pages = buildPageNumbers(pg.page, pg.total_pages);
|
|
pages.forEach(function (p) {
|
|
if (p === '...') {
|
|
html += '<span style="padding:0 4px;color:var(--color-text-muted);">...</span>';
|
|
} else if (p === pg.page) {
|
|
html += '<button class="page-item is-active">' + p + '</button>';
|
|
} else {
|
|
html += '<button class="page-item" data-page="' + p + '">' + p + '</button>';
|
|
}
|
|
});
|
|
|
|
if (pg.page >= pg.total_pages) {
|
|
html += '<button class="page-item page-item--wide is-disabled" disabled>Siguiente</button>';
|
|
} else {
|
|
html += '<button class="page-item page-item--wide" data-page="' + (pg.page + 1) + '">Siguiente</button>';
|
|
}
|
|
|
|
paginationNav.innerHTML = html;
|
|
|
|
paginationNav.querySelectorAll('[data-page]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
pageBody.scrollTo({ top: 0, behavior: 'smooth' });
|
|
loadParts(parseInt(this.dataset.page));
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildPageNumbers(current, total) {
|
|
if (total <= 7) { var a = []; for (var i = 1; i <= total; i++) a.push(i); return a; }
|
|
var p = [1];
|
|
if (current > 3) p.push('...');
|
|
for (var j = Math.max(2, current - 1); j <= Math.min(total - 1, current + 1); j++) p.push(j);
|
|
if (current < total - 2) p.push('...');
|
|
p.push(total);
|
|
return p;
|
|
}
|
|
|
|
// ─── DETAIL PANEL ───
|
|
function openPartDetail(partId) {
|
|
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');
|
|
detailQty = 1;
|
|
qtyDisplay.textContent = '1';
|
|
|
|
apiFetch(API + '/part/' + partId).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;
|
|
}
|
|
|
|
currentDetailPart = data;
|
|
var p = data.part;
|
|
var local = data.local;
|
|
var bodegas = data.bodegas || [];
|
|
var alts = data.alternatives || [];
|
|
|
|
var html = '';
|
|
|
|
// Part info
|
|
html += '<div class="detail-section">';
|
|
if (p.category_name) html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-1);">' + esc(p.category_name) + ' > ' + esc(p.group_name) + '</div>';
|
|
html += '<div class="detail-oem">' + esc(p.oem_part_number) + '</div>';
|
|
html += '<div class="detail-name">' + esc(p.name) + '</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="" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
|
|
html += '</div>';
|
|
|
|
// Local stock
|
|
html += '<div class="detail-section">';
|
|
html += '<div class="detail-section__title">Mi stock</div>';
|
|
if (local && local.stock > 0) {
|
|
html += '<div class="stock-row"><span class="stock-label">Cantidad</span><span class="stock-value stock-value--ok">' + local.stock + ' ' + (local.unit || 'PZA') + '</span></div>';
|
|
html += '<div class="stock-row"><span class="stock-label">Precio publico</span><span class="stock-value">$' + fmt(local.price_1) + '</span></div>';
|
|
if (local.price_2) html += '<div class="stock-row"><span class="stock-label">Precio mayoreo</span><span class="stock-value">$' + fmt(local.price_2) + '</span></div>';
|
|
if (local.price_3) html += '<div class="stock-row"><span class="stock-label">Precio taller</span><span class="stock-value">$' + fmt(local.price_3) + '</span></div>';
|
|
if (local.location) html += '<div class="stock-row"><span class="stock-label">Ubicacion</span><span class="stock-value">' + esc(local.location) + '</span></div>';
|
|
} else {
|
|
html += '<div style="color:var(--color-text-muted);font-size:var(--text-body-sm);">No tienes esta parte en inventario.</div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Bodegas
|
|
if (bodegas.length) {
|
|
html += '<div class="detail-section">';
|
|
html += '<div class="detail-section__title">Disponible en bodegas</div>';
|
|
html += '<table class="bodega-table"><thead><tr><th>Bodega</th><th>Precio</th><th>Stock</th></tr></thead><tbody>';
|
|
bodegas.forEach(function (b) {
|
|
html += '<tr><td>' + esc(b.business_name) + '</td><td>' + (b.price ? '$' + fmt(b.price) : '--') + '</td><td>' + b.stock + '</td></tr>';
|
|
});
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
// Alternatives
|
|
if (alts.length) {
|
|
html += '<div class="detail-section">';
|
|
html += '<div class="detail-section__title">Alternativas / Cross-references</div>';
|
|
alts.forEach(function (a) {
|
|
var stockLabel = a.local_stock > 0
|
|
? '<span class="stock-badge stock-badge--local">Stock: ' + a.local_stock + '</span>'
|
|
: (a.bodega_count > 0 ? '<span class="stock-badge stock-badge--bodega">' + a.bodega_count + ' bod.</span>' : '');
|
|
html += '<div class="alt-item">' +
|
|
'<div><div class="alt-item__pn">' + esc(a.part_number) + '</div>' +
|
|
'<div class="alt-item__mfr">' + esc(a.manufacturer) + (a.name ? ' — ' + esc(a.name) : '') + '</div></div>' +
|
|
'<div class="alt-item__stock">' + stockLabel + '</div>' +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
detailBody.innerHTML = html;
|
|
|
|
// Show footer only if we have local stock
|
|
if (local && local.stock > 0) {
|
|
detailFooter.style.display = '';
|
|
} else {
|
|
detailFooter.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
function closeDetail() {
|
|
detailPanel.classList.remove('is-open');
|
|
detailOverlay.classList.remove('is-visible');
|
|
currentDetailPart = null;
|
|
}
|
|
|
|
detailClose.addEventListener('click', closeDetail);
|
|
detailOverlay.addEventListener('click', closeDetail);
|
|
|
|
qtyMinus.addEventListener('click', function () { if (detailQty > 1) { detailQty--; qtyDisplay.textContent = detailQty; } });
|
|
qtyPlus.addEventListener('click', function () { detailQty++; qtyDisplay.textContent = detailQty; });
|
|
|
|
addToCartBtn.addEventListener('click', function () {
|
|
if (!currentDetailPart) return;
|
|
var p = currentDetailPart.part;
|
|
var local = currentDetailPart.local;
|
|
if (!local) return;
|
|
|
|
addToCart({
|
|
id: p.id_part,
|
|
part_number: p.oem_part_number,
|
|
name: p.name,
|
|
brand: '',
|
|
price: local.price_1,
|
|
tax_rate: local.tax_rate || 0.16,
|
|
unit: local.unit || 'PZA',
|
|
stock: local.stock,
|
|
source: 'local',
|
|
inventory_id: local.inventory_id,
|
|
}, detailQty);
|
|
closeDetail();
|
|
});
|
|
|
|
// ─── SMART SEARCH ───
|
|
var searchTimeout = null;
|
|
|
|
searchInput.addEventListener('input', function () {
|
|
clearTimeout(searchTimeout);
|
|
var q = this.value.trim();
|
|
if (q.length < 2) { searchDropdown.classList.remove('is-visible'); return; }
|
|
searchTimeout = setTimeout(function () { runSearch(q); }, 350);
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
clearTimeout(searchTimeout);
|
|
var q = this.value.trim();
|
|
if (q.length >= 2) runSearch(q);
|
|
}
|
|
if (e.key === 'Escape') {
|
|
searchDropdown.classList.remove('is-visible');
|
|
}
|
|
});
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', function (e) {
|
|
if (!searchDropdown.contains(e.target) && e.target !== searchInput) {
|
|
searchDropdown.classList.remove('is-visible');
|
|
}
|
|
});
|
|
|
|
function runSearch(q) {
|
|
apiFetch(API + '/search?q=' + encodeURIComponent(q) + '&limit=20').then(function (data) {
|
|
if (!data || !data.data || !data.data.length) {
|
|
searchDropdown.innerHTML = '<div style="padding:var(--space-4);color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin resultados para "' + esc(q) + '"</div>';
|
|
searchDropdown.classList.add('is-visible');
|
|
return;
|
|
}
|
|
searchDropdown.innerHTML = data.data.map(function (r) {
|
|
var stockLabel = r.local_stock > 0
|
|
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
|
|
: '';
|
|
return '<div class="search-result-item" data-part-id="' + r.id_part + '">' +
|
|
'<div style="flex:1;">' +
|
|
'<div class="search-result__oem">' + esc(r.oem_part_number) + '</div>' +
|
|
'<div class="search-result__name">' + esc(r.name) + '</div>' +
|
|
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
|
|
'</div>' +
|
|
stockLabel +
|
|
'</div>';
|
|
}).join('');
|
|
searchDropdown.classList.add('is-visible');
|
|
|
|
searchDropdown.querySelectorAll('.search-result-item').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
searchDropdown.classList.remove('is-visible');
|
|
openPartDetail(parseInt(this.dataset.partId));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── CART ───
|
|
function addToCart(item, qty) {
|
|
qty = qty || 1;
|
|
var existing = cartItems.find(function (c) { return c.id === item.id; });
|
|
if (existing) {
|
|
existing.quantity += qty;
|
|
} else {
|
|
cartItems.push({
|
|
id: item.id,
|
|
part_number: item.part_number,
|
|
name: item.name,
|
|
brand: item.brand || '',
|
|
price: item.price,
|
|
tax_rate: item.tax_rate || 0.16,
|
|
unit: item.unit || 'PZA',
|
|
stock: item.stock,
|
|
source: item.source || 'local',
|
|
inventory_id: item.inventory_id,
|
|
quantity: qty,
|
|
});
|
|
}
|
|
saveCart();
|
|
renderCart();
|
|
if (!cartSidebar.classList.contains('open')) toggleCart();
|
|
}
|
|
|
|
function removeFromCart(index) {
|
|
cartItems.splice(index, 1);
|
|
saveCart();
|
|
renderCart();
|
|
}
|
|
|
|
function updateQuantity(index, qty) {
|
|
qty = parseInt(qty);
|
|
if (qty <= 0) { removeFromCart(index); return; }
|
|
cartItems[index].quantity = qty;
|
|
saveCart();
|
|
renderCart();
|
|
}
|
|
|
|
function clearCart() {
|
|
cartItems = [];
|
|
saveCart();
|
|
renderCart();
|
|
}
|
|
|
|
function saveCart() { localStorage.setItem('pos_cart', JSON.stringify(cartItems)); }
|
|
|
|
function renderCart() {
|
|
var total = cartItems.reduce(function (s, c) { return s + c.quantity; }, 0);
|
|
if (cartBadge) {
|
|
cartBadge.textContent = total;
|
|
cartBadge.style.display = total > 0 ? 'flex' : 'none';
|
|
}
|
|
|
|
if (!cartItems.length) {
|
|
cartItemsEl.innerHTML = '';
|
|
cartEmptyEl.style.display = 'block';
|
|
if (checkoutBtn) checkoutBtn.disabled = true;
|
|
cartSubtotalEl.textContent = '$0.00';
|
|
cartTaxEl.textContent = '$0.00';
|
|
cartTotalEl.textContent = '$0.00';
|
|
return;
|
|
}
|
|
|
|
cartEmptyEl.style.display = 'none';
|
|
if (checkoutBtn) checkoutBtn.disabled = false;
|
|
|
|
var subtotal = 0;
|
|
var tax = 0;
|
|
cartItemsEl.innerHTML = cartItems.map(function (c, i) {
|
|
var lineTotal = c.price * c.quantity;
|
|
var lineTax = lineTotal * c.tax_rate;
|
|
subtotal += lineTotal;
|
|
tax += lineTax;
|
|
return '<div class="cart-item">' +
|
|
'<div style="flex:1;">' +
|
|
'<div style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary);">' + esc(c.name) + '</div>' +
|
|
'<div style="font-size:0.75rem;color:var(--color-text-muted);">' + esc(c.part_number) + '</div>' +
|
|
'<div style="margin-top:4px;display:flex;align-items:center;gap:6px;">' +
|
|
'<button data-cart-action="dec" data-idx="' + i + '" style="width:24px;height:24px;border:1px solid var(--color-border);background:var(--color-bg-base);border-radius:var(--radius-sm);cursor:pointer;color:var(--color-text-primary);">-</button>' +
|
|
'<span style="font-weight:600;color:var(--color-text-primary);">' + c.quantity + '</span>' +
|
|
'<button data-cart-action="inc" data-idx="' + i + '" style="width:24px;height:24px;border:1px solid var(--color-border);background:var(--color-bg-base);border-radius:var(--radius-sm);cursor:pointer;color:var(--color-text-primary);">+</button>' +
|
|
'</div></div>' +
|
|
'<div style="text-align:right;">' +
|
|
'<div style="font-weight:600;color:var(--color-text-primary);">$' + fmt(lineTotal) + '</div>' +
|
|
'<button data-cart-action="remove" data-idx="' + i + '" style="font-size:0.75rem;color:var(--color-error,#ef4444);background:none;border:none;cursor:pointer;margin-top:4px;">Quitar</button>' +
|
|
'</div></div>';
|
|
}).join('');
|
|
|
|
cartSubtotalEl.textContent = '$' + fmt(subtotal);
|
|
cartTaxEl.textContent = '$' + fmt(tax);
|
|
cartTotalEl.textContent = '$' + fmt(subtotal + tax);
|
|
|
|
// Wire cart buttons
|
|
cartItemsEl.querySelectorAll('[data-cart-action]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var idx = parseInt(this.dataset.idx);
|
|
var action = this.dataset.cartAction;
|
|
if (action === 'dec') updateQuantity(idx, cartItems[idx].quantity - 1);
|
|
else if (action === 'inc') updateQuantity(idx, cartItems[idx].quantity + 1);
|
|
else if (action === 'remove') removeFromCart(idx);
|
|
});
|
|
});
|
|
}
|
|
|
|
function toggleCart() {
|
|
var isOpen = cartSidebar.classList.toggle('open');
|
|
cartOverlay.classList.toggle('open', isOpen);
|
|
}
|
|
|
|
function goToCheckout() {
|
|
if (!cartItems.length) return;
|
|
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
|
window.location.href = '/pos/sale';
|
|
}
|
|
|
|
cartFab.addEventListener('click', toggleCart);
|
|
cartCloseBtn.addEventListener('click', toggleCart);
|
|
cartOverlay.addEventListener('click', toggleCart);
|
|
checkoutBtn.addEventListener('click', goToCheckout);
|
|
|
|
// ─── OFFLINE FALLBACK ───
|
|
function enterOfflineMode() {
|
|
isOffline = true;
|
|
document.getElementById('offlineBanner').style.display = '';
|
|
document.getElementById('offlineBannerText').innerHTML = '<strong>Modo offline</strong> — Mostrando solo tu inventario local.';
|
|
levelTitle.textContent = 'Inventario local';
|
|
setupLevelFilter(false);
|
|
// TODO: load local inventory via legacy /pos/api/catalog/search endpoint
|
|
showEmpty('Sin conexion al catalogo', 'Verifica tu conexion. El catalogo TecDoc requiere acceso al servidor central.');
|
|
}
|
|
|
|
// ─── BARCODE SCANNER ───
|
|
var barcodeBuffer = '';
|
|
var barcodeTimeout = null;
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
// F1 → focus search
|
|
if (e.key === 'F1') { e.preventDefault(); searchInput.focus(); return; }
|
|
// Escape → close panels
|
|
if (e.key === 'Escape') {
|
|
closeDetail();
|
|
if (cartSidebar.classList.contains('open')) toggleCart();
|
|
return;
|
|
}
|
|
// Barcode scanner detection
|
|
if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;
|
|
if (e.key === 'Enter' && barcodeBuffer.length >= 4) {
|
|
var code = barcodeBuffer.trim();
|
|
barcodeBuffer = '';
|
|
// Search for the barcode
|
|
searchInput.value = code;
|
|
runSearch(code);
|
|
return;
|
|
}
|
|
if (e.key.length === 1) {
|
|
barcodeBuffer += e.key;
|
|
clearTimeout(barcodeTimeout);
|
|
barcodeTimeout = setTimeout(function () { barcodeBuffer = ''; }, 200);
|
|
}
|
|
});
|
|
|
|
// ─── THEME SWITCHER ───
|
|
document.querySelectorAll('[data-theme-switch]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var theme = this.dataset.themeSwitch;
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('pos_theme', theme);
|
|
document.querySelectorAll('[data-theme-switch]').forEach(function (b) {
|
|
b.classList.remove('is-active');
|
|
b.setAttribute('aria-pressed', 'false');
|
|
});
|
|
this.classList.add('is-active');
|
|
this.setAttribute('aria-pressed', 'true');
|
|
});
|
|
// Set initial active state
|
|
var current = localStorage.getItem('pos_theme') || 'industrial';
|
|
if (btn.dataset.themeSwitch === current) {
|
|
btn.classList.add('is-active');
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
} else {
|
|
btn.classList.remove('is-active');
|
|
btn.setAttribute('aria-pressed', 'false');
|
|
}
|
|
});
|
|
|
|
// ─── VEHICLE SELECTOR (dropdown bar) ───
|
|
var vsYear = document.getElementById('vsYear');
|
|
var vsBrand = document.getElementById('vsBrand');
|
|
var vsModel = document.getElementById('vsModel');
|
|
var vsEngine = document.getElementById('vsEngine');
|
|
var vsClear = document.getElementById('vsClear');
|
|
|
|
// Load years on init
|
|
function vsLoadYears() {
|
|
apiFetch(API + '/years-all').then(function (data) {
|
|
if (!data) return;
|
|
var years = data.data || data;
|
|
// If endpoint doesn't exist, generate from 1990-2026
|
|
if (!years || !years.length) {
|
|
years = [];
|
|
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
|
|
}
|
|
vsYear.innerHTML = '<option value="">Año...</option>';
|
|
years.forEach(function (y) {
|
|
vsYear.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
|
|
});
|
|
}).catch(function () {
|
|
// Fallback: generate years statically
|
|
vsYear.innerHTML = '<option value="">Año...</option>';
|
|
for (var y = 2026; y >= 1990; y--) {
|
|
vsYear.innerHTML += '<option value="' + y + '">' + y + '</option>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function vsYearChanged() {
|
|
var yearId = vsYear.value;
|
|
vsBrand.innerHTML = '<option value="">Marca...</option>';
|
|
vsModel.innerHTML = '<option value="">Modelo...</option>';
|
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
|
vsBrand.disabled = true;
|
|
vsModel.disabled = true;
|
|
vsEngine.disabled = true;
|
|
vsClear.style.display = yearId ? '' : 'none';
|
|
|
|
if (!yearId) return;
|
|
|
|
// Load brands filtered by year
|
|
vsBrand.disabled = false;
|
|
apiFetch(API + '/brands?year_id=' + yearId).then(function (data) {
|
|
var brands = data.data || data;
|
|
if (!brands) return;
|
|
vsBrand.innerHTML = '<option value="">Marca...</option>';
|
|
brands.forEach(function (b) {
|
|
vsBrand.innerHTML += '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
|
|
});
|
|
});
|
|
}
|
|
|
|
function vsBrandChanged() {
|
|
var brandId = vsBrand.value;
|
|
var yearId = vsYear.value;
|
|
vsModel.innerHTML = '<option value="">Modelo...</option>';
|
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
|
vsModel.disabled = true;
|
|
vsEngine.disabled = true;
|
|
|
|
if (!brandId) return;
|
|
|
|
// Load models filtered by brand AND year
|
|
vsModel.disabled = false;
|
|
apiFetch(API + '/models?brand_id=' + brandId + (yearId ? '&year_id=' + yearId : '')).then(function (data) {
|
|
var models = data.data || data;
|
|
if (!models) return;
|
|
vsModel.innerHTML = '<option value="">Modelo...</option>';
|
|
models.forEach(function (m) {
|
|
vsModel.innerHTML += '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
|
|
});
|
|
});
|
|
}
|
|
|
|
function vsModelChanged() {
|
|
var modelId = vsModel.value;
|
|
var yearVal = vsYear.value;
|
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
|
vsEngine.disabled = true;
|
|
|
|
if (!modelId || !yearVal) return;
|
|
|
|
vsEngine.disabled = false;
|
|
apiFetch(API + '/engines?model_id=' + modelId + '&year_id=' + yearVal).then(function (data) {
|
|
var engines = data.data || data;
|
|
if (!engines) return;
|
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
|
engines.forEach(function (e) {
|
|
var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
|
|
vsEngine.innerHTML += '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
|
|
});
|
|
// If only 1 engine, auto-select
|
|
if (engines.length === 1) {
|
|
vsEngine.value = engines[0].id_mye;
|
|
vsEngineChanged();
|
|
}
|
|
});
|
|
}
|
|
|
|
function vsEngineChanged() {
|
|
var myeId = vsEngine.value;
|
|
if (!myeId) return;
|
|
|
|
// Update state and load categories
|
|
var yearText = vsYear.options[vsYear.selectedIndex].text;
|
|
var brandText = vsBrand.options[vsBrand.selectedIndex].text;
|
|
var modelText = vsModel.options[vsModel.selectedIndex].text;
|
|
var engineText = vsEngine.options[vsEngine.selectedIndex].text;
|
|
|
|
nav.brand = { id: parseInt(vsBrand.value), name: brandText };
|
|
nav.model = { id: parseInt(vsModel.value), name: modelText };
|
|
nav.year = { id: parseInt(vsYear.value), year: yearText };
|
|
nav.engine = { id_mye: parseInt(myeId), name: engineText };
|
|
nav.level = 'categories';
|
|
|
|
loadCategories();
|
|
|
|
// Scroll to catalog content
|
|
setTimeout(function () {
|
|
var body = document.getElementById('pageBody');
|
|
if (body) body.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 300);
|
|
}
|
|
|
|
function vsClearAll() {
|
|
vsYear.value = '';
|
|
vsBrand.innerHTML = '<option value="">Marca...</option>';
|
|
vsModel.innerHTML = '<option value="">Modelo...</option>';
|
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
|
vsBrand.disabled = true;
|
|
vsModel.disabled = true;
|
|
vsEngine.disabled = true;
|
|
vsClear.style.display = 'none';
|
|
|
|
nav.level = 'brands'; nav.brand = null; nav.model = null; nav.year = null; nav.engine = null; nav.category = null; nav.group = null; currentPage = 1;
|
|
loadBrands();
|
|
}
|
|
|
|
// ─── EXPOSE GLOBALS (for backward compat) ───
|
|
window.CatalogApp = {
|
|
toggleCart: toggleCart,
|
|
goToCheckout: goToCheckout,
|
|
addToCart: addToCart,
|
|
removeFromCart: removeFromCart,
|
|
updateQty: updateQuantity,
|
|
clearCart: clearCart,
|
|
loadPage: function (p) { loadParts(p); },
|
|
vsYearChanged: vsYearChanged,
|
|
vsBrandChanged: vsBrandChanged,
|
|
vsModelChanged: vsModelChanged,
|
|
vsEngineChanged: vsEngineChanged,
|
|
vsClear: vsClearAll,
|
|
};
|
|
|
|
// ─── INIT ───
|
|
renderCart();
|
|
vsLoadYears();
|
|
loadBrands();
|
|
|
|
})();
|