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 = '' + escHtml(it.name) + ''; - } else { - imgHtml = '' + - ''; - } - - 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 ''; + }).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 ''; + }).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 ''; + }).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 ''; + }).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 ''; + }).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 ''; + }).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 + ? '' + esc(p.name) + '' + : ''; + + 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 += ''; + html += ''; } else { - html += ''; + html += ''; } - // Page numbers var pages = buildPageNumbers(pg.page, pg.total_pages); pages.forEach(function (p) { if (p === '...') { - html += ''; + html += '...'; } else if (p === pg.page) { - html += ''; + html += ''; } else { - html += ''; + html += ''; } }); - // Next button if (pg.page >= pg.total_pages) { - html += ''; + html += ''; } else { - html += ''; + html += ''; } 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 += ''; + bodegas.forEach(function (b) { + html += ''; + }); + html += '
BodegaPrecioStock
' + esc(b.business_name) + '' + (b.price ? '$' + fmt(b.price) : '--') + '' + b.stock + '
'; + } + + // 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) + '
' + - '' + + '' + '
'; }).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 - + - + -