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%; 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 { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1061,6 +1072,7 @@
<a href="#brands-section">Marcas</a> <a href="#brands-section">Marcas</a>
<a href="#featured-section">Productos</a> <a href="#featured-section">Productos</a>
<a href="#cta-section">Contacto</a> <a href="#cta-section">Contacto</a>
<a href="admin.html" class="admin-link">⚡ Admin</a>
</nav> </nav>
<div class="header-actions"> <div class="header-actions">
<button class="search-btn" onclick="openSearchModal()">🔍</button> <button class="search-btn" onclick="openSearchModal()">🔍</button>

View File

@@ -503,16 +503,16 @@ class VehicleDashboard {
const myeRecords = await myeRes.json(); const myeRecords = await myeRes.json();
// Merge mye_id into vehicles based on matching fields // 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 => { this.allVehicles = vehicles.map(v => {
const mye = myeRecords.find(m => const mye = myeRecords.find(m =>
m.brand === v.brand && m.brand === v.brand &&
m.model === v.model && m.model === v.model &&
m.year === v.year && m.year === v.year &&
m.engine === v.engine && m.engine === v.engine
(m.trim_level === v.trim_level || (!m.trim_level && !v.trim_level) || (m.trim_level === 'unknown' && v.trim_level === 'unknown'))
); );
return { ...v, mye_id: mye ? mye.id : null }; return { ...v, mye_id: mye ? mye.id : null };
}); }).filter(v => v.mye_id !== null); // Only show vehicles with parts
this.filteredVehicles = [...this.allVehicles]; this.filteredVehicles = [...this.allVehicles];
// Poblar filtros // Poblar filtros
@@ -714,7 +714,79 @@ class VehicleDashboard {
document.getElementById('filtersBar').classList.remove('visible'); 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) { 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.currentView = 'categories';
this.selectedVehicleId = myeId; this.selectedVehicleId = myeId;
this.selectedCategory = null; this.selectedCategory = null;
@@ -737,8 +809,8 @@ class VehicleDashboard {
`; `;
try { try {
// Get all categories (since we don't have vehicle-specific parts yet, show all categories) // Get vehicle-specific categories (only categories with parts for this vehicle)
const response = await fetch('/api/categories'); const response = await fetch(`/api/vehicles/${myeId}/categories`);
if (!response.ok) { if (!response.ok) {
throw new Error('Error al cargar categorías'); throw new Error('Error al cargar categorías');
@@ -824,7 +896,15 @@ class VehicleDashboard {
`; `;
try { 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) { if (!response.ok) {
throw new Error('Error al cargar grupos'); throw new Error('Error al cargar grupos');
@@ -921,15 +1001,23 @@ class VehicleDashboard {
`; `;
try { 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) { if (!response.ok) {
throw new Error('Error al cargar partes'); throw new Error('Error al cargar partes');
} }
const partsData = await response.json(); const partsData = await response.json();
// Handle paginated response // Handle both array response (vehicle parts) and paginated response
this.allParts = partsData.data || partsData; this.allParts = Array.isArray(partsData) ? partsData : (partsData.data || partsData);
this.displayParts(); this.displayParts();
} catch (error) { } 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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catálogo de Autopartes - AutoParts DB</title> <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 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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style> <style>
@@ -135,6 +136,479 @@
color: white; 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 { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1221,16 +1695,97 @@
</a> </a>
<div class="search-container"> <div class="search-container">
<div class="search-box"> <div class="search-box-enhanced">
<input type="text" class="search-input" id="searchInput" <div class="search-input-wrapper">
placeholder="Buscar por número de parte o nombre... (presiona /)" <i class="fas fa-search search-icon"></i>
aria-label="Buscar partes"> <input type="text" class="search-input" id="searchInput"
<button class="vin-btn" onclick="dashboard.openVinDecoder()" title="Decodificar VIN"> placeholder="Buscar partes, números OEM, vehículos... (presiona /)"
<i class="fas fa-barcode"></i> aria-label="Buscar partes"
</button> autocomplete="off"
<button class="search-btn" onclick="dashboard.searchPartNumber()"> oninput="enhancedSearch.onInput(this.value)"
<i class="fas fa-search"></i> onkeydown="enhancedSearch.onKeydown(event)"
</button> 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>
</div> </div>
@@ -1252,6 +1807,9 @@
<a href="customer-landing.html" class="btn btn-secondary btn-icon" title="Ir a inicio"> <a href="customer-landing.html" class="btn btn-secondary btn-icon" title="Ir a inicio">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
</a> </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>
</div> </div>
</header> </header>
@@ -1310,7 +1868,7 @@
<div class="modal-overlay" id="searchResultsModal"> <div class="modal-overlay" id="searchResultsModal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title"> <h2 class="modal-title" id="searchResultsModalLabel">
<i class="fas fa-search"></i> Resultados de Búsqueda <i class="fas fa-search"></i> Resultados de Búsqueda
</h2> </h2>
<button class="modal-close" onclick="dashboard.closeModal('searchResultsModal')">&times;</button> <button class="modal-close" onclick="dashboard.closeModal('searchResultsModal')">&times;</button>
@@ -1367,5 +1925,6 @@
</div> </div>
<script src="dashboard.js"></script> <script src="dashboard.js"></script>
<script src="enhanced-search.js"></script>
</body> </body>
</html> </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

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Import Gonher Filter Catalog PDF into the autoparts database.
Extracts filter part numbers and vehicle compatibility.
"""
import sqlite3
import re
import pypdf
from pathlib import Path
# Database path
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
PDF_PATH = '/tmp/filtros_catalog.pdf'
# Filter type mapping
FILTER_TYPES = {
'ACEITE': {'category': 'Engine', 'group': 'Oil Filters', 'name_prefix': 'Oil Filter'},
'SINTÉTICO': {'category': 'Engine', 'group': 'Oil Filters', 'name_prefix': 'Synthetic Oil Filter'},
'AIRE': {'category': 'Engine', 'group': 'Air Filters', 'name_prefix': 'Air Filter'},
'COMB.': {'category': 'Fuel & Air', 'group': 'Fuel Filters', 'name_prefix': 'Fuel Filter'},
'CABINA': {'category': 'Heat & Air Conditioning', 'group': 'Cabin Air Filters', 'name_prefix': 'Cabin Air Filter'}
}
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_manufacturer(cursor, name='Gonher'):
"""Ensure Gonher manufacturer exists"""
cursor.execute("SELECT id FROM manufacturers WHERE name = ?", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, 'aftermarket', 'standard', 'Mexico')
""", (name,))
return cursor.lastrowid
def get_or_create_group(cursor, category_name, group_name):
"""Get or create a part group"""
# Find category
cursor.execute("SELECT id FROM part_categories WHERE name = ?", (category_name,))
cat_row = cursor.fetchone()
if not cat_row:
print(f" Warning: Category '{category_name}' not found")
return None
category_id = cat_row['id']
# Find or create group
cursor.execute("""
SELECT id FROM part_groups
WHERE category_id = ? AND name = ?
""", (category_id, group_name))
group_row = cursor.fetchone()
if group_row:
return group_row['id']
# Create group
cursor.execute("""
INSERT INTO part_groups (category_id, name, name_es)
VALUES (?, ?, ?)
""", (category_id, group_name, group_name))
return cursor.lastrowid
def parse_year_range(year_str):
"""Parse year range like '2019 - 2016' or '2020- 2018' into list of years"""
year_str = year_str.strip()
# Handle single year
if re.match(r'^\d{4}$', year_str):
return [int(year_str)]
# Handle range with various separators
match = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
if start > end:
start, end = end, start
return list(range(end, start + 1))
return []
def parse_catalog_page(text, current_brand=None):
"""Parse a catalog page and extract vehicle-filter mappings"""
entries = []
lines = text.split('\n')
brand = current_brand
model = None
for line in lines:
line = line.strip()
if not line:
continue
# Skip header lines
if 'AÑO' in line and 'MOTOR' in line:
continue
if 'ACEITE' in line and 'AIRE' in line:
continue
if 'Los filtros Gonher' in line:
continue
if 'Para vehículos anteriores' in line:
continue
if '(Continúa)' in line:
# Extract brand/model from continuation
match = re.match(r'^([A-Z\s]+)\s*\(Continúa\)', line)
if match:
potential = match.group(1).strip()
if len(potential) > 2:
if potential in ['ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO']:
brand = potential
else:
model = potential
continue
# Check if this is a brand line (all caps, single word or known brand)
if re.match(r'^[A-Z][A-Z\s]+$', line) and len(line) > 2:
potential_brand = line.strip()
if potential_brand in ['ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO']:
brand = potential_brand
model = None
continue
# Check if this is a model line (letters/numbers, no year pattern at start)
if brand and re.match(r'^[A-Z][A-Z0-9\s\-]+$', line) and not re.match(r'^\d{4}', line):
if not any(c.isdigit() for c in line[:4]): # Model names don't start with 4 digits
potential_model = line.strip()
if len(potential_model) >= 2 and potential_model not in ['ACEITE', 'AIRE', 'COMB', 'CABINA']:
model = potential_model
continue
# Try to parse data line (year, motor, filters)
# Pattern: YEAR[-YEAR] MOTOR FILTER1 FILTER2 ...
if brand and model:
# Look for year at start
match = re.match(r'^(\d{4}(?:\s*[-]\s*\d{4})?)\s+(.+)$', line)
if match:
year_str = match.group(1)
rest = match.group(2)
# Extract motor (usually like L4-2.0L or V6-3.5L)
motor_match = re.match(r'^([LV]\d+[-][\d.]+L(?:\s+(?:Turbo|TURBOCHARGED|diésel|ELECTRIC))?)\s*(.*)$', rest, re.IGNORECASE)
if motor_match:
motor = motor_match.group(1).strip()
filters_str = motor_match.group(2).strip()
# Parse filter part numbers
# They follow pattern like: GP-149 GPS-149 GA-1113 GAC-92
filter_parts = re.findall(r'[A-Z]+[-][\dA-Z]+(?:\(\d+\))?', filters_str)
years = parse_year_range(year_str)
if years and filter_parts:
for year in years:
entry = {
'brand': brand,
'model': model,
'year': year,
'motor': motor,
'filters': {}
}
# Categorize filters by prefix
for fp in filter_parts:
fp_clean = re.sub(r'\(\d+\)', '', fp) # Remove (1) markers
if fp_clean.startswith('GP-') or fp_clean.startswith('G-'):
entry['filters']['ACEITE'] = fp_clean
elif fp_clean.startswith('GPS-'):
entry['filters']['SINTÉTICO'] = fp_clean
elif fp_clean.startswith('GA-') and not fp_clean.startswith('GAC-') and not fp_clean.startswith('GAVW-'):
entry['filters']['AIRE'] = fp_clean
elif fp_clean.startswith('GG-'):
entry['filters']['COMB.'] = fp_clean
elif fp_clean.startswith('GAC-'):
entry['filters']['CABINA'] = fp_clean
if entry['filters']:
entries.append(entry)
return entries, brand
def import_catalog():
"""Main import function"""
print("="*60)
print("GONHER FILTER CATALOG IMPORT")
print("="*60)
# Read PDF
print(f"\nReading PDF: {PDF_PATH}")
pdf = pypdf.PdfReader(PDF_PATH)
total_pages = len(pdf.pages)
print(f"Total pages: {total_pages}")
# Parse all pages
all_entries = []
current_brand = None
for i in range(total_pages):
text = pdf.pages[i].extract_text()
if text:
entries, current_brand = parse_catalog_page(text, current_brand)
all_entries.extend(entries)
if (i + 1) % 20 == 0:
print(f" Processed {i + 1}/{total_pages} pages, {len(all_entries)} entries so far...")
print(f"\nTotal entries extracted: {len(all_entries)}")
# Get unique filters
unique_filters = {}
for entry in all_entries:
for filter_type, part_num in entry['filters'].items():
if part_num not in unique_filters:
unique_filters[part_num] = filter_type
print(f"Unique filter part numbers: {len(unique_filters)}")
# Import to database
conn = get_db_connection()
cursor = conn.cursor()
# Ensure Gonher manufacturer exists
manufacturer_id = ensure_manufacturer(cursor, 'Gonher')
print(f"\nManufacturer ID: {manufacturer_id}")
# Create parts for each unique filter
print("\nCreating filter parts...")
filter_part_ids = {}
for part_num, filter_type in unique_filters.items():
config = FILTER_TYPES.get(filter_type)
if not config:
continue
group_id = get_or_create_group(cursor, config['category'], config['group'])
if not group_id:
continue
# Check if part exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part_num,))
existing = cursor.fetchone()
if existing:
filter_part_ids[part_num] = existing['id']
else:
# Create new part
name = f"{config['name_prefix']} {part_num}"
name_es = f"Filtro {part_num}"
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description)
VALUES (?, ?, ?, ?, ?)
""", (part_num, name, name_es, group_id, f"Gonher {config['name_prefix']}"))
filter_part_ids[part_num] = cursor.lastrowid
print(f" Created/found {len(filter_part_ids)} filter parts")
# Create vehicle fitments
print("\nCreating vehicle fitments...")
fitments_created = 0
fitments_skipped = 0
for entry in all_entries:
# Find matching vehicle (model_year_engine)
cursor.execute("""
SELECT mye.id
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
WHERE UPPER(b.name) = UPPER(?)
AND UPPER(m.name) = UPPER(?)
AND y.year = ?
LIMIT 1
""", (entry['brand'], entry['model'], entry['year']))
mye_row = cursor.fetchone()
if not mye_row:
fitments_skipped += 1
continue
mye_id = mye_row['id']
# Create fitments for each filter
for filter_type, part_num in entry['filters'].items():
part_id = filter_part_ids.get(part_num)
if not part_id:
continue
# Check if fitment exists
cursor.execute("""
SELECT id FROM vehicle_parts
WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if not cursor.fetchone():
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, fitment_notes)
VALUES (?, ?, 1, ?)
""", (mye_id, part_id, f"Gonher catalog 2022 - {filter_type}"))
fitments_created += 1
print(f" Fitments created: {fitments_created}")
print(f" Entries skipped (vehicle not found): {fitments_skipped}")
conn.commit()
conn.close()
print("\n" + "="*60)
print("IMPORT COMPLETE")
print("="*60)
return {
'total_entries': len(all_entries),
'unique_filters': len(unique_filters),
'parts_created': len(filter_part_ids),
'fitments_created': fitments_created
}
if __name__ == '__main__':
results = import_catalog()
print(f"\nSummary: {results}")

View File

@@ -0,0 +1,511 @@
#!/usr/bin/env python3
"""
IMPORTADOR COMPLETO DEL CATÁLOGO GONHER 2022
- Crea vehículos faltantes
- Crea partes de filtros Gonher
- Crea referencias cruzadas con otras marcas (AC Delco, Fram, etc.)
- Crea fitments (vincula partes a vehículos)
"""
import sqlite3
import re
import pypdf
from pathlib import Path
from collections import defaultdict
# Paths
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
PDF_PATH = '/tmp/filtros_catalog.pdf'
# Filter type configuration
FILTER_TYPES = {
'ACEITE': {'category': 'Engine', 'group': 'Oil Filters', 'prefix': 'Oil Filter', 'prefix_es': 'Filtro de Aceite'},
'SINTÉTICO': {'category': 'Engine', 'group': 'Oil Filters', 'prefix': 'Synthetic Oil Filter', 'prefix_es': 'Filtro de Aceite Sintético'},
'AIRE': {'category': 'Engine', 'group': 'Air Filters', 'prefix': 'Air Filter', 'prefix_es': 'Filtro de Aire'},
'COMB.': {'category': 'Fuel & Air', 'group': 'Fuel Filters', 'prefix': 'Fuel Filter', 'prefix_es': 'Filtro de Combustible'},
'CABINA': {'category': 'Heat & Air Conditioning', 'group': 'Cabin Air Filters', 'prefix': 'Cabin Air Filter', 'prefix_es': 'Filtro de Cabina'}
}
# Known brands in catalog
CATALOG_BRANDS = [
'ACURA', 'ALFA ROMEO', 'AUDI', 'BMW', 'BUICK', 'CADILLAC',
'CHEVROLET', 'CHRYSLER', 'DODGE', 'FIAT', 'FORD', 'GMC',
'HONDA', 'HYUNDAI', 'INFINITI', 'JAGUAR', 'JEEP', 'KIA',
'LEXUS', 'LINCOLN', 'MAZDA', 'MERCEDES BENZ', 'MERCURY',
'MINI', 'MITSUBISHI', 'NISSAN', 'PEUGEOT', 'PONTIAC',
'PORSCHE', 'RAM', 'RENAULT', 'SEAT', 'SMART', 'SUBARU',
'SUZUKI', 'TOYOTA', 'VOLKSWAGEN', 'VOLVO'
]
# Cross-reference brands
XREF_BRANDS = ['AC DELCO', 'FRAM', 'INTERFIL', 'MANN', 'MOTORCRAFT']
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_manufacturer(cursor, name, type_='aftermarket', quality='standard', country=None):
"""Create manufacturer if not exists"""
cursor.execute("SELECT id FROM manufacturers WHERE UPPER(name) = UPPER(?)", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, ?, ?, ?)
""", (name, type_, quality, country))
return cursor.lastrowid
def ensure_brand(cursor, name):
"""Create brand if not exists"""
cursor.execute("SELECT id FROM brands WHERE UPPER(name) = UPPER(?)", (name,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO brands (name) VALUES (?)", (name,))
return cursor.lastrowid
def ensure_model(cursor, brand_id, name):
"""Create model if not exists"""
cursor.execute("""
SELECT id FROM models
WHERE brand_id = ? AND UPPER(name) = UPPER(?)
""", (brand_id, name))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO models (brand_id, name) VALUES (?, ?)", (brand_id, name))
return cursor.lastrowid
def ensure_year(cursor, year):
"""Create year if not exists"""
cursor.execute("SELECT id FROM years WHERE year = ?", (year,))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("INSERT INTO years (year) VALUES (?)", (year,))
return cursor.lastrowid
def ensure_engine(cursor, name):
"""Create engine if not exists"""
cursor.execute("SELECT id FROM engines WHERE name = ?", (name,))
row = cursor.fetchone()
if row:
return row['id']
# Parse engine details from name
displacement = None
cylinders = None
fuel_type = 'gasoline' # lowercase to match DB constraint
# Parse displacement and cylinders from patterns like "L4-2.0L" or "V6-3.5L"
match = re.match(r'([LV])(\d+)[-]?([\d.]+)L?', name)
if match:
engine_type = match.group(1) # L or V
cylinders = int(match.group(2))
displacement = int(float(match.group(3)) * 1000)
if 'DIESEL' in name.upper() or 'DIÉSEL' in name.upper():
fuel_type = 'diesel'
elif 'ELECTRIC' in name.upper():
fuel_type = 'electric'
elif 'HYBRID' in name.upper():
fuel_type = 'hybrid'
# Note: 'TURBO' is not a fuel type, it's a modifier - default to gasoline
cursor.execute("""
INSERT INTO engines (name, displacement_cc, cylinders, fuel_type)
VALUES (?, ?, ?, ?)
""", (name, displacement, cylinders, fuel_type))
return cursor.lastrowid
def ensure_mye(cursor, model_id, year_id, engine_id):
"""Create model_year_engine if not exists"""
cursor.execute("""
SELECT id FROM model_year_engine
WHERE model_id = ? AND year_id = ? AND engine_id = ?
""", (model_id, year_id, engine_id))
row = cursor.fetchone()
if row:
return row['id']
cursor.execute("""
INSERT INTO model_year_engine (model_id, year_id, engine_id)
VALUES (?, ?, ?)
""", (model_id, year_id, engine_id))
return cursor.lastrowid
def get_or_create_group(cursor, category_name, group_name):
"""Get or create part group"""
cursor.execute("SELECT id FROM part_categories WHERE name = ?", (category_name,))
cat_row = cursor.fetchone()
if not cat_row:
return None
category_id = cat_row['id']
cursor.execute("""
SELECT id FROM part_groups WHERE category_id = ? AND name = ?
""", (category_id, group_name))
group_row = cursor.fetchone()
if group_row:
return group_row['id']
cursor.execute("""
INSERT INTO part_groups (category_id, name, name_es)
VALUES (?, ?, ?)
""", (category_id, group_name, group_name))
return cursor.lastrowid
def parse_year_range(year_str):
"""Parse year range into list of years"""
year_str = year_str.strip()
if re.match(r'^\d{4}$', year_str):
return [int(year_str)]
match = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
if start > end:
start, end = end, start
return list(range(end, start + 1))
return []
def classify_filter(part_number):
"""Classify filter type by part number prefix"""
part_number = part_number.upper()
if part_number.startswith('GP-') or part_number.startswith('GPS-'):
if part_number.startswith('GPS-'):
return 'SINTÉTICO'
return 'ACEITE'
elif part_number.startswith('GA-') and not part_number.startswith('GAC-') and not part_number.startswith('GAVW-'):
return 'AIRE'
elif part_number.startswith('GG-'):
return 'COMB.'
elif part_number.startswith('GAC-'):
return 'CABINA'
elif part_number.startswith('G-'):
return 'ACEITE' # Generic oil filter
return None
def extract_vehicle_entries(pdf):
"""Extract all vehicle entries from catalog"""
entries = []
current_brand = None
current_model = None
for page in pdf.pages:
text = page.extract_text()
if not text:
continue
for line in text.split('\n'):
line = line.strip()
if not line:
continue
# Skip header/footer
if 'AÑO' in line and 'MOTOR' in line:
continue
if 'Los filtros Gonher' in line:
continue
# Brand detection
if line in CATALOG_BRANDS:
current_brand = line
current_model = None
continue
# Handle (Continúa) lines
if '(Continúa)' in line:
match = re.match(r'^([A-Z][A-Z0-9\s\-]+)\s*\(Continúa\)', line)
if match:
potential = match.group(1).strip()
if potential in CATALOG_BRANDS:
current_brand = potential
elif current_brand:
current_model = potential
continue
# Model detection
if current_brand:
if re.match(r'^[A-Z][A-Z0-9\s\-/]+$', line) and not re.match(r'^\d{4}', line):
if line not in ['ACEITE', 'AIRE', 'COMB', 'CABINA', 'SINTÉTICO', 'AÑO', 'MOTOR']:
if not re.match(r'^G[APCS]?[-]?\d', line): # Not a part number
current_model = line
continue
# Data line with year
if current_brand and current_model:
match = re.match(r'^(\d{4}(?:\s*[-]\s*\d{4})?)\s+(.+)$', line)
if match:
year_str = match.group(1)
rest = match.group(2)
# Extract motor
motor_match = re.match(r'^([LV]\d+[-][\d.]+L(?:\s+(?:Turbo|TURBOCHARGED|diésel|ELECTRIC|HYBRID))?)\s*(.*)$', rest, re.IGNORECASE)
if motor_match:
motor = motor_match.group(1).strip()
filters_str = motor_match.group(2).strip()
# Parse filter part numbers
filter_parts = re.findall(r'G[A-Z]*[-]?[\dA-Z]+(?:\(\d+\))?', filters_str)
years = parse_year_range(year_str)
if years:
for year in years:
entry = {
'brand': current_brand,
'model': current_model,
'year': year,
'motor': motor,
'filters': {}
}
for fp in filter_parts:
fp_clean = re.sub(r'\(\d+\)', '', fp)
filter_type = classify_filter(fp_clean)
if filter_type:
entry['filters'][filter_type] = fp_clean
if entry['filters']:
entries.append(entry)
return entries
def extract_cross_references(pdf):
"""Extract cross-reference data from catalog"""
xrefs = []
current_brand = None
# Cross-references are typically in pages 117+
for i in range(117, len(pdf.pages)):
text = pdf.pages[i].extract_text()
if not text:
continue
for line in text.split('\n'):
line = line.strip()
# Brand header
if line in XREF_BRANDS:
current_brand = line
continue
# Cross-reference line
if current_brand:
match = re.match(r'^([A-Z0-9\-/]+)\s+(G[A-Z]*[-]?\d+[A-Z]*)$', line)
if match:
xrefs.append({
'brand': current_brand,
'part_number': match.group(1),
'gonher_part': match.group(2)
})
return xrefs
def main():
print("=" * 70)
print("IMPORTADOR COMPLETO - CATÁLOGO GONHER 2022")
print("=" * 70)
# Read PDF
print(f"\n[1/7] Leyendo PDF: {PDF_PATH}")
pdf = pypdf.PdfReader(PDF_PATH)
print(f" Total páginas: {len(pdf.pages)}")
# Extract data
print("\n[2/7] Extrayendo datos del catálogo...")
vehicle_entries = extract_vehicle_entries(pdf)
cross_refs = extract_cross_references(pdf)
print(f" Entradas de vehículos: {len(vehicle_entries)}")
print(f" Referencias cruzadas: {len(cross_refs)}")
# Get unique filters
unique_filters = {}
for entry in vehicle_entries:
for filter_type, part_num in entry['filters'].items():
if part_num not in unique_filters:
unique_filters[part_num] = filter_type
print(f" Filtros únicos: {len(unique_filters)}")
# Connect to database
conn = get_db()
cursor = conn.cursor()
# Create manufacturers
print("\n[3/7] Creando fabricantes...")
manufacturers = {
'Gonher': ensure_manufacturer(cursor, 'Gonher', 'aftermarket', 'standard', 'Mexico'),
'AC Delco': ensure_manufacturer(cursor, 'AC Delco', 'oem', 'oem', 'USA'),
'Fram': ensure_manufacturer(cursor, 'Fram', 'aftermarket', 'standard', 'USA'),
'Interfil': ensure_manufacturer(cursor, 'Interfil', 'aftermarket', 'economy', 'Mexico'),
'Mann': ensure_manufacturer(cursor, 'Mann', 'aftermarket', 'premium', 'Germany'),
'Motorcraft': ensure_manufacturer(cursor, 'Motorcraft', 'oem', 'oem', 'USA'),
}
print(f" Fabricantes: {list(manufacturers.keys())}")
# Create vehicles
print("\n[4/7] Creando vehículos faltantes...")
vehicles_created = 0
mye_cache = {}
for entry in vehicle_entries:
cache_key = (entry['brand'], entry['model'], entry['year'], entry['motor'])
if cache_key in mye_cache:
continue
# Check if vehicle exists
cursor.execute("""
SELECT mye.id FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
JOIN engines e ON mye.engine_id = e.id
WHERE UPPER(b.name) = UPPER(?) AND UPPER(m.name) = UPPER(?)
AND y.year = ? AND e.name = ?
LIMIT 1
""", (entry['brand'], entry['model'], entry['year'], entry['motor']))
existing = cursor.fetchone()
if existing:
mye_cache[cache_key] = existing['id']
else:
# Create vehicle
brand_id = ensure_brand(cursor, entry['brand'])
model_id = ensure_model(cursor, brand_id, entry['model'])
year_id = ensure_year(cursor, entry['year'])
engine_id = ensure_engine(cursor, entry['motor'])
mye_id = ensure_mye(cursor, model_id, year_id, engine_id)
mye_cache[cache_key] = mye_id
vehicles_created += 1
print(f" Vehículos creados: {vehicles_created}")
# Create filter parts
print("\n[5/7] Creando partes de filtros...")
filter_parts = {}
parts_created = 0
for part_num, filter_type in unique_filters.items():
config = FILTER_TYPES.get(filter_type)
if not config:
continue
group_id = get_or_create_group(cursor, config['category'], config['group'])
if not group_id:
continue
# Check if part exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part_num,))
existing = cursor.fetchone()
if existing:
filter_parts[part_num] = existing['id']
else:
name = f"{config['prefix']} {part_num}"
name_es = f"{config['prefix_es']} {part_num}"
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description)
VALUES (?, ?, ?, ?, ?)
""", (part_num, name, name_es, group_id, f"Gonher {config['prefix']}"))
filter_parts[part_num] = cursor.lastrowid
parts_created += 1
print(f" Partes creadas: {parts_created}")
# Create fitments
print("\n[6/7] Creando fitments (vehículo-parte)...")
fitments_created = 0
for entry in vehicle_entries:
cache_key = (entry['brand'], entry['model'], entry['year'], entry['motor'])
mye_id = mye_cache.get(cache_key)
if not mye_id:
continue
for filter_type, part_num in entry['filters'].items():
part_id = filter_parts.get(part_num)
if not part_id:
continue
# Check if fitment exists
cursor.execute("""
SELECT id FROM vehicle_parts
WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if not cursor.fetchone():
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, fitment_notes)
VALUES (?, ?, 1, ?)
""", (mye_id, part_id, f"Catálogo Gonher 2022 - {filter_type}"))
fitments_created += 1
print(f" Fitments creados: {fitments_created}")
# Create cross-references
print("\n[7/7] Creando referencias cruzadas...")
xrefs_created = 0
for xref in cross_refs:
gonher_part_id = filter_parts.get(xref['gonher_part'])
if not gonher_part_id:
# Part might not exist yet, try to find by OEM number
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (xref['gonher_part'],))
row = cursor.fetchone()
if row:
gonher_part_id = row['id']
else:
continue
# Check if cross-reference exists
cursor.execute("""
SELECT id FROM part_cross_references
WHERE part_id = ? AND cross_reference_number = ?
""", (gonher_part_id, xref['part_number']))
if not cursor.fetchone():
# Map brand to reference type
ref_type = 'interchange'
if xref['brand'] in ['AC DELCO', 'MOTORCRAFT']:
ref_type = 'oem_alternate'
cursor.execute("""
INSERT INTO part_cross_references (part_id, cross_reference_number, reference_type)
VALUES (?, ?, ?)
""", (gonher_part_id, xref['part_number'], ref_type))
xrefs_created += 1
print(f" Referencias cruzadas creadas: {xrefs_created}")
# Commit
conn.commit()
conn.close()
print("\n" + "=" * 70)
print("IMPORTACIÓN COMPLETADA")
print("=" * 70)
print(f"""
RESUMEN:
- Vehículos creados: {vehicles_created:,}
- Partes creadas: {parts_created:,}
- Fitments creados: {fitments_created:,}
- Cross-refs creadas: {xrefs_created:,}
- Fabricantes: {len(manufacturers)}
""")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""
Script para poblar la base de datos con datos de demo realistas.
Incluye partes OEM, fabricantes aftermarket, partes alternativas y fitments.
"""
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent.parent / 'vehicle_database.db'
# Demo Parts Data - Partes realistas comunes
DEMO_PARTS = [
# Brake System (category 2, group varies)
{'oem': '04465-33450', 'name': 'Front Brake Pad Set', 'name_es': 'Juego de Pastillas Delanteras', 'group': 'Brake Pads/Shoes', 'desc': 'Ceramic front brake pads', 'weight': 1.2},
{'oem': '04466-33180', 'name': 'Rear Brake Pad Set', 'name_es': 'Juego de Pastillas Traseras', 'group': 'Brake Pads/Shoes', 'desc': 'Ceramic rear brake pads', 'weight': 0.9},
{'oem': '43512-33130', 'name': 'Front Brake Rotor', 'name_es': 'Disco de Freno Delantero', 'group': 'Brake Rotors', 'desc': 'Vented front brake disc', 'weight': 8.5},
{'oem': '42431-33130', 'name': 'Rear Brake Rotor', 'name_es': 'Disco de Freno Trasero', 'group': 'Brake Rotors', 'desc': 'Solid rear brake disc', 'weight': 5.2},
{'oem': '47750-33210', 'name': 'Brake Caliper Front Right', 'name_es': 'Caliper Delantero Derecho', 'group': 'Brake Calipers', 'desc': 'Front right brake caliper assembly', 'weight': 4.5},
{'oem': '47730-33210', 'name': 'Brake Caliper Front Left', 'name_es': 'Caliper Delantero Izquierdo', 'group': 'Brake Calipers', 'desc': 'Front left brake caliper assembly', 'weight': 4.5},
# Engine (category 6)
{'oem': '90915-YZZD4', 'name': 'Oil Filter', 'name_es': 'Filtro de Aceite', 'group': 'Oil Filters', 'desc': 'Spin-on oil filter element', 'weight': 0.3},
{'oem': '17801-0P010', 'name': 'Air Filter', 'name_es': 'Filtro de Aire', 'group': 'Air Filters', 'desc': 'Engine air filter element', 'weight': 0.25},
{'oem': '23300-79525', 'name': 'Fuel Filter', 'name_es': 'Filtro de Combustible', 'group': 'Fuel Filters', 'desc': 'In-line fuel filter', 'weight': 0.4},
{'oem': '90048-51003', 'name': 'Spark Plug Iridium', 'name_es': 'Bujia de Iridio', 'group': 'Spark Plugs', 'desc': 'Long-life iridium spark plug', 'weight': 0.05},
{'oem': '13568-09130', 'name': 'Timing Belt', 'name_es': 'Banda de Tiempo', 'group': 'Timing Components', 'desc': 'Engine timing belt', 'weight': 0.35},
{'oem': 'SU003-02574', 'name': 'Serpentine Belt', 'name_es': 'Banda Serpentina', 'group': 'Belts', 'desc': 'Multi-rib accessory drive belt', 'weight': 0.2},
{'oem': '16100-09515', 'name': 'Water Pump', 'name_es': 'Bomba de Agua', 'group': 'Water Pumps', 'desc': 'Engine coolant water pump', 'weight': 2.1},
{'oem': '16363-0P030', 'name': 'Thermostat', 'name_es': 'Termostato', 'group': 'Thermostats', 'desc': 'Engine thermostat with housing', 'weight': 0.3},
# Suspension (category 11)
{'oem': '48157-42010', 'name': 'Front Strut Mount', 'name_es': 'Soporte de Amortiguador Delantero', 'group': 'Strut Mounts', 'desc': 'Front suspension strut mount', 'weight': 0.8},
{'oem': '48609-48020', 'name': 'Front Strut Assembly', 'name_es': 'Amortiguador Delantero Completo', 'group': 'Struts/Shocks', 'desc': 'Complete front strut assembly', 'weight': 6.5},
{'oem': '48530-09S00', 'name': 'Rear Shock Absorber', 'name_es': 'Amortiguador Trasero', 'group': 'Struts/Shocks', 'desc': 'Rear shock absorber', 'weight': 3.2},
{'oem': '48068-33070', 'name': 'Front Lower Control Arm', 'name_es': 'Brazo de Control Inferior Delantero', 'group': 'Control Arms', 'desc': 'Front lower control arm with bushing', 'weight': 4.8},
{'oem': '48725-33050', 'name': 'Rear Trailing Arm', 'name_es': 'Brazo Trasero', 'group': 'Control Arms', 'desc': 'Rear suspension trailing arm', 'weight': 3.5},
{'oem': '48815-33070', 'name': 'Sway Bar Link Front', 'name_es': 'Enlace de Barra Estabilizadora', 'group': 'Sway Bar Links', 'desc': 'Front stabilizer bar end link', 'weight': 0.4},
# Steering (category 10)
{'oem': '45046-39335', 'name': 'Outer Tie Rod End', 'name_es': 'Terminal Exterior de Direccion', 'group': 'Tie Rods', 'desc': 'Outer steering tie rod end', 'weight': 0.6},
{'oem': '45503-39165', 'name': 'Inner Tie Rod', 'name_es': 'Terminal Interior de Direccion', 'group': 'Tie Rods', 'desc': 'Inner steering tie rod', 'weight': 0.5},
{'oem': '44200-33480', 'name': 'Power Steering Pump', 'name_es': 'Bomba de Direccion', 'group': 'Power Steering', 'desc': 'Hydraulic power steering pump', 'weight': 3.8},
{'oem': '45510-33310', 'name': 'Steering Rack', 'name_es': 'Cremallera de Direccion', 'group': 'Steering Racks', 'desc': 'Power steering rack and pinion', 'weight': 12.5},
# Electrical (category 5)
{'oem': '28100-21030', 'name': 'Starter Motor', 'name_es': 'Motor de Arranque', 'group': 'Starters', 'desc': 'Engine starter motor', 'weight': 4.2},
{'oem': '27060-37030', 'name': 'Alternator', 'name_es': 'Alternador', 'group': 'Alternators', 'desc': '100 amp alternator', 'weight': 5.8},
{'oem': '90919-02240', 'name': 'Ignition Coil', 'name_es': 'Bobina de Ignicion', 'group': 'Ignition Coils', 'desc': 'Direct ignition coil', 'weight': 0.35},
{'oem': '89467-33040', 'name': 'Oxygen Sensor', 'name_es': 'Sensor de Oxigeno', 'group': 'Sensors', 'desc': 'Upstream O2 sensor', 'weight': 0.15},
# Cooling System (category 3)
{'oem': '16400-0W010', 'name': 'Radiator', 'name_es': 'Radiador', 'group': 'Radiators', 'desc': 'Engine cooling radiator', 'weight': 8.5},
{'oem': '16711-31310', 'name': 'Radiator Fan', 'name_es': 'Ventilador de Radiador', 'group': 'Cooling Fans', 'desc': 'Electric radiator cooling fan', 'weight': 3.2},
{'oem': '16361-0P030', 'name': 'Radiator Hose Upper', 'name_es': 'Manguera Superior de Radiador', 'group': 'Hoses', 'desc': 'Upper radiator coolant hose', 'weight': 0.4},
# Exhaust (category 7)
{'oem': '17140-31620', 'name': 'Catalytic Converter', 'name_es': 'Convertidor Catalitico', 'group': 'Catalytic Converters', 'desc': 'Three-way catalytic converter', 'weight': 6.5},
{'oem': '17430-31410', 'name': 'Muffler', 'name_es': 'Silenciador', 'group': 'Mufflers', 'desc': 'Rear exhaust muffler', 'weight': 8.2},
# Fuel System (category 8)
{'oem': '23220-31100', 'name': 'Fuel Pump', 'name_es': 'Bomba de Combustible', 'group': 'Fuel Pumps', 'desc': 'Electric in-tank fuel pump', 'weight': 0.8},
{'oem': '23250-31010', 'name': 'Fuel Injector', 'name_es': 'Inyector de Combustible', 'group': 'Fuel Injectors', 'desc': 'Multi-port fuel injector', 'weight': 0.1},
# Drivetrain (category 4)
{'oem': '31470-52011', 'name': 'Clutch Kit', 'name_es': 'Kit de Embrague', 'group': 'Clutches', 'desc': 'Complete clutch replacement kit', 'weight': 7.5},
{'oem': '43502-35210', 'name': 'CV Axle Front', 'name_es': 'Flecha Homocinética Delantera', 'group': 'CV Axles', 'desc': 'Front CV axle shaft assembly', 'weight': 5.2},
# HVAC (category 9)
{'oem': '88310-33250', 'name': 'AC Compressor', 'name_es': 'Compresor de AC', 'group': 'AC Compressors', 'desc': 'Air conditioning compressor', 'weight': 6.8},
{'oem': '87103-33110', 'name': 'Blower Motor', 'name_es': 'Motor de Ventilador', 'group': 'Blower Motors', 'desc': 'HVAC blower motor', 'weight': 1.5},
{'oem': '87139-YZZ05', 'name': 'Cabin Air Filter', 'name_es': 'Filtro de Cabina', 'group': 'Cabin Filters', 'desc': 'Cabin air filter element', 'weight': 0.15},
]
# Demo Manufacturers
DEMO_MANUFACTURERS = [
{'name': 'Brembo', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Italy'},
{'name': 'Bosch', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
{'name': 'Denso', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'ACDelco', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Raybestos', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Wagner', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Monroe', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'KYB', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'NGK', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'Aisin', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Japan'},
{'name': 'Gates', 'type': 'aftermarket', 'tier': 'premium', 'country': 'USA'},
{'name': 'Continental', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
{'name': 'Moog', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Dorman', 'type': 'aftermarket', 'tier': 'economy', 'country': 'USA'},
{'name': 'Centric', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Beck/Arnley', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Cardone', 'type': 'remanufactured', 'tier': 'standard', 'country': 'USA'},
{'name': 'Standard Motor', 'type': 'aftermarket', 'tier': 'standard', 'country': 'USA'},
{'name': 'Spectra Premium', 'type': 'aftermarket', 'tier': 'economy', 'country': 'Canada'},
{'name': 'TRW', 'type': 'aftermarket', 'tier': 'premium', 'country': 'Germany'},
]
def get_group_id(cursor, group_name):
"""Get or create a group by name"""
cursor.execute("SELECT id FROM part_groups WHERE name = ?", (group_name,))
row = cursor.fetchone()
if row:
return row[0]
return None
def populate_parts(cursor):
"""Populate OEM parts"""
print("Adding OEM parts...")
added = 0
for part in DEMO_PARTS:
group_id = get_group_id(cursor, part['group'])
if not group_id:
print(f" Warning: Group '{part['group']}' not found, skipping {part['oem']}")
continue
# Check if part already exists
cursor.execute("SELECT id FROM parts WHERE oem_part_number = ?", (part['oem'],))
if cursor.fetchone():
continue
cursor.execute("""
INSERT INTO parts (oem_part_number, name, name_es, group_id, description, weight_kg)
VALUES (?, ?, ?, ?, ?, ?)
""", (part['oem'], part['name'], part['name_es'], group_id, part['desc'], part['weight']))
added += 1
print(f" Added {added} OEM parts")
return added
def populate_manufacturers(cursor):
"""Populate manufacturers"""
print("Adding manufacturers...")
added = 0
for mfr in DEMO_MANUFACTURERS:
cursor.execute("SELECT id FROM manufacturers WHERE name = ?", (mfr['name'],))
if cursor.fetchone():
continue
cursor.execute("""
INSERT INTO manufacturers (name, type, quality_tier, country)
VALUES (?, ?, ?, ?)
""", (mfr['name'], mfr['type'], mfr['tier'], mfr['country']))
added += 1
print(f" Added {added} manufacturers")
return added
def populate_aftermarket(cursor):
"""Populate aftermarket parts linked to OEM"""
print("Adding aftermarket alternatives...")
# Get all parts and manufacturers
cursor.execute("SELECT id, oem_part_number, name FROM parts")
parts = cursor.fetchall()
cursor.execute("SELECT id, name, quality_tier FROM manufacturers WHERE type != 'oem'")
manufacturers = cursor.fetchall()
added = 0
import random
for part in parts:
part_id, oem_num, part_name = part
# Add 2-4 aftermarket alternatives per OEM part
num_alternatives = random.randint(2, 4)
selected_mfrs = random.sample(manufacturers, min(num_alternatives, len(manufacturers)))
for mfr in selected_mfrs:
mfr_id, mfr_name, tier = mfr
# Check if already exists
cursor.execute("""
SELECT id FROM aftermarket_parts WHERE oem_part_id = ? AND manufacturer_id = ?
""", (part_id, mfr_id))
if cursor.fetchone():
continue
# Generate part number
prefix = mfr_name[:3].upper()
part_num = f"{prefix}-{oem_num.replace('-', '')[:6]}"
# Set price based on tier
base_price = random.uniform(20, 200)
if tier == 'premium':
price = base_price * 1.5
warranty = 36
elif tier == 'economy':
price = base_price * 0.6
warranty = 12
else:
price = base_price
warranty = 24
cursor.execute("""
INSERT INTO aftermarket_parts (oem_part_id, manufacturer_id, part_number, name, quality_tier, price_usd, warranty_months)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (part_id, mfr_id, part_num, part_name, tier, round(price, 2), warranty))
added += 1
print(f" Added {added} aftermarket parts")
return added
def populate_fitments(cursor):
"""Create fitments linking parts to vehicles"""
print("Adding fitments (linking parts to vehicles)...")
# Get some vehicle configurations (first 50)
cursor.execute("""
SELECT mye.id, b.name as brand, m.name as model, y.year
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id
JOIN brands b ON m.brand_id = b.id
JOIN years y ON mye.year_id = y.id
WHERE b.name IN ('TOYOTA', 'HONDA', 'FORD', 'CHEVROLET', 'NISSAN')
ORDER BY RANDOM()
LIMIT 100
""")
vehicles = cursor.fetchall()
# Get all parts
cursor.execute("SELECT id, name FROM parts")
parts = cursor.fetchall()
added = 0
import random
for vehicle in vehicles:
mye_id = vehicle[0]
# Assign 10-20 random parts to each vehicle
num_parts = random.randint(10, 20)
selected_parts = random.sample(parts, min(num_parts, len(parts)))
for part in selected_parts:
part_id = part[0]
# Check if fitment already exists
cursor.execute("""
SELECT id FROM vehicle_parts WHERE model_year_engine_id = ? AND part_id = ?
""", (mye_id, part_id))
if cursor.fetchone():
continue
# Determine position based on part name
part_name = part[1].lower()
position = None
quantity = 1
if 'front' in part_name:
position = 'front'
elif 'rear' in part_name:
position = 'rear'
elif 'left' in part_name:
position = 'left'
elif 'right' in part_name:
position = 'right'
# Some parts need multiples
if 'spark plug' in part_name or 'injector' in part_name:
quantity = 4
elif 'pad' in part_name or 'rotor' in part_name:
quantity = 2 if position else 1
cursor.execute("""
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, position)
VALUES (?, ?, ?, ?)
""", (mye_id, part_id, quantity, position))
added += 1
print(f" Added {added} fitments")
return added
def main():
print("=" * 50)
print("Populating Demo Data")
print("=" * 50)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
populate_parts(cursor)
populate_manufacturers(cursor)
populate_aftermarket(cursor)
populate_fitments(cursor)
conn.commit()
print("\nDemo data populated successfully!")
# Show final counts
print("\n=== Final Counts ===")
for table in ['parts', 'manufacturers', 'aftermarket_parts', 'vehicle_parts']:
cursor.execute(f"SELECT COUNT(*) FROM {table}")
count = cursor.fetchone()[0]
print(f" {table}: {count}")
except Exception as e:
conn.rollback()
print(f"Error: {e}")
raise
finally:
conn.close()
if __name__ == '__main__':
main()

Binary file not shown.