// 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 [brandsRes, vehiclesRes, partsRes, categoriesRes] = await Promise.all([
fetch('/api/brands'),
fetch('/api/vehicles'),
fetch('/api/parts'),
fetch('/api/categories')
]);
if (brandsRes.ok && vehiclesRes.ok) {
const brands = await brandsRes.json();
const vehiclesData = await vehiclesRes.json();
const vehicles = vehiclesData.data || vehiclesData;
// Contar modelos únicos
const uniqueModels = new Set(vehicles.map(v => `${v.brand}-${v.model}`));
this.stats.brands = brands.length;
this.stats.models = uniqueModels.size;
this.stats.vehicles = vehiclesData.pagination ? vehiclesData.pagination.total : vehicles.length;
const brandsEl = document.getElementById('totalBrands');
const modelsEl = document.getElementById('totalModels');
if (brandsEl) brandsEl.textContent = this.stats.brands;
if (modelsEl) modelsEl.textContent = this.stats.models > 1000 ? Math.floor(this.stats.models/1000) + 'K+' : this.stats.models;
}
if (partsRes.ok) {
const partsData = await partsRes.json();
// Handle paginated response
this.stats.parts = partsData.pagination ? partsData.pagination.total : (partsData.data ? partsData.data.length : partsData.length || 0);
const partsEl = document.getElementById('totalParts');
if (partsEl) partsEl.textContent = this.stats.parts;
}
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
Volver a marcas
`;
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}
Volver a marcas
`;
}
}
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}
Volver a modelos
`;
}
}
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 = 'Todos los años ';
years.forEach(year => {
yearFilter.innerHTML += `${year} `;
});
}
if (enginesRes.ok) {
const engines = await enginesRes.json();
const engineFilter = document.getElementById('engineFilter');
engineFilter.innerHTML = 'Todos los motores ';
engines.forEach(engine => {
engineFilter.innerHTML += `${engine} `;
});
}
} 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 => `
${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}
` : ''}
Ver Partes
`).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.
Volver a modelos
`;
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}
Volver a vehículos
`;
}
}
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
Volver a vehículos
`;
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('')}
Volver a vehículos
`;
// 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}
Volver a categorías
`;
}
}
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
Volver a categorías
`;
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 = `
`;
}
container.innerHTML = `
${this.selectedCategory.name_es || this.selectedCategory.name}
${diagramStripHtml}
${groups.map(group => `
${group.name_es || group.name}
Ver Partes
Diagramas
`).join('')}
Volver a categorías
`;
// 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}
Volver a grupos
`;
}
}
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
Volver a grupos
`;
return;
}
container.innerHTML = `
OEM #
Nombre
Grupo
Acción
${this.allParts.map(part => `
${part.oem_part_number || 'N/A'}
${part.name_es || part.name || 'Sin nombre'}
${part.group_name || 'N/A'}
Ver
`).join('')}
Volver a grupos
`;
}
async showPartDetail(partId) {
// FASE 5: Use focus management for modal
const contentContainer = document.getElementById('partDetailContent');
contentContainer.innerHTML = `
`;
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'}
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 = `
`;
}
}
// 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
Marca
Número de Parte
Nombre
Calidad
Precio
${rows}
`;
}
// 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}
Volver a grupos
`;
}
}
// 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
Volver a grupos
`;
return;
}
container.innerHTML = `
${diagrams.map(diagram => `
${diagram.thumbnail_url
? `
`
: `
`
}
${diagram.name_es || diagram.name || 'Diagrama'}
`).join('')}
Volver a grupos
`;
// 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 = `
`;
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.svg_content
? diagram.svg_content
: diagram.image_url
? `
`
: `
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 `
`;
}
// 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 = '';
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 = '';
}
}
_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 = '';
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 = `
`;
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
Ver Partes
`;
} else {
matchCard = `
Vehiculo no encontrado en la base de datos
Buscar Manualmente
`;
}
resultContainer.innerHTML = `
${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 = `
`;
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.
Ver Categorias del Vehiculo
`;
} 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 ? `
Ver Categorias del Vehiculo
` : ''}
`;
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 += `
Ver Todas las Categorias
`;
}
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();
});