From c6b3ca9bdfb066a6b88644cc085cb4e8bbf657ad Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Thu, 14 May 2026 21:26:10 +0000 Subject: [PATCH] fix(brand-catalog): add JWT auth token to all API requests brand-catalog.js was missing Authorization header on fetch calls, causing 401 Unauthorized errors. Now reads pos_token from localStorage and includes Bearer token in every request. Also handles 401 responses by redirecting to /pos/login. Bump JS cache bust to v=2. --- .kimi/plan.md | 39 +++++++++ pos/static/js/brand-catalog.js | 154 +++++++++++++++++++++------------ pos/templates/catalog.html | 2 +- 3 files changed, 137 insertions(+), 58 deletions(-) create mode 100644 .kimi/plan.md 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 = '
' + + var html = '
' + '' + '
'; - brands.forEach(b => { + brands.forEach(function(b) { html += '
' + '
' + escapeHtml(b.name) + '
' + '
' + (b.part_count || 0) + ' refacciones
' + @@ -84,12 +114,12 @@ }, filterBrands: function(query) { - const q = query.toLowerCase().trim(); + var q = query.toLowerCase().trim(); if (!q) { this.renderBrandList(this._allBrands); return; } - const filtered = this._allBrands.filter(function(b) { + var filtered = this._allBrands.filter(function(b) { return b.name.toLowerCase().indexOf(q) !== -1; }); this.renderBrandList(filtered); @@ -107,12 +137,17 @@ this.setBreadcrumb( 'Marcas' + escapeHtml(brandName) + '' ); - fetch('/pos/api/catalog/brand-categories?brand=' + encodeURIComponent(brandName)) - .then(r => r.json()) - .then(data => { - this.loading(false); + var self = this; + fetch('/pos/api/catalog/brand-categories?brand=' + encodeURIComponent(brandName), { headers: this._headers() }) + .then(function(r) { + if (!self._checkAuth(r)) return null; + return r.json(); + }) + .then(function(data) { + if (!data) return; + self.loading(false); if (!data.categories || !data.categories.length) { - this.setContent( + self.setContent( '
' + '

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 += '
' + '
' + escapeHtml(c.name) + '
' + '
' + c.part_count + ' refacciones
' + '
'; }); - this.setContent(html); + self.setContent(html); }) - .catch(err => { - this.loading(false); - this.setContent('

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 = '
' + + var html = '
' + ''; 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 + 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 ? '' + p.local_stock + ' en stock' : 'Sin stock local'; html += '
' + @@ -221,13 +260,14 @@ }); html += '
'; - // Pagination - const hasPrev = this._offset > 0; - const hasNext = (this._offset + this._limit) < this._total; + 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 += '
' + '' + - 'Pagina ' + (Math.floor(this._offset / this._limit) + 1) + ' de ' + (Math.ceil(this._total / this._limit) || 1) + '' + + 'Pagina ' + pageNum + ' de ' + totalPages + '' + '' + '
'; @@ -248,14 +288,14 @@ goToPage: function(newOffset) { if (newOffset < 0) return; this._offset = newOffset; - const searchInput = document.getElementById('partsSearchInput'); - const term = searchInput ? searchInput.value : ''; + var searchInput = document.getElementById('partsSearchInput'); + var 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; }); + var part = this._lastItems.find(function(p) { return p.id === partId; }); if (!part) { alert('Error: no se encontro la refaccion'); return; @@ -273,8 +313,8 @@ source: 'oem-brand', inventory_id: null }, 1); - const btn = event.target; - const oldText = btn.textContent; + 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); @@ -286,7 +326,7 @@ function escapeHtml(text) { if (!text) return ''; - const div = document.createElement('div'); + var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html index d36b272..61769ff 100644 --- a/pos/templates/catalog.html +++ b/pos/templates/catalog.html @@ -294,6 +294,6 @@ - +