diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js
index 780275e..3aa4cab 100644
--- a/pos/static/js/catalog.js
+++ b/pos/static/js/catalog.js
@@ -1,262 +1,658 @@
// /home/Autopartes/pos/static/js/catalog.js
-// Catalog UI: browsable inventory with cart, barcode scanner, external lookup
-// Aligned with design-system catalog.html IDs and class names.
+// Catalog UI: TecDoc vehicle navigation with local stock enrichment, cart, detail panel.
(function () {
'use strict';
- const API = '/pos/api';
- const token = localStorage.getItem('pos_token');
+ var API = '/pos/api/catalog';
+ var token = localStorage.getItem('pos_token');
if (!token) { window.location.href = '/pos/login'; return; }
- const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
+ var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
- // ─── DOM refs (design-system IDs) ───
- const productGrid = document.getElementById('productGrid');
- const emptyState = document.getElementById('emptyState');
- const emptyTitle = document.getElementById('emptyStateTitle');
- const emptySubtitle = document.getElementById('emptyStateSubtitle');
- const searchInput = document.getElementById('inp-part');
- const partSearchBtn = document.getElementById('partSearchBtn');
- const cartSidebar = document.getElementById('cartSidebar');
- const cartOverlay = document.getElementById('cartOverlay');
- const cartItemsEl = document.getElementById('cartItems');
- const cartEmptyEl = document.getElementById('cartEmpty');
- const cartSubtotalEl = document.getElementById('cartSubtotal');
- const cartTaxEl = document.getElementById('cartTax');
- const cartTotalEl = document.getElementById('cartTotal');
- const cartBadge = document.getElementById('cartBadge');
- const checkoutBtn = document.getElementById('checkoutBtn');
- const paginationNav = document.querySelector('.pagination');
+ // ─── 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');
- // ─── State ───
- let currentPage = 1;
- let currentFilters = {};
- let cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]');
- let barcodeBuffer = '';
- let barcodeTimeout = null;
- let catalogItemsMap = {}; // id -> item for cart lookup
+ // ─── 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}
+ };
- // ─── API helpers ───
- async function apiFetch(url, opts) {
- try {
- var resp = await fetch(url, Object.assign({ headers: headers }, opts || {}));
- if (resp.status === 401) {
- localStorage.removeItem('pos_token');
- window.location.href = '/pos/login';
+ 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;
- }
- return resp.json();
- } catch (e) {
- console.error('API fetch error:', e);
- return null;
- }
+ });
}
- // ─── Catalog loading ───
- async function loadCatalog(page, filters) {
- currentPage = page || 1;
- currentFilters = filters || currentFilters;
- var params = new URLSearchParams({ page: currentPage, per_page: 30 });
- if (currentFilters.q) params.set('q', currentFilters.q);
- if (currentFilters.category) params.set('category', currentFilters.category);
- if (currentFilters.brand) params.set('brand', currentFilters.brand);
- if (currentFilters.vehicle_brand) params.set('vehicle_brand', currentFilters.vehicle_brand);
+ // ─── 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'); }
- var data = await apiFetch(API + '/catalog/search?' + params.toString());
- if (!data) return;
-
- var items = data.data || [];
- renderGrid(items);
- renderPagination(data.pagination || {});
- updateResultsCount(data.pagination || {});
+ function showEmpty(title, subtitle) {
+ emptyTitle.textContent = title;
+ emptySubtitle.textContent = subtitle || '';
+ emptyState.classList.add('is-visible');
+ navGrid.innerHTML = '';
+ partsGrid.style.display = 'none';
}
- function renderGrid(items) {
- // Clear demo/hardcoded cards
- productGrid.innerHTML = '';
+ function fmt(n) { return (parseFloat(n) || 0).toFixed(2); }
- if (!items.length) {
- productGrid.style.display = 'none';
- emptyState.classList.add('is-visible');
- emptyTitle.textContent = 'No se encontraron productos';
- emptySubtitle.textContent = currentFilters.q
- ? 'No hay resultados para "' + currentFilters.q + '". Intenta con otro termino.'
- : 'Intenta con otro termino de busqueda o verifica el numero de parte';
- return;
- }
-
- emptyState.classList.remove('is-visible');
- productGrid.style.display = '';
-
- catalogItemsMap = {};
- items.forEach(function (it) { catalogItemsMap[it.id] = it; });
-
- productGrid.innerHTML = items.map(function (it) {
- var stockClass, stockLabel;
- if (it.stock <= 0) {
- stockClass = 'stock-out';
- stockLabel = 'Agotado';
- } else if (it.low_stock) {
- stockClass = 'stock-low';
- stockLabel = 'Ultimas ' + it.stock;
- } else {
- stockClass = 'stock-ok';
- stockLabel = 'En stock';
- }
-
- var isOut = it.stock <= 0;
- var imgHtml;
- if (it.image_url) {
- imgHtml = ' ';
- } else {
- imgHtml = ' ' +
- 'IMG ';
- }
-
- return '' +
- '' +
- imgHtml +
- '
' + stockLabel + '
' +
- '
' +
- '' +
- (it.category_name ? '
' + escHtml(it.category_name) + '
' : '') +
- '
' + escHtml(it.name) + '
' +
- '
' + escHtml(it.part_number || '') + '
' +
- '
' +
- '' + escHtml(it.brand || '') + ' ' +
- '
' +
- '
' +
- '' +
- ' ';
- }).join('');
+ function esc(s) {
+ if (!s) return '';
+ var d = document.createElement('div');
+ d.textContent = s;
+ return d.innerHTML;
}
- function renderPagination(pg) {
- if (!paginationNav) return;
- if (!pg || pg.total_pages <= 1) {
- paginationNav.innerHTML = '';
- return;
- }
+ // ─── 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 += '/ ';
+ if (i < parts.length - 1 && parts[i].action) {
+ html += '' + esc(parts[i].label) + ' ';
+ } else {
+ html += '' + esc(parts[i].label) + ' ';
+ }
+ }
+ 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 '
' +
+ '
' + esc(b.name_brand) + '
' +
+ '
';
+ }).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 '' +
+ '
' + esc(m.name_model) + '
' +
+ '
';
+ }).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 '' +
+ '
' + y.year_car + '
' +
+ '
';
+ }).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 '' +
+ '
' + esc(e.name_engine) + '
' +
+ (e.trim_level ? '
' + esc(e.trim_level) + '
' : '') +
+ '
';
+ }).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 '' +
+ '
' + esc(c.name) + '
' +
+ '
' + c.part_count + ' partes
' +
+ '
';
+ }).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 '' +
+ '
' + esc(g.name) + '
' +
+ '
' + g.part_count + ' partes
' +
+ '
';
+ }).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 = 'En stock: ' + p.local_stock + ' ';
+ } else if (p.bodega_count > 0) {
+ stockBadge = '' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + ' ';
+ } else {
+ stockBadge = 'Sin stock ';
+ }
+
+ var imgHtml = p.image_url
+ ? ' '
+ : ' ';
+
+ return '' +
+ '' + imgHtml + '
' +
+ '' +
+ '
' + esc(p.oem_part_number) + '
' +
+ '
' + esc(p.name) + '
' +
+ '
' +
+ '' +
+ ' ';
+ }).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 = '';
- // Previous button
if (pg.page <= 1) {
- html += '' +
- ' Anterior ';
+ html += 'Anterior ';
} else {
- html += '' +
- ' Anterior ';
+ html += 'Anterior ';
}
- // Page numbers
var pages = buildPageNumbers(pg.page, pg.total_pages);
pages.forEach(function (p) {
if (p === '...') {
- html += '… ';
+ html += '... ';
} else if (p === pg.page) {
- html += '' + p + ' ';
+ html += '' + p + ' ';
} else {
- html += '' + p + ' ';
+ html += '' + p + ' ';
}
});
- // Next button
if (pg.page >= pg.total_pages) {
- html += 'Siguiente ' +
- ' ';
+ html += 'Siguiente ';
} else {
- html += 'Siguiente ' +
- ' ';
+ html += 'Siguiente ';
}
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 arr = [];
- for (var i = 1; i <= total; i++) arr.push(i);
- return arr;
- }
- var pages = [1];
- if (current > 3) pages.push('...');
- for (var j = Math.max(2, current - 1); j <= Math.min(total - 1, current + 1); j++) {
- pages.push(j);
- }
- if (current < total - 2) pages.push('...');
- pages.push(total);
- return pages;
+ 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;
}
- function updateResultsCount(pg) {
- var el = document.querySelector('.results-count');
- if (!el || !pg) return;
- var total = pg.total || 0;
- el.innerHTML = '' + total.toLocaleString('es-MX') + ' partes encontradas';
+ // ─── DETAIL PANEL ───
+ function openPartDetail(partId) {
+ detailBody.innerHTML = '';
+ 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 = 'Error al cargar detalle.
';
+ return;
+ }
+
+ currentDetailPart = data;
+ var p = data.part;
+ var local = data.local;
+ var bodegas = data.bodegas || [];
+ var alts = data.alternatives || [];
+
+ var html = '';
+
+ // Part info
+ html += '';
+ if (p.category_name) html += '
' + esc(p.category_name) + ' > ' + esc(p.group_name) + '
';
+ html += '
' + esc(p.oem_part_number) + '
';
+ html += '
' + esc(p.name) + '
';
+ if (p.description) html += '
' + esc(p.description) + '
';
+ if (p.image_url) html += '
';
+ html += '
';
+
+ // Local stock
+ html += '';
+ html += '
Mi stock
';
+ if (local && local.stock > 0) {
+ html += '
Cantidad ' + local.stock + ' ' + (local.unit || 'PZA') + '
';
+ html += '
Precio publico $' + fmt(local.price_1) + '
';
+ if (local.price_2) html += '
Precio mayoreo $' + fmt(local.price_2) + '
';
+ if (local.price_3) html += '
Precio taller $' + fmt(local.price_3) + '
';
+ if (local.location) html += '
Ubicacion ' + esc(local.location) + '
';
+ } else {
+ html += '
No tienes esta parte en inventario.
';
+ }
+ html += '
';
+
+ // Bodegas
+ if (bodegas.length) {
+ html += '';
+ html += '
Disponible en bodegas
';
+ html += '
Bodega Precio Stock ';
+ bodegas.forEach(function (b) {
+ html += '' + esc(b.business_name) + ' ' + (b.price ? '$' + fmt(b.price) : '--') + ' ' + b.stock + ' ';
+ });
+ html += '
';
+ }
+
+ // Alternatives
+ if (alts.length) {
+ html += '';
+ html += '
Alternativas / Cross-references
';
+ alts.forEach(function (a) {
+ var stockLabel = a.local_stock > 0
+ ? '
Stock: ' + a.local_stock + ' '
+ : (a.bodega_count > 0 ? '
' + a.bodega_count + ' bod. ' : '');
+ html += '
' +
+ '
' + esc(a.part_number) + '
' +
+ '
' + esc(a.manufacturer) + (a.name ? ' — ' + esc(a.name) : '') + '
' +
+ '
' + stockLabel + '
' +
+ '
';
+ });
+ html += '
';
+ }
+
+ detailBody.innerHTML = html;
+
+ // Show footer only if we have local stock
+ if (local && local.stock > 0) {
+ detailFooter.style.display = '';
+ } else {
+ detailFooter.style.display = 'none';
+ }
+ });
}
- // ─── Barcode scanner ───
- async function lookupBarcode(code) {
- var data = await apiFetch(API + '/catalog/barcode/' + encodeURIComponent(code));
- if (!data || data.error) { alert('Parte no encontrada: ' + code); return; }
- addToCart(data);
+ function closeDetail() {
+ detailPanel.classList.remove('is-open');
+ detailOverlay.classList.remove('is-visible');
+ currentDetailPart = null;
}
- document.addEventListener('keydown', function (e) {
- if (e.key === 'F1') {
+ 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();
- if (searchInput) searchInput.focus();
- return;
+ clearTimeout(searchTimeout);
+ var q = this.value.trim();
+ if (q.length >= 2) runSearch(q);
}
- if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'SELECT') return;
- if (e.key === 'Enter' && barcodeBuffer.length >= 4) {
- lookupBarcode(barcodeBuffer.trim());
- barcodeBuffer = '';
- return;
- }
- if (e.key.length === 1) {
- barcodeBuffer += e.key;
- clearTimeout(barcodeTimeout);
- barcodeTimeout = setTimeout(function () { barcodeBuffer = ''; }, 200);
+ if (e.key === 'Escape') {
+ searchDropdown.classList.remove('is-visible');
}
});
- // ─── Cart ───
- function addToCartById(id) {
- var it = catalogItemsMap[id];
- if (it) addToCart(it);
+ // 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 = 'Sin resultados para "' + esc(q) + '"
';
+ searchDropdown.classList.add('is-visible');
+ return;
+ }
+ searchDropdown.innerHTML = data.data.map(function (r) {
+ var stockLabel = r.local_stock > 0
+ ? 'Stock: ' + r.local_stock + ' '
+ : '';
+ return '' +
+ '
' +
+ '
' + esc(r.oem_part_number) + '
' +
+ '
' + esc(r.name) + '
' +
+ (r.vehicle_info ? '
' + esc(r.vehicle_info) + '
' : '') +
+ '
' +
+ stockLabel +
+ '
';
+ }).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));
+ });
+ });
+ });
}
- function addToCart(item) {
+ // ─── CART ───
+ function addToCart(item, qty) {
+ qty = qty || 1;
var existing = cartItems.find(function (c) { return c.id === item.id; });
if (existing) {
- existing.quantity += 1;
+ existing.quantity += qty;
} else {
cartItems.push({
- id: item.id, part_number: item.part_number, name: item.name,
- brand: item.brand, price: item.price_1 || item.price, tax_rate: item.tax_rate || 0.16,
- unit: item.unit || 'PZA', stock: item.stock, quantity: 1
+ 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();
- // Brief open to show item was added
- if (!cartSidebar.classList.contains('open')) {
- toggleCart();
- }
+ if (!cartSidebar.classList.contains('open')) toggleCart();
}
function removeFromCart(index) {
@@ -273,15 +669,13 @@
renderCart();
}
- function clearCartFn() {
+ function clearCart() {
cartItems = [];
saveCart();
renderCart();
}
- function saveCart() {
- localStorage.setItem('pos_cart', JSON.stringify(cartItems));
- }
+ function saveCart() { localStorage.setItem('pos_cart', JSON.stringify(cartItems)); }
function renderCart() {
var total = cartItems.reduce(function (s, c) { return s + c.quantity; }, 0);
@@ -312,27 +706,38 @@
tax += lineTax;
return '' +
'
' +
- '
' + escHtml(c.name) + '
' +
- '
' + escHtml(c.part_number) + '
' +
+ '
' + esc(c.name) + '
' +
+ '
' + esc(c.part_number) + '
' +
'
' +
- '- ' +
+ '- ' +
'' + c.quantity + ' ' +
- '+ ' +
+ '+ ' +
'
' +
'
' +
'
$' + fmt(lineTotal) + '
' +
- '
Quitar ' +
+ '
Quitar ' +
'
';
}).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');
- if (cartOverlay) cartOverlay.classList.toggle('open', isOpen);
+ cartOverlay.classList.toggle('open', isOpen);
}
function goToCheckout() {
@@ -341,122 +746,89 @@
window.location.href = '/pos/sale';
}
- // ─── External availability ───
- async function checkExternalAvailability(partNumber) {
- var pn = partNumber || currentFilters.q || '';
- if (!pn) return;
- alert('Buscando "' + pn + '" en bodegas Nexus...');
+ cartFab.addEventListener('click', toggleCart);
+ cartCloseBtn.addEventListener('click', toggleCart);
+ cartOverlay.addEventListener('click', toggleCart);
+ checkoutBtn.addEventListener('click', goToCheckout);
- var data = await apiFetch(API + '/catalog/external-availability/' + encodeURIComponent(pn));
- if (!data || !data.data || !data.data.length) {
- alert('No se encontraron resultados externos para "' + pn + '"');
+ // ─── OFFLINE FALLBACK ───
+ function enterOfflineMode() {
+ isOffline = true;
+ document.getElementById('offlineBanner').style.display = '';
+ document.getElementById('offlineBannerText').innerHTML = 'Modo offline — 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;
}
- var msg = data.data.map(function (r) {
- return (r.name || r.part_number || pn) + ' - Stock: ' + (r.stock || 'N/A');
- }).join('\n');
- alert('Resultados externos:\n' + msg);
- }
+ // 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);
+ }
+ });
- // ─── Helpers ───
- function fmt(n) { return (parseFloat(n) || 0).toFixed(2); }
- function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
-
- // ─── Search input ───
- var searchTimeout = null;
- if (searchInput) {
- searchInput.addEventListener('input', function () {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(function () {
- currentFilters.q = searchInput.value.trim();
- loadCatalog(1, currentFilters);
- }, 300);
- });
- searchInput.addEventListener('keydown', function (e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- clearTimeout(searchTimeout);
- currentFilters.q = searchInput.value.trim();
- loadCatalog(1, currentFilters);
- }
- });
- }
-
- // ─── Part search button ───
- if (partSearchBtn) {
- partSearchBtn.addEventListener('click', function () {
- if (searchInput) {
- currentFilters.q = searchInput.value.trim();
- loadCatalog(1, currentFilters);
- }
- });
- }
-
- // ─── Filter chips (dynamic from inventory brands) ───
- async function loadBrandChips() {
- var data = await apiFetch(API + '/catalog/brands');
- if (!data || !data.data) return;
- var container = document.getElementById('brandChips');
- if (!container) return;
- container.innerHTML = '';
- data.data.forEach(function(b) {
- var btn = document.createElement('button');
- btn.className = 'chip';
- btn.setAttribute('data-chip', b.name);
- btn.setAttribute('aria-pressed', 'false');
- btn.textContent = b.name + ' (' + b.count + ')';
- container.appendChild(btn);
- });
- // Re-wire all chip clicks (including the "Todos" chip)
- wireChipClicks();
- }
-
- function wireChipClicks() {
- document.querySelectorAll('[data-chip]').forEach(function (chip) {
- chip.addEventListener('click', function () {
- var isActive = chip.classList.contains('is-active');
- document.querySelectorAll('[data-chip]').forEach(function (c) {
- c.classList.remove('is-active');
- c.setAttribute('aria-pressed', 'false');
- });
- if (!isActive && chip.dataset.chip) {
- chip.classList.add('is-active');
- chip.setAttribute('aria-pressed', 'true');
- currentFilters.brand = chip.dataset.chip;
- delete currentFilters.chip;
- } else {
- // "Todos" or deselect
- document.querySelector('[data-chip=""]').classList.add('is-active');
- document.querySelector('[data-chip=""]').setAttribute('aria-pressed', 'true');
- delete currentFilters.brand;
- delete currentFilters.chip;
- }
- loadCatalog(1, currentFilters);
+ // ─── 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');
+ }
+ });
- loadBrandChips();
- wireChipClicks();
-
- // ─── Expose globals for inline onclick handlers ───
+ // ─── EXPOSE GLOBALS (for backward compat) ───
window.CatalogApp = {
toggleCart: toggleCart,
goToCheckout: goToCheckout,
- addToCart: addToCartById,
+ addToCart: addToCart,
removeFromCart: removeFromCart,
updateQty: updateQuantity,
- clearCart: clearCartFn,
- loadPage: function (p) { loadCatalog(p); },
- checkExternal: checkExternalAvailability
+ clearCart: clearCart,
+ loadPage: function (p) { loadParts(p); },
};
- // Also keep legacy window._ handlers for backward compat
- window.toggleCart = toggleCart;
- window.goToCheckout = goToCheckout;
- window.checkExternalAvailability = checkExternalAvailability;
-
- // ─── Init ───
+ // ─── INIT ───
renderCart();
- loadCatalog(1, {});
+ loadBrands();
+
})();
diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html
index e76f11a..77bea4e 100644
--- a/pos/templates/catalog.html
+++ b/pos/templates/catalog.html
@@ -4,7 +4,7 @@
- Catálogo — Nexus Autoparts POS
+ Catalogo — Nexus Autoparts POS
-
+
Tema:
-
Industrial
-
Moderno
@@ -1637,17 +458,11 @@
-
+
-
-
-
-
-
Modo offline — Funciones limitadas. Solo consultas en cache disponibles.
+
+ Modo offline — Mostrando solo tu inventario local.
×