Brand catalog now follows the same navigation as the regular catalog: 1. Brands -> 2. Models -> 3. Years -> 4. Engines -> 5. Categories -> 6. Parts Backend: - Add /mye-parts endpoint for MYE-specific parts with category filter - Uses existing /models, /years, /engines, /categories endpoints Frontend: - Complete rewrite of brand-catalog.js with breadcrumb navigation - State machine: brands -> models -> years -> engines -> categories -> parts - Search and pagination preserved at parts level - Breadcrumb allows jumping back to any previous step
486 lines
20 KiB
JavaScript
486 lines
20 KiB
JavaScript
(function() {
|
|
const BrandCatalog = {
|
|
state: 'brands',
|
|
_allBrands: [],
|
|
_lastItems: [],
|
|
_offset: 0,
|
|
_limit: 50,
|
|
_total: 0,
|
|
|
|
// Navigation state
|
|
nav: {
|
|
brand: null,
|
|
brandId: null,
|
|
model: null,
|
|
modelId: null,
|
|
year: null,
|
|
yearId: null,
|
|
engine: null,
|
|
myeId: null,
|
|
category: null,
|
|
categoryId: null
|
|
},
|
|
|
|
el: function(id) { return document.getElementById(id); },
|
|
|
|
_getToken: function() {
|
|
return localStorage.getItem('pos_token');
|
|
},
|
|
|
|
_headers: function() {
|
|
var token = this._getToken();
|
|
return {
|
|
'Authorization': 'Bearer ' + (token || ''),
|
|
'Content-Type': 'application/json'
|
|
};
|
|
},
|
|
|
|
_checkAuth: function(resp) {
|
|
if (resp.status === 401) {
|
|
localStorage.removeItem('pos_token');
|
|
window.location.href = '/pos/login';
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
show: function() {
|
|
if (!this._getToken()) {
|
|
window.location.href = '/pos/login';
|
|
return;
|
|
}
|
|
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.state = 'brands';
|
|
this.nav = { brand: null, brandId: null, model: null, modelId: null, year: null, yearId: null, engine: null, myeId: null, category: null, categoryId: null };
|
|
this._allBrands = [];
|
|
this._lastItems = [];
|
|
this._offset = 0;
|
|
this._total = 0;
|
|
this.el('brandCatalogSearch').innerHTML = '';
|
|
},
|
|
|
|
loading: function(on) {
|
|
this.el('brandCatalogLoading').style.display = on ? 'block' : 'none';
|
|
},
|
|
|
|
setContent: function(html) {
|
|
this.el('brandCatalogContent').innerHTML = html;
|
|
},
|
|
|
|
setSearch: function(html) {
|
|
this.el('brandCatalogSearch').innerHTML = html;
|
|
},
|
|
|
|
setBreadcrumb: function(html) {
|
|
this.el('brandCatalogBreadcrumb').innerHTML = html;
|
|
},
|
|
|
|
buildBreadcrumb: function() {
|
|
var parts = [];
|
|
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.loadBrands()" style="color:var(--color-primary);text-decoration:none;">Marcas</a>');
|
|
if (this.nav.brand) {
|
|
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.brand) + '</a>');
|
|
}
|
|
if (this.nav.model) {
|
|
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.model) + '</a>');
|
|
}
|
|
if (this.nav.year) {
|
|
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')" style="color:var(--color-primary);text-decoration:none;">' + this.nav.year + '</a>');
|
|
}
|
|
if (this.nav.engine) {
|
|
parts.push('<a href="javascript:void(0)" onclick="BrandCatalog.selectEngine(' + this.nav.myeId + ',' + JSON.stringify(this.nav.engine) + ')" style="color:var(--color-primary);text-decoration:none;">' + escapeHtml(this.nav.engine) + '</a>');
|
|
}
|
|
if (this.nav.category) {
|
|
parts.push('<strong>' + escapeHtml(this.nav.category) + '</strong>');
|
|
}
|
|
this.setBreadcrumb(parts.join(' › '));
|
|
},
|
|
|
|
// ---------- BRANDS ----------
|
|
loadBrands: function() {
|
|
this.loading(true);
|
|
this.state = 'brands';
|
|
this.reset();
|
|
this.setBreadcrumb('<strong>Marcas de vehiculo</strong>');
|
|
this.setSearch(
|
|
'<input type="text" id="brandSearchInput" placeholder="Buscar marca..." ' +
|
|
'style="width:100%;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
|
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);' +
|
|
'outline:none;" oninput="BrandCatalog.filterBrands(this.value)">'
|
|
);
|
|
var self = this;
|
|
fetch('/pos/api/catalog/vehicle-brands', { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
self._allBrands = data.brands || [];
|
|
if (!self._allBrands.length) {
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron marcas.</p>');
|
|
return;
|
|
}
|
|
self.renderBrandList(self._allBrands);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar marcas: ' + escapeHtml(err.message) + '</p>');
|
|
});
|
|
},
|
|
|
|
renderBrandList: function(brands) {
|
|
var html = '';
|
|
brands.forEach(function(b) {
|
|
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectBrand(' + JSON.stringify(b.name) + ',' + b.id + ')">' +
|
|
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(b.name) + '</div>' +
|
|
'</div>';
|
|
});
|
|
this.setContent(html);
|
|
},
|
|
|
|
filterBrands: function(query) {
|
|
var q = query.toLowerCase().trim();
|
|
if (!q) {
|
|
this.renderBrandList(this._allBrands);
|
|
return;
|
|
}
|
|
var filtered = this._allBrands.filter(function(b) {
|
|
return b.name.toLowerCase().indexOf(q) !== -1;
|
|
});
|
|
this.renderBrandList(filtered);
|
|
},
|
|
|
|
selectBrand: function(brandName, brandId) {
|
|
this.nav.brand = brandName;
|
|
this.nav.brandId = brandId;
|
|
this.loadModels(brandId);
|
|
},
|
|
|
|
// ---------- MODELS ----------
|
|
loadModels: function(brandId) {
|
|
this.loading(true);
|
|
this.state = 'models';
|
|
this.setSearch('');
|
|
this.buildBreadcrumb();
|
|
var self = this;
|
|
fetch('/pos/api/catalog/models?brand_id=' + encodeURIComponent(brandId), { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
var models = data.data || [];
|
|
if (!models.length) {
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron modelos.</p>');
|
|
return;
|
|
}
|
|
var html = '';
|
|
models.forEach(function(m) {
|
|
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')">' +
|
|
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
|
'</div>';
|
|
});
|
|
self.setContent(html);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar modelos: ' + escapeHtml(err.message) + '</p>');
|
|
});
|
|
},
|
|
|
|
selectModel: function(modelId, modelName) {
|
|
this.nav.model = modelName;
|
|
this.nav.modelId = modelId;
|
|
this.loadYears(modelId);
|
|
},
|
|
|
|
// ---------- YEARS ----------
|
|
loadYears: function(modelId) {
|
|
this.loading(true);
|
|
this.state = 'years';
|
|
this.setSearch('');
|
|
this.buildBreadcrumb();
|
|
var self = this;
|
|
fetch('/pos/api/catalog/years?model_id=' + encodeURIComponent(modelId), { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
var years = data.data || [];
|
|
if (!years.length) {
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron años.</p>');
|
|
return;
|
|
}
|
|
var html = '';
|
|
years.forEach(function(y) {
|
|
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectYear(' + y.id_year + ',' + y.year_car + ')">' +
|
|
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + y.year_car + '</div>' +
|
|
'</div>';
|
|
});
|
|
self.setContent(html);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar años: ' + escapeHtml(err.message) + '</p>');
|
|
});
|
|
},
|
|
|
|
selectYear: function(yearId, yearCar) {
|
|
this.nav.year = yearCar;
|
|
this.nav.yearId = yearId;
|
|
this.loadEngines(this.nav.modelId, yearId);
|
|
},
|
|
|
|
// ---------- ENGINES ----------
|
|
loadEngines: function(modelId, yearId) {
|
|
this.loading(true);
|
|
this.state = 'engines';
|
|
this.setSearch('');
|
|
this.buildBreadcrumb();
|
|
var self = this;
|
|
fetch('/pos/api/catalog/engines?model_id=' + encodeURIComponent(modelId) + '&year_id=' + encodeURIComponent(yearId), { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
var engines = data.data || [];
|
|
if (!engines.length) {
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron motores.</p>');
|
|
return;
|
|
}
|
|
var html = '';
|
|
engines.forEach(function(e) {
|
|
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')">' +
|
|
'<div style="font-size:var(--text-h4);font-family:var(--font-heading);margin-bottom:4px;">' + escapeHtml(e.name_engine) + '</div>' +
|
|
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + escapeHtml(e.trim_level || '') + '</div>' +
|
|
'</div>';
|
|
});
|
|
self.setContent(html);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar motores: ' + escapeHtml(err.message) + '</p>');
|
|
});
|
|
},
|
|
|
|
selectEngine: function(myeId, engineName) {
|
|
this.nav.engine = engineName;
|
|
this.nav.myeId = myeId;
|
|
this.loadCategories(myeId);
|
|
},
|
|
|
|
// ---------- CATEGORIES ----------
|
|
loadCategories: function(myeId) {
|
|
this.loading(true);
|
|
this.state = 'categories';
|
|
this.setSearch('');
|
|
this.buildBreadcrumb();
|
|
var self = this;
|
|
fetch('/pos/api/catalog/categories?mye_id=' + encodeURIComponent(myeId) + '&mode=oem', { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
var categories = data.data || [];
|
|
if (!categories.length) {
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">No se encontraron categorias.</p>');
|
|
return;
|
|
}
|
|
var html = '';
|
|
categories.forEach(function(c) {
|
|
html += '<div class="catalog-category-card" onclick="BrandCatalog.selectCategory(' + c.id_part_category + ',' + 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 || 0) + ' refacciones</div>' +
|
|
'</div>';
|
|
});
|
|
self.setContent(html);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.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.nav.category = catName;
|
|
this.nav.categoryId = catId;
|
|
this._offset = 0;
|
|
this.loadParts(this.nav.myeId, catId, '');
|
|
},
|
|
|
|
// ---------- PARTS ----------
|
|
loadParts: function(myeId, categoryId, searchTerm) {
|
|
this.loading(true);
|
|
this.state = 'parts';
|
|
this.buildBreadcrumb();
|
|
this.setSearch(
|
|
'<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center;">' +
|
|
'<input type="text" id="partsSearchInput" placeholder="Buscar refaccion..." value="' + escapeHtml(searchTerm || '') + '" ' +
|
|
'style="flex:1;min-width:200px;padding:10px 14px;border:1px solid var(--color-border);border-radius:var(--radius-md);' +
|
|
'font-size:var(--text-body);background:var(--color-surface);color:var(--color-text-primary);outline:none;" ' +
|
|
'onkeydown="if(event.key===\'Enter\')BrandCatalog.searchParts(this.value)">' +
|
|
'<button class="btn btn--primary btn--sm" onclick="BrandCatalog.searchParts(document.getElementById(\'partsSearchInput\').value)">Buscar</button>' +
|
|
'<button class="btn btn--secondary btn--sm" onclick="BrandCatalog.clearPartsSearch()">Limpiar</button>' +
|
|
'</div>'
|
|
);
|
|
var url = '/pos/api/catalog/mye-parts?mye_id=' + encodeURIComponent(myeId) + '&category_id=' + encodeURIComponent(categoryId) +
|
|
'&limit=' + this._limit + '&offset=' + this._offset;
|
|
if (searchTerm) {
|
|
url += '&search=' + encodeURIComponent(searchTerm);
|
|
}
|
|
var self = this;
|
|
fetch(url, { headers: this._headers() })
|
|
.then(function(r) {
|
|
if (!self._checkAuth(r)) return null;
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
self.loading(false);
|
|
self._lastItems = data.items || [];
|
|
self._total = data.total || 0;
|
|
self._offset = data.offset || 0;
|
|
self.renderPartsList(data.items || [], searchTerm);
|
|
})
|
|
.catch(function(err) {
|
|
self.loading(false);
|
|
self.setContent('<p style="grid-column:1/-1;text-align:center;color:var(--color-error);">Error al cargar refacciones: ' + escapeHtml(err.message) + '</p>');
|
|
});
|
|
},
|
|
|
|
renderPartsList: function(items, searchTerm) {
|
|
var html = '';
|
|
if (!items.length) {
|
|
html += '<div style="grid-column:1/-1;text-align:center;padding:var(--space-8);">' +
|
|
'<p style="color:var(--color-text-muted);font-size:var(--text-body-lg);">No se encontraron refacciones.</p>' +
|
|
'<button class="btn btn--primary" style="margin-top:var(--space-3);" onclick="BrandCatalog.loadCategories(' + this.nav.myeId + ')">Volver a categorias</button>' +
|
|
'</div>';
|
|
this.setContent(html);
|
|
return;
|
|
}
|
|
|
|
var startIdx = this._offset + 1;
|
|
var endIdx = this._offset + items.length;
|
|
html += '<div style="grid-column:1/-1;font-size:var(--text-body-sm);color:var(--color-text-muted);margin-bottom:var(--space-2);">' +
|
|
'Mostrando ' + startIdx + '-' + endIdx + ' de ' + this._total + ' refacciones' +
|
|
'</div>';
|
|
|
|
html += '<div style="grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3);">';
|
|
items.forEach(function(p) {
|
|
var price = p.local_price ? '$' + Number(p.local_price).toFixed(2) : 'Consultar precio';
|
|
var img = '/pos/static/images/placeholder-part.png';
|
|
var stockBadge = p.local_stock > 0
|
|
? '<span style="display:inline-block;background:var(--color-success);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">' + p.local_stock + ' en stock</span>'
|
|
: '<span style="display:inline-block;background:var(--color-text-muted);color:#fff;font-size:11px;padding:2px 8px;border-radius:var(--radius-sm);margin-left:6px;">Sin stock local</span>';
|
|
html += '<div class="catalog-category-card" style="padding:0;overflow:hidden;display:flex;flex-direction:column;">' +
|
|
'<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);flex:1;display:flex;flex-direction:column;">' +
|
|
'<div style="font-weight:600;font-size:var(--text-body);margin-bottom:4px;">' + escapeHtml(p.oem_part_number || 'N/A') + stockBadge + '</div>' +
|
|
'<div style="font-size:var(--text-body-sm);color:var(--color-text-muted);margin-bottom:8px;flex:1;">' + 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>';
|
|
|
|
var hasPrev = this._offset > 0;
|
|
var hasNext = (this._offset + this._limit) < this._total;
|
|
var pageNum = Math.floor(this._offset / this._limit) + 1;
|
|
var totalPages = Math.ceil(this._total / this._limit) || 1;
|
|
html += '<div style="grid-column:1/-1;display:flex;justify-content:center;align-items:center;gap:var(--space-3);padding:var(--space-4) 0;">' +
|
|
'<button class="btn btn--secondary" ' + (hasPrev ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
|
' onclick="BrandCatalog.goToPage(' + (this._offset - this._limit) + ')">← Anterior</button>' +
|
|
'<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">Pagina ' + pageNum + ' de ' + totalPages + '</span>' +
|
|
'<button class="btn btn--secondary" ' + (hasNext ? '' : 'disabled style="opacity:0.5;cursor:not-allowed;"') +
|
|
' onclick="BrandCatalog.goToPage(' + (this._offset + this._limit) + ')">Siguiente →</button>' +
|
|
'</div>';
|
|
|
|
this.setContent(html);
|
|
},
|
|
|
|
searchParts: function(term) {
|
|
this._offset = 0;
|
|
this.loadParts(this.nav.myeId, this.nav.categoryId, term);
|
|
},
|
|
|
|
clearPartsSearch: function() {
|
|
this._offset = 0;
|
|
this.loadParts(this.nav.myeId, this.nav.categoryId, '');
|
|
},
|
|
|
|
goToPage: function(newOffset) {
|
|
if (newOffset < 0) return;
|
|
this._offset = newOffset;
|
|
var searchInput = document.getElementById('partsSearchInput');
|
|
var term = searchInput ? searchInput.value : '';
|
|
this.loadParts(this.nav.myeId, this.nav.categoryId, term);
|
|
},
|
|
|
|
addToCart: function(partId, event) {
|
|
if (event) event.stopPropagation();
|
|
var 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);
|
|
var btn = event.target;
|
|
var 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 '';
|
|
var div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
window.BrandCatalog = BrandCatalog;
|
|
})();
|