diff --git a/pos/blueprints/catalog_bp.py b/pos/blueprints/catalog_bp.py index 8835809..a466b82 100644 --- a/pos/blueprints/catalog_bp.py +++ b/pos/blueprints/catalog_bp.py @@ -763,3 +763,107 @@ def brand_parts(): finally: cur.close() return _with_conns(_query) + + +@catalog_bp.route('/mye-parts', methods=['GET']) +@require_auth('catalog.view') +def mye_parts(): + """Return parts for a specific MYE + category (brand-catalog flow). + + Skips the group/subgroup level and goes directly from category to parts. + """ + mye_id = request.args.get('mye_id', type=int) + 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) + + if not mye_id: + return jsonify({'error': 'mye_id required'}), 400 + + def _query(master, tenant, branch_id): + cur = master.cursor() + try: + # Build dynamic filters + cat_filter = "" + search_filter = "" + params = [mye_id] + + 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 + 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, + pg.id_part_group, pg.name_part_group, + pc.id_part_category, pc.name_part_category + FROM vehicle_parts vp + JOIN parts p ON p.id_part = vp.part_id + JOIN part_groups pg ON pg.id_part_group = p.group_id + JOIN part_categories pc ON pc.id_part_category = pg.category_id + WHERE vp.model_year_engine_id = %s + {cat_filter} + {search_filter} + ORDER BY p.id_part + LIMIT %s OFFSET %s + """, query_params + [limit, offset]) + + part_rows = cur.fetchall() + part_ids = [r[0] for r in part_rows] + + # Count total + cur.execute(f""" + SELECT COUNT(DISTINCT p.id_part) + FROM vehicle_parts vp + JOIN parts p ON p.id_part = vp.part_id + JOIN part_groups pg ON pg.id_part_group = p.group_id + JOIN part_categories pc ON pc.id_part_category = pg.category_id + WHERE vp.model_year_engine_id = %s + {cat_filter} + {search_filter} + """, params) + total = cur.fetchone()[0] + + # Enrich with local stock if available + local_stock = {} + if tenant and part_ids: + try: + from services.catalog_service import _get_local_stock_bulk + local_stock = _get_local_stock_bulk(tenant, part_ids) + except Exception: + pass + + items = [] + for r in part_rows: + part_id = r[0] + stock_info = local_stock.get(part_id, {}) + items.append({ + 'id': part_id, + 'oem_part_number': r[1], + 'name': r[2], + 'group': {'id': r[3], 'name': r[4]}, + 'category': {'id': r[5], 'name': r[6]}, + 'local_stock': stock_info.get('stock', 0), + 'local_price': stock_info.get('price', None), + }) + + return jsonify({ + 'mye_id': mye_id, + 'category_id': category_id, + 'search': search, + 'items': items, + 'total': total, + 'limit': limit, + 'offset': offset, + }) + finally: + cur.close() + return _with_conns(_query) diff --git a/pos/static/js/brand-catalog.js b/pos/static/js/brand-catalog.js index b02c667..35c680a 100644 --- a/pos/static/js/brand-catalog.js +++ b/pos/static/js/brand-catalog.js @@ -1,14 +1,26 @@ (function() { const BrandCatalog = { - currentBrand: null, - currentCategory: null, state: 'brands', - _lastItems: [], _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() { @@ -49,11 +61,10 @@ }, reset: function() { - this.currentBrand = null; - this.currentCategory = null; this.state = 'brands'; - this._lastItems = []; + 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 = ''; @@ -75,10 +86,32 @@ this.el('brandCatalogBreadcrumb').innerHTML = html; }, + buildBreadcrumb: function() { + var parts = []; + parts.push('Marcas'); + if (this.nav.brand) { + parts.push('' + escapeHtml(this.nav.brand) + ''); + } + if (this.nav.model) { + parts.push('' + escapeHtml(this.nav.model) + ''); + } + if (this.nav.year) { + parts.push('' + this.nav.year + ''); + } + if (this.nav.engine) { + parts.push('' + escapeHtml(this.nav.engine) + ''); + } + if (this.nav.category) { + parts.push('' + escapeHtml(this.nav.category) + ''); + } + this.setBreadcrumb(parts.join(' › ')); + }, + // ---------- BRANDS ---------- loadBrands: function() { this.loading(true); this.state = 'brands'; + this.reset(); this.setBreadcrumb('Marcas de vehiculo'); this.setSearch( '' + + html += '
No se encontraron categorias para ' + escapeHtml(brandName) + '.
' + - '' + - 'No se encontraron modelos.
'); return; } var html = ''; - data.categories.forEach(function(c) { - html += 'Error al cargar modelos: ' + escapeHtml(err.message) + '
'); + }); + }, + + 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('No se encontraron años.
'); + return; + } + var html = ''; + years.forEach(function(y) { + html += 'Error al cargar años: ' + escapeHtml(err.message) + '
'); + }); + }, + + 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('No se encontraron motores.
'); + return; + } + var html = ''; + engines.forEach(function(e) { + html += 'Error al cargar motores: ' + escapeHtml(err.message) + '
'); + }); + }, + + 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('No se encontraron categorias.
'); + return; + } + var html = ''; + categories.forEach(function(c) { + html += 'No se encontraron refacciones.
' + - '' + + '' + '