Initial commit: Sistema Autoparts DB
- 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
This commit is contained in:
440
dashboard/dashboard.js
Normal file
440
dashboard/dashboard.js
Normal file
@@ -0,0 +1,440 @@
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user