fix(pos): rewrite catalog.js to match design system HTML structure + add cart sidebar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
// /home/Autopartes/pos/static/js/catalog.js
|
// /home/Autopartes/pos/static/js/catalog.js
|
||||||
// Catalog UI: browsable inventory with cart, barcode scanner, external lookup
|
// Catalog UI: browsable inventory with cart, barcode scanner, external lookup
|
||||||
|
// Aligned with design-system catalog.html IDs and class names.
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -10,99 +11,201 @@
|
|||||||
|
|
||||||
const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
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 ───
|
// ─── State ───
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let currentFilters = {};
|
let currentFilters = {};
|
||||||
let cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]');
|
let cartItems = JSON.parse(localStorage.getItem('pos_cart') || '[]');
|
||||||
let barcodeBuffer = '';
|
let barcodeBuffer = '';
|
||||||
let barcodeTimeout = null;
|
let barcodeTimeout = null;
|
||||||
|
let catalogItemsMap = {}; // id -> item for cart lookup
|
||||||
|
|
||||||
// ─── API helpers ───
|
// ─── API helpers ───
|
||||||
async function apiFetch(url, opts) {
|
async function apiFetch(url, opts) {
|
||||||
const resp = await fetch(url, Object.assign({ headers: headers }, opts || {}));
|
try {
|
||||||
if (resp.status === 401) { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; }
|
var resp = await fetch(url, Object.assign({ headers: headers }, opts || {}));
|
||||||
return resp.json();
|
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 ───
|
// ─── Catalog loading ───
|
||||||
async function loadCatalog(page, filters) {
|
async function loadCatalog(page, filters) {
|
||||||
currentPage = page || 1;
|
currentPage = page || 1;
|
||||||
currentFilters = filters || currentFilters;
|
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.q) params.set('q', currentFilters.q);
|
||||||
if (currentFilters.category) params.set('category', currentFilters.category);
|
if (currentFilters.category) params.set('category', currentFilters.category);
|
||||||
if (currentFilters.brand) params.set('brand', currentFilters.brand);
|
if (currentFilters.brand) params.set('brand', currentFilters.brand);
|
||||||
if (currentFilters.vehicle_brand) params.set('vehicle_brand', currentFilters.vehicle_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;
|
if (!data) return;
|
||||||
|
|
||||||
renderGrid(data.data || []);
|
var items = data.data || [];
|
||||||
|
renderGrid(items);
|
||||||
renderPagination(data.pagination || {});
|
renderPagination(data.pagination || {});
|
||||||
renderActiveFilters();
|
updateResultsCount(data.pagination || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGrid(items) {
|
function renderGrid(items) {
|
||||||
const grid = document.getElementById('catalogGrid');
|
// Clear demo/hardcoded cards
|
||||||
|
productGrid.innerHTML = '';
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
grid.innerHTML = '<div class="empty-state"><p>No se encontraron productos</p><button onclick="checkExternalAvailability()" style="margin-top:12px; padding:8px 16px; cursor:pointer;">Buscar en bodegas Nexus</button></div>';
|
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;
|
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 '<div class="card catalog-card" onclick="window._addToCart(' + it.id + ')" data-id="' + it.id + '">' +
|
|
||||||
(it.image_url ? '<img src="' + it.image_url + '" alt="" style="width:100%;height:120px;object-fit:contain;margin-bottom:8px;">' : '<div style="height:120px;background:#f0f0f0;display:flex;align-items:center;justify-content:center;margin-bottom:8px;border-radius:4px;color:#aaa;">Sin imagen</div>') +
|
|
||||||
'<div style="font-weight:600;font-size:0.9rem;margin-bottom:4px;">' + escHtml(it.name) + '</div>' +
|
|
||||||
'<div style="font-size:0.8rem;color:#666;margin-bottom:4px;">' + escHtml(it.part_number) + (it.brand ? ' · ' + escHtml(it.brand) : '') + '</div>' +
|
|
||||||
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
|
||||||
'<span style="font-weight:700;font-size:1rem;">$' + fmt(it.price_1) + '</span>' +
|
|
||||||
'<span class="stock-badge ' + stockClass + '">' + stockLabel + '</span>' +
|
|
||||||
'</div></div>';
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Store items for cart lookup
|
emptyState.classList.remove('is-visible');
|
||||||
window._catalogItems = {};
|
productGrid.style.display = '';
|
||||||
items.forEach(function (it) { window._catalogItems[it.id] = it; });
|
|
||||||
|
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 = '<img src="' + escHtml(it.image_url) + '" alt="' + escHtml(it.name) + '" style="width:100%;height:100%;object-fit:contain;">';
|
||||||
|
} else {
|
||||||
|
imgHtml = '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>' +
|
||||||
|
'<span class="product-card__image-label" aria-hidden="true">IMG</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<article class="product-card" role="listitem" data-id="' + it.id + '">' +
|
||||||
|
'<div class="product-card__image">' +
|
||||||
|
imgHtml +
|
||||||
|
'<div class="stock-badge ' + stockClass + '" role="status">' + stockLabel + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="product-card__body">' +
|
||||||
|
(it.category_name ? '<div class="product-card__category">' + escHtml(it.category_name) + '</div>' : '') +
|
||||||
|
'<div class="product-card__name">' + escHtml(it.name) + '</div>' +
|
||||||
|
'<div class="product-card__oem" title="Numero de parte">' + escHtml(it.part_number || '') + '</div>' +
|
||||||
|
'<div class="product-card__brand">' +
|
||||||
|
'<span>' + escHtml(it.brand || '') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="product-card__footer">' +
|
||||||
|
'<div class="product-card__pricing">' +
|
||||||
|
'<div class="product-card__price">$' + fmt(it.price_1) + '</div>' +
|
||||||
|
'<div class="product-card__price-unit">MXN / ' + escHtml(it.unit || 'PZA') + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
(isOut
|
||||||
|
? '<button class="btn-add" disabled style="opacity:0.45;cursor:not-allowed;" aria-label="Producto agotado">' +
|
||||||
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg> Agotado</button>'
|
||||||
|
: '<button class="btn-add" onclick="CatalogApp.addToCart(' + it.id + ')" aria-label="Agregar ' + escHtml(it.name) + ' al carrito">' +
|
||||||
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg> Agregar</button>'
|
||||||
|
) +
|
||||||
|
'</div>' +
|
||||||
|
'</article>';
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPagination(pg) {
|
function renderPagination(pg) {
|
||||||
var el = document.getElementById('pagination');
|
if (!paginationNav) return;
|
||||||
if (!pg || pg.total_pages <= 1) { el.innerHTML = ''; return; }
|
if (!pg || pg.total_pages <= 1) {
|
||||||
var html = '<button ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadPage(' + (pg.page - 1) + ')"') + '>« Anterior</button>';
|
paginationNav.innerHTML = '';
|
||||||
html += '<span style="padding:6px 12px; font-size:0.85rem;">' + pg.page + ' / ' + pg.total_pages + '</span>';
|
return;
|
||||||
html += '<button ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadPage(' + (pg.page + 1) + ')"') + '>Siguiente »</button>';
|
}
|
||||||
el.innerHTML = html;
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
if (pg.page <= 1) {
|
||||||
|
html += '<button class="page-item page-item--wide is-disabled" disabled aria-label="Pagina anterior">' +
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15,18 9,12 15,6"/></svg> Anterior</button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="page-item page-item--wide" onclick="CatalogApp.loadPage(' + (pg.page - 1) + ')" aria-label="Pagina anterior">' +
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15,18 9,12 15,6"/></svg> Anterior</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page numbers
|
||||||
|
var pages = buildPageNumbers(pg.page, pg.total_pages);
|
||||||
|
pages.forEach(function (p) {
|
||||||
|
if (p === '...') {
|
||||||
|
html += '<span class="page-ellipsis" aria-hidden="true">…</span>';
|
||||||
|
} else if (p === pg.page) {
|
||||||
|
html += '<button class="page-item is-active" aria-label="Pagina ' + p + ', actual" aria-current="page">' + p + '</button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="page-item" onclick="CatalogApp.loadPage(' + p + ')" aria-label="Ir a pagina ' + p + '">' + p + '</button>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
if (pg.page >= pg.total_pages) {
|
||||||
|
html += '<button class="page-item page-item--wide is-disabled" disabled aria-label="Pagina siguiente">Siguiente ' +
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"/></svg></button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="page-item page-item--wide" onclick="CatalogApp.loadPage(' + (pg.page + 1) + ')" aria-label="Pagina siguiente">Siguiente ' +
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"/></svg></button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationNav.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderActiveFilters() {
|
function buildPageNumbers(current, total) {
|
||||||
var el = document.getElementById('activeFilters');
|
if (total <= 7) {
|
||||||
var chips = [];
|
var arr = [];
|
||||||
if (currentFilters.category) chips.push('<span class="chip active" onclick="window._removeFilter(\'category\')">Cat: ' + currentFilters.category + ' ×</span>');
|
for (var i = 1; i <= total; i++) arr.push(i);
|
||||||
if (currentFilters.brand) chips.push('<span class="chip active" onclick="window._removeFilter(\'brand\')">' + escHtml(currentFilters.brand) + ' ×</span>');
|
return arr;
|
||||||
if (currentFilters.vehicle_brand) chips.push('<span class="chip active" onclick="window._removeFilter(\'vehicle_brand\')">Vehiculo: ' + escHtml(currentFilters.vehicle_brand) + ' ×</span>');
|
}
|
||||||
el.innerHTML = chips.join('');
|
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 ───
|
function updateResultsCount(pg) {
|
||||||
async function loadCategories() {
|
var el = document.querySelector('.results-count');
|
||||||
var data = await apiFetch(API + '/catalog/categories');
|
if (!el || !pg) return;
|
||||||
if (!data) return;
|
var total = pg.total || 0;
|
||||||
var ul = document.getElementById('categoryList');
|
el.innerHTML = '<strong>' + total.toLocaleString('es-MX') + '</strong> partes encontradas';
|
||||||
var cats = data.data || [];
|
|
||||||
if (!cats.length) { ul.innerHTML = '<li style="color:#999;">Sin categorias</li>'; return; }
|
|
||||||
ul.innerHTML = '<li onclick="window._filterCat(null)" class="' + (!currentFilters.category ? 'active' : '') + '">Todas</li>' +
|
|
||||||
cats.map(function (c) { return '<li onclick="window._filterCat(' + c.id + ')" class="' + (currentFilters.category == c.id ? 'active' : '') + '">Cat #' + c.id + ' <small>(' + c.count + ')</small></li>'; }).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 = '<li style="color:#999;">Sin marcas</li>'; return; }
|
|
||||||
ul.innerHTML = '<li onclick="window._filterBrand(null)" class="' + (!currentFilters.brand ? 'active' : '') + '">Todas</li>' +
|
|
||||||
brands.map(function (b) { return '<li onclick="window._filterBrand(\'' + escHtml(b.name) + '\')" class="' + (currentFilters.brand === b.name ? 'active' : '') + '">' + escHtml(b.name) + ' <small>(' + b.count + ')</small></li>'; }).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Barcode scanner ───
|
// ─── Barcode scanner ───
|
||||||
@@ -112,10 +215,13 @@
|
|||||||
addToCart(data);
|
addToCart(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for rapid keypress (barcode scanners type fast, then Enter)
|
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
if (e.key === 'F1') { e.preventDefault(); document.getElementById('searchInput').focus(); return; }
|
if (e.key === 'F1') {
|
||||||
if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;
|
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) {
|
if (e.key === 'Enter' && barcodeBuffer.length >= 4) {
|
||||||
lookupBarcode(barcodeBuffer.trim());
|
lookupBarcode(barcodeBuffer.trim());
|
||||||
barcodeBuffer = '';
|
barcodeBuffer = '';
|
||||||
@@ -129,6 +235,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ─── Cart ───
|
// ─── Cart ───
|
||||||
|
function addToCartById(id) {
|
||||||
|
var it = catalogItemsMap[id];
|
||||||
|
if (it) addToCart(it);
|
||||||
|
}
|
||||||
|
|
||||||
function addToCart(item) {
|
function addToCart(item) {
|
||||||
var existing = cartItems.find(function (c) { return c.id === item.id; });
|
var existing = cartItems.find(function (c) { return c.id === item.id; });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -136,12 +247,16 @@
|
|||||||
} else {
|
} else {
|
||||||
cartItems.push({
|
cartItems.push({
|
||||||
id: item.id, part_number: item.part_number, name: item.name,
|
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
|
unit: item.unit || 'PZA', stock: item.stock, quantity: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
saveCart();
|
saveCart();
|
||||||
renderCart();
|
renderCart();
|
||||||
|
// Brief open to show item was added
|
||||||
|
if (!cartSidebar.classList.contains('open')) {
|
||||||
|
toggleCart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromCart(index) {
|
function removeFromCart(index) {
|
||||||
@@ -169,60 +284,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCart() {
|
function renderCart() {
|
||||||
var badge = document.getElementById('cartBadge');
|
|
||||||
var total = cartItems.reduce(function (s, c) { return s + c.quantity; }, 0);
|
var total = cartItems.reduce(function (s, c) { return s + c.quantity; }, 0);
|
||||||
badge.textContent = total;
|
if (cartBadge) {
|
||||||
badge.style.display = total > 0 ? 'flex' : 'none';
|
cartBadge.textContent = total;
|
||||||
|
cartBadge.style.display = total > 0 ? 'flex' : 'none';
|
||||||
var container = document.getElementById('cartItems');
|
}
|
||||||
var empty = document.getElementById('cartEmpty');
|
|
||||||
var checkoutBtn = document.getElementById('checkoutBtn');
|
|
||||||
|
|
||||||
if (!cartItems.length) {
|
if (!cartItems.length) {
|
||||||
container.innerHTML = '';
|
cartItemsEl.innerHTML = '';
|
||||||
empty.style.display = 'block';
|
cartEmptyEl.style.display = 'block';
|
||||||
checkoutBtn.disabled = true;
|
if (checkoutBtn) checkoutBtn.disabled = true;
|
||||||
document.getElementById('cartSubtotal').textContent = '$0.00';
|
cartSubtotalEl.textContent = '$0.00';
|
||||||
document.getElementById('cartTax').textContent = '$0.00';
|
cartTaxEl.textContent = '$0.00';
|
||||||
document.getElementById('cartTotal').textContent = '$0.00';
|
cartTotalEl.textContent = '$0.00';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
empty.style.display = 'none';
|
cartEmptyEl.style.display = 'none';
|
||||||
checkoutBtn.disabled = false;
|
if (checkoutBtn) checkoutBtn.disabled = false;
|
||||||
|
|
||||||
var subtotal = 0;
|
var subtotal = 0;
|
||||||
var tax = 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 lineTotal = c.price * c.quantity;
|
||||||
var lineTax = lineTotal * c.tax_rate;
|
var lineTax = lineTotal * c.tax_rate;
|
||||||
subtotal += lineTotal;
|
subtotal += lineTotal;
|
||||||
tax += lineTax;
|
tax += lineTax;
|
||||||
return '<div class="cart-item">' +
|
return '<div class="cart-item">' +
|
||||||
'<div style="flex:1;">' +
|
'<div style="flex:1;">' +
|
||||||
'<div style="font-weight:600;font-size:0.85rem;">' + escHtml(c.name) + '</div>' +
|
'<div style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary);">' + escHtml(c.name) + '</div>' +
|
||||||
'<div style="font-size:0.75rem;color:#666;">' + escHtml(c.part_number) + '</div>' +
|
'<div style="font-size:0.75rem;color:var(--color-text-muted);">' + escHtml(c.part_number) + '</div>' +
|
||||||
'<div style="margin-top:4px;display:flex;align-items:center;gap:6px;">' +
|
'<div style="margin-top:4px;display:flex;align-items:center;gap:6px;">' +
|
||||||
'<button onclick="window._updateQty(' + i + ',' + (c.quantity - 1) + ')" style="width:24px;height:24px;border:1px solid #ddd;background:#fff;border-radius:4px;cursor:pointer;">-</button>' +
|
'<button onclick="CatalogApp.updateQty(' + i + ',' + (c.quantity - 1) + ')" style="width:24px;height:24px;border:1px solid var(--color-border);background:var(--color-bg-base);border-radius:var(--radius-sm);cursor:pointer;color:var(--color-text-primary);">-</button>' +
|
||||||
'<span style="font-weight:600;">' + c.quantity + '</span>' +
|
'<span style="font-weight:600;color:var(--color-text-primary);">' + c.quantity + '</span>' +
|
||||||
'<button onclick="window._updateQty(' + i + ',' + (c.quantity + 1) + ')" style="width:24px;height:24px;border:1px solid #ddd;background:#fff;border-radius:4px;cursor:pointer;">+</button>' +
|
'<button onclick="CatalogApp.updateQty(' + i + ',' + (c.quantity + 1) + ')" style="width:24px;height:24px;border:1px solid var(--color-border);background:var(--color-bg-base);border-radius:var(--radius-sm);cursor:pointer;color:var(--color-text-primary);">+</button>' +
|
||||||
'</div></div>' +
|
'</div></div>' +
|
||||||
'<div style="text-align:right;">' +
|
'<div style="text-align:right;">' +
|
||||||
'<div style="font-weight:600;">$' + fmt(lineTotal) + '</div>' +
|
'<div style="font-weight:600;color:var(--color-text-primary);">$' + fmt(lineTotal) + '</div>' +
|
||||||
'<button onclick="window._removeFromCart(' + i + ')" style="font-size:0.75rem;color:#ef4444;background:none;border:none;cursor:pointer;margin-top:4px;">Quitar</button>' +
|
'<button onclick="CatalogApp.removeFromCart(' + i + ')" style="font-size:0.75rem;color:var(--color-error,#ef4444);background:none;border:none;cursor:pointer;margin-top:4px;">Quitar</button>' +
|
||||||
'</div></div>';
|
'</div></div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
document.getElementById('cartSubtotal').textContent = '$' + fmt(subtotal);
|
cartSubtotalEl.textContent = '$' + fmt(subtotal);
|
||||||
document.getElementById('cartTax').textContent = '$' + fmt(tax);
|
cartTaxEl.textContent = '$' + fmt(tax);
|
||||||
document.getElementById('cartTotal').textContent = '$' + fmt(subtotal + tax);
|
cartTotalEl.textContent = '$' + fmt(subtotal + tax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCart() {
|
function toggleCart() {
|
||||||
document.getElementById('cartSidebar').classList.toggle('open');
|
var isOpen = cartSidebar.classList.toggle('open');
|
||||||
|
if (cartOverlay) cartOverlay.classList.toggle('open', isOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToCheckout() {
|
function goToCheckout() {
|
||||||
|
if (!cartItems.length) return;
|
||||||
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
localStorage.setItem('pos_cart', JSON.stringify(cartItems));
|
||||||
window.location.href = '/pos/sale';
|
window.location.href = '/pos/sale';
|
||||||
}
|
}
|
||||||
@@ -231,19 +345,17 @@
|
|||||||
async function checkExternalAvailability(partNumber) {
|
async function checkExternalAvailability(partNumber) {
|
||||||
var pn = partNumber || currentFilters.q || '';
|
var pn = partNumber || currentFilters.q || '';
|
||||||
if (!pn) return;
|
if (!pn) return;
|
||||||
var section = document.getElementById('externalSection');
|
alert('Buscando "' + pn + '" en bodegas Nexus...');
|
||||||
var results = document.getElementById('externalResults');
|
|
||||||
section.style.display = 'block';
|
|
||||||
results.innerHTML = '<p>Buscando en bodegas...</p>';
|
|
||||||
|
|
||||||
var data = await apiFetch(API + '/catalog/external-availability/' + encodeURIComponent(pn));
|
var data = await apiFetch(API + '/catalog/external-availability/' + encodeURIComponent(pn));
|
||||||
if (!data || !data.data || !data.data.length) {
|
if (!data || !data.data || !data.data.length) {
|
||||||
results.innerHTML = '<p>No se encontraron resultados externos para "' + escHtml(pn) + '"</p>';
|
alert('No se encontraron resultados externos para "' + pn + '"');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
results.innerHTML = '<ul>' + data.data.map(function (r) {
|
var msg = data.data.map(function (r) {
|
||||||
return '<li><strong>' + escHtml(r.name || r.part_number || pn) + '</strong> — Stock: ' + (r.stock || 'N/A') + '</li>';
|
return (r.name || r.part_number || pn) + ' - Stock: ' + (r.stock || 'N/A');
|
||||||
}).join('') + '</ul>';
|
}).join('\n');
|
||||||
|
alert('Resultados externos:\n' + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ───
|
// ─── Helpers ───
|
||||||
@@ -251,43 +363,73 @@
|
|||||||
function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||||
|
|
||||||
// ─── Search input ───
|
// ─── Search input ───
|
||||||
var searchInput = document.getElementById('searchInput');
|
|
||||||
var searchTimeout = null;
|
var searchTimeout = null;
|
||||||
searchInput.addEventListener('input', function () {
|
if (searchInput) {
|
||||||
clearTimeout(searchTimeout);
|
searchInput.addEventListener('input', function () {
|
||||||
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);
|
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);
|
loadCatalog(1, currentFilters);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Expose globals for inline handlers ───
|
// ─── Expose globals for inline onclick handlers ───
|
||||||
window._addToCart = function (id) {
|
window.CatalogApp = {
|
||||||
var it = window._catalogItems && window._catalogItems[id];
|
toggleCart: toggleCart,
|
||||||
if (it) addToCart(it);
|
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(); };
|
// Also keep legacy window._ handlers for backward compat
|
||||||
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.toggleCart = toggleCart;
|
||||||
window.goToCheckout = goToCheckout;
|
window.goToCheckout = goToCheckout;
|
||||||
window.clearCart = clearCartFn;
|
|
||||||
window.checkExternalAvailability = checkExternalAvailability;
|
window.checkExternalAvailability = checkExternalAvailability;
|
||||||
|
|
||||||
// ─── Init ───
|
// ─── Init ───
|
||||||
renderCart();
|
renderCart();
|
||||||
loadCatalog(1, {});
|
loadCatalog(1, {});
|
||||||
loadCategories();
|
|
||||||
loadBrands();
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1483,6 +1483,134 @@
|
|||||||
padding: var(--space-1); opacity: 0.7; color: inherit;
|
padding: var(--space-1); opacity: 0.7; color: inherit;
|
||||||
}
|
}
|
||||||
.banner__dismiss:hover { opacity: 1; }
|
.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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -2172,6 +2300,33 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Cart FAB (floating action button) -->
|
||||||
|
<button class="cart-fab" id="cartFab" onclick="CatalogApp.toggleCart()" aria-label="Abrir carrito">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg>
|
||||||
|
<span class="cart-fab__badge" id="cartBadge">0</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Cart overlay -->
|
||||||
|
<div class="cart-overlay" id="cartOverlay" onclick="CatalogApp.toggleCart()"></div>
|
||||||
|
|
||||||
|
<!-- Cart sidebar -->
|
||||||
|
<aside class="cart-sidebar" id="cartSidebar">
|
||||||
|
<div class="cart-header">
|
||||||
|
<h3>Carrito</h3>
|
||||||
|
<button onclick="CatalogApp.toggleCart()" class="btn btn-ghost" aria-label="Cerrar carrito" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--color-text-secondary);padding:var(--space-1);">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="cart-items" id="cartItems"></div>
|
||||||
|
<div class="cart-empty" id="cartEmpty" style="display:none;padding:2rem;text-align:center;color:var(--color-text-muted);">Carrito vacio</div>
|
||||||
|
<div class="cart-footer">
|
||||||
|
<div class="cart-totals">
|
||||||
|
<div>Subtotal: <span id="cartSubtotal">$0.00</span></div>
|
||||||
|
<div>IVA 16%: <span id="cartTax">$0.00</span></div>
|
||||||
|
<div style="font-weight:bold;font-size:1.2em;">Total: <span id="cartTotal">$0.00</span></div>
|
||||||
|
</div>
|
||||||
|
<button id="checkoutBtn" class="btn btn-primary" style="width:100%;margin-top:var(--space-3);padding:var(--space-3);font-size:var(--text-body);font-weight:var(--font-weight-semibold);cursor:pointer;" onclick="CatalogApp.goToCheckout()">Ir a cobrar →</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- Offline Banner -->
|
<!-- Offline Banner -->
|
||||||
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
||||||
<span class="banner__icon"></span>
|
<span class="banner__icon"></span>
|
||||||
@@ -2179,6 +2334,7 @@
|
|||||||
<button class="banner__dismiss" onclick="document.getElementById('offlineBanner').style.display='none'" aria-label="Cerrar">×</button>
|
<button class="banner__dismiss" onclick="document.getElementById('offlineBanner').style.display='none'" aria-label="Cerrar">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/pos/static/js/catalog.js"></script>
|
||||||
<script src="/pos/static/js/offline-banner.js"></script>
|
<script src="/pos/static/js/offline-banner.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user