diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js index a775fc0..b91c53b 100644 --- a/pos/static/js/catalog.js +++ b/pos/static/js/catalog.js @@ -1,5 +1,6 @@ // /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. (function () { 'use strict'; @@ -10,99 +11,201 @@ const 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'); + // ─── 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 // ─── API helpers ─── async function apiFetch(url, opts) { - const resp = await fetch(url, Object.assign({ headers: headers }, opts || {})); - if (resp.status === 401) { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; } - return resp.json(); + try { + var resp = await fetch(url, Object.assign({ headers: headers }, opts || {})); + if (resp.status === 401) { + localStorage.removeItem('pos_token'); + window.location.href = '/pos/login'; + 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; - const params = new URLSearchParams({ page: currentPage, per_page: 30 }); + 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); - const data = await apiFetch(API + '/catalog/search?' + params.toString()); + var data = await apiFetch(API + '/catalog/search?' + params.toString()); if (!data) return; - renderGrid(data.data || []); + var items = data.data || []; + renderGrid(items); renderPagination(data.pagination || {}); - renderActiveFilters(); + updateResultsCount(data.pagination || {}); } function renderGrid(items) { - const grid = document.getElementById('catalogGrid'); + // Clear demo/hardcoded cards + productGrid.innerHTML = ''; + if (!items.length) { - grid.innerHTML = '

No se encontraron productos

'; + 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; } - grid.innerHTML = items.map(function (it) { - var stockClass = it.stock <= 0 ? 'stock-badge--zero' : (it.low_stock ? 'stock-badge--low' : 'stock-badge--ok'); - var stockLabel = it.stock <= 0 ? 'Agotado' : it.stock + ' ' + (it.unit || 'PZA'); - return '
' + - (it.image_url ? '' : '
Sin imagen
') + - '
' + escHtml(it.name) + '
' + - '
' + escHtml(it.part_number) + (it.brand ? ' · ' + escHtml(it.brand) : '') + '
' + - '
' + - '$' + fmt(it.price_1) + '' + - '' + stockLabel + '' + - '
'; - }).join(''); - // Store items for cart lookup - window._catalogItems = {}; - items.forEach(function (it) { window._catalogItems[it.id] = it; }); + 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 renderPagination(pg) { - var el = document.getElementById('pagination'); - if (!pg || pg.total_pages <= 1) { el.innerHTML = ''; return; } - var html = ''; - html += '' + pg.page + ' / ' + pg.total_pages + ''; - html += ''; - el.innerHTML = html; + if (!paginationNav) return; + if (!pg || pg.total_pages <= 1) { + paginationNav.innerHTML = ''; + return; + } + + var html = ''; + + // Previous button + if (pg.page <= 1) { + html += ''; + } else { + html += ''; + } + + // Page numbers + var pages = buildPageNumbers(pg.page, pg.total_pages); + pages.forEach(function (p) { + if (p === '...') { + html += ''; + } else if (p === pg.page) { + html += ''; + } else { + html += ''; + } + }); + + // Next button + if (pg.page >= pg.total_pages) { + html += ''; + } else { + html += ''; + } + + paginationNav.innerHTML = html; } - function renderActiveFilters() { - var el = document.getElementById('activeFilters'); - var chips = []; - if (currentFilters.category) chips.push('Cat: ' + currentFilters.category + ' ×'); - if (currentFilters.brand) chips.push('' + escHtml(currentFilters.brand) + ' ×'); - if (currentFilters.vehicle_brand) chips.push('Vehiculo: ' + escHtml(currentFilters.vehicle_brand) + ' ×'); - el.innerHTML = chips.join(''); + 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; } - // ─── Sidebar filters ─── - async function loadCategories() { - var data = await apiFetch(API + '/catalog/categories'); - if (!data) return; - var ul = document.getElementById('categoryList'); - var cats = data.data || []; - if (!cats.length) { ul.innerHTML = '
  • Sin categorias
  • '; return; } - ul.innerHTML = '
  • Todas
  • ' + - cats.map(function (c) { return '
  • Cat #' + c.id + ' (' + c.count + ')
  • '; }).join(''); - } - - async function loadBrands() { - var data = await apiFetch(API + '/catalog/brands'); - if (!data) return; - var ul = document.getElementById('brandList'); - var brands = data.data || []; - if (!brands.length) { ul.innerHTML = '
  • Sin marcas
  • '; return; } - ul.innerHTML = '
  • Todas
  • ' + - brands.map(function (b) { return '
  • ' + escHtml(b.name) + ' (' + b.count + ')
  • '; }).join(''); + 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'; } // ─── Barcode scanner ─── @@ -112,10 +215,13 @@ addToCart(data); } - // Listen for rapid keypress (barcode scanners type fast, then Enter) document.addEventListener('keydown', function (e) { - if (e.key === 'F1') { e.preventDefault(); document.getElementById('searchInput').focus(); return; } - if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return; + if (e.key === 'F1') { + e.preventDefault(); + if (searchInput) searchInput.focus(); + return; + } + 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 = ''; @@ -129,6 +235,11 @@ }); // ─── Cart ─── + function addToCartById(id) { + var it = catalogItemsMap[id]; + if (it) addToCart(it); + } + function addToCart(item) { var existing = cartItems.find(function (c) { return c.id === item.id; }); if (existing) { @@ -136,12 +247,16 @@ } else { cartItems.push({ id: item.id, part_number: item.part_number, name: item.name, - brand: item.brand, price: item.price_1, tax_rate: item.tax_rate || 0.16, + 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 }); } saveCart(); renderCart(); + // Brief open to show item was added + if (!cartSidebar.classList.contains('open')) { + toggleCart(); + } } function removeFromCart(index) { @@ -169,60 +284,59 @@ } function renderCart() { - var badge = document.getElementById('cartBadge'); var total = cartItems.reduce(function (s, c) { return s + c.quantity; }, 0); - badge.textContent = total; - badge.style.display = total > 0 ? 'flex' : 'none'; - - var container = document.getElementById('cartItems'); - var empty = document.getElementById('cartEmpty'); - var checkoutBtn = document.getElementById('checkoutBtn'); + if (cartBadge) { + cartBadge.textContent = total; + cartBadge.style.display = total > 0 ? 'flex' : 'none'; + } if (!cartItems.length) { - container.innerHTML = ''; - empty.style.display = 'block'; - checkoutBtn.disabled = true; - document.getElementById('cartSubtotal').textContent = '$0.00'; - document.getElementById('cartTax').textContent = '$0.00'; - document.getElementById('cartTotal').textContent = '$0.00'; + cartItemsEl.innerHTML = ''; + cartEmptyEl.style.display = 'block'; + if (checkoutBtn) checkoutBtn.disabled = true; + cartSubtotalEl.textContent = '$0.00'; + cartTaxEl.textContent = '$0.00'; + cartTotalEl.textContent = '$0.00'; return; } - empty.style.display = 'none'; - checkoutBtn.disabled = false; + cartEmptyEl.style.display = 'none'; + if (checkoutBtn) checkoutBtn.disabled = false; var subtotal = 0; var tax = 0; - container.innerHTML = cartItems.map(function (c, i) { + cartItemsEl.innerHTML = cartItems.map(function (c, i) { var lineTotal = c.price * c.quantity; var lineTax = lineTotal * c.tax_rate; subtotal += lineTotal; tax += lineTax; return '
    ' + '
    ' + - '
    ' + escHtml(c.name) + '
    ' + - '
    ' + escHtml(c.part_number) + '
    ' + + '
    ' + escHtml(c.name) + '
    ' + + '
    ' + escHtml(c.part_number) + '
    ' + '
    ' + - '' + - '' + c.quantity + '' + - '' + + '' + + '' + c.quantity + '' + + '' + '
    ' + '
    ' + - '
    $' + fmt(lineTotal) + '
    ' + - '' + + '
    $' + fmt(lineTotal) + '
    ' + + '' + '
    '; }).join(''); - document.getElementById('cartSubtotal').textContent = '$' + fmt(subtotal); - document.getElementById('cartTax').textContent = '$' + fmt(tax); - document.getElementById('cartTotal').textContent = '$' + fmt(subtotal + tax); + cartSubtotalEl.textContent = '$' + fmt(subtotal); + cartTaxEl.textContent = '$' + fmt(tax); + cartTotalEl.textContent = '$' + fmt(subtotal + tax); } function toggleCart() { - document.getElementById('cartSidebar').classList.toggle('open'); + var isOpen = cartSidebar.classList.toggle('open'); + if (cartOverlay) cartOverlay.classList.toggle('open', isOpen); } function goToCheckout() { + if (!cartItems.length) return; localStorage.setItem('pos_cart', JSON.stringify(cartItems)); window.location.href = '/pos/sale'; } @@ -231,19 +345,17 @@ async function checkExternalAvailability(partNumber) { var pn = partNumber || currentFilters.q || ''; if (!pn) return; - var section = document.getElementById('externalSection'); - var results = document.getElementById('externalResults'); - section.style.display = 'block'; - results.innerHTML = '

    Buscando en bodegas...

    '; + alert('Buscando "' + pn + '" en bodegas Nexus...'); var data = await apiFetch(API + '/catalog/external-availability/' + encodeURIComponent(pn)); if (!data || !data.data || !data.data.length) { - results.innerHTML = '

    No se encontraron resultados externos para "' + escHtml(pn) + '"

    '; + alert('No se encontraron resultados externos para "' + pn + '"'); return; } - results.innerHTML = ''; + 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); } // ─── Helpers ─── @@ -251,43 +363,73 @@ function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ─── Search input ─── - var searchInput = document.getElementById('searchInput'); var searchTimeout = null; - searchInput.addEventListener('input', function () { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(function () { - currentFilters.q = searchInput.value.trim(); - loadCatalog(1, currentFilters); - }, 350); - }); - searchInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); + if (searchInput) { + searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); - currentFilters.q = searchInput.value.trim(); + 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 ─── + document.querySelectorAll('[data-chip]').forEach(function (chip) { + chip.addEventListener('click', function () { + var isActive = chip.classList.contains('is-active'); + // Deactivate all chips first for single-select behavior + document.querySelectorAll('[data-chip]').forEach(function (c) { + c.classList.remove('is-active'); + c.setAttribute('aria-pressed', 'false'); + }); + if (!isActive) { + chip.classList.add('is-active'); + chip.setAttribute('aria-pressed', 'true'); + currentFilters.category = chip.dataset.chip; + } else { + delete currentFilters.category; + } loadCatalog(1, currentFilters); - } + }); }); - // ─── Expose globals for inline handlers ─── - window._addToCart = function (id) { - var it = window._catalogItems && window._catalogItems[id]; - if (it) addToCart(it); + // ─── Expose globals for inline onclick handlers ─── + window.CatalogApp = { + toggleCart: toggleCart, + goToCheckout: goToCheckout, + addToCart: addToCartById, + removeFromCart: removeFromCart, + updateQty: updateQuantity, + clearCart: clearCartFn, + loadPage: function (p) { loadCatalog(p); }, + checkExternal: checkExternalAvailability }; - window._loadPage = function (p) { loadCatalog(p); }; - window._removeFilter = function (key) { delete currentFilters[key]; loadCatalog(1); loadCategories(); loadBrands(); }; - window._filterCat = function (id) { if (id) currentFilters.category = id; else delete currentFilters.category; loadCatalog(1); loadCategories(); }; - window._filterBrand = function (name) { if (name) currentFilters.brand = name; else delete currentFilters.brand; loadCatalog(1); loadBrands(); }; - window._removeFromCart = removeFromCart; - window._updateQty = updateQuantity; + + // Also keep legacy window._ handlers for backward compat window.toggleCart = toggleCart; window.goToCheckout = goToCheckout; - window.clearCart = clearCartFn; window.checkExternalAvailability = checkExternalAvailability; // ─── Init ─── renderCart(); loadCatalog(1, {}); - loadCategories(); - loadBrands(); })(); diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html index 958672e..7830064 100644 --- a/pos/templates/catalog.html +++ b/pos/templates/catalog.html @@ -1483,6 +1483,134 @@ padding: var(--space-1); opacity: 0.7; color: inherit; } .banner__dismiss:hover { opacity: 1; } + + /* ========================================================================= + CART SIDEBAR + ========================================================================= */ + + .cart-fab { + position: fixed; + bottom: var(--space-6); + right: var(--space-6); + z-index: var(--z-modal); + width: 56px; + height: 56px; + border-radius: var(--radius-full); + background: var(--color-primary); + color: var(--color-text-inverse); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-lg); + transition: var(--transition-fast); + } + + .cart-fab:hover { + transform: scale(1.08); + box-shadow: var(--shadow-xl); + } + + .cart-fab__badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + padding: 0 5px; + border-radius: var(--radius-full); + background: var(--color-error, #ef4444); + color: #fff; + font-size: 11px; + font-weight: var(--font-weight-bold); + display: none; + align-items: center; + justify-content: center; + line-height: 1; + } + + .cart-sidebar { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 360px; + max-width: 100vw; + z-index: var(--z-modal); + background: var(--color-bg-elevated); + border-left: 1px solid var(--color-border); + box-shadow: var(--shadow-xl); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform var(--duration-normal) var(--ease-in-out); + } + + .cart-sidebar.open { + transform: translateX(0); + } + + .cart-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; + } + + .cart-header h3 { + font-family: var(--font-heading); + font-size: var(--text-h5); + font-weight: var(--heading-weight-secondary); + color: var(--color-text-primary); + } + + .cart-items { + flex: 1; + overflow-y: auto; + padding: var(--space-3) var(--space-4); + } + + .cart-item { + display: flex; + gap: var(--space-3); + padding: var(--space-3) 0; + border-bottom: 1px solid var(--color-border); + } + + .cart-item:last-child { + border-bottom: none; + } + + .cart-footer { + padding: var(--space-4) var(--space-5); + border-top: 1px solid var(--color-border); + flex-shrink: 0; + background: var(--color-bg-elevated); + } + + .cart-totals { + display: flex; + flex-direction: column; + gap: var(--space-1); + font-size: var(--text-body-sm); + color: var(--color-text-secondary); + } + + .cart-overlay { + position: fixed; + inset: 0; + z-index: calc(var(--z-modal) - 1); + background: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(2px); + display: none; + } + + .cart-overlay.open { + display: block; + } @@ -2172,6 +2300,33 @@ }); + + + + +
    + + + + +