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:
79
dashboard/README.md
Normal file
79
dashboard/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Vehicle Database Dashboard
|
||||
|
||||
A web-based dashboard for searching and filtering vehicle data from your database.
|
||||
|
||||
## Features
|
||||
|
||||
- Filter vehicles by brand, model, year, and engine
|
||||
- Responsive web interface with Bootstrap
|
||||
- Real-time filtering and search
|
||||
- Detailed vehicle information display
|
||||
- Modern UI with cards and badges
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.x
|
||||
- Flask (installed via `sudo apt-get install python3-flask`)
|
||||
- SQLite database with vehicle data (created in the vehicle_database directory)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Make sure you have the vehicle database created in the `../vehicle_database/vehicle_database.db` path
|
||||
2. Install Flask: `sudo apt-get install python3-flask`
|
||||
3. Run the dashboard server: `python3 server.py`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Start the server:
|
||||
```bash
|
||||
cd dashboard
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
2. Open your web browser and navigate to `http://localhost:5000`
|
||||
|
||||
3. Use the filters on the left panel to search for vehicles:
|
||||
- Select a brand from the dropdown
|
||||
- Select a model (based on the selected brand)
|
||||
- Select a year
|
||||
- Select an engine type
|
||||
- Click "Search Vehicles" to apply filters
|
||||
|
||||
4. The results will appear in the right panel with detailed information
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The dashboard uses the following API endpoints:
|
||||
|
||||
- `GET /api/brands` - Get all vehicle brands
|
||||
- `GET /api/models?brand=[brand]` - Get models for a specific brand
|
||||
- `GET /api/years` - Get all years
|
||||
- `GET /api/engines` - Get all engines
|
||||
- `GET /api/vehicles?[filters]` - Search vehicles with optional filters
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
dashboard/
|
||||
├── index.html # Main dashboard page
|
||||
├── dashboard.js # Frontend JavaScript
|
||||
├── server.py # Flask backend
|
||||
├── requirements.txt # Python dependencies
|
||||
├── start_dashboard.sh # Startup script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
You can customize the dashboard by:
|
||||
|
||||
- Modifying the CSS styles in index.html
|
||||
- Adding more filters in the JavaScript
|
||||
- Changing the layout in index.html
|
||||
- Adding more vehicle details in the display
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If the server won't start, make sure the vehicle database exists
|
||||
- If filters don't populate, check that the database has data
|
||||
- If the page doesn't load, verify that Flask is installed correctly
|
||||
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();
|
||||
});
|
||||
367
dashboard/index.html
Normal file
367
dashboard/index.html
Normal file
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Base de Datos de Vehículos</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #2c3e50;
|
||||
--secondary-color: #3498db;
|
||||
--accent-color: #e74c3c;
|
||||
--bg-color: #ecf0f1;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), #1a252f);
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.breadcrumb-nav {
|
||||
background: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.breadcrumb-nav .breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-nav .breadcrumb-item a {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-nav .breadcrumb-item a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.breadcrumb-nav .breadcrumb-item.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tarjetas de marcas */
|
||||
.brand-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.brand-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.brand-card .brand-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--secondary-color), #2980b9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brand-card .brand-name {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.brand-card .brand-count {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.brand-card .brand-country {
|
||||
font-size: 0.85rem;
|
||||
color: #95a5a6;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Tarjetas de modelos */
|
||||
.model-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.model-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.12);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.model-card .model-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model-card .model-info {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Tarjetas de vehículos */
|
||||
.vehicle-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vehicle-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.vehicle-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), #1a252f);
|
||||
color: white;
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.vehicle-header .vehicle-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.vehicle-header .vehicle-engine {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.vehicle-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.vehicle-specs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
background: var(--bg-color);
|
||||
padding: 0.6rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.spec-item i {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 0.3rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spec-item .spec-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Filtros en vista de vehículos */
|
||||
.filters-bar {
|
||||
background: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.filters-bar .form-select {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Grid de contenido */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.content-grid.brands-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.content-grid.models-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.content-grid.vehicles-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
/* Estados */
|
||||
.loading-state, .empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.loading-state i, .empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
/* Botón volver */
|
||||
.btn-back {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #2980b9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Estadísticas */
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item .stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-item .stat-label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-grid.brands-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-grid.brands-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h1><i class="fas fa-car-side"></i> Base de Datos de Vehículos</h1>
|
||||
<p class="lead mb-0">Explora vehículos por marca, modelo y especificaciones</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="stats-bar justify-content-md-end">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalBrands">0</div>
|
||||
<div class="stat-label">Marcas</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalModels">0</div>
|
||||
<div class="stat-label">Modelos</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="totalVehicles">0</div>
|
||||
<div class="stat-label">Vehículos</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Breadcrumb navegación -->
|
||||
<div class="breadcrumb-nav" id="breadcrumbNav">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb" id="breadcrumb">
|
||||
<li class="breadcrumb-item active">
|
||||
<i class="fas fa-home"></i> Marcas
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Barra de filtros (solo visible en vista de vehículos) -->
|
||||
<div class="filters-bar" id="filtersBar" style="display: none;">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto">
|
||||
<label class="col-form-label fw-bold">Filtrar por:</label>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="yearFilter" class="form-select">
|
||||
<option value="">Todos los años</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="engineFilter" class="form-select">
|
||||
<option value="">Todos los motores</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span id="resultCount" class="badge bg-secondary fs-6">0 resultados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor principal -->
|
||||
<div id="mainContent">
|
||||
<div class="loading-state">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<h4>Cargando...</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
dashboard/requirements.txt
Normal file
1
dashboard/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Flask==2.3.3
|
||||
248
dashboard/server.py
Normal file
248
dashboard/server.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from flask import Flask, render_template, jsonify, request, send_from_directory
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
app = Flask(__name__, static_folder='.')
|
||||
|
||||
# Database path - adjust as needed
|
||||
DATABASE_PATH = '../vehicle_database/vehicle_database.db'
|
||||
|
||||
def get_db_connection():
|
||||
"""Get a connection to the vehicle database"""
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
conn.row_factory = sqlite3.Row # This enables column access by name
|
||||
return conn
|
||||
|
||||
def get_all_brands():
|
||||
"""Get all unique brands from the database"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT DISTINCT name FROM brands ORDER BY name")
|
||||
brands = [row['name'] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return brands
|
||||
|
||||
def get_all_years():
|
||||
"""Get all unique years from the database"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT DISTINCT year FROM years ORDER BY year DESC")
|
||||
years = [row['year'] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return years
|
||||
|
||||
def get_all_engines():
|
||||
"""Get all unique engines from the database"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT DISTINCT name FROM engines ORDER BY name")
|
||||
engines = [row['name'] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return engines
|
||||
|
||||
def get_models_by_brand(brand_name=None):
|
||||
"""Get all models, optionally filtered by brand"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
if brand_name:
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT m.name
|
||||
FROM models m
|
||||
JOIN brands b ON m.brand_id = b.id
|
||||
WHERE b.name = ?
|
||||
ORDER BY m.name
|
||||
""", (brand_name,))
|
||||
else:
|
||||
cursor.execute("SELECT DISTINCT name FROM models ORDER BY name")
|
||||
|
||||
models = [row['name'] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return models
|
||||
|
||||
def search_vehicles(brand=None, model=None, year=None, engine=None):
|
||||
"""Search for vehicles based on filters"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
b.name AS brand,
|
||||
m.name AS model,
|
||||
y.year,
|
||||
e.name AS engine,
|
||||
e.power_hp,
|
||||
e.displacement_cc,
|
||||
e.cylinders,
|
||||
e.fuel_type,
|
||||
mye.trim_level,
|
||||
mye.drivetrain,
|
||||
mye.transmission
|
||||
FROM model_year_engine mye
|
||||
JOIN models m ON mye.model_id = m.id
|
||||
JOIN brands b ON m.brand_id = b.id
|
||||
JOIN years y ON mye.year_id = y.id
|
||||
JOIN engines e ON mye.engine_id = e.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
if brand:
|
||||
query += " AND b.name = ?"
|
||||
params.append(brand)
|
||||
if model:
|
||||
query += " AND m.name = ?"
|
||||
params.append(model)
|
||||
if year:
|
||||
query += " AND y.year = ?"
|
||||
params.append(int(year))
|
||||
if engine:
|
||||
query += " AND e.name = ?"
|
||||
params.append(engine)
|
||||
|
||||
query += " ORDER BY b.name, m.name, y.year"
|
||||
|
||||
cursor.execute(query, params)
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Convert to list of dictionaries
|
||||
vehicles = []
|
||||
for row in results:
|
||||
vehicle = {
|
||||
'brand': row['brand'],
|
||||
'model': row['model'],
|
||||
'year': row['year'],
|
||||
'engine': row['engine'],
|
||||
'power_hp': row['power_hp'] or 0,
|
||||
'displacement_cc': row['displacement_cc'] or 0,
|
||||
'cylinders': row['cylinders'] or 0,
|
||||
'fuel_type': row['fuel_type'] or 'unknown',
|
||||
'trim_level': row['trim_level'] or 'unknown',
|
||||
'drivetrain': row['drivetrain'] or 'unknown',
|
||||
'transmission': row['transmission'] or 'unknown'
|
||||
}
|
||||
vehicles.append(vehicle)
|
||||
|
||||
return vehicles
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the main dashboard page"""
|
||||
return send_from_directory('.', 'index.html')
|
||||
|
||||
@app.route('/<path:path>')
|
||||
def static_files(path):
|
||||
"""Serve static files"""
|
||||
return send_from_directory('.', path)
|
||||
|
||||
@app.route('/api/brands')
|
||||
def api_brands():
|
||||
"""API endpoint to get all brands"""
|
||||
brands = get_all_brands()
|
||||
return jsonify(brands)
|
||||
|
||||
@app.route('/api/years')
|
||||
def api_years():
|
||||
"""API endpoint to get years, optionally filtered by brand and/or model"""
|
||||
brand = request.args.get('brand')
|
||||
model = request.args.get('model')
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT y.year
|
||||
FROM years y
|
||||
JOIN model_year_engine mye ON y.id = mye.year_id
|
||||
JOIN models m ON mye.model_id = m.id
|
||||
JOIN brands b ON m.brand_id = b.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
if brand:
|
||||
query += " AND b.name = ?"
|
||||
params.append(brand)
|
||||
if model:
|
||||
query += " AND m.name = ?"
|
||||
params.append(model)
|
||||
|
||||
query += " ORDER BY y.year DESC"
|
||||
|
||||
cursor.execute(query, params)
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
years = [row['year'] for row in results]
|
||||
return jsonify(years)
|
||||
|
||||
@app.route('/api/engines')
|
||||
def api_engines():
|
||||
"""API endpoint to get engines, optionally filtered by brand, model, and/or year"""
|
||||
brand = request.args.get('brand')
|
||||
model = request.args.get('model')
|
||||
year = request.args.get('year')
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT e.name
|
||||
FROM engines e
|
||||
JOIN model_year_engine mye ON e.id = mye.engine_id
|
||||
JOIN models m ON mye.model_id = m.id
|
||||
JOIN brands b ON m.brand_id = b.id
|
||||
JOIN years y ON mye.year_id = y.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
if brand:
|
||||
query += " AND b.name = ?"
|
||||
params.append(brand)
|
||||
if model:
|
||||
query += " AND m.name = ?"
|
||||
params.append(model)
|
||||
if year:
|
||||
query += " AND y.year = ?"
|
||||
params.append(int(year))
|
||||
|
||||
query += " ORDER BY e.name"
|
||||
|
||||
cursor.execute(query, params)
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
engines = [row['name'] for row in results]
|
||||
return jsonify(engines)
|
||||
|
||||
@app.route('/api/models')
|
||||
def api_models():
|
||||
"""API endpoint to get models, optionally filtered by brand"""
|
||||
brand = request.args.get('brand')
|
||||
models = get_models_by_brand(brand)
|
||||
return jsonify(models)
|
||||
|
||||
@app.route('/api/vehicles')
|
||||
def api_vehicles():
|
||||
"""API endpoint to search for vehicles"""
|
||||
brand = request.args.get('brand')
|
||||
model = request.args.get('model')
|
||||
year = request.args.get('year')
|
||||
engine = request.args.get('engine')
|
||||
|
||||
vehicles = search_vehicles(brand, model, year, engine)
|
||||
return jsonify(vehicles)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Check if database exists
|
||||
if not os.path.exists(DATABASE_PATH):
|
||||
print(f"Database not found at {DATABASE_PATH}")
|
||||
print("Please make sure the vehicle database is created first.")
|
||||
exit(1)
|
||||
|
||||
print("Starting Vehicle Dashboard Server...")
|
||||
print("Visit http://localhost:5000 to access the dashboard locally")
|
||||
print("Visit http://192.168.10.198:5000 to access the dashboard from other computers on the network")
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
30
dashboard/start_dashboard.sh
Executable file
30
dashboard/start_dashboard.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Startup script for Vehicle Dashboard
|
||||
|
||||
echo "Vehicle Dashboard Startup Script"
|
||||
echo "================================"
|
||||
|
||||
# Check if the vehicle database exists
|
||||
if [ ! -f "../vehicle_database/vehicle_database.db" ]; then
|
||||
echo "Error: Vehicle database not found!"
|
||||
echo "Please make sure you have created the vehicle database first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Vehicle database found."
|
||||
|
||||
# Check if Flask is available
|
||||
if python3 -c "import flask" &> /dev/null; then
|
||||
echo "Flask is available."
|
||||
else
|
||||
echo "Installing Flask..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-flask
|
||||
fi
|
||||
|
||||
echo "Starting Vehicle Dashboard Server..."
|
||||
echo "Access the dashboard at: http://localhost:5000"
|
||||
echo "Press Ctrl+C to stop the server."
|
||||
|
||||
cd /home/Autopartes/dashboard
|
||||
python3 server.py
|
||||
Reference in New Issue
Block a user