Files
Autoparts-DB/pos/static/js/catalog.js
consultoria-as 0f979b7912 feat(pos): parsear nombres de modelos — solo nombre primario visible
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>
2026-04-02 08:23:31 +00:00

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();
})();