Add admin panel, enhanced search, Gonher import and expand API

- Add admin interface (admin.html, admin.js) for managing catalog data
- Add enhanced search module with advanced filtering capabilities
- Expand server.py with new API endpoints and admin functionality
- Add Gonher catalog import scripts (import_gonher_catalog.py, import_gonher_complete.py)
- Add demo data population script and sample CSV data
- Update customer landing page and dashboard with UI improvements
- Update database with enriched vehicle and parts data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 00:35:05 +00:00
parent 6fb2a52f86
commit e66b18f6ae
15 changed files with 7308 additions and 141 deletions

1599
dashboard/admin.html Normal file

File diff suppressed because it is too large Load Diff

1690
dashboard/admin.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,17 @@
width: 100%;
}
.nav-links a.admin-link {
color: var(--accent);
font-weight: 600;
opacity: 0.8;
transition: opacity 0.3s;
}
.nav-links a.admin-link:hover {
opacity: 1;
}
.header-actions {
display: flex;
align-items: center;
@@ -1061,6 +1072,7 @@
<a href="#brands-section">Marcas</a>
<a href="#featured-section">Productos</a>
<a href="#cta-section">Contacto</a>
<a href="admin.html" class="admin-link">⚡ Admin</a>
</nav>
<div class="header-actions">
<button class="search-btn" onclick="openSearchModal()">🔍</button>

View File

@@ -503,16 +503,16 @@ class VehicleDashboard {
const myeRecords = await myeRes.json();
// Merge mye_id into vehicles based on matching fields
// Only keep vehicles that have a matching mye_id (i.e., have parts)
this.allVehicles = vehicles.map(v => {
const mye = myeRecords.find(m =>
m.brand === v.brand &&
m.model === v.model &&
m.year === v.year &&
m.engine === v.engine &&
(m.trim_level === v.trim_level || (!m.trim_level && !v.trim_level) || (m.trim_level === 'unknown' && v.trim_level === 'unknown'))
m.engine === v.engine
);
return { ...v, mye_id: mye ? mye.id : null };
});
}).filter(v => v.mye_id !== null); // Only show vehicles with parts
this.filteredVehicles = [...this.allVehicles];
// Poblar filtros
@@ -714,7 +714,79 @@ class VehicleDashboard {
document.getElementById('filtersBar').classList.remove('visible');
}
// Navigate to vehicle from search results
async navigateToVehicle(myeId, brand, model, year) {
// Set the state for breadcrumb navigation
this.selectedBrand = brand;
this.selectedModel = model;
this.selectedYear = year;
// Add vehicle to allVehicles if not already there (for breadcrumb)
if (!this.allVehicles.find(v => v.mye_id === myeId)) {
this.allVehicles.push({
mye_id: myeId,
brand: brand,
model: model,
year: year
});
}
// Navigate to categories
await this.goToCategories(myeId);
}
// Navigate to vehicle and directly to a specific category
async navigateToVehicleCategory(myeId, brand, model, year, categoryId) {
// Set the state for breadcrumb navigation
this.selectedBrand = brand;
this.selectedModel = model;
this.selectedYear = year;
// Add vehicle to allVehicles if not already there (for breadcrumb)
if (!this.allVehicles.find(v => v.mye_id === myeId)) {
this.allVehicles.push({
mye_id: myeId,
brand: brand,
model: model,
year: year
});
}
this.selectedVehicleId = myeId;
// Load categories if not available (needed for breadcrumb)
if (this.allCategories.length === 0) {
try {
const response = await fetch('/api/categories');
if (response.ok) {
this.allCategories = await response.json();
}
} catch (e) {
console.error('Error loading categories:', e);
}
}
// Navigate directly to the category's groups
await this.goToGroups(categoryId);
}
async goToCategories(myeId) {
// Validate myeId before proceeding
if (!myeId || myeId === 'null' || myeId === 'undefined') {
const container = document.getElementById('mainContent');
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h4>Vehículo sin partes disponibles</h4>
<p>Este vehículo no tiene partes registradas en el catálogo.</p>
<button class="btn btn-back mt-3" onclick="dashboard.goToModels('${this.selectedBrand}')">
<i class="fas fa-arrow-left"></i> Volver a modelos
</button>
</div>
`;
return;
}
this.currentView = 'categories';
this.selectedVehicleId = myeId;
this.selectedCategory = null;
@@ -737,8 +809,8 @@ class VehicleDashboard {
`;
try {
// Get all categories (since we don't have vehicle-specific parts yet, show all categories)
const response = await fetch('/api/categories');
// Get vehicle-specific categories (only categories with parts for this vehicle)
const response = await fetch(`/api/vehicles/${myeId}/categories`);
if (!response.ok) {
throw new Error('Error al cargar categorías');
@@ -824,7 +896,15 @@ class VehicleDashboard {
`;
try {
const response = await fetch(`/api/categories/${categoryId}/groups`);
// Use vehicle-specific endpoint when a vehicle is selected
let url;
if (this.selectedVehicleId) {
url = `/api/vehicles/${this.selectedVehicleId}/groups?category_id=${categoryId}`;
} else {
url = `/api/categories/${categoryId}/groups`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Error al cargar grupos');
@@ -921,15 +1001,23 @@ class VehicleDashboard {
`;
try {
const response = await fetch(`/api/parts?group_id=${groupId}`);
// Use vehicle-specific endpoint when a vehicle is selected
let url;
if (this.selectedVehicleId) {
url = `/api/vehicles/${this.selectedVehicleId}/parts?group_id=${groupId}`;
} else {
url = `/api/parts?group_id=${groupId}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Error al cargar partes');
}
const partsData = await response.json();
// Handle paginated response
this.allParts = partsData.data || partsData;
// Handle both array response (vehicle parts) and paginated response
this.allParts = Array.isArray(partsData) ? partsData : (partsData.data || partsData);
this.displayParts();
} catch (error) {

View File

@@ -0,0 +1,677 @@
/**
* Enhanced Search Component
* Features: Autocomplete, filters, recent searches, keyboard navigation, highlighting
*/
const enhancedSearch = {
// Configuration
config: {
minChars: 2,
debounceMs: 300,
maxResults: 8,
maxRecent: 5,
storageKey: 'autopartes_recent_searches'
},
// State
state: {
query: '',
results: { parts: [], vehicles: [] },
highlightedIndex: -1,
isOpen: false,
isLoading: false,
filtersVisible: false,
debounceTimer: null
},
// DOM elements cache
elements: {},
// Initialize
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')
};
},
// Load categories for filter
async loadCategories() {
try {
const response = await fetch('/api/categories');
const categories = await response.json();
if (this.elements.categoryFilter) {
this.elements.categoryFilter.innerHTML = '<option value="">Todas las categorías</option>' +
this.flattenCategories(categories).map(cat =>
`<option value="${cat.id}">${cat.name}</option>`
).join('');
}
} catch (e) {
console.error('Error loading categories:', e);
}
},
flattenCategories(categories, result = []) {
categories.forEach(cat => {
result.push(cat);
if (cat.children && cat.children.length) {
this.flattenCategories(cat.children, result);
}
});
return result;
},
// Event handlers
onInput(value) {
this.state.query = value.trim();
// Clear existing timer
if (this.state.debounceTimer) {
clearTimeout(this.state.debounceTimer);
}
if (this.state.query.length < this.config.minChars) {
this.showRecent();
return;
}
// Debounce search
this.state.debounceTimer = setTimeout(() => {
this.performSearch();
}, this.config.debounceMs);
},
onKeydown(event) {
const allItems = this.getAllResultItems();
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.highlightNext(allItems);
break;
case 'ArrowUp':
event.preventDefault();
this.highlightPrevious(allItems);
break;
case 'Enter':
event.preventDefault();
if (this.state.highlightedIndex >= 0 && allItems[this.state.highlightedIndex]) {
allItems[this.state.highlightedIndex].click();
} else if (this.state.query.length >= this.config.minChars) {
this.viewAllResults();
}
break;
case 'Escape':
this.close();
this.elements.input.blur();
break;
case 'Tab':
// Autocomplete with highlighted item
if (this.state.isOpen && this.state.highlightedIndex >= 0 && allItems[this.state.highlightedIndex]) {
event.preventDefault();
const item = allItems[this.state.highlightedIndex];
const autocompleteText = item.dataset.autocomplete;
if (autocompleteText && this.elements.input) {
this.elements.input.value = autocompleteText;
this.state.query = autocompleteText;
this.performSearch();
}
}
break;
}
},
onFocus() {
if (this.state.query.length >= this.config.minChars) {
this.open();
} else {
this.showRecent();
}
},
// Navigation helpers
getAllResultItems() {
return Array.from(this.elements.dropdown.querySelectorAll('.search-result-item'));
},
highlightNext(items) {
if (items.length === 0) return;
this.state.highlightedIndex++;
if (this.state.highlightedIndex >= items.length) {
this.state.highlightedIndex = 0;
}
this.updateHighlight(items);
},
highlightPrevious(items) {
if (items.length === 0) return;
this.state.highlightedIndex--;
if (this.state.highlightedIndex < 0) {
this.state.highlightedIndex = items.length - 1;
}
this.updateHighlight(items);
},
updateHighlight(items) {
items.forEach((item, index) => {
item.classList.toggle('highlighted', index === this.state.highlightedIndex);
if (index === this.state.highlightedIndex) {
item.scrollIntoView({ block: 'nearest' });
}
});
},
// Search
async performSearch() {
const query = this.state.query;
const categoryId = this.elements.categoryFilter?.value;
const searchType = this.elements.typeFilter?.value || 'all';
this.showLoading(true);
this.open();
try {
let url = `/api/search?q=${encodeURIComponent(query)}&limit=${this.config.maxResults}`;
if (categoryId) url += `&category_id=${categoryId}`;
if (searchType !== 'all') url += `&type=${searchType}`;
const response = await fetch(url);
const data = await response.json();
this.state.results = {
parts: data.parts || [],
vehicles: data.vehicles || [],
vehicleParts: data.vehicle_parts || [],
matchedVehicle: data.matched_vehicle || null
};
this.renderResults();
} catch (e) {
console.error('Search error:', e);
this.showNoResults();
} finally {
this.showLoading(false);
}
},
applyFilters() {
if (this.state.query.length >= this.config.minChars) {
this.performSearch();
}
},
// Rendering
renderResults() {
const { parts, vehicles, vehicleParts, matchedVehicle } = this.state.results;
const hasVehicleParts = vehicleParts && vehicleParts.length > 0;
const hasResults = parts.length > 0 || vehicles.length > 0 || hasVehicleParts;
// Hide recent searches when showing results
if (this.elements.recent) this.elements.recent.style.display = 'none';
// If we have matched vehicle + parts (combined search result)
if (hasVehicleParts && matchedVehicle && this.elements.partsResults && this.elements.partsResultsList) {
this.elements.partsResults.style.display = 'block';
// Show vehicle header
const vehicleHeader = `
<div class="vehicle-parts-header">
<i class="fas fa-car"></i>
<span>${matchedVehicle.brand} ${matchedVehicle.model} ${matchedVehicle.year}</span>
<small>${matchedVehicle.engine}</small>
</div>
`;
this.elements.partsResultsList.innerHTML = vehicleHeader + vehicleParts.map((part, index) =>
this.renderVehiclePartItem(part, matchedVehicle, index)
).join('');
// Hide regular vehicles section
if (this.elements.vehiclesResults) {
this.elements.vehiclesResults.style.display = 'none';
}
} else {
// Regular parts
if (parts.length > 0 && this.elements.partsResults && this.elements.partsResultsList) {
this.elements.partsResults.style.display = 'block';
this.elements.partsResultsList.innerHTML = parts.map((part, index) =>
this.renderPartItem(part, index)
).join('');
} else if (this.elements.partsResults) {
this.elements.partsResults.style.display = 'none';
}
// Vehicles
if (vehicles.length > 0 && this.elements.vehiclesResults && this.elements.vehiclesResultsList) {
this.elements.vehiclesResults.style.display = 'block';
this.elements.vehiclesResultsList.innerHTML = vehicles.map((vehicle, index) =>
this.renderVehicleItem(vehicle, parts.length + index)
).join('');
} else if (this.elements.vehiclesResults) {
this.elements.vehiclesResults.style.display = 'none';
}
}
// No results
if (this.elements.noResults) {
this.elements.noResults.style.display = hasResults ? 'none' : 'block';
}
// Footer
if (this.elements.footer) {
this.elements.footer.style.display = hasResults ? 'flex' : 'none';
}
// Auto-highlight first result for quick Enter selection
if (hasResults) {
this.state.highlightedIndex = 0;
const firstItem = this.elements.dropdown.querySelector('.search-result-item');
if (firstItem) {
firstItem.classList.add('highlighted');
}
} else {
this.state.highlightedIndex = -1;
}
},
renderPartItem(part, index) {
const title = this.highlightText(part.name, this.state.query);
const partNumber = part.matched_number || part.oem_part_number;
const subtitle = this.highlightText(partNumber, this.state.query);
const categoryBadge = part.category_name ? `<span class="search-category-badge">${part.category_name}</span>` : '';
// Match type badge with better labels
const matchLabels = {
'aftermarket': 'Aftermarket',
'cross_reference': 'Cross-Ref'
};
const matchBadge = part.match_type && part.match_type !== 'oem'
? `<span class="search-result-badge ${part.match_type}">${matchLabels[part.match_type] || part.match_type}</span>`
: '';
// Text for Tab autocomplete
const autocompleteText = part.oem_part_number || part.name;
return `
<div class="search-result-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)}" onclick="enhancedSearch.selectPart(${part.id}, '${this.escapeHtml(part.name)}')">
<div class="search-result-icon">
${part.image_url ? `<img src="${part.image_url}" alt="">` : '<i class="fas fa-cog"></i>'}
</div>
<div class="search-result-info">
<div class="search-result-title">${title}</div>
<div class="search-result-subtitle">
<span class="part-number">${subtitle}</span>
${categoryBadge}
</div>
</div>
${matchBadge}
</div>
`;
},
// Render a part that's linked to a specific vehicle (combined search)
renderVehiclePartItem(part, vehicle, index) {
const title = this.highlightText(part.name_es || part.name, this.state.query);
const subtitle = this.highlightText(part.oem_part_number, this.state.query);
const groupBadge = part.group_name ? `<span class="search-category-badge">${part.group_name}</span>` : '';
// Escape data for onclick
const vehicleData = JSON.stringify({
id: vehicle.id,
brand: vehicle.brand,
model: vehicle.model,
year: vehicle.year
}).replace(/'/g, "\\'").replace(/"/g, '&quot;');
const autocompleteText = `${vehicle.brand} ${vehicle.model} ${vehicle.year} ${part.name}`;
return `
<div class="search-result-item vehicle-part-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)}"
onclick="enhancedSearch.selectVehiclePart('${vehicleData}', ${part.id}, ${part.category_id}, ${part.group_id})">
<div class="search-result-icon">
${part.image_url ? `<img src="${part.image_url}" alt="">` : '<i class="fas fa-cog"></i>'}
</div>
<div class="search-result-info">
<div class="search-result-title">${title}</div>
<div class="search-result-subtitle">
<span class="part-number">${subtitle}</span>
${groupBadge}
</div>
</div>
<span class="search-result-badge vehicle-part-badge">
<i class="fas fa-check-circle"></i> Compatible
</span>
</div>
`;
},
renderVehicleItem(vehicle, index) {
const title = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
const subtitle = vehicle.engine || '';
// Escape vehicle data for onclick
const vehicleData = JSON.stringify({
id: vehicle.id,
brand: vehicle.brand,
model: vehicle.model,
year: vehicle.year
}).replace(/'/g, "\\'").replace(/"/g, '&quot;');
// Quick category buttons (IDs match database)
const categories = [
{ 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' }
];
const categoryButtons = categories.map(cat =>
`<button class="vehicle-category-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicleCategory('${vehicleData}', ${cat.id})" title="${cat.name}">
<i class="fas ${cat.icon}"></i>
</button>`
).join('');
// Text for Tab autocomplete (vehicle name for adding part search)
const autocompleteText = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
return `
<div class="search-result-item vehicle-item" data-index="${index}" data-autocomplete="${this.escapeHtml(autocompleteText)} ">
<div class="search-result-icon"><i class="fas fa-car"></i></div>
<div class="search-result-info">
<div class="search-result-title">${this.highlightText(title, this.state.query)}</div>
<div class="search-result-subtitle">${this.highlightText(subtitle, this.state.query)}</div>
<div class="vehicle-categories-row">
${categoryButtons}
<button class="vehicle-category-btn vehicle-all-btn" onclick="event.stopPropagation(); enhancedSearch.selectVehicle('${vehicleData}')" title="Ver todas las categorías">
<i class="fas fa-th"></i> Todo
</button>
</div>
</div>
</div>
`;
},
highlightText(text, query) {
if (!query || !text) return text;
const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
},
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
escapeHtml(str) {
return str.replace(/'/g, "\\'").replace(/"/g, '\\"');
},
// Selection handlers
selectPart(partId, name) {
this.saveRecentSearch(name);
this.close();
// Open part detail modal via dashboard
if (typeof dashboard !== 'undefined' && typeof dashboard.showPartDetail === 'function') {
dashboard.showPartDetail(partId);
} else {
// Fallback: search in dashboard
window.location.href = `/?search=${encodeURIComponent(name)}`;
}
},
// Select a part that's specific to a vehicle (from combined search)
selectVehiclePart(vehicleDataStr, partId, categoryId, groupId) {
try {
const vehicle = JSON.parse(vehicleDataStr.replace(/&quot;/g, '"'));
const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
this.saveRecentSearch(this.state.query);
this.close();
// Navigate to vehicle's category/group and show part detail
if (typeof dashboard !== 'undefined') {
// Set up vehicle context first
if (typeof dashboard.navigateToVehicleCategory === 'function') {
dashboard.navigateToVehicleCategory(vehicle.id, vehicle.brand, vehicle.model, vehicle.year, categoryId);
// Then show part detail after a short delay
setTimeout(() => {
if (typeof dashboard.showPartDetail === 'function') {
dashboard.showPartDetail(partId);
}
}, 500);
}
}
} catch (e) {
console.error('Error selecting vehicle part:', e);
}
},
selectVehicle(vehicleDataStr) {
try {
// Parse vehicle data from JSON string
const vehicle = JSON.parse(vehicleDataStr.replace(/&quot;/g, '"'));
const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
this.saveRecentSearch(displayName);
this.close();
// Navigate directly to the vehicle's categories
if (typeof dashboard !== 'undefined' && typeof dashboard.navigateToVehicle === 'function') {
dashboard.navigateToVehicle(vehicle.id, vehicle.brand, vehicle.model, vehicle.year);
} else {
console.log('Navigating to vehicle:', vehicle);
}
} catch (e) {
console.error('Error parsing vehicle data:', e);
}
},
selectVehicleCategory(vehicleDataStr, categoryId) {
try {
const vehicle = JSON.parse(vehicleDataStr.replace(/&quot;/g, '"'));
const displayName = `${vehicle.brand} ${vehicle.model} ${vehicle.year}`;
this.saveRecentSearch(displayName);
this.close();
// Navigate to vehicle and then to specific category
if (typeof dashboard !== 'undefined' && typeof dashboard.navigateToVehicleCategory === 'function') {
dashboard.navigateToVehicleCategory(vehicle.id, vehicle.brand, vehicle.model, vehicle.year, categoryId);
} else {
console.log('Navigating to vehicle category:', vehicle, categoryId);
}
} catch (e) {
console.error('Error parsing vehicle data:', e);
}
},
viewAllResults() {
if (this.state.query) {
this.saveRecentSearch(this.state.query);
this.close();
// Open search modal with full results
if (typeof dashboard !== 'undefined' && typeof dashboard.searchPartNumber === 'function') {
dashboard.searchPartNumber();
} else {
// Fallback: reload page with search parameter
console.log('Dashboard not available, search query:', this.state.query);
}
}
},
// Recent searches
getRecentSearches() {
try {
return JSON.parse(localStorage.getItem(this.config.storageKey)) || [];
} catch {
return [];
}
},
saveRecentSearch(query) {
if (!query || query.length < 2) return;
let recent = this.getRecentSearches();
// Remove if exists
recent = recent.filter(r => r.toLowerCase() !== query.toLowerCase());
// Add to front
recent.unshift(query);
// Limit
recent = recent.slice(0, this.config.maxRecent);
localStorage.setItem(this.config.storageKey, JSON.stringify(recent));
this.renderRecentSearches();
},
clearRecent() {
localStorage.removeItem(this.config.storageKey);
this.renderRecentSearches();
},
renderRecentSearches() {
const recent = this.getRecentSearches();
if (recent.length === 0 || !this.elements.recent) {
if (this.elements.recent) this.elements.recent.style.display = 'none';
return;
}
if (this.elements.recentItems) {
this.elements.recentItems.innerHTML = recent.map(term =>
`<span class="search-recent-item" onclick="enhancedSearch.searchRecent('${this.escapeHtml(term)}')">${term}</span>`
).join('');
}
},
searchRecent(term) {
if (this.elements.input) {
this.elements.input.value = term;
}
this.state.query = term;
this.performSearch();
},
showRecent() {
const recent = this.getRecentSearches();
if (recent.length > 0 && this.elements.recent) {
this.elements.recent.style.display = 'block';
if (this.elements.partsResults) this.elements.partsResults.style.display = 'none';
if (this.elements.vehiclesResults) this.elements.vehiclesResults.style.display = 'none';
if (this.elements.noResults) this.elements.noResults.style.display = 'none';
if (this.elements.footer) this.elements.footer.style.display = 'none';
this.open();
} else {
this.close();
}
},
// Filters toggle
toggleFilters() {
this.state.filtersVisible = !this.state.filtersVisible;
if (this.elements.filters) {
this.elements.filters.style.display = this.state.filtersVisible ? 'flex' : 'none';
}
const toggle = document.querySelector('.search-filters-toggle');
if (toggle) {
toggle.classList.toggle('active', this.state.filtersVisible);
}
},
// UI state
open() {
this.state.isOpen = true;
if (this.elements.dropdown) this.elements.dropdown.classList.add('active');
},
close() {
this.state.isOpen = false;
if (this.elements.dropdown) this.elements.dropdown.classList.remove('active');
this.state.highlightedIndex = -1;
},
showLoading(show) {
this.state.isLoading = show;
if (this.elements.loading) this.elements.loading.style.display = show ? 'block' : 'none';
},
showNoResults() {
if (this.elements.partsResults) this.elements.partsResults.style.display = 'none';
if (this.elements.vehiclesResults) this.elements.vehiclesResults.style.display = 'none';
if (this.elements.noResults) this.elements.noResults.style.display = 'block';
if (this.elements.footer) this.elements.footer.style.display = 'none';
if (this.elements.recent) this.elements.recent.style.display = 'none';
},
setupClickOutside() {
document.addEventListener('click', (e) => {
const searchBox = document.querySelector('.search-box-enhanced');
if (searchBox && !searchBox.contains(e.target)) {
this.close();
}
});
}
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
enhancedSearch.init();
});
// Global keyboard shortcut for search
document.addEventListener('keydown', (e) => {
// "/" to focus search (when not in an input)
if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
e.preventDefault();
const input = document.getElementById('searchInput');
if (input) {
input.focus();
input.select();
}
}
// Ctrl+K or Cmd+K to focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const input = document.getElementById('searchInput');
if (input) {
input.focus();
input.select();
}
}
});

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catálogo de Autopartes - AutoParts DB</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
@@ -135,6 +136,479 @@
color: white;
}
/* Enhanced Search Styles */
.search-box-enhanced {
position: relative;
width: 100%;
max-width: 600px;
}
.search-input-wrapper {
display: flex;
align-items: center;
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
}
.search-input-wrapper:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(255, 107, 53, 0.15);
}
.search-input-wrapper .search-icon {
padding: 0 0.5rem 0 1rem;
color: var(--text-secondary);
font-size: 0.9rem;
}
.search-input-wrapper .search-input {
flex: 1;
padding: 0.9rem 0.5rem;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 0.95rem;
outline: none;
min-width: 200px;
}
.search-filters-toggle {
padding: 0.5rem 0.75rem;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.2s;
}
.search-filters-toggle:hover,
.search-filters-toggle.active {
color: var(--accent);
}
.search-loading {
padding: 0 0.75rem;
}
.search-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Search Dropdown */
.search-dropdown {
display: none;
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
max-height: 70vh;
overflow-y: auto;
z-index: 1000;
}
.search-dropdown.active {
display: block;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Search Filters */
.search-filters {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.search-filters .filter-group {
flex: 1;
}
.search-filters label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
text-transform: uppercase;
}
.search-filters select {
width: 100%;
padding: 0.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.85rem;
}
/* Recent Searches */
.search-recent {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
}
.search-section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.search-section-title i {
font-size: 0.7rem;
}
.clear-recent {
margin-left: auto;
cursor: pointer;
color: var(--accent);
font-size: 0.7rem;
}
.clear-recent:hover {
text-decoration: underline;
}
.search-recent-items {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.search-recent-item {
padding: 0.4rem 0.8rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 0.8rem;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.search-recent-item:hover {
background: var(--accent);
color: white;
border-color: var(--accent);
}
/* Search Results */
.search-results-container {
padding: 0.5rem;
}
.search-results-section {
margin-bottom: 0.5rem;
}
.search-results-section .search-section-title {
padding: 0.5rem;
}
.search-results-list {
display: flex;
flex-direction: column;
}
.search-result-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.search-result-item:hover {
background: rgba(255, 107, 53, 0.15);
}
.search-result-item.highlighted {
background: rgba(255, 107, 53, 0.2);
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.search-result-icon {
width: 40px;
height: 40px;
background: var(--bg-secondary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
flex-shrink: 0;
}
.search-result-icon img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
}
.search-result-info {
flex: 1;
min-width: 0;
}
.search-result-title {
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-result-title .highlight {
background: rgba(255, 107, 53, 0.3);
color: var(--accent);
padding: 0 2px;
border-radius: 2px;
}
.search-result-subtitle {
font-size: 0.8rem;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-result-badge {
padding: 0.2rem 0.5rem;
background: var(--bg-secondary);
border-radius: 4px;
font-size: 0.7rem;
color: var(--text-secondary);
flex-shrink: 0;
}
.search-result-badge.aftermarket {
background: rgba(76, 175, 80, 0.15);
color: #66bb6a;
}
.search-result-badge.cross_reference {
background: rgba(255, 152, 0, 0.15);
color: #ffb74d;
}
.search-result-badge.vehicle-badge {
background: rgba(33, 150, 243, 0.15);
color: #42a5f5;
}
/* Vehicle category buttons in search */
.vehicle-item {
flex-wrap: wrap;
}
.vehicle-categories-row {
display: flex;
gap: 0.4rem;
margin-top: 0.5rem;
width: 100%;
}
.vehicle-category-btn {
padding: 0.35rem 0.6rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.3rem;
}
.vehicle-category-btn:hover {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.vehicle-category-btn i {
font-size: 0.8rem;
}
.vehicle-all-btn {
margin-left: auto;
background: rgba(33, 150, 243, 0.1);
border-color: rgba(33, 150, 243, 0.3);
color: #42a5f5;
}
.vehicle-all-btn:hover {
background: #42a5f5;
color: white;
}
/* Vehicle parts search results (combined search) */
.vehicle-parts-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.8rem;
background: linear-gradient(135deg, rgba(33, 150, 243, 0.15), rgba(33, 150, 243, 0.05));
border-radius: 8px;
margin-bottom: 0.5rem;
border-left: 3px solid #42a5f5;
}
.vehicle-parts-header i {
color: #42a5f5;
}
.vehicle-parts-header span {
font-weight: 600;
color: var(--text-primary);
}
.vehicle-parts-header small {
color: var(--text-secondary);
font-size: 0.8rem;
margin-left: auto;
}
.vehicle-part-item {
border-left: 2px solid transparent;
}
.vehicle-part-item:hover {
border-left-color: var(--accent);
}
.search-result-badge.vehicle-part-badge {
background: rgba(76, 175, 80, 0.15);
color: #66bb6a;
font-size: 0.65rem;
}
.search-result-badge.vehicle-part-badge i {
font-size: 0.6rem;
}
.search-category-badge {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
background: var(--bg-secondary);
border-radius: 3px;
color: var(--text-secondary);
margin-left: 0.5rem;
}
.search-result-subtitle .part-number {
font-family: 'Roboto Mono', monospace;
font-weight: 500;
}
.search-suggestion-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.search-tag {
padding: 0.3rem 0.75rem;
background: var(--bg-secondary);
border-radius: 20px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
}
.search-tag:hover {
background: var(--accent);
color: white;
}
/* No Results */
.search-no-results {
padding: 2rem;
text-align: center;
color: var(--text-secondary);
}
.search-no-results i {
font-size: 2rem;
margin-bottom: 0.5rem;
opacity: 0.5;
}
.search-no-results p {
font-weight: 500;
margin-bottom: 0.25rem;
}
.search-no-results span {
font-size: 0.85rem;
}
/* Dropdown Footer */
.search-dropdown-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.search-hint {
font-size: 0.75rem;
color: var(--text-secondary);
}
.search-hint kbd {
padding: 0.15rem 0.4rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 4px;
font-family: inherit;
font-size: 0.7rem;
}
.search-view-all {
padding: 0.5rem 1rem;
background: var(--accent);
border: none;
border-radius: 6px;
color: white;
font-size: 0.8rem;
cursor: pointer;
transition: background 0.2s;
}
.search-view-all:hover {
background: var(--accent-hover);
}
.header-actions {
display: flex;
align-items: center;
@@ -1221,16 +1695,97 @@
</a>
<div class="search-container">
<div class="search-box">
<input type="text" class="search-input" id="searchInput"
placeholder="Buscar por número de parte o nombre... (presiona /)"
aria-label="Buscar partes">
<button class="vin-btn" onclick="dashboard.openVinDecoder()" title="Decodificar VIN">
<i class="fas fa-barcode"></i>
</button>
<button class="search-btn" onclick="dashboard.searchPartNumber()">
<i class="fas fa-search"></i>
</button>
<div class="search-box-enhanced">
<div class="search-input-wrapper">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="searchInput"
placeholder="Buscar partes, números OEM, vehículos... (presiona /)"
aria-label="Buscar partes"
autocomplete="off"
oninput="enhancedSearch.onInput(this.value)"
onkeydown="enhancedSearch.onKeydown(event)"
onfocus="enhancedSearch.onFocus()">
<div class="search-filters-toggle" onclick="enhancedSearch.toggleFilters()">
<i class="fas fa-sliders-h"></i>
</div>
<button class="vin-btn" onclick="dashboard.openVinDecoder()" title="Decodificar VIN">
<i class="fas fa-barcode"></i>
</button>
<div class="search-loading" id="searchLoading" style="display: none;">
<div class="search-spinner"></div>
</div>
</div>
<!-- Dropdown de resultados -->
<div class="search-dropdown" id="searchDropdown">
<!-- Filtros -->
<div class="search-filters" id="searchFilters" style="display: none;">
<div class="filter-group">
<label>Categoría</label>
<select id="searchCategoryFilter" onchange="enhancedSearch.applyFilters()">
<option value="">Todas</option>
</select>
</div>
<div class="filter-group">
<label>Buscar en</label>
<select id="searchTypeFilter" onchange="enhancedSearch.applyFilters()">
<option value="all">Todo</option>
<option value="parts">Solo Partes</option>
<option value="vehicles">Solo Vehículos</option>
</select>
</div>
</div>
<!-- Búsquedas recientes -->
<div class="search-recent" id="searchRecent">
<div class="search-section-title">
<i class="fas fa-history"></i> Búsquedas recientes
<span class="clear-recent" onclick="enhancedSearch.clearRecent()">Limpiar</span>
</div>
<div class="search-recent-items" id="searchRecentItems"></div>
</div>
<!-- Resultados -->
<div class="search-results-container" id="searchResultsContainer">
<!-- Parts results -->
<div class="search-results-section" id="partsResults" style="display: none;">
<div class="search-section-title"><i class="fas fa-cog"></i> Partes</div>
<div class="search-results-list" id="partsResultsList"></div>
</div>
<!-- Vehicles results -->
<div class="search-results-section" id="vehiclesResults" style="display: none;">
<div class="search-section-title"><i class="fas fa-car"></i> Vehículos</div>
<div class="search-results-list" id="vehiclesResultsList"></div>
</div>
<!-- No results -->
<div class="search-no-results" id="searchNoResults" style="display: none;">
<i class="fas fa-search"></i>
<p>No se encontraron resultados</p>
<span>Intenta con otros términos de búsqueda</span>
<div class="search-suggestions" style="margin-top: 1rem;">
<span style="display: block; margin-bottom: 0.5rem; font-size: 0.8rem;">Búsquedas populares:</span>
<div class="search-suggestion-tags">
<span class="search-tag" onclick="enhancedSearch.searchRecent('brake')">brake</span>
<span class="search-tag" onclick="enhancedSearch.searchRecent('filter')">filter</span>
<span class="search-tag" onclick="enhancedSearch.searchRecent('spark plug')">spark plug</span>
<span class="search-tag" onclick="enhancedSearch.searchRecent('camry')">camry</span>
</div>
</div>
</div>
</div>
<!-- Footer con acciones -->
<div class="search-dropdown-footer" id="searchFooter" style="display: none;">
<span class="search-hint">
<kbd>↑↓</kbd> navegar <kbd>Enter</kbd> seleccionar <kbd>Esc</kbd> cerrar
</span>
<button class="search-view-all" onclick="enhancedSearch.viewAllResults()">
Ver todos los resultados <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
@@ -1252,6 +1807,9 @@
<a href="customer-landing.html" class="btn btn-secondary btn-icon" title="Ir a inicio">
<i class="fas fa-home"></i>
</a>
<a href="admin.html" class="btn btn-primary btn-icon" title="Panel de administración">
<i class="fas fa-cog"></i>
</a>
</div>
</div>
</header>
@@ -1310,7 +1868,7 @@
<div class="modal-overlay" id="searchResultsModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">
<h2 class="modal-title" id="searchResultsModalLabel">
<i class="fas fa-search"></i> Resultados de Búsqueda
</h2>
<button class="modal-close" onclick="dashboard.closeModal('searchResultsModal')">&times;</button>
@@ -1367,5 +1925,6 @@
</div>
<script src="dashboard.js"></script>
<script src="enhanced-search.js"></script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
oem_part_id,manufacturer_id,part_number,name,name_es,quality_tier,price_usd,warranty_months
1,1,P06050,Premium Brake Pad Set,Juego de Pastillas Premium,premium,89.99,36
1,5,PGD923,Professional Brake Pads,Pastillas Profesionales,standard,49.99,24
1,14,D923,Economy Brake Pads,Pastillas Economicas,economy,29.99,12
3,2,72226WS,Premium Oil Filter,Filtro de Aceite Premium,premium,12.99,12
3,4,PF63E,Professional Oil Filter,Filtro de Aceite Profesional,standard,8.99,12
4,2,5067WS,Premium Air Filter,Filtro de Aire Premium,premium,24.99,12
4,3,1A3000,High Flow Air Filter,Filtro de Aire Alto Flujo,premium,29.99,12
6,9,ILKAR7B11,Iridium IX Spark Plug,Bujia de Iridio IX,premium,14.99,24
6,2,9652,Platinum Spark Plug,Bujia de Platino,standard,9.99,12
7,11,T245,Timing Belt,Banda de Tiempo,premium,45.99,36
8,11,K060685,Serpentine Belt,Banda Serpentina,premium,32.99,24
8,12,4060685,Multi-V Belt,Banda Multi-V,standard,24.99,12
1 oem_part_id manufacturer_id part_number name name_es quality_tier price_usd warranty_months
2 1 1 P06050 Premium Brake Pad Set Juego de Pastillas Premium premium 89.99 36
3 1 5 PGD923 Professional Brake Pads Pastillas Profesionales standard 49.99 24
4 1 14 D923 Economy Brake Pads Pastillas Economicas economy 29.99 12
5 3 2 72226WS Premium Oil Filter Filtro de Aceite Premium premium 12.99 12
6 3 4 PF63E Professional Oil Filter Filtro de Aceite Profesional standard 8.99 12
7 4 2 5067WS Premium Air Filter Filtro de Aire Premium premium 24.99 12
8 4 3 1A3000 High Flow Air Filter Filtro de Aire Alto Flujo premium 29.99 12
9 6 9 ILKAR7B11 Iridium IX Spark Plug Bujia de Iridio IX premium 14.99 24
10 6 2 9652 Platinum Spark Plug Bujia de Platino standard 9.99 12
11 7 11 T245 Timing Belt Banda de Tiempo premium 45.99 36
12 8 11 K060685 Serpentine Belt Banda Serpentina premium 32.99 24
13 8 12 4060685 Multi-V Belt Banda Multi-V standard 24.99 12

View File

@@ -0,0 +1,11 @@
model_year_engine_id,part_id,quantity_required,position,fitment_notes
1,1,1,front,Fits all trims
1,2,1,rear,Fits all trims
1,3,1,,Recommended every 5000 miles
1,4,1,,Recommended every 15000 miles
1,6,4,,Check gap before installation
2,1,1,front,Fits all trims
2,2,1,rear,Fits all trims
2,3,1,,Recommended every 5000 miles
3,1,1,front,Sport model requires different pads
3,3,1,,Synthetic oil recommended
1 model_year_engine_id part_id quantity_required position fitment_notes
2 1 1 1 front Fits all trims
3 1 2 1 rear Fits all trims
4 1 3 1 Recommended every 5000 miles
5 1 4 1 Recommended every 15000 miles
6 1 6 4 Check gap before installation
7 2 1 1 front Fits all trims
8 2 2 1 rear Fits all trims
9 2 3 1 Recommended every 5000 miles
10 3 1 1 front Sport model requires different pads
11 3 3 1 Synthetic oil recommended

View File

@@ -0,0 +1,16 @@
name,type,quality_tier,country,website
Brembo,aftermarket,premium,Italy,https://www.brembo.com
Bosch,aftermarket,premium,Germany,https://www.bosch.com
Denso,aftermarket,premium,Japan,https://www.denso.com
ACDelco,aftermarket,standard,USA,https://www.acdelco.com
Raybestos,aftermarket,standard,USA,https://www.raybestos.com
Wagner,aftermarket,standard,USA,https://www.wagnerbrake.com
Monroe,aftermarket,standard,USA,https://www.monroe.com
KYB,aftermarket,premium,Japan,https://www.kyb.com
NGK,aftermarket,premium,Japan,https://www.ngk.com
Aisin,aftermarket,premium,Japan,https://www.aisin.com
Gates,aftermarket,premium,USA,https://www.gates.com
Continental,aftermarket,premium,Germany,https://www.continental.com
Moog,aftermarket,standard,USA,https://www.moogparts.com
Dorman,aftermarket,economy,USA,https://www.dormanproducts.com
Centric,aftermarket,standard,USA,https://www.centricparts.com
1 name type quality_tier country website
2 Brembo aftermarket premium Italy https://www.brembo.com
3 Bosch aftermarket premium Germany https://www.bosch.com
4 Denso aftermarket premium Japan https://www.denso.com
5 ACDelco aftermarket standard USA https://www.acdelco.com
6 Raybestos aftermarket standard USA https://www.raybestos.com
7 Wagner aftermarket standard USA https://www.wagnerbrake.com
8 Monroe aftermarket standard USA https://www.monroe.com
9 KYB aftermarket premium Japan https://www.kyb.com
10 NGK aftermarket premium Japan https://www.ngk.com
11 Aisin aftermarket premium Japan https://www.aisin.com
12 Gates aftermarket premium USA https://www.gates.com
13 Continental aftermarket premium Germany https://www.continental.com
14 Moog aftermarket standard USA https://www.moogparts.com
15 Dorman aftermarket economy USA https://www.dormanproducts.com
16 Centric aftermarket standard USA https://www.centricparts.com

View File

@@ -0,0 +1,11 @@
oem_part_number,name,name_es,group_id,description,description_es,weight_kg,material
04465-33450,Front Brake Pad Set,Juego de Pastillas de Freno Delanteras,5,Premium ceramic brake pads for front wheels,Pastillas de freno de ceramica premium para ruedas delanteras,1.2,Ceramic
04465-33460,Rear Brake Pad Set,Juego de Pastillas de Freno Traseras,5,Premium ceramic brake pads for rear wheels,Pastillas de freno de ceramica premium para ruedas traseras,0.9,Ceramic
90915-YZZD4,Oil Filter,Filtro de Aceite,15,High-efficiency oil filter,Filtro de aceite de alta eficiencia,0.3,Steel/Paper
16400-0W010,Air Filter,Filtro de Aire,16,Engine air filter element,Elemento de filtro de aire del motor,0.2,Paper
23300-79525,Fuel Filter,Filtro de Combustible,17,Inline fuel filter,Filtro de combustible en linea,0.4,Steel
90048-51003,Spark Plug,Bujia,20,Iridium spark plug,Bujia de iridio,0.05,Iridium
13568-09130,Timing Belt,Banda de Tiempo,25,Engine timing belt,Banda de distribucion del motor,0.3,Rubber
SU003-02574,Serpentine Belt,Banda Serpentina,26,Multi-rib drive belt,Banda de transmision multi-canal,0.2,EPDM Rubber
17801-0P010,Engine Air Filter,Filtro de Aire del Motor,16,High flow air filter,Filtro de aire de alto flujo,0.25,Cotton/Paper
48157-42010,Front Strut Mount,Soporte de Amortiguador Delantero,30,Front suspension strut mount,Soporte del amortiguador de suspension delantera,0.8,Steel/Rubber
1 oem_part_number name name_es group_id description description_es weight_kg material
2 04465-33450 Front Brake Pad Set Juego de Pastillas de Freno Delanteras 5 Premium ceramic brake pads for front wheels Pastillas de freno de ceramica premium para ruedas delanteras 1.2 Ceramic
3 04465-33460 Rear Brake Pad Set Juego de Pastillas de Freno Traseras 5 Premium ceramic brake pads for rear wheels Pastillas de freno de ceramica premium para ruedas traseras 0.9 Ceramic
4 90915-YZZD4 Oil Filter Filtro de Aceite 15 High-efficiency oil filter Filtro de aceite de alta eficiencia 0.3 Steel/Paper
5 16400-0W010 Air Filter Filtro de Aire 16 Engine air filter element Elemento de filtro de aire del motor 0.2 Paper
6 23300-79525 Fuel Filter Filtro de Combustible 17 Inline fuel filter Filtro de combustible en linea 0.4 Steel
7 90048-51003 Spark Plug Bujia 20 Iridium spark plug Bujia de iridio 0.05 Iridium
8 13568-09130 Timing Belt Banda de Tiempo 25 Engine timing belt Banda de distribucion del motor 0.3 Rubber
9 SU003-02574 Serpentine Belt Banda Serpentina 26 Multi-rib drive belt Banda de transmision multi-canal 0.2 EPDM Rubber
10 17801-0P010 Engine Air Filter Filtro de Aire del Motor 16 High flow air filter Filtro de aire de alto flujo 0.25 Cotton/Paper
11 48157-42010 Front Strut Mount Soporte de Amortiguador Delantero 30 Front suspension strut mount Soporte del amortiguador de suspension delantera 0.8 Steel/Rubber

File diff suppressed because it is too large Load Diff