feat(catalog): wire up brand-first OEM catalog UI
- Add brand-catalog.js overlay: Brands -> Categories -> Parts flow - Update catalog.html: 'Por Marca' button opens BrandCatalog overlay - Optimize /vehicle-brands to query brands table (fast) instead of 256M part_vehicle_preview - Keep /brand-categories and /brand-parts using exact match on part_vehicle_preview - Integrate addToCart with existing CatalogApp cart
This commit is contained in:
188
pos/static/js/brand-catalog.js
Normal file
188
pos/static/js/brand-catalog.js
Normal file
@@ -0,0 +1,188 @@
|
||||
(function() {
|
||||
const BrandCatalog = {
|
||||
currentBrand: null,
|
||||
currentCategory: null,
|
||||
state: 'brands',
|
||||
_lastItems: [],
|
||||
|
||||
el: function(id) { return document.getElementById(id); },
|
||||
|
||||
show: function() {
|
||||
this.el('brandCatalogOverlay').style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
this.loadBrands();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el('brandCatalogOverlay').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
this.reset();
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.currentBrand = null;
|
||||
this.currentCategory = null;
|
||||
this.state = 'brands';
|
||||
this._lastItems = [];
|
||||
},
|
||||
|
||||
loading: function(on) {
|
||||
this.el('brandCatalogLoading').style.display = on ? 'block' : 'none';
|
||||
},
|
||||
|
||||
setContent: function(html) {
|
||||
this.el('brandCatalogContent').innerHTML = html;
|
||||
},
|
||||
|
||||
setBreadcrumb: function(html) {
|
||||
this.el('brandCatalogBreadcrumb').innerHTML = html;
|
||||
},
|
||||
|
||||
loadBrands: function() {
|
||||
this.loading(true);
|
||||
this.state = 'brands';
|
||||
this.setBreadcrumb('<strong>Marcas de vehiculo</strong>');
|
||||
fetch('/pos/api/catalog/vehicle-brands')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.loading(false);
|
||||
if (!data.brands || !data.brands.length) {
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron marcas.</p>');
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
data.brands.forEach(b => {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(b.name) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (b.part_count || 0) + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
this.setContent(html);
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading(false);
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar marcas: ' + escapeHtml(err.message) + '</p>');
|
||||
});
|
||||
},
|
||||
|
||||
selectBrand: function(brandName) {
|
||||
this.currentBrand = brandName;
|
||||
this.loadCategories(brandName);
|
||||
},
|
||||
|
||||
loadCategories: function(brandName) {
|
||||
this.loading(true);
|
||||
this.state = 'categories';
|
||||
this.setBreadcrumb(
|
||||
'<a href="javascript:void(0)" onclick="BrandCatalog.loadBrands()" style="color:var(--color-primary);text-decoration:none;">Marcas</a> › <strong>' + escapeHtml(brandName) + '</strong>'
|
||||
);
|
||||
fetch('/pos/api/catalog/brand-categories?brand=' + encodeURIComponent(brandName))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.loading(false);
|
||||
if (!data.categories || !data.categories.length) {
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron categorias para esta marca.</p>');
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
data.categories.forEach(c => {
|
||||
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectCategory(' + c.id + ', ' + JSON.stringify(c.name) + ')">' +
|
||||
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(c.name) + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + c.part_count + ' refacciones</div>' +
|
||||
'</div>';
|
||||
});
|
||||
this.setContent(html);
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading(false);
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar categorias: ' + escapeHtml(err.message) + '</p>');
|
||||
});
|
||||
},
|
||||
|
||||
selectCategory: function(catId, catName) {
|
||||
this.currentCategory = { id: catId, name: catName };
|
||||
this.loadParts(this.currentBrand, catId);
|
||||
},
|
||||
|
||||
loadParts: function(brandName, categoryId) {
|
||||
this.loading(true);
|
||||
this.state = 'parts';
|
||||
this.setBreadcrumb(
|
||||
'<a href="javascript:void(0)" onclick="BrandCatalog.loadBrands()" style="color:var(--color-primary);text-decoration:none;">Marcas</a> › ' +
|
||||
'<a href="javascript:void(0)" onclick="BrandCatalog.selectBrand(' + JSON.stringify(brandName) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(brandName) + '</a> › ' +
|
||||
'<strong>' + escapeHtml(this.currentCategory.name) + '</strong>'
|
||||
);
|
||||
fetch('/pos/api/catalog/brand-parts?brand=' + encodeURIComponent(brandName) + '&category_id=' + encodeURIComponent(categoryId))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.loading(false);
|
||||
this._lastItems = data.items || [];
|
||||
if (!data.items || !data.items.length) {
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron refacciones.</p>');
|
||||
return;
|
||||
}
|
||||
let html = '<div style="grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3);">';
|
||||
data.items.forEach(p => {
|
||||
const price = p.local_price ? '$' + Number(p.local_price).toFixed(2) : 'Consultar precio';
|
||||
const img = '/pos/static/images/placeholder-part.png';
|
||||
html += '<div class="catalog-category-card" style="padding:0;overflow:hidden;">' +
|
||||
'<div style="height:160px;background:#f5f5f5;display:flex;align-items:center;justify-content:center;">' +
|
||||
'<img src="' + escapeHtml(img) + '" alt="" style="max-width:100%;max-height:100%;object-fit:contain;">' +
|
||||
'</div>' +
|
||||
'<div style="padding:var(--space-3);">' +
|
||||
'<div style="font-weight:600;font-size:var(--text-body);margin-bottom:4px;">' + escapeHtml(p.oem_part_number || 'N/A') + '</div>' +
|
||||
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);margin-bottom:8px;min-height:36px;">' + escapeHtml(p.name || '') + '</div>' +
|
||||
'<div style="font-size:var(--text-h5);font-weight:700;color:var(--color-primary);margin-bottom:8px;">' + price + '</div>' +
|
||||
'<button class="btn btn--primary btn--sm" style="width:100%;" onclick="BrandCatalog.addToCart(' + p.id + ', event)">Agregar</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
this.setContent(html);
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading(false);
|
||||
this.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar refacciones: ' + escapeHtml(err.message) + '</p>');
|
||||
});
|
||||
},
|
||||
|
||||
addToCart: function(partId, event) {
|
||||
if (event) event.stopPropagation();
|
||||
const part = this._lastItems.find(function(p) { return p.id === partId; });
|
||||
if (!part) {
|
||||
alert('Error: no se encontro la refaccion');
|
||||
return;
|
||||
}
|
||||
if (window.CatalogApp && CatalogApp.addToCart) {
|
||||
CatalogApp.addToCart({
|
||||
id: part.id,
|
||||
part_number: part.oem_part_number || 'N/A',
|
||||
name: part.name || 'Refaccion',
|
||||
brand: '',
|
||||
price: part.local_price || 0,
|
||||
tax_rate: 0.16,
|
||||
unit: 'PZA',
|
||||
stock: part.local_stock || 0,
|
||||
source: 'oem-brand',
|
||||
inventory_id: null
|
||||
}, 1);
|
||||
const btn = event.target;
|
||||
const oldText = btn.textContent;
|
||||
btn.textContent = 'Agregado!';
|
||||
btn.style.background = 'var(--color-success)';
|
||||
setTimeout(function() { btn.textContent = oldText; btn.style.background = ''; }, 1500);
|
||||
return;
|
||||
}
|
||||
alert('Carrito no disponible. Asegurate de que la pagina haya cargado completamente.');
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
window.BrandCatalog = BrandCatalog;
|
||||
})();
|
||||
Reference in New Issue
Block a user