Files
Autoparts-DB/dashboard/enhanced-search.min.js
consultoria-as 21959f1b37 FASE 7d: Lazy Loading + Minificación + Auto-serve minified
Cambios implementados:

1. Lazy loading de imágenes:
   - catalog.js: loading="lazy" decoding="async" en part cards y detail panel
   - inventory.js: lazy loading en imagen de detalle de item

2. Minificación de assets:
   - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard
   - 25 archivos .min.js + 5 .min.css generados en pos/static/
   - 14 archivos .min.js + 8 .min.css generados en dashboard/

3. Nginx auto-serve minified:
   - try_files $1.min.js antes de servir .js original
   - try_files $1.min.css antes de servir .css original
   - Transparente para los templates HTML (cero cambios en HTML)

4. Cache warming script:
   - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches
   - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas
   - Corre en background, procesa ~1.5M parts

Tests: 73/73 pasando
2026-04-27 08:34:24 +00:00

1 line
14 KiB
JavaScript

const enhancedSearch={config:{minChars:2,debounceMs:300,maxResults:8,maxRecent:5,storageKey:"nexus_recent_searches"},state:{query:"",results:{parts:[],vehicles:[]},highlightedIndex:-1,isOpen:!1,isLoading:!1,filtersVisible:!1,debounceTimer:null},elements:{},init(){this.cacheElements(),this.loadCategories(),this.renderRecentSearches(),this.setupClickOutside()},cacheElements(){this.elements={input:document.getElementById("searchInput"),dropdown:document.getElementById("searchDropdown"),loading:document.getElementById("searchLoading"),filters:document.getElementById("searchFilters"),recent:document.getElementById("searchRecent"),recentItems:document.getElementById("searchRecentItems"),resultsContainer:document.getElementById("searchResultsContainer"),partsResults:document.getElementById("partsResults"),partsResultsList:document.getElementById("partsResultsList"),vehiclesResults:document.getElementById("vehiclesResults"),vehiclesResultsList:document.getElementById("vehiclesResultsList"),noResults:document.getElementById("searchNoResults"),footer:document.getElementById("searchFooter"),categoryFilter:document.getElementById("searchCategoryFilter"),typeFilter:document.getElementById("searchTypeFilter")}},async loadCategories(){try{const e=await fetch("/api/categories"),t=await e.json();this.elements.categoryFilter&&(this.elements.categoryFilter.innerHTML='<option value="">Todas las categorías</option>'+this.flattenCategories(t).map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""))}catch(e){console.error("Error loading categories:",e)}},flattenCategories(e,t=[]){return e.forEach((e=>{t.push(e),e.children&&e.children.length&&this.flattenCategories(e.children,t)})),t},onInput(e){this.state.query=e.trim(),this.state.debounceTimer&&clearTimeout(this.state.debounceTimer),this.state.query.length<this.config.minChars?this.showRecent():this.state.debounceTimer=setTimeout((()=>{this.performSearch()}),this.config.debounceMs)},onKeydown(e){const t=this.getAllResultItems();switch(e.key){case"ArrowDown":e.preventDefault(),this.highlightNext(t);break;case"ArrowUp":e.preventDefault(),this.highlightPrevious(t);break;case"Enter":e.preventDefault(),this.state.highlightedIndex>=0&&t[this.state.highlightedIndex]?t[this.state.highlightedIndex].click():this.state.query.length>=this.config.minChars&&this.viewAllResults();break;case"Escape":this.close(),this.elements.input.blur();break;case"Tab":if(this.state.isOpen&&this.state.highlightedIndex>=0&&t[this.state.highlightedIndex]){e.preventDefault();const s=t[this.state.highlightedIndex].dataset.autocomplete;s&&this.elements.input&&(this.elements.input.value=s,this.state.query=s,this.performSearch())}}},onFocus(){this.state.query.length>=this.config.minChars?this.open():this.showRecent()},getAllResultItems(){return Array.from(this.elements.dropdown.querySelectorAll(".search-result-item"))},highlightNext(e){0!==e.length&&(this.state.highlightedIndex++,this.state.highlightedIndex>=e.length&&(this.state.highlightedIndex=0),this.updateHighlight(e))},highlightPrevious(e){0!==e.length&&(this.state.highlightedIndex--,this.state.highlightedIndex<0&&(this.state.highlightedIndex=e.length-1),this.updateHighlight(e))},updateHighlight(e){e.forEach(((e,t)=>{e.classList.toggle("highlighted",t===this.state.highlightedIndex),t===this.state.highlightedIndex&&e.scrollIntoView({block:"nearest"})}))},async performSearch(){const e=this.state.query,t=this.elements.categoryFilter?.value,s=this.elements.typeFilter?.value||"all";this.showLoading(!0),this.open();try{let i=`/api/search?q=${encodeURIComponent(e)}&limit=${this.config.maxResults}`;t&&(i+=`&category_id=${t}`),"all"!==s&&(i+=`&type=${s}`);const a=await fetch(i),n=await a.json();this.state.results={parts:n.parts||[],vehicles:n.vehicles||[],vehicleParts:n.vehicle_parts||[],matchedVehicle:n.matched_vehicle||null},this.renderResults()}catch(e){console.error("Search error:",e),this.showNoResults()}finally{this.showLoading(!1)}},applyFilters(){this.state.query.length>=this.config.minChars&&this.performSearch()},renderResults(){const{parts:e,vehicles:t,vehicleParts:s,matchedVehicle:i}=this.state.results,a=s&&s.length>0,n=e.length>0||t.length>0||a;if(this.elements.recent&&(this.elements.recent.style.display="none"),a&&i&&this.elements.partsResults&&this.elements.partsResultsList){this.elements.partsResults.style.display="block";const e=`\n <div class="vehicle-parts-header">\n <i class="fas fa-car"></i>\n <span>${i.brand} ${i.model} ${i.year}</span>\n <small>${i.engine}</small>\n </div>\n `;this.elements.partsResultsList.innerHTML=e+s.map(((e,t)=>this.renderVehiclePartItem(e,i,t))).join(""),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none")}else e.length>0&&this.elements.partsResults&&this.elements.partsResultsList?(this.elements.partsResults.style.display="block",this.elements.partsResultsList.innerHTML=e.map(((e,t)=>this.renderPartItem(e,t))).join("")):this.elements.partsResults&&(this.elements.partsResults.style.display="none"),t.length>0&&this.elements.vehiclesResults&&this.elements.vehiclesResultsList?(this.elements.vehiclesResults.style.display="block",this.elements.vehiclesResultsList.innerHTML=t.map(((t,s)=>this.renderVehicleItem(t,e.length+s))).join("")):this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none");if(this.elements.noResults&&(this.elements.noResults.style.display=n?"none":"block"),this.elements.footer&&(this.elements.footer.style.display=n?"flex":"none"),n){this.state.highlightedIndex=0;const e=this.elements.dropdown.querySelector(".search-result-item");e&&e.classList.add("highlighted")}else this.state.highlightedIndex=-1},renderPartItem(e,t){const s=this.highlightText(e.name,this.state.query),i=e.matched_number||e.oem_part_number,a=this.highlightText(i,this.state.query),n=e.category_name?`<span class="search-category-badge">${e.category_name}</span>`:"",l=e.match_type&&"oem"!==e.match_type?`<span class="search-result-badge ${e.match_type}">${{aftermarket:"Aftermarket",cross_reference:"Cross-Ref"}[e.match_type]||e.match_type}</span>`:"",r=e.oem_part_number||e.name;return`\n <div class="search-result-item" data-index="${t}" data-autocomplete="${this.escapeHtml(r)}" onclick="enhancedSearch.selectPart(${e.id}, '${this.escapeHtml(e.name)}')">\n <div class="search-result-icon">\n ${e.image_url?`<img src="${e.image_url}" alt="">`:'<i class="fas fa-cog"></i>'}\n </div>\n <div class="search-result-info">\n <div class="search-result-title">${s}</div>\n <div class="search-result-subtitle">\n <span class="part-number">${a}</span>\n ${n}\n </div>\n </div>\n ${l}\n </div>\n `},renderVehiclePartItem(e,t,s){const i=this.highlightText(e.name_es||e.name,this.state.query),a=this.highlightText(e.oem_part_number,this.state.query),n=e.group_name?`<span class="search-category-badge">${e.group_name}</span>`:"",l=JSON.stringify({id:t.id,brand:t.brand,model:t.model,year:t.year}).replace(/'/g,"\\'").replace(/"/g,"&quot;"),r=`${t.brand} ${t.model} ${t.year} ${e.name}`;return`\n <div class="search-result-item vehicle-part-item" data-index="${s}" data-autocomplete="${this.escapeHtml(r)}"\n onclick="enhancedSearch.selectVehiclePart('${l}', ${e.id}, ${e.category_id}, ${e.group_id})">\n <div class="search-result-icon">\n ${e.image_url?`<img src="${e.image_url}" alt="">`:'<i class="fas fa-cog"></i>'}\n </div>\n <div class="search-result-info">\n <div class="search-result-title">${i}</div>\n <div class="search-result-subtitle">\n <span class="part-number">${a}</span>\n ${n}\n </div>\n </div>\n <span class="search-result-badge vehicle-part-badge">\n <i class="fas fa-check-circle"></i> Compatible\n </span>\n </div>\n `},renderVehicleItem(e,t){const s=`${e.brand} ${e.model} ${e.year}`,i=e.engine||"",a=JSON.stringify({id:e.id,brand:e.brand,model:e.model,year:e.year}).replace(/'/g,"\\'").replace(/"/g,"&quot;"),n=[{id:6,icon:"fa-cog",name:"Motor"},{id:2,icon:"fa-compact-disc",name:"Frenos"},{id:5,icon:"fa-bolt",name:"Eléctrico"},{id:11,icon:"fa-truck-monster",name:"Suspensión"},{id:8,icon:"fa-gas-pump",name:"Combustible"},{id:12,icon:"fa-gears",name:"Transmisión"}].map((e=>`<button class="vehicle-category-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicleCategory('${a}', ${e.id})" title="${e.name}">\n <i class="fas ${e.icon}"></i>\n </button>`)).join(""),l=`${e.brand} ${e.model} ${e.year}`;return`\n <div class="search-result-item vehicle-item" data-index="${t}" data-autocomplete="${this.escapeHtml(l)} ">\n <div class="search-result-icon"><i class="fas fa-car"></i></div>\n <div class="search-result-info">\n <div class="search-result-title">${this.highlightText(s,this.state.query)}</div>\n <div class="search-result-subtitle">${this.highlightText(i,this.state.query)}</div>\n <div class="vehicle-categories-row">\n ${n}\n <button class="vehicle-category-btn vehicle-all-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicle('${a}')" title="Ver todas las categorías">\n <i class="fas fa-th"></i> Todo\n </button>\n </div>\n </div>\n </div>\n `},highlightText(e,t){if(!t||!e)return e;const s=new RegExp(`(${this.escapeRegex(t)})`,"gi");return e.replace(s,'<span class="highlight">$1</span>')},escapeRegex:e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),escapeHtml:e=>e.replace(/'/g,"\\'").replace(/"/g,'\\"'),selectPart(e,t){this.saveRecentSearch(t),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.showPartDetail?dashboard.showPartDetail(e):window.location.href=`/?search=${encodeURIComponent(t)}`},selectVehiclePart(e,t,s,i){try{const i=JSON.parse(e.replace(/&quot;/g,'"'));i.brand,i.model,i.year;this.saveRecentSearch(this.state.query),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicleCategory&&(dashboard.navigateToVehicleCategory(i.id,i.brand,i.model,i.year,s),setTimeout((()=>{"function"==typeof dashboard.showPartDetail&&dashboard.showPartDetail(t)}),500))}catch(e){console.error("Error selecting vehicle part:",e)}},selectVehicle(e){try{const t=JSON.parse(e.replace(/&quot;/g,'"')),s=`${t.brand} ${t.model} ${t.year}`;this.saveRecentSearch(s),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicle?dashboard.navigateToVehicle(t.id,t.brand,t.model,t.year):console.log("Navigating to vehicle:",t)}catch(e){console.error("Error parsing vehicle data:",e)}},selectVehicleCategory(e,t){try{const s=JSON.parse(e.replace(/&quot;/g,'"')),i=`${s.brand} ${s.model} ${s.year}`;this.saveRecentSearch(i),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicleCategory?dashboard.navigateToVehicleCategory(s.id,s.brand,s.model,s.year,t):console.log("Navigating to vehicle category:",s,t)}catch(e){console.error("Error parsing vehicle data:",e)}},viewAllResults(){this.state.query&&(this.saveRecentSearch(this.state.query),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.searchPartNumber?dashboard.searchPartNumber():console.log("Dashboard not available, search query:",this.state.query))},getRecentSearches(){try{return JSON.parse(localStorage.getItem(this.config.storageKey))||[]}catch{return[]}},saveRecentSearch(e){if(!e||e.length<2)return;let t=this.getRecentSearches();t=t.filter((t=>t.toLowerCase()!==e.toLowerCase())),t.unshift(e),t=t.slice(0,this.config.maxRecent),localStorage.setItem(this.config.storageKey,JSON.stringify(t)),this.renderRecentSearches()},clearRecent(){localStorage.removeItem(this.config.storageKey),this.renderRecentSearches()},renderRecentSearches(){const e=this.getRecentSearches();0!==e.length&&this.elements.recent?this.elements.recentItems&&(this.elements.recentItems.innerHTML=e.map((e=>`<span class="search-recent-item" onclick="enhancedSearch.searchRecent('${this.escapeHtml(e)}')">${e}</span>`)).join("")):this.elements.recent&&(this.elements.recent.style.display="none")},searchRecent(e){this.elements.input&&(this.elements.input.value=e),this.state.query=e,this.performSearch()},showRecent(){this.getRecentSearches().length>0&&this.elements.recent?(this.elements.recent.style.display="block",this.elements.partsResults&&(this.elements.partsResults.style.display="none"),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none"),this.elements.noResults&&(this.elements.noResults.style.display="none"),this.elements.footer&&(this.elements.footer.style.display="none"),this.open()):this.close()},toggleFilters(){this.state.filtersVisible=!this.state.filtersVisible,this.elements.filters&&(this.elements.filters.style.display=this.state.filtersVisible?"flex":"none");const e=document.querySelector(".search-filters-toggle");e&&e.classList.toggle("active",this.state.filtersVisible)},open(){this.state.isOpen=!0,this.elements.dropdown&&this.elements.dropdown.classList.add("active")},close(){this.state.isOpen=!1,this.elements.dropdown&&this.elements.dropdown.classList.remove("active"),this.state.highlightedIndex=-1},showLoading(e){this.state.isLoading=e,this.elements.loading&&(this.elements.loading.style.display=e?"block":"none")},showNoResults(){this.elements.partsResults&&(this.elements.partsResults.style.display="none"),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none"),this.elements.noResults&&(this.elements.noResults.style.display="block"),this.elements.footer&&(this.elements.footer.style.display="none"),this.elements.recent&&(this.elements.recent.style.display="none")},setupClickOutside(){document.addEventListener("click",(e=>{const t=document.querySelector(".search-box-enhanced");t&&!t.contains(e.target)&&this.close()}))}};document.addEventListener("DOMContentLoaded",(()=>{enhancedSearch.init()})),document.addEventListener("keydown",(e=>{if("/"===e.key&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName)){e.preventDefault();const t=document.getElementById("searchInput");t&&(t.focus(),t.select())}if((e.ctrlKey||e.metaKey)&&"k"===e.key){e.preventDefault();const t=document.getElementById("searchInput");t&&(t.focus(),t.select())}}));