- Fix breadcrumb group name fetch using existing /api/categories/<id>/groups
- Fix diagramModalLabel → diagramModalTitle DOM ID mismatch
- Replace bootstrap.Modal.getInstance() with classList.remove('active') for modal close
- Escape single quotes in brand/model names in breadcrumb onclick handlers
- Implement editAftermarket() form population in admin panel
- Handle VIN decoder response wrapper in landing page
- Fetch models count from API instead of hardcoded '13K+'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1965 lines
80 KiB
JavaScript
1965 lines
80 KiB
JavaScript
// 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 vehicles = await vehiclesRes.json();
|
|
|
|
// 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 = 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: '<i class="fas fa-home"></i> Marcas', active: true });
|
|
} else if (this.currentView === 'models') {
|
|
items.push({ label: '<i class="fas fa-home"></i> Marcas', action: 'dashboard.goToBrands()' });
|
|
items.push({ label: this.selectedBrand, active: true });
|
|
} else if (this.currentView === 'vehicles') {
|
|
items.push({ label: '<i class="fas fa-home"></i> 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: '<i class="fas fa-home"></i> 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: '<i class="fas fa-home"></i> 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: '<i class="fas fa-home"></i> 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 ? '<span class="breadcrumb-separator">/</span>' : '';
|
|
|
|
if (item.action) {
|
|
return `<span class="breadcrumb-item"><a href="#" onclick="${item.action}; return false;">${item.label}</a></span>${separator}`;
|
|
} else if (item.active) {
|
|
return `<span class="breadcrumb-item active">${item.label}</span>`;
|
|
} else {
|
|
return `<span class="breadcrumb-item">${item.label}</span>${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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando marcas...</h4>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const [brandsRes, vehiclesRes] = await Promise.all([
|
|
fetch('/api/brands'),
|
|
fetch('/api/vehicles')
|
|
]);
|
|
|
|
if (!brandsRes.ok || !vehiclesRes.ok) {
|
|
throw new Error('Error al cargar datos');
|
|
}
|
|
|
|
const brands = await brandsRes.json();
|
|
const vehicles = await vehiclesRes.json();
|
|
|
|
// Contar modelos y vehículos por marca
|
|
const brandStats = {};
|
|
brands.forEach(brand => {
|
|
brandStats[brand] = { models: new Set(), vehicles: 0 };
|
|
});
|
|
|
|
vehicles.forEach(v => {
|
|
if (brandStats[v.brand]) {
|
|
brandStats[v.brand].models.add(v.model);
|
|
brandStats[v.brand].vehicles++;
|
|
}
|
|
});
|
|
|
|
if (brands.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-car"></i>
|
|
<h4>No hay marcas disponibles</h4>
|
|
<p>Agrega algunas marcas a la base de datos</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="content-grid brands-grid">
|
|
${brands.map(brand => `
|
|
<div class="brand-card" onclick="dashboard.goToModels('${brand}')">
|
|
<div class="brand-icon">
|
|
<i class="fas fa-car"></i>
|
|
</div>
|
|
<div class="brand-name">${brand}</div>
|
|
<div class="brand-count">
|
|
${brandStats[brand].models.size} modelos
|
|
</div>
|
|
<div class="brand-count">
|
|
${brandStats[brand].vehicles} vehículos
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`;
|
|
|
|
// FASE 5: Make brand cards keyboard accessible
|
|
this.makeCardsAccessible('#mainContent', '.brand-card');
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar marcas</h4>
|
|
<p>${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando modelos de ${brand}...</h4>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const [modelsRes, vehiclesRes] = await Promise.all([
|
|
fetch(`/api/models?brand=${encodeURIComponent(brand)}`),
|
|
fetch(`/api/vehicles?brand=${encodeURIComponent(brand)}`)
|
|
]);
|
|
|
|
if (!modelsRes.ok || !vehiclesRes.ok) {
|
|
throw new Error('Error al cargar datos');
|
|
}
|
|
|
|
const models = await modelsRes.json();
|
|
const vehicles = await vehiclesRes.json();
|
|
|
|
// Contar vehículos y años por modelo
|
|
const modelStats = {};
|
|
models.forEach(model => {
|
|
modelStats[model] = { years: new Set(), vehicles: 0, engines: new Set() };
|
|
});
|
|
|
|
vehicles.forEach(v => {
|
|
if (modelStats[v.model]) {
|
|
modelStats[v.model].years.add(v.year);
|
|
modelStats[v.model].vehicles++;
|
|
modelStats[v.model].engines.add(v.engine);
|
|
}
|
|
});
|
|
|
|
if (models.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-car-side"></i>
|
|
<h4>No hay modelos para ${brand}</h4>
|
|
<p>Esta marca no tiene modelos registrados</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToBrands()">
|
|
<i class="fas fa-arrow-left"></i> Volver a marcas
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="content-grid models-grid">
|
|
${models.map(model => {
|
|
const stats = modelStats[model];
|
|
const yearsArray = Array.from(stats.years).sort((a, b) => b - a);
|
|
const yearRange = yearsArray.length > 0
|
|
? (yearsArray.length > 1
|
|
? `${yearsArray[yearsArray.length - 1]} - ${yearsArray[0]}`
|
|
: `${yearsArray[0]}`)
|
|
: 'N/A';
|
|
|
|
return `
|
|
<div class="model-card" onclick="dashboard.goToVehicles('${brand}', '${model}')">
|
|
<div class="model-name">${model}</div>
|
|
<div class="model-info">
|
|
<i class="fas fa-calendar-alt"></i> ${yearRange}
|
|
</div>
|
|
<div class="model-info">
|
|
<i class="fas fa-cogs"></i> ${stats.engines.size} motores
|
|
</div>
|
|
<div class="model-info">
|
|
<i class="fas fa-list"></i> ${stats.vehicles} variantes
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>`;
|
|
|
|
// FASE 5: Make model cards keyboard accessible
|
|
this.makeCardsAccessible('#mainContent', '.model-card');
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar modelos</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToBrands()">
|
|
<i class="fas fa-arrow-left"></i> Volver a marcas
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando vehículos...</h4>
|
|
</div>
|
|
`;
|
|
|
|
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)}`),
|
|
fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`)
|
|
]);
|
|
|
|
if (!vehiclesRes.ok || !myeRes.ok) {
|
|
throw new Error('Error al cargar vehículos');
|
|
}
|
|
|
|
const vehicles = await vehiclesRes.json();
|
|
const myeRecords = await myeRes.json();
|
|
|
|
// Merge mye_id into vehicles based on matching fields
|
|
// Only keep vehicles that have a matching mye_id (i.e., have parts)
|
|
this.allVehicles = vehicles.map(v => {
|
|
const mye = myeRecords.find(m =>
|
|
m.brand === v.brand &&
|
|
m.model === v.model &&
|
|
m.year === v.year &&
|
|
m.engine === v.engine
|
|
);
|
|
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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar vehículos</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToModels('${brand}')">
|
|
<i class="fas fa-arrow-left"></i> Volver a modelos
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 = '<option value="">Todos los años</option>';
|
|
years.forEach(year => {
|
|
yearFilter.innerHTML += `<option value="${year}">${year}</option>`;
|
|
});
|
|
}
|
|
|
|
if (enginesRes.ok) {
|
|
const engines = await enginesRes.json();
|
|
const engineFilter = document.getElementById('engineFilter');
|
|
engineFilter.innerHTML = '<option value="">Todos los motores</option>';
|
|
engines.forEach(engine => {
|
|
engineFilter.innerHTML += `<option value="${engine}">${engine}</option>`;
|
|
});
|
|
}
|
|
|
|
} 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 = `
|
|
<div class="state-container">
|
|
<i class="fas fa-car"></i>
|
|
<h4>No se encontraron vehículos</h4>
|
|
<p>Intenta ajustar los filtros</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="content-grid vehicles-grid">
|
|
${this.filteredVehicles.map(v => `
|
|
<div class="vehicle-card">
|
|
<div class="vehicle-header">
|
|
<div class="vehicle-title">${v.year} ${v.brand} ${v.model}</div>
|
|
<div class="vehicle-engine">${v.engine}</div>
|
|
</div>
|
|
<div class="vehicle-body">
|
|
<div class="vehicle-specs">
|
|
<div class="spec-item">
|
|
<i class="fas fa-gas-pump"></i>
|
|
<div class="spec-value">${this.formatFuelType(v.fuel_type)}</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-bolt"></i>
|
|
<div class="spec-value">${v.power_hp || 'N/A'} HP</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-sync-alt"></i>
|
|
<div class="spec-value">${v.torque_nm || 'N/A'} Nm</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
<div class="spec-value">${this.formatDisplacement(v.displacement_cc)}</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-circle-notch"></i>
|
|
<div class="spec-value">${v.cylinders || 'N/A'} Cil</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-cog"></i>
|
|
<div class="spec-value">${this.getEngineConfig(v.engine)}</div>
|
|
</div>
|
|
</div>
|
|
${v.trim_level && v.trim_level !== 'unknown' ? `
|
|
<div style="text-align: center; margin-top: 0.75rem;">
|
|
<span style="background: var(--accent); color: white; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem;">${v.trim_level}</span>
|
|
</div>
|
|
` : ''}
|
|
<button class="btn-parts" onclick="dashboard.goToCategories(${v.mye_id})">
|
|
<i class="fas fa-cogs"></i> Ver Partes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Vehículo sin partes disponibles</h4>
|
|
<p>Este vehículo no tiene partes registradas en el catálogo.</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToModels('${this.selectedBrand}')">
|
|
<i class="fas fa-arrow-left"></i> Volver a modelos
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
this.currentView = 'categories';
|
|
this.selectedVehicleId = myeId;
|
|
this.selectedCategory = null;
|
|
|
|
// 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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando categorías...</h4>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar categorías</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')">
|
|
<i class="fas fa-arrow-left"></i> Volver a vehículos
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
displayCategories() {
|
|
const container = document.getElementById('mainContent');
|
|
|
|
if (this.allCategories.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-folder-open"></i>
|
|
<h4>No hay categorías disponibles</h4>
|
|
<p>Este vehículo no tiene partes registradas</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')">
|
|
<i class="fas fa-arrow-left"></i> Volver a vehículos
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="content-grid categories-grid">
|
|
${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 `
|
|
<div class="category-card" onclick="dashboard.goToGroups(${cat.id})">
|
|
<div class="category-icon">
|
|
<i class="fas ${iconClass}"></i>
|
|
</div>
|
|
<div class="category-name">${displayName}</div>
|
|
<div class="category-count">
|
|
${cat.children ? cat.children.length + ' subcategorías' : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-back" onclick="dashboard.goToVehicles('${this.selectedBrand}', '${this.selectedModel}')">
|
|
<i class="fas fa-arrow-left"></i> Volver a vehículos
|
|
</button>
|
|
</div>`;
|
|
|
|
// 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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando grupos...</h4>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
this.displayGroups(groups, categoryId);
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar grupos</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToCategories(${this.selectedVehicleId})">
|
|
<i class="fas fa-arrow-left"></i> Volver a categorías
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
displayGroups(groups, categoryId) {
|
|
const container = document.getElementById('mainContent');
|
|
|
|
if (groups.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-folder-open"></i>
|
|
<h4>No hay grupos en esta categoría</h4>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToCategories(${this.selectedVehicleId})">
|
|
<i class="fas fa-arrow-left"></i> Volver a categorías
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<h4 class="mb-3">${this.selectedCategory.name_es || this.selectedCategory.name}</h4>
|
|
<div class="content-grid categories-grid">
|
|
${groups.map(group => `
|
|
<div class="category-card">
|
|
<div class="category-icon" style="background: linear-gradient(135deg, var(--secondary-color), #2980b9);" onclick="dashboard.goToParts(${group.id})">
|
|
<i class="fas fa-layer-group"></i>
|
|
</div>
|
|
<div class="category-name" onclick="dashboard.goToParts(${group.id})">${group.name_es || group.name}</div>
|
|
<div class="mt-2">
|
|
<button class="btn btn-sm btn-primary me-1" onclick="dashboard.goToParts(${group.id})">
|
|
<i class="fas fa-cogs"></i> Ver Partes
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="dashboard.goToDiagrams(${group.id})" title="Ver Diagramas">
|
|
<i class="fas fa-project-diagram"></i> Diagramas
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-back" onclick="dashboard.goToCategories(${this.selectedVehicleId})">
|
|
<i class="fas fa-arrow-left"></i> Volver a categorías
|
|
</button>
|
|
</div>`;
|
|
|
|
// 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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando partes...</h4>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar partes</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToGroups(${this.selectedCategory.id})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
displayParts() {
|
|
const container = document.getElementById('mainContent');
|
|
|
|
if (this.allParts.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-box-open"></i>
|
|
<h4>No hay partes disponibles</h4>
|
|
<p>Este grupo no tiene partes registradas aún</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToGroups(${this.selectedCategory.id})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="parts-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><i class="fas fa-barcode"></i> OEM #</th>
|
|
<th><i class="fas fa-tag"></i> Nombre</th>
|
|
<th><i class="fas fa-layer-group"></i> Grupo</th>
|
|
<th><i class="fas fa-eye"></i> Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${this.allParts.map(part => `
|
|
<tr>
|
|
<td><span class="part-oem-badge">${part.oem_part_number || 'N/A'}</span></td>
|
|
<td>${part.name_es || part.name || 'Sin nombre'}</td>
|
|
<td>${part.group_name || 'N/A'}</td>
|
|
<td>
|
|
<button class="btn-view" onclick="dashboard.showPartDetail(${part.id})">
|
|
<i class="fas fa-search"></i> Ver
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-back" onclick="dashboard.goToGroups(${this.selectedCategory.id})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async showPartDetail(partId) {
|
|
// FASE 5: Use focus management for modal
|
|
const contentContainer = document.getElementById('partDetailContent');
|
|
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Cargando detalles...</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h4 class="mb-3">${part.name_es || part.name || 'Sin nombre'}</h4>
|
|
</div>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label">Número OEM</span>
|
|
<span class="part-detail-value"><span class="part-oem-badge">${part.oem_part_number || 'N/A'}</span></span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label">Categoría</span>
|
|
<span class="part-detail-value">${part.category_name_es || part.category_name || 'N/A'}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label">Grupo</span>
|
|
<span class="part-detail-value">${part.group_name_es || part.group_name || 'N/A'}</span>
|
|
</div>
|
|
${part.description || part.description_es ? `
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label">Descripción</span>
|
|
<span class="part-detail-value">${part.description_es || part.description}</span>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- FASE 2: Cross-Referencias Section -->
|
|
${this.renderCrossReferences(crossRefs)}
|
|
|
|
<!-- FASE 2: Alternativas Aftermarket Section -->
|
|
${this.renderAlternatives(alternatives)}
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
|
<p class="mt-2">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 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 `<span class="crossref-badge">${refNumber}${brand}</span>`;
|
|
}).join('');
|
|
|
|
return `
|
|
<div class="crossref-section">
|
|
<h5><i class="fas fa-exchange-alt"></i> Cross-Referencias</h5>
|
|
<div>
|
|
${badges}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// FASE 2: Render alternatives section
|
|
renderAlternatives(alternatives) {
|
|
if (!alternatives || alternatives.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
const rows = alternatives.map(alt => `
|
|
<tr>
|
|
<td>${alt.brand || 'N/A'}</td>
|
|
<td>${alt.part_number || 'N/A'}</td>
|
|
<td>${alt.name_es || alt.name || 'N/A'}</td>
|
|
<td>${this.getQualityBadge(alt.quality_tier)}</td>
|
|
<td class="price-tag">${this.formatPrice(alt.price)}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="alternatives-section">
|
|
<h5><i class="fas fa-clone"></i> Alternativas Aftermarket</h5>
|
|
<table class="alternatives-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Marca</th>
|
|
<th>Número de Parte</th>
|
|
<th>Nombre</th>
|
|
<th>Calidad</th>
|
|
<th>Precio</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 `<span class="quality-badge ${tierInfo.class}">${tierInfo.label}</span>`;
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Buscando "${searchTerm}"...</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
|
<p class="mt-2">Error al buscar: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// FASE 2: Display search results
|
|
showSearchResults(results, searchTerm) {
|
|
const contentContainer = document.getElementById('searchResultsContent');
|
|
const modalTitle = document.getElementById('searchResultsModalLabel');
|
|
|
|
modalTitle.innerHTML = `<i class="fas fa-search"></i> Resultados para "${searchTerm}"`;
|
|
|
|
if (!results || results.length === 0) {
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-search fa-2x text-muted"></i>
|
|
<p class="mt-2">No se encontraron resultados para "${searchTerm}"</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const resultItems = results.map(part => `
|
|
<div class="search-result-item" onclick="dashboard.showPartDetailFromSearch(${part.id})">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="search-result-part-number">
|
|
<span class="part-oem-badge">${part.oem_part_number || part.part_number || 'N/A'}</span>
|
|
</div>
|
|
<div class="search-result-name mt-1">${part.name_es || part.name || 'Sin nombre'}</div>
|
|
</div>
|
|
<div class="text-end">
|
|
${part.quality_tier ? this.getQualityBadge(part.quality_tier) : ''}
|
|
${part.brand ? `<div class="text-muted small mt-1">${part.brand}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
contentContainer.innerHTML = `
|
|
<p class="text-muted mb-3">${results.length} resultado${results.length !== 1 ? 's' : ''} encontrado${results.length !== 1 ? 's' : ''}</p>
|
|
<div class="search-results-list">
|
|
${resultItems}
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<h4>Cargando diagramas...</h4>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<h4>Error al cargar diagramas</h4>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// FASE 3: Display diagram thumbnails grid
|
|
displayDiagramThumbnails(diagrams, groupId) {
|
|
const container = document.getElementById('mainContent');
|
|
|
|
if (!diagrams || diagrams.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-project-diagram"></i>
|
|
<h4>No hay diagramas disponibles</h4>
|
|
<p>Este grupo no tiene diagramas registrados</p>
|
|
<button class="btn btn-back mt-3" onclick="dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="diagram-viewer">
|
|
<div class="diagram-header">
|
|
<h5 class="mb-0"><i class="fas fa-project-diagram"></i> Diagramas del Grupo</h5>
|
|
<span class="badge bg-light text-dark">${diagrams.length} diagrama${diagrams.length !== 1 ? 's' : ''}</span>
|
|
</div>
|
|
<div class="diagram-thumbnails">
|
|
${diagrams.map(diagram => `
|
|
<div class="diagram-thumbnail" onclick="dashboard.showDiagram(${diagram.id})">
|
|
<div style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f8f9fa; border-radius: 4px;">
|
|
${diagram.thumbnail_url
|
|
? `<img src="${diagram.thumbnail_url}" alt="${diagram.name_es || diagram.name}">`
|
|
: `<i class="fas fa-project-diagram fa-3x text-muted"></i>`
|
|
}
|
|
</div>
|
|
<div class="diagram-thumbnail-name">${diagram.name_es || diagram.name || 'Diagrama'}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-back" onclick="dashboard.goToGroups(${this.selectedCategory ? this.selectedCategory.id : 0})">
|
|
<i class="fas fa-arrow-left"></i> Volver a grupos
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Cargando diagrama...</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `<i class="fas fa-project-diagram"></i> ${diagram.name_es || diagram.name || 'Diagrama'}`;
|
|
|
|
this.currentDiagramZoom = 1;
|
|
this.renderDiagramWithHotspots(diagram);
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
|
<p class="mt-2">Error al cargar diagrama: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// FASE 3: Render diagram with interactive hotspots
|
|
renderDiagramWithHotspots(diagram) {
|
|
const contentContainer = document.getElementById('diagramModalContent');
|
|
const hotspots = diagram.hotspots || [];
|
|
|
|
contentContainer.innerHTML = `
|
|
<div class="diagram-header">
|
|
<span>${diagram.description_es || diagram.description || ''}</span>
|
|
<div class="zoom-controls">
|
|
<button onclick="dashboard.zoomDiagram(-0.1)" title="Reducir">
|
|
<i class="fas fa-minus"></i>
|
|
</button>
|
|
<button onclick="dashboard.zoomDiagram(0)" title="Restablecer">
|
|
<i class="fas fa-redo"></i>
|
|
</button>
|
|
<button onclick="dashboard.zoomDiagram(0.1)" title="Ampliar">
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="diagram-container" style="overflow: auto;">
|
|
<div class="diagram-svg-wrapper" id="diagramWrapper" style="transform: scale(${this.currentDiagramZoom}); transform-origin: center center;">
|
|
${diagram.svg_content
|
|
? diagram.svg_content
|
|
: diagram.image_url
|
|
? `<img src="${diagram.image_url}" alt="${diagram.name_es || diagram.name}" style="max-width: 100%;">`
|
|
: `<div class="text-center text-muted py-5">
|
|
<i class="fas fa-image fa-4x"></i>
|
|
<p class="mt-3">No hay imagen de diagrama disponible</p>
|
|
</div>`
|
|
}
|
|
${hotspots.map((hotspot, index) => this.renderHotspot(hotspot, index)).join('')}
|
|
</div>
|
|
</div>
|
|
${hotspots.length > 0 ? `
|
|
<div class="p-3 border-top">
|
|
<h6 class="mb-2"><i class="fas fa-info-circle"></i> Leyenda de Partes</h6>
|
|
<div class="row">
|
|
${hotspots.map((hotspot, index) => `
|
|
<div class="col-md-4 col-sm-6 mb-2">
|
|
<span class="badge bg-primary me-1">${index + 1}</span>
|
|
<span class="small">${hotspot.name_es || hotspot.name || hotspot.label || 'Parte ' + (index + 1)}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
`;
|
|
}
|
|
|
|
// 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 `
|
|
<div class="hotspot"
|
|
style="left: ${x}px; top: ${y}px; width: ${width}px; height: ${height}px;"
|
|
onclick="dashboard.onHotspotClick(${JSON.stringify(hotspot).replace(/"/g, '"')})"
|
|
title="${hotspot.name_es || hotspot.name || hotspot.label || ''}">
|
|
<div class="hotspot-label" style="left: ${width + 5}px; top: 0;">
|
|
${index + 1}
|
|
</div>
|
|
<svg width="${width}" height="${height}" style="position: absolute; top: 0; left: 0;">
|
|
<circle cx="${width/2}" cy="${height/2}" r="${Math.min(width, height)/2 - 2}"
|
|
fill="rgba(231, 76, 60, 0.3)"
|
|
stroke="var(--accent-color)"
|
|
stroke-width="2"/>
|
|
</svg>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 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 = `
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle"></i> Por favor ingresa un VIN
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (vin.length !== 17) {
|
|
resultContainer.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle"></i> El VIN debe tener exactamente 17 caracteres (actual: ${vin.length})
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Check for invalid characters (I, O, Q are not allowed in VINs)
|
|
if (/[IOQ]/i.test(vin)) {
|
|
resultContainer.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle"></i> El VIN contiene caracteres invalidos. Las letras I, O y Q no se permiten en VINs.
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Validate VIN format
|
|
if (!/^[A-HJ-NPR-Z0-9]{17}$/i.test(vin)) {
|
|
resultContainer.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle"></i> El VIN contiene caracteres invalidos. Solo se permiten letras (excepto I, O, Q) y numeros.
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
resultContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Decodificando VIN...</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-circle"></i> ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="vehicle-match-card">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<i class="fas fa-check-circle fa-2x me-2"></i>
|
|
<strong>Vehiculo encontrado en la base de datos</strong>
|
|
</div>
|
|
<button class="btn btn-light" onclick="dashboard.viewVinParts('${vin}', ${myeId})">
|
|
<i class="fas fa-cogs"></i> Ver Partes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
matchCard = `
|
|
<div class="vehicle-match-card vehicle-no-match">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<i class="fas fa-info-circle fa-2x me-2"></i>
|
|
<strong>Vehiculo no encontrado en la base de datos</strong>
|
|
</div>
|
|
<button class="btn btn-light" onclick="dashboard.searchManuallyFromVin('${make}', '${model}', '${year}')">
|
|
<i class="fas fa-search"></i> Buscar Manualmente
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
resultContainer.innerHTML = `
|
|
<div class="vin-result">
|
|
<div class="vin-result-header">
|
|
<span class="vin-badge">${vin}</span>
|
|
<span class="badge bg-success"><i class="fas fa-check"></i> VIN Valido</span>
|
|
</div>
|
|
|
|
<h5 class="mb-3">${year} ${make} ${model} ${trim}</h5>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-car"></i> Marca</span>
|
|
<span class="part-detail-value">${make}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-car-side"></i> Modelo</span>
|
|
<span class="part-detail-value">${model}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-calendar-alt"></i> Año</span>
|
|
<span class="part-detail-value">${year}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-cogs"></i> Motor</span>
|
|
<span class="part-detail-value">${engine}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-gas-pump"></i> Combustible</span>
|
|
<span class="part-detail-value">${fuelType}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-exchange-alt"></i> Transmision</span>
|
|
<span class="part-detail-value">${transmission}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-road"></i> Traccion</span>
|
|
<span class="part-detail-value">${driveType}</span>
|
|
</div>
|
|
<div class="part-detail-row">
|
|
<span class="part-detail-label"><i class="fas fa-globe"></i> Pais</span>
|
|
<span class="part-detail-value">${country}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${matchCard}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 = `<i class="fas fa-cogs"></i> Partes para VIN: ${vin.substring(0, 8)}...`;
|
|
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Cargando partes...</p>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle"></i> No se encontraron partes especificas para este VIN.
|
|
<br><br>
|
|
<button class="btn btn-primary" onclick="bootstrap.Modal.getInstance(document.getElementById('searchResultsModal')).hide(); setTimeout(() => dashboard.goToCategories(${myeId}), 300);">
|
|
<i class="fas fa-folder-open"></i> Ver Categorias del Vehiculo
|
|
</button>
|
|
</div>
|
|
`;
|
|
} else {
|
|
contentContainer.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
|
<p class="mt-2">Error al cargar partes: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-box-open fa-2x text-muted"></i>
|
|
<p class="mt-2">No se encontraron partes para este VIN</p>
|
|
${myeId ? `
|
|
<button class="btn btn-primary mt-2" onclick="bootstrap.Modal.getInstance(document.getElementById('searchResultsModal')).hide(); setTimeout(() => dashboard.goToCategories(${myeId}), 300);">
|
|
<i class="fas fa-folder-open"></i> Ver Categorias del Vehiculo
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
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 = `<p class="text-muted mb-3">${parts.length} parte${parts.length !== 1 ? 's' : ''} encontrada${parts.length !== 1 ? 's' : ''}</p>`;
|
|
|
|
for (const [category, categoryParts] of Object.entries(grouped)) {
|
|
html += `
|
|
<div class="search-results-section">
|
|
<h5><i class="fas fa-folder"></i> ${category}</h5>
|
|
<div class="search-results-list" style="max-height: none;">
|
|
${categoryParts.map(part => `
|
|
<div class="search-result-item" onclick="dashboard.showPartDetailFromSearch(${part.id})">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="search-result-part-number">
|
|
<span class="part-oem-badge">${part.oem_part_number || part.part_number || 'N/A'}</span>
|
|
</div>
|
|
<div class="search-result-name mt-1">${part.name_es || part.name || 'Sin nombre'}</div>
|
|
</div>
|
|
<div class="text-end">
|
|
${part.quality_tier ? this.getQualityBadge(part.quality_tier) : ''}
|
|
${part.group_name_es || part.group_name ? `<div class="text-muted small mt-1">${part.group_name_es || part.group_name}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (myeId) {
|
|
html += `
|
|
<div class="mt-3 text-center">
|
|
<button class="btn btn-primary" onclick="bootstrap.Modal.getInstance(document.getElementById('searchResultsModal')).hide(); setTimeout(() => dashboard.goToCategories(${myeId}), 300);">
|
|
<i class="fas fa-folder-open"></i> Ver Todas las Categorias
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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();
|
|
});
|