diff --git a/pos/blueprints/catalog_bp.py b/pos/blueprints/catalog_bp.py
index 3c43544..61c0b50 100644
--- a/pos/blueprints/catalog_bp.py
+++ b/pos/blueprints/catalog_bp.py
@@ -663,9 +663,10 @@ def brand_categories():
@catalog_bp.route('/brand-parts', methods=['GET'])
@require_auth('catalog.view')
def brand_parts():
- """Return parts for a given vehicle brand + category."""
+ """Return parts for a given vehicle brand + category, optionally filtered by search term."""
brand = request.args.get('brand', '')
category_id = request.args.get('category_id', type=int)
+ search = request.args.get('search', '').strip()
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
@@ -675,13 +676,22 @@ def brand_parts():
def _query(master, tenant, branch_id):
cur = master.cursor()
try:
- # Get parts from the brand catalog
+ # Build dynamic filters
params = [brand]
cat_filter = ""
+ search_filter = ""
+
if category_id:
cat_filter = "AND pc.id_part_category = %s"
params.append(category_id)
+ if search:
+ search_filter = "AND (p.oem_part_number ILIKE %s OR COALESCE(NULLIF(p.name_es, ''), p.name_part) ILIKE %s)"
+ like_term = f"%{search}%"
+ params.extend([like_term, like_term])
+
+ # Get parts from the brand catalog
+ query_params = list(params)
cur.execute(f"""
SELECT DISTINCT p.id_part, p.oem_part_number,
COALESCE(NULLIF(p.name_es, ''), p.name_part) as name,
@@ -693,9 +703,10 @@ def brand_parts():
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE pvp.name_brand = %s
{cat_filter}
+ {search_filter}
ORDER BY p.id_part
LIMIT %s OFFSET %s
- """, params + [limit, offset])
+ """, query_params + [limit, offset])
part_rows = cur.fetchall()
part_ids = [r[0] for r in part_rows]
@@ -709,6 +720,7 @@ def brand_parts():
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE pvp.name_brand = %s
{cat_filter}
+ {search_filter}
""", params)
total = cur.fetchone()[0]
@@ -738,6 +750,7 @@ def brand_parts():
return jsonify({
'brand': brand,
'category_id': category_id,
+ 'search': search,
'items': items,
'total': total,
'limit': limit,
diff --git a/pos/static/js/brand-catalog.js b/pos/static/js/brand-catalog.js
index 108826f..6c0d835 100644
--- a/pos/static/js/brand-catalog.js
+++ b/pos/static/js/brand-catalog.js
@@ -4,6 +4,10 @@
currentCategory: null,
state: 'brands',
_lastItems: [],
+ _allBrands: [],
+ _offset: 0,
+ _limit: 50,
+ _total: 0,
el: function(id) { return document.getElementById(id); },
@@ -24,6 +28,9 @@
this.currentCategory = null;
this.state = 'brands';
this._lastItems = [];
+ this._allBrands = [];
+ this._offset = 0;
+ this._total = 0;
},
loading: function(on) {
@@ -38,6 +45,7 @@
this.el('brandCatalogBreadcrumb').innerHTML = html;
},
+ // ---------- BRANDS ----------
loadBrands: function() {
this.loading(true);
this.state = 'brands';
@@ -46,18 +54,12 @@
.then(r => r.json())
.then(data => {
this.loading(false);
- if (!data.brands || !data.brands.length) {
+ this._allBrands = data.brands || [];
+ if (!this._allBrands.length) {
this.setContent('
No se encontraron marcas.
');
return;
}
- let html = '';
- data.brands.forEach(b => {
- html += '' +
- '
' + escapeHtml(b.name) + '
' +
- '
' + (b.part_count || 0) + ' refacciones
' +
- '
';
- });
- this.setContent(html);
+ this.renderBrandList(this._allBrands);
})
.catch(err => {
this.loading(false);
@@ -65,11 +67,40 @@
});
},
+ renderBrandList: function(brands) {
+ let html = '' +
+ '' +
+ '
';
+ brands.forEach(b => {
+ html += '' +
+ '
' + escapeHtml(b.name) + '
' +
+ '
' + (b.part_count || 0) + ' refacciones
' +
+ '
';
+ });
+ this.setContent(html);
+ },
+
+ filterBrands: function(query) {
+ const q = query.toLowerCase().trim();
+ if (!q) {
+ this.renderBrandList(this._allBrands);
+ return;
+ }
+ const filtered = this._allBrands.filter(function(b) {
+ return b.name.toLowerCase().indexOf(q) !== -1;
+ });
+ this.renderBrandList(filtered);
+ },
+
selectBrand: function(brandName) {
this.currentBrand = brandName;
this.loadCategories(brandName);
},
+ // ---------- CATEGORIES ----------
loadCategories: function(brandName) {
this.loading(true);
this.state = 'categories';
@@ -81,7 +112,12 @@
.then(data => {
this.loading(false);
if (!data.categories || !data.categories.length) {
- this.setContent('No se encontraron categorias para esta marca.
');
+ this.setContent(
+ '' +
+ '
No se encontraron categorias para ' + escapeHtml(brandName) + '.
' +
+ '
' +
+ '
'
+ );
return;
}
let html = '';
@@ -101,10 +137,12 @@
selectCategory: function(catId, catName) {
this.currentCategory = { id: catId, name: catName };
- this.loadParts(this.currentBrand, catId);
+ this._offset = 0;
+ this.loadParts(this.currentBrand, catId, '');
},
- loadParts: function(brandName, categoryId) {
+ // ---------- PARTS ----------
+ loadParts: function(brandName, categoryId, searchTerm) {
this.loading(true);
this.state = 'parts';
this.setBreadcrumb(
@@ -112,33 +150,23 @@
'' + escapeHtml(brandName) + ' › ' +
'' + escapeHtml(this.currentCategory.name) + ''
);
- fetch('/pos/api/catalog/brand-parts?brand=' + encodeURIComponent(brandName) + '&category_id=' + encodeURIComponent(categoryId))
+ let url = '/pos/api/catalog/brand-parts?brand=' + encodeURIComponent(brandName) + '&category_id=' + encodeURIComponent(categoryId) +
+ '&limit=' + this._limit + '&offset=' + this._offset;
+ if (searchTerm) {
+ url += '&search=' + encodeURIComponent(searchTerm);
+ }
+ fetch(url)
.then(r => r.json())
.then(data => {
this.loading(false);
this._lastItems = data.items || [];
+ this._total = data.total || 0;
+ this._offset = data.offset || 0;
if (!data.items || !data.items.length) {
- this.setContent('No se encontraron refacciones.
');
+ this.renderPartsList([], searchTerm);
return;
}
- let html = '';
- 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 += '
' +
- '
' +
- '
 + ')
' +
- '
' +
- '
' +
- '
' + escapeHtml(p.oem_part_number || 'N/A') + '
' +
- '
' + escapeHtml(p.name || '') + '
' +
- '
' + price + '
' +
- '
' +
- '
' +
- '
';
- });
- html += '
';
- this.setContent(html);
+ this.renderPartsList(data.items, searchTerm);
})
.catch(err => {
this.loading(false);
@@ -146,6 +174,85 @@
});
},
+ renderPartsList: function(items, searchTerm) {
+ let html = '' +
+ '' +
+ '' +
+ '' +
+ '
';
+
+ if (!items.length) {
+ html += '' +
+ '
No se encontraron refacciones.
' +
+ '
' +
+ '
';
+ this.setContent(html);
+ return;
+ }
+
+ // Stats line
+ const startIdx = this._offset + 1;
+ const endIdx = this._offset + items.length;
+ html += '' +
+ 'Mostrando ' + startIdx + '-' + endIdx + ' de ' + this._total + ' refacciones' +
+ '
';
+
+ html += '';
+ items.forEach(p => {
+ const price = p.local_price ? '$' + Number(p.local_price).toFixed(2) : 'Consultar precio';
+ const img = '/pos/static/images/placeholder-part.png';
+ const stockBadge = p.local_stock > 0
+ ? '
' + p.local_stock + ' en stock'
+ : '
Sin stock local';
+ html += '
' +
+ '
' +
+ '
 + ')
' +
+ '
' +
+ '
' +
+ '
' + escapeHtml(p.oem_part_number || 'N/A') + stockBadge + '
' +
+ '
' + escapeHtml(p.name || '') + '
' +
+ '
' + price + '
' +
+ '
' +
+ '
' +
+ '
';
+ });
+ html += '
';
+
+ // Pagination
+ const hasPrev = this._offset > 0;
+ const hasNext = (this._offset + this._limit) < this._total;
+ html += '' +
+ '' +
+ 'Pagina ' + (Math.floor(this._offset / this._limit) + 1) + ' de ' + (Math.ceil(this._total / this._limit) || 1) + '' +
+ '' +
+ '
';
+
+ this.setContent(html);
+ },
+
+ searchParts: function(term) {
+ this._offset = 0;
+ this.loadParts(this.currentBrand, this.currentCategory.id, term);
+ },
+
+ clearPartsSearch: function() {
+ this._offset = 0;
+ this.loadParts(this.currentBrand, this.currentCategory.id, '');
+ },
+
+ goToPage: function(newOffset) {
+ if (newOffset < 0) return;
+ this._offset = newOffset;
+ const searchInput = document.getElementById('partsSearchInput');
+ const term = searchInput ? searchInput.value : '';
+ this.loadParts(this.currentBrand, this.currentCategory.id, term);
+ },
+
addToCart: function(partId, event) {
if (event) event.stopPropagation();
const part = this._lastItems.find(function(p) { return p.id === partId; });