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 = '' + data.data.map(function (r) {
+ return '- ' + escHtml(r.name || r.part_number || pn) + ' — Stock: ' + (r.stock || 'N/A') + '
';
+ }).join('') + '
';
+ }
+
+ // ─── 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Disponibilidad en Bodegas Nexus
+
+
+
+
+
+
+
+
+
+
+