diff --git a/.kimi/plan.md b/.kimi/plan.md new file mode 100644 index 0000000..df84217 --- /dev/null +++ b/.kimi/plan.md @@ -0,0 +1,39 @@ +# Plan: Catálogo por Marca de Vehículo + +## Resumen +Reorganizar el catálogo para que la navegación principal sea: +**Marca de vehículo → Categoría/Sistema → Partes compatibles** + +Ejemplo: Toyota → Frenos → [balatas Bosch, discos Brembo, pastillas NGK...] + +## Opción recomendada: Materialized View + +No tocamos la tabla masiva `vehicle_parts` (billones de rows). Creamos una materialized view que agregue por marca + categoría. + +### Cambios DB (Master) +1. Crear `brand_catalog_parts` MV desde `vehicle_parts → MYE → models → brands` +2. Agregar índices: `(brand_id, category_id)`, `(brand_id, part_id)` +3. Crear función `refresh_brand_catalog()` para refrescar + +### Cambios Backend +1. Nuevos endpoints: + - `GET /catalog/vehicle-brands` → lista marcas con conteo de partes + - `GET /catalog/brand-categories?brand_id=` → categorías disponibles para esa marca + - `GET /catalog/brand-parts?brand_id=&category_id=` → partes compatibles +2. Modificar `catalog_service.py` con filtros por marca + +### Cambios Frontend +1. Nueva vista inicial: grid de marcas de vehículo (tarjetas con logo/contador) +2. Click en marca → lista de categorías/sistemas (frenos, motor, suspensión...) +3. Click en categoría → grid de partes compatibles con esa marca +4. Filtro opcional: modelo/año/motor para refinar resultados + +### Datos +- `vehicle_parts` ya tiene todo. La MV solo agrega/distinct. +- Las marcas fabricantes (Bosch, NGK) se muestran como badges en cada parte. + +## Tiempo estimado +- DB + Backend: 2-3 horas +- Frontend: 2-3 horas +- Testing: 1 hora +- Total: ~6 horas diff --git a/pos/static/js/brand-catalog.js b/pos/static/js/brand-catalog.js index 6c0d835..7e3f959 100644 --- a/pos/static/js/brand-catalog.js +++ b/pos/static/js/brand-catalog.js @@ -9,9 +9,34 @@ _limit: 50, _total: 0, + _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; + }, + el: function(id) { return document.getElementById(id); }, show: function() { + if (!this._getToken()) { + window.location.href = '/pos/login'; + return; + } this.el('brandCatalogOverlay').style.display = 'block'; document.body.style.overflow = 'hidden'; this.loadBrands(); @@ -50,31 +75,36 @@ this.loading(true); this.state = 'brands'; this.setBreadcrumb('Marcas de vehiculo'); - fetch('/pos/api/catalog/vehicle-brands') - .then(r => r.json()) - .then(data => { - this.loading(false); - this._allBrands = data.brands || []; - if (!this._allBrands.length) { - this.setContent('
No se encontraron marcas.
'); + 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('No se encontraron marcas.
'); return; } - this.renderBrandList(this._allBrands); + self.renderBrandList(self._allBrands); }) - .catch(err => { - this.loading(false); - this.setContent('Error al cargar marcas: ' + escapeHtml(err.message) + '
'); + .catch(function(err) { + self.loading(false); + self.setContent('Error al cargar marcas: ' + escapeHtml(err.message) + '
'); }); }, renderBrandList: function(brands) { - let html = 'No se encontraron categorias para ' + escapeHtml(brandName) + '.
' + '' + @@ -120,18 +155,18 @@ ); return; } - let html = ''; - data.categories.forEach(c => { + var html = ''; + data.categories.forEach(function(c) { html += 'Error al cargar categorias: ' + escapeHtml(err.message) + '
'); + .catch(function(err) { + self.loading(false); + self.setContent('Error al cargar categorias: ' + escapeHtml(err.message) + '
'); }); }, @@ -150,32 +185,37 @@ '' + escapeHtml(brandName) + ' › ' + '' + escapeHtml(this.currentCategory.name) + '' ); - let url = '/pos/api/catalog/brand-parts?brand=' + encodeURIComponent(brandName) + '&category_id=' + encodeURIComponent(categoryId) + + var 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; + 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; if (!data.items || !data.items.length) { - this.renderPartsList([], searchTerm); + self.renderPartsList([], searchTerm); return; } - this.renderPartsList(data.items, searchTerm); + self.renderPartsList(data.items, searchTerm); }) - .catch(err => { - this.loading(false); - this.setContent('Error al cargar refacciones: ' + escapeHtml(err.message) + '
'); + .catch(function(err) { + self.loading(false); + self.setContent('Error al cargar refacciones: ' + escapeHtml(err.message) + '
'); }); }, renderPartsList: function(items, searchTerm) { - let html = '