diff --git a/pos/static/css/common.css b/pos/static/css/common.css index 86caa36..e79fc00 100644 --- a/pos/static/css/common.css +++ b/pos/static/css/common.css @@ -55,3 +55,31 @@ body { border-radius: var(--radius); padding: 24px; } + +/* Catalog grid */ +.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; } +.catalog-card { cursor: pointer; transition: all 0.2s; } +.catalog-card:hover { border-color: var(--color-primary); transform: translateY(-2px); box-shadow: var(--shadow); } +.stock-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; } +.stock-badge--ok { background: #dcfce7; color: #166534; } +.stock-badge--low { background: #fef9c3; color: #854d0e; } +.stock-badge--zero { background: #fecaca; color: #991b1b; } + +/* Cart sidebar */ +.cart-sidebar { position: fixed; right: 0; top: 0; bottom: 0; width: 360px; background: var(--color-surface); border-left: 1px solid var(--color-border); padding: 20px; overflow-y: auto; transform: translateX(100%); transition: transform 0.3s; z-index: 50; } +.cart-sidebar.open { transform: translateX(0); } +.cart-item { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--color-border); } +.cart-total { font-family: var(--font-mono); font-size: 1.3rem; font-weight: 700; } + +/* Search bar */ +.search-bar { display: flex; gap: 8px; margin-bottom: 20px; } +.search-bar input { flex: 1; padding: 10px 16px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 1rem; } +.search-bar input:focus { outline: none; border-color: var(--color-primary); } + +/* Filter chips */ +.filter-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; } +.chip { padding: 4px 12px; border-radius: 20px; border: 1px solid var(--color-border); font-size: 0.8rem; cursor: pointer; background: transparent; } +.chip.active { background: var(--color-primary); color: white; border-color: var(--color-primary); } + +/* External availability */ +.external-results { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: var(--radius); padding: 16px; margin-top: 12px; } diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js new file mode 100644 index 0000000..a775fc0 --- /dev/null +++ b/pos/static/js/catalog.js @@ -0,0 +1,293 @@ +// /home/Autopartes/pos/static/js/catalog.js +// Catalog UI: browsable inventory with cart, barcode scanner, external lookup + +(function () { + 'use strict'; + + const API = '/pos/api'; + const token = localStorage.getItem('pos_token'); + if (!token) { window.location.href = '/pos/login'; return; } + + const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; + + // ─── State ─── + let currentPage = 1; + let currentFilters = {}; + let cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]'); + let barcodeBuffer = ''; + let barcodeTimeout = null; + + // ─── 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(); + } + + // ─── Catalog loading ─── + async function loadCatalog(page, filters) { + currentPage = page || 1; + currentFilters = filters || currentFilters; + const 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()); + if (!data) return; + + renderGrid(data.data || []); + renderPagination(data.pagination || {}); + renderActiveFilters(); + } + + function renderGrid(items) { + const grid = document.getElementById('catalogGrid'); + if (!items.length) { + grid.innerHTML = '

No se encontraron productos

'; + 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; }); + } + + 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; + } + + 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(''); + } + + // ─── 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(''); + } + + // ─── 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); + } + + // 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 === 'Enter' && barcodeBuffer.length >= 4) { + lookupBarcode(barcodeBuffer.trim()); + barcodeBuffer = ''; + return; + } + if (e.key.length === 1) { + barcodeBuffer += e.key; + clearTimeout(barcodeTimeout); + barcodeTimeout = setTimeout(function () { barcodeBuffer = ''; }, 200); + } + }); + + // ─── Cart ─── + function addToCart(item) { + var existing = cartItems.find(function (c) { return c.id === item.id; }); + if (existing) { + existing.quantity += 1; + } 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, + unit: item.unit || 'PZA', stock: item.stock, quantity: 1 + }); + } + saveCart(); + renderCart(); + } + + 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 clearCartFn() { + cartItems = []; + saveCart(); + renderCart(); + } + + function saveCart() { + localStorage.setItem('pos_cart', JSON.stringify(cartItems)); + } + + 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 (!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'; + return; + } + + empty.style.display = 'none'; + checkoutBtn.disabled = false; + + var subtotal = 0; + var tax = 0; + container.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) + '
    ' + + '
    ' + + '' + + '' + c.quantity + '' + + '' + + '
    ' + + '
    ' + + '
    $' + fmt(lineTotal) + '
    ' + + '' + + '
    '; + }).join(''); + + document.getElementById('cartSubtotal').textContent = '$' + fmt(subtotal); + document.getElementById('cartTax').textContent = '$' + fmt(tax); + document.getElementById('cartTotal').textContent = '$' + fmt(subtotal + tax); + } + + function toggleCart() { + document.getElementById('cartSidebar').classList.toggle('open'); + } + + function goToCheckout() { + localStorage.setItem('pos_cart', JSON.stringify(cartItems)); + window.location.href = '/pos/sale'; + } + + // ─── External availability ─── + 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...

    '; + + 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) + '"

    '; + return; + } + results.innerHTML = ''; + } + + // ─── 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 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(); + clearTimeout(searchTimeout); + currentFilters.q = searchInput.value.trim(); + loadCatalog(1, currentFilters); + } + }); + + // ─── Expose globals for inline handlers ─── + window._addToCart = function (id) { + var it = window._catalogItems && window._catalogItems[id]; + if (it) addToCart(it); + }; + 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; + 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 new file mode 100644 index 0000000..7d36555 --- /dev/null +++ b/pos/templates/catalog.html @@ -0,0 +1,91 @@ + + + + + + + Catalogo - Nexus POS + + + + +
    +

    Catalogo

    + + +
    + +
    + + +
    +
    +
    + + + +
    +
    + + +
    +
    +

    Carrito

    + +
    +
    +
    Carrito vacio
    +
    +
    + Subtotal$0.00 +
    +
    + IVA$0.00 +
    +
    + Total$0.00 +
    + + +
    + + + +