- Base de datos SQLite con información de vehículos - Dashboard web con Flask y Bootstrap - Scripts de web scraping para RockAuto - Interfaz CLI para consultas - Documentación completa del proyecto Incluye: - 12 marcas de vehículos - 10,923 modelos - 10,919 especificaciones de motores - 12,075 combinaciones modelo-año-motor
441 lines
16 KiB
JavaScript
441 lines
16 KiB
JavaScript
// Vehicle Dashboard JavaScript - Navegación por tarjetas
|
|
class VehicleDashboard {
|
|
constructor() {
|
|
this.currentView = 'brands'; // brands, models, vehicles
|
|
this.selectedBrand = null;
|
|
this.selectedModel = null;
|
|
this.allVehicles = [];
|
|
this.filteredVehicles = [];
|
|
this.stats = { brands: 0, models: 0, vehicles: 0 };
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
await this.loadStats();
|
|
await this.showBrands();
|
|
this.bindFilterEvents();
|
|
}
|
|
|
|
async loadStats() {
|
|
try {
|
|
const [brandsRes, vehiclesRes] = await Promise.all([
|
|
fetch('/api/brands'),
|
|
fetch('/api/vehicles')
|
|
]);
|
|
|
|
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;
|
|
|
|
document.getElementById('totalBrands').textContent = this.stats.brands;
|
|
document.getElementById('totalModels').textContent = this.stats.models;
|
|
document.getElementById('totalVehicles').textContent = this.stats.vehicles;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
}
|
|
}
|
|
|
|
updateBreadcrumb() {
|
|
const breadcrumb = document.getElementById('breadcrumb');
|
|
let html = '';
|
|
|
|
if (this.currentView === 'brands') {
|
|
html = `<li class="breadcrumb-item active"><i class="fas fa-home"></i> Marcas</li>`;
|
|
} else if (this.currentView === 'models') {
|
|
html = `
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="dashboard.goToBrands(); return false;">
|
|
<i class="fas fa-home"></i> Marcas
|
|
</a>
|
|
</li>
|
|
<li class="breadcrumb-item active">${this.selectedBrand}</li>
|
|
`;
|
|
} else if (this.currentView === 'vehicles') {
|
|
html = `
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="dashboard.goToBrands(); return false;">
|
|
<i class="fas fa-home"></i> Marcas
|
|
</a>
|
|
</li>
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="dashboard.goToModels('${this.selectedBrand}'); return false;">
|
|
${this.selectedBrand}
|
|
</a>
|
|
</li>
|
|
<li class="breadcrumb-item active">${this.selectedModel}</li>
|
|
`;
|
|
}
|
|
|
|
breadcrumb.innerHTML = html;
|
|
}
|
|
|
|
async showBrands() {
|
|
this.currentView = 'brands';
|
|
this.selectedBrand = null;
|
|
this.selectedModel = null;
|
|
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>`;
|
|
|
|
} 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.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>`;
|
|
|
|
} 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.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 {
|
|
const response = await fetch(
|
|
`/api/vehicles?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Error al cargar vehículos');
|
|
}
|
|
|
|
this.allVehicles = await response.json();
|
|
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();
|
|
}
|
|
|
|
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="empty-state">
|
|
<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">${v.fuel_type || 'N/A'}</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
<div class="spec-value">${v.power_hp || 0} HP</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-cogs"></i>
|
|
<div class="spec-value">${v.transmission || 'N/A'}</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-road"></i>
|
|
<div class="spec-value">${v.drivetrain || 'N/A'}</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-cube"></i>
|
|
<div class="spec-value">${v.cylinders || 0} cil.</div>
|
|
</div>
|
|
<div class="spec-item">
|
|
<i class="fas fa-oil-can"></i>
|
|
<div class="spec-value">${v.displacement_cc || 0} cc</div>
|
|
</div>
|
|
</div>
|
|
${v.trim_level && v.trim_level !== 'unknown' ? `
|
|
<div class="mt-2 text-center">
|
|
<span class="badge bg-primary">${v.trim_level}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
goToBrands() {
|
|
this.showBrands();
|
|
}
|
|
|
|
showFilters() {
|
|
document.getElementById('filtersBar').style.display = 'block';
|
|
// Reset filters
|
|
document.getElementById('yearFilter').value = '';
|
|
document.getElementById('engineFilter').value = '';
|
|
}
|
|
|
|
hideFilters() {
|
|
document.getElementById('filtersBar').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Initialize dashboard globally
|
|
let dashboard;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
dashboard = new VehicleDashboard();
|
|
});
|