// Vehicle Dashboard JavaScript - Navegacion por tarjetas class VehicleDashboard { constructor() { this.currentView = 'brands'; // brands, models, vehicles, categories, groups, parts, diagrams this.selectedBrand = null; this.selectedModel = null; this.selectedYear = null; // FASE 5: Track selected year for breadcrumb this.selectedVehicleId = null; this.selectedCategory = null; this.selectedGroupId = null; this.selectedGroup = null; // FASE 5: Track selected group for breadcrumb this.allVehicles = []; this.filteredVehicles = []; this.allCategories = []; this.allParts = []; this.stats = { brands: 0, models: 0, vehicles: 0, parts: 0 }; this.currentDiagramZoom = 1; // FASE 3: Zoom level for diagram viewer this.lastFocusedElement = null; // FASE 5: Track focus for modal management this.init(); } async init() { await this.loadStats(); await this.showBrands(); this.bindFilterEvents(); this.bindKeyboardShortcuts(); // FASE 5: Keyboard shortcuts this.bindSearchEvents(); // Bind search input events this.initDarkMode(); // FASE 5: Dark mode } bindSearchEvents() { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.searchPartNumber(); } }); } } async loadStats() { try { const [statsRes, brandsRes, categoriesRes] = await Promise.all([ fetch('/api/catalog/stats'), fetch('/api/brands'), fetch('/api/categories') ]); if (statsRes.ok) { const s = await statsRes.json(); this.stats.brands = s.brands; this.stats.models = s.models; this.stats.vehicles = s.vehicles; this.stats.parts = s.parts; const fmt = n => n > 1000 ? Math.floor(n/1000) + 'K+' : n; const brandsEl = document.getElementById('totalBrands'); const modelsEl = document.getElementById('totalModels'); const partsEl = document.getElementById('totalParts'); if (brandsEl) brandsEl.textContent = fmt(this.stats.brands); if (modelsEl) modelsEl.textContent = fmt(this.stats.models); if (partsEl) partsEl.textContent = fmt(this.stats.parts); } if (brandsRes.ok) { // Still needed for brand list rendering await brandsRes.json(); } if (categoriesRes.ok) { this.allCategories = await categoriesRes.json(); } } catch (error) { console.error('Error loading stats:', error); } } updateBreadcrumb() { const breadcrumb = document.getElementById('breadcrumb'); let items = []; // Build breadcrumb items based on current view if (this.currentView === 'brands') { items.push({ label: ' Marcas', active: true }); } else if (this.currentView === 'models') { items.push({ label: ' Marcas', action: 'dashboard.goToBrands()' }); items.push({ label: this.selectedBrand, active: true }); } else if (this.currentView === 'vehicles') { items.push({ label: ' Marcas', action: 'dashboard.goToBrands()' }); items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand.replace(/'/g, "\\'")}')` }); items.push({ label: this.selectedModel, active: true }); } else if (this.currentView === 'categories') { items.push({ label: ' Marcas', action: 'dashboard.goToBrands()' }); items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand.replace(/'/g, "\\'")}')` }); items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand.replace(/'/g, "\\'")}', '${this.selectedModel.replace(/'/g, "\\'")}')` }); if (this.selectedYear) items.push({ label: this.selectedYear }); items.push({ label: 'Categorías', active: true }); } else if (this.currentView === 'groups') { items.push({ label: ' Marcas', action: 'dashboard.goToBrands()' }); items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand.replace(/'/g, "\\'")}')` }); items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand.replace(/'/g, "\\'")}', '${this.selectedModel.replace(/'/g, "\\'")}')` }); if (this.selectedYear) items.push({ label: this.selectedYear }); items.push({ label: 'Categorías', action: `dashboard.goToCategories(${this.selectedVehicleId})` }); items.push({ label: this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Grupos', active: true }); } else if (this.currentView === 'parts') { const groupName = this.selectedGroup ? (this.selectedGroup.name_es || this.selectedGroup.name) : 'Grupo'; items.push({ label: ' Marcas', action: 'dashboard.goToBrands()' }); items.push({ label: this.selectedBrand, action: `dashboard.goToModels('${this.selectedBrand.replace(/'/g, "\\'")}')` }); items.push({ label: this.selectedModel, action: `dashboard.goToVehicles('${this.selectedBrand.replace(/'/g, "\\'")}', '${this.selectedModel.replace(/'/g, "\\'")}')` }); if (this.selectedYear) items.push({ label: this.selectedYear }); items.push({ label: 'Categorías', action: `dashboard.goToCategories(${this.selectedVehicleId})` }); items.push({ label: this.selectedCategory ? (this.selectedCategory.name_es || this.selectedCategory.name) : 'Categoría', action: `dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0})` }); items.push({ label: groupName, active: true }); } // Generate HTML breadcrumb.innerHTML = items.map((item, i) => { const isLast = i === items.length - 1; const separator = !isLast ? '/' : ''; if (item.action) { return `${item.label}${separator}`; } else if (item.active) { return `${item.label}`; } else { return `${item.label}${separator}`; } }).join(''); } // FASE 5: Keyboard shortcuts bindKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Check if user is typing in an input/textarea const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA'; // Close modals with Escape (always works) if (e.key === 'Escape') { this.closeAllModals(); return; } // Skip other shortcuts if typing if (isTyping) { return; } // Focus search input with "/" or Ctrl+K if (e.key === '/' || (e.ctrlKey && e.key === 'k')) { e.preventDefault(); const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.focus(); } return; } // Toggle dark mode with Ctrl+D if (e.ctrlKey && e.key === 'd') { e.preventDefault(); this.toggleDarkMode(); return; } // Go back one level with Backspace if (e.key === 'Backspace') { e.preventDefault(); this.goBack(); return; } }); } // FASE 5: Close all open modals (custom modal system) closeAllModals() { const modals = ['partDetailModal', 'searchResultsModal', 'diagramModal', 'vinDecoderModal']; modals.forEach(modalId => { this.closeModal(modalId); }); } // Custom modal open openModal(modalId) { this.lastFocusedElement = document.activeElement; const modal = document.getElementById(modalId); if (modal) { modal.classList.add('active'); // Focus first focusable element setTimeout(() => { const focusable = modal.querySelector('input, button, [tabindex]:not([tabindex="-1"])'); if (focusable) focusable.focus(); }, 100); } } // Custom modal close closeModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove('active'); if (this.lastFocusedElement) { this.lastFocusedElement.focus(); } } } // FASE 5: Go back one level in navigation goBack() { switch (this.currentView) { case 'models': this.goToBrands(); break; case 'vehicles': this.goToModels(this.selectedBrand); break; case 'categories': this.goToVehicles(this.selectedBrand, this.selectedModel); break; case 'groups': this.goToCategories(this.selectedVehicleId); break; case 'parts': if (this.selectedCategory) { this.goToGroups(this.selectedCategory.id); } else { this.goToCategories(this.selectedVehicleId); } break; default: // Already at top level (brands) break; } } // Theme is now always dark, no toggle needed initDarkMode() { // Dark theme is the default and only theme } toggleDarkMode() { // No-op: single theme design } updateDarkModeIcon() { // No-op: no toggle button in new design } // FASE 5: Make cards keyboard accessible makeCardsAccessible(containerSelector, cardSelector) { const container = document.querySelector(containerSelector); if (!container) return; const cards = container.querySelectorAll(cardSelector); cards.forEach((card, index) => { // Add accessibility attributes card.setAttribute('tabindex', '0'); card.setAttribute('role', 'button'); // Get the card text for aria-label const cardText = card.textContent.trim().replace(/\s+/g, ' '); card.setAttribute('aria-label', cardText.substring(0, 100)); // Add keyboard event handler card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); } }); }); } // FASE 5: Open modal with focus management (custom modal system) openModalWithFocus(modalId) { this.openModal(modalId); return { hide: () => this.closeModal(modalId) }; } async showBrands() { this.currentView = 'brands'; this.selectedBrand = null; this.selectedModel = null; this.selectedYear = null; // FASE 5: Reset year this.selectedGroup = null; // FASE 5: Reset group this.updateBreadcrumb(); this.hideFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando marcas...

`; try { const brandsRes = await fetch('/api/brands?detailed=true'); if (!brandsRes.ok) { throw new Error('Error al cargar datos'); } const brands = await brandsRes.json(); // Build brandStats from detailed response const brandStats = {}; brands.forEach(b => { brandStats[b.name] = { models: { size: b.model_count }, vehicles: b.vehicle_count }; }); if (brands.length === 0) { container.innerHTML = `

No hay marcas disponibles

Agrega algunas marcas a la base de datos

`; return; } container.innerHTML = `
${brands.map(b => `
${b.name}
${b.model_count} modelos
${b.vehicle_count} vehículos
`).join('')}
`; // FASE 5: Make brand cards keyboard accessible this.makeCardsAccessible('#mainContent', '.brand-card'); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar marcas

${error.message}

`; } } async goToModels(brand) { this.currentView = 'models'; this.selectedBrand = brand; this.selectedModel = null; this.selectedYear = null; // FASE 5: Reset year this.selectedGroup = null; // FASE 5: Reset group this.updateBreadcrumb(); this.hideFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando modelos de ${brand}...

`; try { const modelsRes = await fetch(`/api/models?brand=${encodeURIComponent(brand)}&detailed=true`); if (!modelsRes.ok) { throw new Error('Error al cargar datos'); } const models = await modelsRes.json(); if (models.length === 0) { container.innerHTML = `

No hay modelos para ${brand}

Esta marca no tiene modelos registrados

`; return; } container.innerHTML = `
${models.map(m => { const yearRange = m.year_count > 1 ? `${m.year_min} - ${m.year_max}` : `${m.year_min}`; return `
${m.name}
${yearRange}
${m.engine_count} motores
${m.vehicle_count} variantes
`; }).join('')}
`; // FASE 5: Make model cards keyboard accessible this.makeCardsAccessible('#mainContent', '.model-card'); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar modelos

${error.message}

`; } } async goToVehicles(brand, model) { this.currentView = 'vehicles'; this.selectedBrand = brand; this.selectedModel = model; this.selectedYear = null; // FASE 5: Reset year (will be set when selecting vehicle) this.selectedGroup = null; // FASE 5: Reset group this.updateBreadcrumb(); this.showFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando vehículos...

`; try { // Fetch both vehicles info and model_year_engine IDs const [vehiclesRes, myeRes] = await Promise.all([ fetch(`/api/vehicles?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&per_page=100`), fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&per_page=100`) ]); if (!vehiclesRes.ok || !myeRes.ok) { throw new Error('Error al cargar vehículos'); } const vehiclesData = await vehiclesRes.json(); const myeData = await myeRes.json(); const vehicles = vehiclesData.data || vehiclesData; const myeRecords = myeData.data || myeData; // 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 ); 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 await this.populateFilters(brand, model); this.displayVehicles(); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar vehículos

${error.message}

`; } } async populateFilters(brand, model) { try { const [yearsRes, enginesRes] = await Promise.all([ fetch(`/api/years?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`), fetch(`/api/engines?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`) ]); if (yearsRes.ok) { const years = await yearsRes.json(); const yearFilter = document.getElementById('yearFilter'); yearFilter.innerHTML = ''; years.forEach(year => { yearFilter.innerHTML += ``; }); } if (enginesRes.ok) { const engines = await enginesRes.json(); const engineFilter = document.getElementById('engineFilter'); engineFilter.innerHTML = ''; engines.forEach(engine => { engineFilter.innerHTML += ``; }); } } catch (error) { console.error('Error populating filters:', error); } } bindFilterEvents() { document.getElementById('yearFilter').addEventListener('change', () => { this.applyFilters(); }); document.getElementById('engineFilter').addEventListener('change', () => { this.applyFilters(); }); } applyFilters() { const year = document.getElementById('yearFilter').value; const engine = document.getElementById('engineFilter').value; this.filteredVehicles = this.allVehicles.filter(v => { return (!year || v.year.toString() === year) && (!engine || v.engine === engine); }); this.displayVehicles(); } // Helper: Extract engine configuration from engine name getEngineConfig(engineName) { if (!engineName) return 'N/A'; const upper = engineName.toUpperCase(); // Match patterns like V6, V8, I4, H4, W12 const vMatch = upper.match(/V(\d+)/); if (vMatch) return `V${vMatch[1]}`; const iMatch = upper.match(/I(\d+)|INLINE[- ]?(\d+)/); if (iMatch) return `I${iMatch[1] || iMatch[2]}`; const hMatch = upper.match(/H(\d+)|FLAT[- ]?(\d+)/); if (hMatch) return `H${hMatch[1] || hMatch[2]}`; const wMatch = upper.match(/W(\d+)/); if (wMatch) return `W${wMatch[1]}`; // Try to derive from cylinder count in name const cylMatch = upper.match(/(\d)[- ]?CYL/); if (cylMatch) return `${cylMatch[1]} Cil`; if (upper.includes('ELECTRIC')) return 'EV'; if (upper.includes('ROTARY')) return 'Rotary'; return 'N/A'; } // Helper: Format displacement (cc to L) formatDisplacement(cc) { if (!cc || cc === 0) return 'N/A'; const liters = (cc / 1000).toFixed(1); return `${liters}L`; } // Helper: Format fuel type in Spanish formatFuelType(fuel) { const types = { 'gasoline': 'Gasolina', 'diesel': 'Diésel', 'electric': 'Eléctrico', 'hybrid': 'Híbrido', 'other': 'Otro' }; return types[fuel] || fuel || 'N/A'; } displayVehicles() { const container = document.getElementById('mainContent'); const resultCount = document.getElementById('resultCount'); resultCount.textContent = `${this.filteredVehicles.length} resultado${this.filteredVehicles.length !== 1 ? 's' : ''}`; if (this.filteredVehicles.length === 0) { container.innerHTML = `

No se encontraron vehículos

Intenta ajustar los filtros

`; return; } container.innerHTML = `
${this.filteredVehicles.map(v => `
${v.year} ${v.brand} ${v.model}
${v.engine}
${this.formatFuelType(v.fuel_type)}
${v.power_hp || 'N/A'} HP
${v.torque_nm || 'N/A'} Nm
${this.formatDisplacement(v.displacement_cc)}
${v.cylinders || 'N/A'} Cil
${this.getEngineConfig(v.engine)}
${v.trim_level && v.trim_level !== 'unknown' ? `
${v.trim_level}
` : ''}
`).join('')}
`; } goToBrands() { this.showBrands(); } showFilters() { document.getElementById('filtersBar').classList.add('visible'); // Reset filters document.getElementById('yearFilter').value = ''; document.getElementById('engineFilter').value = ''; } hideFilters() { 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 = `

Vehículo sin partes disponibles

Este vehículo no tiene partes registradas en el catálogo.

`; return; } this.currentView = 'categories'; this.selectedVehicleId = myeId; this.selectedCategory = null; // FASE 5: Find the vehicle to get year for breadcrumb const vehicle = this.allVehicles.find(v => v.mye_id === myeId); if (vehicle) { this.selectedYear = vehicle.year; } this.updateBreadcrumb(); this.hideFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando categorías...

`; try { // 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'); } this.allCategories = await response.json(); this.displayCategories(); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar categorías

${error.message}

`; } } displayCategories() { const container = document.getElementById('mainContent'); if (this.allCategories.length === 0) { container.innerHTML = `

No hay categorías disponibles

Este vehículo no tiene partes registradas

`; return; } container.innerHTML = `
${this.allCategories.map(cat => { // Use icon_name directly from database (e.g., "fa-cog", "fa-bolt") const iconClass = cat.icon_name || 'fa-cog'; const displayName = cat.name_es || cat.name; return `
${displayName}
${cat.children ? cat.children.length + ' subcategorías' : ''}
`; }).join('')}
`; // FASE 5: Make category cards keyboard accessible this.makeCardsAccessible('#mainContent', '.category-card'); } async goToGroups(categoryId) { this.currentView = 'groups'; const category = this.allCategories.find(c => c.id === categoryId); this.selectedCategory = category || { id: categoryId, name: 'Categoria' }; this.selectedGroup = null; // FASE 5: Reset group this.updateBreadcrumb(); this.hideFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando grupos...

`; try { // 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'); } const groups = await response.json(); // Fetch diagrams for Suspension (11) or Steering (10) when vehicle is selected let vehicleDiagrams = []; if (this.selectedVehicleId && (categoryId === 10 || categoryId === 11)) { try { const diagRes = await fetch(`/api/vehicles/${this.selectedVehicleId}/diagrams/by-category?category_id=${categoryId}`); if (diagRes.ok) { const catGroups = await diagRes.json(); for (const cg of catGroups) { vehicleDiagrams.push(...cg.diagrams); } } } catch (e) { console.error('Error loading diagrams for strip:', e); } } this.displayGroups(groups, categoryId, vehicleDiagrams); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar grupos

${error.message}

`; } } displayGroups(groups, categoryId, vehicleDiagrams = []) { const container = document.getElementById('mainContent'); if (groups.length === 0 && vehicleDiagrams.length === 0) { container.innerHTML = `

No hay grupos en esta categoría

`; return; } // Build diagram strip HTML if diagrams are available let diagramStripHtml = ''; if (vehicleDiagrams.length > 0) { // Store diagram list for the viewer this._currentDiagramList = vehicleDiagrams; diagramStripHtml = `
Diagramas MOOG para tu vehículo
${vehicleDiagrams.length} diagrama${vehicleDiagrams.length !== 1 ? 's' : ''}
${vehicleDiagrams.map((d, idx) => { const type = (d.name || '')[0]; const typeLabel = type === 'F' ? 'Delantera' : type === 'S' ? 'Dirección' : type === 'R' ? 'Trasera' : ''; const imgSrc = d.image_url || '/static/diagrams/moog/' + d.name + '.jpg'; return `
${d.name}
${d.name}
${typeLabel}
`; }).join('')}
`; } container.innerHTML = `

${this.selectedCategory.name_es || this.selectedCategory.name}

${diagramStripHtml}
${groups.map(group => `
${group.name_es || group.name}
`).join('')}
`; // FASE 5: Make group cards keyboard accessible this.makeCardsAccessible('#mainContent', '.category-card'); } async goToParts(groupId) { this.currentView = 'parts'; this.selectedGroupId = groupId; // FASE 5: Fetch group details for breadcrumb try { const response = await fetch(`/api/categories/${this.selectedCategory ? this.selectedCategory.id : 0}/groups`); if (response.ok) { const groups = await response.json(); this.selectedGroup = groups.find(g => g.id === groupId) || { id: groupId, name: 'Grupo' }; } } catch (error) { console.error('Error fetching group details:', error); this.selectedGroup = { id: groupId, name: 'Grupo' }; } this.updateBreadcrumb(); this.hideFilters(); const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando partes...

`; try { // 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 both array response (vehicle parts) and paginated response this.allParts = Array.isArray(partsData) ? partsData : (partsData.data || partsData); this.displayParts(); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar partes

${error.message}

`; } } displayParts() { const container = document.getElementById('mainContent'); if (this.allParts.length === 0) { container.innerHTML = `

No hay partes disponibles

Este grupo no tiene partes registradas aún

`; return; } container.innerHTML = `
${this.allParts.map(part => ` `).join('')}
OEM # Nombre Grupo Acción
${part.oem_part_number || 'N/A'} ${part.name_es || part.name || 'Sin nombre'} ${part.group_name || 'N/A'}
`; } async showPartDetail(partId) { // FASE 5: Use focus management for modal const contentContainer = document.getElementById('partDetailContent'); contentContainer.innerHTML = `

Cargando detalles...

`; const modal = this.openModalWithFocus('partDetailModal'); try { // Fetch part details, alternatives, and cross-references in parallel const [partRes, alternativesRes, crossRefsRes] = await Promise.all([ fetch(`/api/parts/${partId}`), fetch(`/api/parts/${partId}/alternatives`), fetch(`/api/parts/${partId}/cross-references`) ]); if (!partRes.ok) { throw new Error('Error al cargar detalle de la parte'); } const part = await partRes.json(); const alternatives = alternativesRes.ok ? await alternativesRes.json() : []; const crossRefs = crossRefsRes.ok ? await crossRefsRes.json() : []; contentContainer.innerHTML = `

${part.name_es || part.name || 'Sin nombre'}

${part.image_url ? `
${part.oem_part_number || ''}
` : ''}
Número OEM ${part.oem_part_number || 'N/A'}
Categoría ${part.category_name_es || part.category_name || 'N/A'}
Grupo ${part.group_name_es || part.group_name || 'N/A'}
${part.description || part.description_es ? `
Descripción ${part.description_es || part.description}
` : ''} ${this.renderCrossReferences(crossRefs)} ${this.renderAlternatives(alternatives)} `; } catch (error) { console.error('Error:', error); contentContainer.innerHTML = `

${error.message}

`; } } // FASE 2: Render cross-references section renderCrossReferences(crossRefs) { if (!crossRefs || crossRefs.length === 0) { return ''; } const badges = crossRefs.map(ref => { const refNumber = ref.cross_reference_number || ref.part_number || ref; const brand = ref.brand ? ` (${ref.brand})` : ''; return `${refNumber}${brand}`; }).join(''); return `
Cross-Referencias
${badges}
`; } // FASE 2: Render alternatives section renderAlternatives(alternatives) { if (!alternatives || alternatives.length === 0) { return ''; } const rows = alternatives.map(alt => ` ${alt.brand || 'N/A'} ${alt.part_number || 'N/A'} ${alt.name_es || alt.name || 'N/A'} ${this.getQualityBadge(alt.quality_tier)} ${this.formatPrice(alt.price)} `).join(''); return `
Alternativas Aftermarket
${rows}
Marca Número de Parte Nombre Calidad Precio
`; } // FASE 2: Get quality tier badge HTML getQualityBadge(tier) { const tiers = { 'economy': { class: 'quality-economy', label: 'Económico' }, 'standard': { class: 'quality-standard', label: 'Estándar' }, 'premium': { class: 'quality-premium', label: 'Premium' }, 'oem': { class: 'quality-oem', label: 'OEM' } }; const tierInfo = tiers[tier?.toLowerCase()] || tiers['standard']; return `${tierInfo.label}`; } // FASE 2: Format price as currency formatPrice(price) { if (price === null || price === undefined) { return 'N/A'; } return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(price); } // FASE 2/4: Search by part number or general search async searchPartNumber() { const searchInputEl = document.getElementById('searchInput'); const searchTerm = searchInputEl.value.trim(); if (!searchTerm) { return; } // FASE 4: Check if input looks like a VIN (17 characters alphanumeric) if (searchTerm.length === 17 && /^[A-HJ-NPR-Z0-9]{17}$/i.test(searchTerm)) { // Offer to decode VIN if (confirm('El texto parece ser un VIN. ¿Deseas decodificarlo?')) { document.getElementById('vinInput').value = searchTerm.toUpperCase(); this.openVinDecoder(); return; } } // FASE 5: Use focus management for modal const contentContainer = document.getElementById('searchResultsContent'); contentContainer.innerHTML = `

Buscando "${searchTerm}"...

`; const modal = this.openModalWithFocus('searchResultsModal'); try { // FASE 4: Use full-text search endpoint for general search let response; // Try part number search first response = await fetch(`/api/search/part-number/${encodeURIComponent(searchTerm)}`); let results = []; if (response.ok) { results = await response.json(); } // If no results from part number, try general search if (results.length === 0) { const searchResponse = await fetch(`/api/search?q=${encodeURIComponent(searchTerm)}`); if (searchResponse.ok) { const searchData = await searchResponse.json(); results = searchData.parts || searchData || []; } } this.showSearchResults(results, searchTerm); } catch (error) { console.error('Error:', error); contentContainer.innerHTML = `

Error al buscar: ${error.message}

`; } } // FASE 2: Display search results showSearchResults(results, searchTerm) { const contentContainer = document.getElementById('searchResultsContent'); const modalTitle = document.getElementById('searchResultsModalLabel'); modalTitle.innerHTML = ` Resultados para "${searchTerm}"`; if (!results || results.length === 0) { contentContainer.innerHTML = `

No se encontraron resultados para "${searchTerm}"

`; return; } const resultItems = results.map(part => `
${part.oem_part_number || part.part_number || 'N/A'}
${part.name_es || part.name || 'Sin nombre'}
${part.quality_tier ? this.getQualityBadge(part.quality_tier) : ''} ${part.brand ? `
${part.brand}
` : ''}
`).join(''); contentContainer.innerHTML = `

${results.length} resultado${results.length !== 1 ? 's' : ''} encontrado${results.length !== 1 ? 's' : ''}

${resultItems}
`; // FASE 5: Make search result items keyboard accessible this.makeCardsAccessible('#searchResultsContent', '.search-result-item'); } // FASE 2: Show part detail from search results (closes search modal first) showPartDetailFromSearch(partId) { // Close search results modal const searchModalEl = document.getElementById('searchResultsModal'); if (searchModalEl) { searchModalEl.classList.remove('active'); } // Small delay to allow modal transition, then show part detail setTimeout(() => { this.showPartDetail(partId); }, 300); } // FASE 3: Go to diagrams view for a group async goToDiagrams(groupId) { const container = document.getElementById('mainContent'); container.innerHTML = `

Cargando diagramas...

`; try { const response = await fetch(`/api/groups/${groupId}/diagrams`); if (!response.ok) { throw new Error('Error al cargar diagramas'); } const diagrams = await response.json(); this.displayDiagramThumbnails(diagrams, groupId); } catch (error) { console.error('Error:', error); container.innerHTML = `

Error al cargar diagramas

${error.message}

`; } } // FASE 3: Display diagram thumbnails grid displayDiagramThumbnails(diagrams, groupId) { const container = document.getElementById('mainContent'); if (!diagrams || diagrams.length === 0) { container.innerHTML = `

No hay diagramas disponibles

Este grupo no tiene diagramas registrados

`; return; } container.innerHTML = `
Diagramas del Grupo
${diagrams.length} diagrama${diagrams.length !== 1 ? 's' : ''}
${diagrams.map(diagram => `
${diagram.thumbnail_url ? `${diagram.name_es || diagram.name}` : `` }
${diagram.name_es || diagram.name || 'Diagrama'}
`).join('')}
`; // FASE 5: Make diagram thumbnails keyboard accessible this.makeCardsAccessible('#mainContent', '.diagram-thumbnail'); } // FASE 3: Show diagram in modal with hotspots async showDiagram(diagramId) { // FASE 5: Use focus management for modal const contentContainer = document.getElementById('diagramModalContent'); const modalTitle = document.getElementById('diagramModalTitle'); contentContainer.innerHTML = `

Cargando diagrama...

`; const modal = this.openModalWithFocus('diagramModal'); try { const response = await fetch(`/api/diagrams/${diagramId}`); if (!response.ok) { throw new Error('Error al cargar diagrama'); } const diagram = await response.json(); modalTitle.innerHTML = ` ${diagram.name_es || diagram.name || 'Diagrama'}`; this.currentDiagramZoom = 1; this.renderDiagramWithHotspots(diagram); } catch (error) { console.error('Error:', error); contentContainer.innerHTML = `

Error al cargar diagrama: ${error.message}

`; } } // FASE 3: Render diagram with interactive hotspots renderDiagramWithHotspots(diagram) { const contentContainer = document.getElementById('diagramModalContent'); const hotspots = diagram.hotspots || []; contentContainer.innerHTML = `
${diagram.description_es || diagram.description || ''}
${diagram.svg_content ? diagram.svg_content : diagram.image_url ? `${diagram.name_es || diagram.name}` : `

No hay imagen de diagrama disponible

` } ${hotspots.map((hotspot, index) => this.renderHotspot(hotspot, index)).join('')}
${hotspots.length > 0 ? `
Leyenda de Partes
${hotspots.map((hotspot, index) => `
${index + 1} ${hotspot.name_es || hotspot.name || hotspot.label || 'Parte ' + (index + 1)}
`).join('')}
` : ''} `; } // FASE 3: Render individual hotspot marker renderHotspot(hotspot, index) { const x = hotspot.x || hotspot.position_x || 0; const y = hotspot.y || hotspot.position_y || 0; const width = hotspot.width || 30; const height = hotspot.height || 30; return `
${index + 1}
`; } // FASE 3: Handle hotspot click onHotspotClick(hotspot) { if (hotspot.part_id) { // Close diagram modal first const diagramModalEl = document.getElementById('diagramModal'); if (diagramModalEl) { diagramModalEl.classList.remove('active'); } // Small delay to allow modal transition, then show part detail setTimeout(() => { this.showPartDetail(hotspot.part_id); }, 300); } else { // Show tooltip or alert with hotspot info const name = hotspot.name_es || hotspot.name || hotspot.label || 'Parte'; const description = hotspot.description_es || hotspot.description || ''; alert(`${name}${description ? '\n\n' + description : ''}`); } } // FASE 3: Zoom diagram controls zoomDiagram(delta) { const wrapper = document.getElementById('diagramWrapper'); if (!wrapper) return; if (delta === 0) { // Reset zoom this.currentDiagramZoom = 1; } else { // Adjust zoom with limits this.currentDiagramZoom = Math.max(0.5, Math.min(2, this.currentDiagramZoom + delta)); } wrapper.style.transform = `scale(${this.currentDiagramZoom})`; } // ================================================================ // FASE 6: Full-screen Diagram Viewer (split layout) // ================================================================ openDiagramViewer(diagramId, indexInList) { this._dvCurrentIndex = typeof indexInList === 'number' ? indexInList : -1; this._dvDiagramList = this._currentDiagramList || []; this._dvZoom = 1; this._dvDragging = false; const overlay = document.getElementById('diagramViewerOverlay'); overlay.classList.add('active'); document.body.style.overflow = 'hidden'; this._loadDiagramInViewer(diagramId); this._bindDiagramViewerEvents(); } closeDiagramViewer() { const overlay = document.getElementById('diagramViewerOverlay'); overlay.classList.remove('active'); document.body.style.overflow = ''; this._unbindDiagramViewerEvents(); } async _loadDiagramInViewer(diagramId) { const titleEl = document.getElementById('dvTitle'); const subtitleEl = document.getElementById('dvSubtitle'); const imgWrapper = document.getElementById('dvImgWrapper'); const img = document.getElementById('dvImg'); const partsList = document.getElementById('dvPartsList'); const partsCount = document.getElementById('dvPartsCount'); // Show loading in parts partsList.innerHTML = '

Cargando...

'; partsCount.textContent = '...'; try { // Fetch diagram detail + parts in parallel const [diagRes, partsRes] = await Promise.all([ fetch(`/api/diagrams/${diagramId}`), fetch(`/api/diagrams/${diagramId}/parts${this.selectedVehicleId ? '?mye_id=' + this.selectedVehicleId : ''}`) ]); const diagram = await diagRes.json(); const parts = await partsRes.json(); // Update title const type = (diagram.name || '')[0]; const typeLabel = type === 'F' ? 'Suspensión Delantera' : type === 'S' ? 'Dirección' : type === 'R' ? 'Suspensión Trasera' : diagram.group_name || ''; titleEl.textContent = diagram.name || 'Diagrama'; subtitleEl.textContent = diagram.name_es || typeLabel; // Update image const imgSrc = diagram.image_url || (diagram.image_path ? '/' + diagram.image_path : ''); img.src = imgSrc; img.alt = diagram.name_es || diagram.name; this._dvZoom = 1; imgWrapper.style.transform = ''; imgWrapper.classList.remove('zoomed'); document.getElementById('dvZoomLevel').textContent = '100%'; // Render hotspots on image this._renderViewerHotspots(diagram.hotspots || [], imgWrapper); // Render parts list this._renderViewerParts(parts, diagram.hotspots || []); // Update nav button states const prevBtn = document.getElementById('dvPrevBtn'); const nextBtn = document.getElementById('dvNextBtn'); prevBtn.disabled = this._dvCurrentIndex <= 0; nextBtn.disabled = this._dvCurrentIndex < 0 || this._dvCurrentIndex >= this._dvDiagramList.length - 1; prevBtn.style.opacity = prevBtn.disabled ? '0.3' : '1'; nextBtn.style.opacity = nextBtn.disabled ? '0.3' : '1'; } catch (e) { console.error('Error loading diagram in viewer:', e); partsList.innerHTML = '

Error cargando diagrama

'; } } _renderViewerHotspots(hotspots, wrapper) { // Remove existing hotspot markers wrapper.querySelectorAll('.hotspot-marker').forEach(el => el.remove()); if (!hotspots || hotspots.length === 0) return; hotspots.forEach((hs, idx) => { // coords stored as "x%,y%" (percentage-based) const coords = (hs.coords || '').split(','); if (coords.length < 2) return; const xPct = parseFloat(coords[0]); const yPct = parseFloat(coords[1]); if (isNaN(xPct) || isNaN(yPct)) return; const marker = document.createElement('div'); marker.className = 'hotspot-marker pulse'; marker.style.left = xPct + '%'; marker.style.top = yPct + '%'; marker.dataset.partId = hs.part_id || ''; marker.dataset.callout = hs.callout_number || (idx + 1); marker.title = hs.part_name || hs.label || 'Parte ' + (idx + 1); marker.innerHTML = `${hs.callout_number || (idx + 1)}`; marker.addEventListener('click', () => { this._highlightPartInList(hs.part_id); // Highlight this marker wrapper.querySelectorAll('.hotspot-marker').forEach(m => m.classList.remove('active')); marker.classList.add('active'); }); wrapper.appendChild(marker); }); } _renderViewerParts(parts, hotspots) { const listEl = document.getElementById('dvPartsList'); const countEl = document.getElementById('dvPartsCount'); countEl.textContent = parts.length; if (!parts || parts.length === 0) { listEl.innerHTML = '

No hay partes vinculadas

'; return; } // Build a hotspot lookup by part_id const hotspotMap = {}; (hotspots || []).forEach((hs, idx) => { if (hs.part_id) hotspotMap[hs.part_id] = hs.callout_number || (idx + 1); }); // Group by group_name const grouped = {}; parts.forEach(p => { const g = p.group_name_es || p.group_name || 'Otros'; if (!grouped[g]) grouped[g] = []; grouped[g].push(p); }); let html = ''; for (const [group, groupParts] of Object.entries(grouped)) { html += `
${group}
`; for (const p of groupParts) { const callout = hotspotMap[p.id]; let xrefHtml = ''; if (p.cross_references && p.cross_references.length > 0) { xrefHtml = `
${p.cross_references.map(x => `${x.number}`).join('')}
`; } html += `
${callout ? `${callout}` : ''}
${p.part_number || p.oem_part_number}
${p.name_es || p.name || ''}
${xrefHtml}
`; } } listEl.innerHTML = html; } _highlightPartInList(partId) { if (!partId) return; const listEl = document.getElementById('dvPartsList'); listEl.querySelectorAll('.dv-part-item').forEach(el => el.classList.remove('highlighted')); const target = listEl.querySelector(`.dv-part-item[data-part-id="${partId}"]`); if (target) { target.classList.add('highlighted'); target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } _onViewerPartClick(partId) { // Highlight in list this._highlightPartInList(partId); // Highlight matching hotspot on image const wrapper = document.getElementById('dvImgWrapper'); wrapper.querySelectorAll('.hotspot-marker').forEach(m => { m.classList.remove('active'); if (m.dataset.partId == partId) { m.classList.add('active'); } }); } _dvNavigate(delta) { const newIdx = this._dvCurrentIndex + delta; if (newIdx < 0 || newIdx >= this._dvDiagramList.length) return; this._dvCurrentIndex = newIdx; const d = this._dvDiagramList[newIdx]; if (d) this._loadDiagramInViewer(d.id); } _dvSetZoom(level) { this._dvZoom = Math.max(0.25, Math.min(4, level)); const wrapper = document.getElementById('dvImgWrapper'); if (this._dvZoom !== 1) { wrapper.classList.add('zoomed'); wrapper.style.transform = `scale(${this._dvZoom})`; } else { wrapper.classList.remove('zoomed'); wrapper.style.transform = ''; } document.getElementById('dvZoomLevel').textContent = `${Math.round(this._dvZoom * 100)}%`; } _bindDiagramViewerEvents() { // Avoid duplicate bindings if (this._dvBound) return; this._dvBound = true; this._dvHandlers = { close: () => this.closeDiagramViewer(), prev: () => this._dvNavigate(-1), next: () => this._dvNavigate(1), zoomIn: () => this._dvSetZoom(this._dvZoom + 0.25), zoomOut: () => this._dvSetZoom(this._dvZoom - 0.25), zoomFit: () => this._dvSetZoom(1), keydown: (e) => { const overlay = document.getElementById('diagramViewerOverlay'); if (!overlay.classList.contains('active')) return; if (e.key === 'Escape') this.closeDiagramViewer(); if (e.key === 'ArrowLeft') this._dvNavigate(-1); if (e.key === 'ArrowRight') this._dvNavigate(1); if (e.key === '+' || e.key === '=') this._dvSetZoom(this._dvZoom + 0.25); if (e.key === '-') this._dvSetZoom(this._dvZoom - 0.25); }, wheel: (e) => { const overlay = document.getElementById('diagramViewerOverlay'); if (!overlay.classList.contains('active')) return; e.preventDefault(); const delta = e.deltaY > 0 ? -0.15 : 0.15; this._dvSetZoom(this._dvZoom + delta); }, partsFilter: (e) => { const q = e.target.value.toLowerCase(); document.querySelectorAll('#dvPartsList .dv-part-item').forEach(el => { el.style.display = el.textContent.toLowerCase().includes(q) ? '' : 'none'; }); }, mousedown: (e) => { if (this._dvZoom <= 1) return; this._dvDragging = true; this._dvDragStart = { x: e.clientX, y: e.clientY }; const container = document.getElementById('dvImgContainer'); this._dvScrollStart = { x: container.scrollLeft, y: container.scrollTop }; container.style.cursor = 'grabbing'; }, mousemove: (e) => { if (!this._dvDragging) return; const container = document.getElementById('dvImgContainer'); container.scrollLeft = this._dvScrollStart.x - (e.clientX - this._dvDragStart.x); container.scrollTop = this._dvScrollStart.y - (e.clientY - this._dvDragStart.y); }, mouseup: () => { this._dvDragging = false; const container = document.getElementById('dvImgContainer'); if (container) container.style.cursor = ''; } }; document.getElementById('dvCloseBtn').addEventListener('click', this._dvHandlers.close); document.getElementById('dvPrevBtn').addEventListener('click', this._dvHandlers.prev); document.getElementById('dvNextBtn').addEventListener('click', this._dvHandlers.next); document.getElementById('dvZoomIn').addEventListener('click', this._dvHandlers.zoomIn); document.getElementById('dvZoomOut').addEventListener('click', this._dvHandlers.zoomOut); document.getElementById('dvZoomFit').addEventListener('click', this._dvHandlers.zoomFit); document.getElementById('dvPartsFilter').addEventListener('input', this._dvHandlers.partsFilter); document.addEventListener('keydown', this._dvHandlers.keydown); document.getElementById('dvImgContainer').addEventListener('wheel', this._dvHandlers.wheel, { passive: false }); document.getElementById('dvImgContainer').addEventListener('mousedown', this._dvHandlers.mousedown); window.addEventListener('mousemove', this._dvHandlers.mousemove); window.addEventListener('mouseup', this._dvHandlers.mouseup); } _unbindDiagramViewerEvents() { if (!this._dvBound) return; this._dvBound = false; document.getElementById('dvCloseBtn')?.removeEventListener('click', this._dvHandlers.close); document.getElementById('dvPrevBtn')?.removeEventListener('click', this._dvHandlers.prev); document.getElementById('dvNextBtn')?.removeEventListener('click', this._dvHandlers.next); document.getElementById('dvZoomIn')?.removeEventListener('click', this._dvHandlers.zoomIn); document.getElementById('dvZoomOut')?.removeEventListener('click', this._dvHandlers.zoomOut); document.getElementById('dvZoomFit')?.removeEventListener('click', this._dvHandlers.zoomFit); document.getElementById('dvPartsFilter')?.removeEventListener('input', this._dvHandlers.partsFilter); document.removeEventListener('keydown', this._dvHandlers.keydown); document.getElementById('dvImgContainer')?.removeEventListener('wheel', this._dvHandlers.wheel); document.getElementById('dvImgContainer')?.removeEventListener('mousedown', this._dvHandlers.mousedown); window.removeEventListener('mousemove', this._dvHandlers.mousemove); window.removeEventListener('mouseup', this._dvHandlers.mouseup); } // FASE 4: Open VIN decoder modal openVinDecoder() { // Clear previous results document.getElementById('vinResult').innerHTML = ''; // FASE 5: Use focus management for modal this.openModalWithFocus('vinDecoderModal'); } // FASE 4: Decode VIN async decodeVin() { const vinInput = document.getElementById('vinInput'); const vin = vinInput.value.trim().toUpperCase(); const resultContainer = document.getElementById('vinResult'); // Validate VIN if (!vin) { resultContainer.innerHTML = `
Por favor ingresa un VIN
`; return; } if (vin.length !== 17) { resultContainer.innerHTML = `
El VIN debe tener exactamente 17 caracteres (actual: ${vin.length})
`; return; } // Check for invalid characters (I, O, Q are not allowed in VINs) if (/[IOQ]/i.test(vin)) { resultContainer.innerHTML = `
El VIN contiene caracteres invalidos. Las letras I, O y Q no se permiten en VINs.
`; return; } // Validate VIN format if (!/^[A-HJ-NPR-Z0-9]{17}$/i.test(vin)) { resultContainer.innerHTML = `
El VIN contiene caracteres invalidos. Solo se permiten letras (excepto I, O, Q) y numeros.
`; return; } resultContainer.innerHTML = `

Decodificando VIN...

`; try { const response = await fetch(`/api/vin/decode/${encodeURIComponent(vin)}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || errorData.message || 'Error al decodificar VIN'); } const data = await response.json(); this.showVinResult(data, vin); } catch (error) { console.error('Error:', error); resultContainer.innerHTML = `
${error.message}
`; } } // FASE 4: Show VIN decode result showVinResult(data, vin) { const resultContainer = document.getElementById('vinResult'); // Build vehicle info from decoded data const vehicleInfo = data.vehicle || data; const make = vehicleInfo.make || vehicleInfo.brand || 'Desconocido'; const model = vehicleInfo.model || 'Desconocido'; const year = vehicleInfo.year || vehicleInfo.model_year || 'Desconocido'; const engine = vehicleInfo.engine || vehicleInfo.engine_description || 'N/A'; const trim = vehicleInfo.trim || vehicleInfo.trim_level || ''; const bodyType = vehicleInfo.body_type || vehicleInfo.body_class || 'N/A'; const driveType = vehicleInfo.drive_type || vehicleInfo.drivetrain || 'N/A'; const fuelType = vehicleInfo.fuel_type || 'N/A'; const transmission = vehicleInfo.transmission || 'N/A'; const country = vehicleInfo.country || vehicleInfo.plant_country || 'N/A'; // Check if we have a match in our database const hasMatch = data.matched || data.database_match || vehicleInfo.mye_id; const myeId = vehicleInfo.mye_id || data.mye_id; let matchCard = ''; if (hasMatch && myeId) { matchCard = `
Vehiculo encontrado en la base de datos
`; } else { matchCard = `
Vehiculo no encontrado en la base de datos
`; } resultContainer.innerHTML = `
${vin} VIN Valido
${year} ${make} ${model} ${trim}
Marca ${make}
Modelo ${model}
Año ${year}
Motor ${engine}
Combustible ${fuelType}
Transmision ${transmission}
Traccion ${driveType}
Pais ${country}
${matchCard}
`; } // FASE 4: View parts for a VIN async viewVinParts(vin, myeId) { // Close VIN modal const vinModalEl = document.getElementById('vinDecoderModal'); if (vinModalEl) { vinModalEl.classList.remove('active'); } // FASE 5: Use focus management for modal const contentContainer = document.getElementById('searchResultsContent'); const modalTitle = document.getElementById('searchResultsModalLabel'); modalTitle.innerHTML = ` Partes para VIN: ${vin.substring(0, 8)}...`; contentContainer.innerHTML = `

Cargando partes...

`; const modal = this.openModalWithFocus('searchResultsModal'); try { const response = await fetch(`/api/vin/${encodeURIComponent(vin)}/parts`); if (!response.ok) { throw new Error('Error al cargar partes'); } const data = await response.json(); const parts = data.parts || data; this.displayVinParts(parts, vin, myeId); } catch (error) { console.error('Error:', error); // If VIN parts endpoint fails, try to use the mye_id to show categories if (myeId) { contentContainer.innerHTML = `
No se encontraron partes especificas para este VIN.

`; } else { contentContainer.innerHTML = `

Error al cargar partes: ${error.message}

`; } } } // FASE 4: Display parts from VIN lookup grouped by category displayVinParts(parts, vin, myeId) { const contentContainer = document.getElementById('searchResultsContent'); if (!parts || parts.length === 0) { contentContainer.innerHTML = `

No se encontraron partes para este VIN

${myeId ? ` ` : ''}
`; return; } // Group parts by category const grouped = {}; parts.forEach(part => { const category = part.category_name_es || part.category_name || 'Sin Categoria'; if (!grouped[category]) { grouped[category] = []; } grouped[category].push(part); }); let html = `

${parts.length} parte${parts.length !== 1 ? 's' : ''} encontrada${parts.length !== 1 ? 's' : ''}

`; for (const [category, categoryParts] of Object.entries(grouped)) { html += `
${category}
${categoryParts.map(part => `
${part.oem_part_number || part.part_number || 'N/A'}
${part.name_es || part.name || 'Sin nombre'}
${part.quality_tier ? this.getQualityBadge(part.quality_tier) : ''} ${part.group_name_es || part.group_name ? `
${part.group_name_es || part.group_name}
` : ''}
`).join('')}
`; } if (myeId) { html += `
`; } contentContainer.innerHTML = html; } // FASE 4: Search manually from VIN (when no database match) searchManuallyFromVin(make, model, year) { // Close VIN modal const vinModal = bootstrap.Modal.getInstance(document.getElementById('vinDecoderModal')); if (vinModal) { vinModal.hide(); } // Navigate to brand/model if they exist in our database setTimeout(async () => { try { // Check if brand exists const brandsRes = await fetch('/api/brands'); if (brandsRes.ok) { const brands = await brandsRes.json(); const matchedBrand = brands.find(b => b.toLowerCase() === make.toLowerCase() || b.toLowerCase().includes(make.toLowerCase()) || make.toLowerCase().includes(b.toLowerCase()) ); if (matchedBrand) { // Brand exists, go to models this.goToModels(matchedBrand); return; } } // Brand not found, show all brands alert(`La marca "${make}" no se encontro en la base de datos. Mostrando todas las marcas disponibles.`); this.goToBrands(); } catch (error) { console.error('Error:', error); this.goToBrands(); } }, 300); } } // Initialize dashboard globally let dashboard; document.addEventListener('DOMContentLoaded', () => { dashboard = new VehicleDashboard(); });