Implement complete autoparts catalog system (5 phases)

FASE 1: Parts Database
- Added part_categories, part_groups, parts, vehicle_parts tables
- 12 categories, 190 groups with Spanish translations
- API endpoints for categories, groups, parts CRUD

FASE 2: Cross-References & Aftermarket
- Added manufacturers, aftermarket_parts, part_cross_references tables
- 24 manufacturers, quality tier system (economy/standard/premium/oem)
- Part number search across OEM and aftermarket

FASE 3: Exploded Diagrams
- Added diagrams, vehicle_diagrams, diagram_hotspots tables
- SVG viewer with zoom controls and interactive hotspots
- 3 sample diagrams (brake, oil filter, suspension)

FASE 4: Search & VIN Decoder
- SQLite FTS5 full-text search with auto-sync triggers
- NHTSA VIN decoder API integration with 30-day cache
- Unified search endpoint

FASE 5: Optimization & UX
- API pagination (page/per_page, max 100 items)
- Dark mode with localStorage persistence
- Keyboard shortcuts (/, Ctrl+K, Escape, Backspace, Ctrl+D)
- Breadcrumb navigation
- ARIA accessibility (labels, roles, focus management)
- Skip link for keyboard users

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 07:13:46 +00:00
parent 61474f7abe
commit d4d1c9b7ba
14 changed files with 7262 additions and 41 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Base de Datos de Vehículos</title>
<title>Catálogo de Autopartes</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>
@@ -12,26 +12,50 @@
--secondary-color: #3498db;
--accent-color: #e74c3c;
--bg-color: #ecf0f1;
--text-color: #212529;
--card-bg: #ffffff;
--border-color: #dee2e6;
--header-gradient-start: #2c3e50;
--header-gradient-end: #1a252f;
--table-hover-bg: #f8f9fa;
--muted-text: #7f8c8d;
}
[data-theme="dark"] {
--primary-color: #4a6fa5;
--secondary-color: #5dade2;
--accent-color: #e74c3c;
--bg-color: #1a1a2e;
--text-color: #eee;
--card-bg: #16213e;
--border-color: #0f3460;
--header-gradient-start: #0f3460;
--header-gradient-end: #1a1a2e;
--table-hover-bg: #1f2a44;
--muted-text: #a0a0a0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
.dashboard-header {
background: linear-gradient(135deg, var(--primary-color), #1a252f);
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
.breadcrumb-nav {
background: white;
background: var(--card-bg);
padding: 1rem 1.5rem;
border-radius: 10px;
margin-bottom: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: background-color 0.3s ease;
}
.breadcrumb-nav .breadcrumb {
@@ -49,13 +73,13 @@
}
.breadcrumb-nav .breadcrumb-item.active {
color: var(--primary-color);
color: var(--text-color);
font-weight: 600;
}
/* Tarjetas de marcas */
.brand-card {
background: white;
background: var(--card-bg);
border-radius: 15px;
padding: 2rem;
text-align: center;
@@ -87,24 +111,24 @@
.brand-card .brand-name {
font-size: 1.3rem;
font-weight: 700;
color: var(--primary-color);
color: var(--text-color);
margin-bottom: 0.5rem;
}
.brand-card .brand-count {
color: #7f8c8d;
color: var(--muted-text);
font-size: 0.9rem;
}
.brand-card .brand-country {
font-size: 0.85rem;
color: #95a5a6;
color: var(--muted-text);
margin-top: 0.5rem;
}
/* Tarjetas de modelos */
.model-card {
background: white;
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
@@ -122,18 +146,18 @@
.model-card .model-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
color: var(--text-color);
margin-bottom: 0.5rem;
}
.model-card .model-info {
color: #7f8c8d;
color: var(--muted-text);
font-size: 0.9rem;
}
/* Tarjetas de vehículos */
.vehicle-card {
background: white;
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
@@ -146,7 +170,7 @@
}
.vehicle-header {
background: linear-gradient(135deg, var(--primary-color), #1a252f);
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
color: white;
padding: 1.2rem;
}
@@ -178,6 +202,7 @@
border-radius: 8px;
text-align: center;
font-size: 0.85rem;
transition: background-color 0.3s ease;
}
.spec-item i {
@@ -188,20 +213,24 @@
.spec-item .spec-value {
font-weight: 600;
color: var(--primary-color);
color: var(--text-color);
}
/* Filtros en vista de vehículos */
.filters-bar {
background: white;
background: var(--card-bg);
padding: 1rem 1.5rem;
border-radius: 10px;
margin-bottom: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: background-color 0.3s ease;
}
.filters-bar .form-select {
border-radius: 8px;
background-color: var(--card-bg);
color: var(--text-color);
border-color: var(--border-color);
}
/* Grid de contenido */
@@ -222,17 +251,187 @@
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.content-grid.categories-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* Tarjetas de categorías */
.category-card {
background: var(--card-bg);
border-radius: 15px;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
border: 2px solid transparent;
height: 100%;
}
.category-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
border-color: var(--accent-color);
}
.category-card .category-icon {
width: 70px;
height: 70px;
background: linear-gradient(135deg, var(--accent-color), #c0392b);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 1.8rem;
color: white;
}
.category-card .category-name {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.category-card .category-count {
color: var(--muted-text);
font-size: 0.85rem;
}
/* Tabla de partes */
.parts-table {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
transition: background-color 0.3s ease;
}
.parts-table table {
width: 100%;
margin-bottom: 0;
}
.parts-table thead {
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
color: white;
}
.parts-table thead th {
padding: 1rem;
font-weight: 600;
border: none;
}
.parts-table tbody tr {
transition: background-color 0.2s ease;
}
.parts-table tbody tr:hover {
background-color: var(--table-hover-bg);
}
.parts-table tbody td {
padding: 0.8rem 1rem;
vertical-align: middle;
border-bottom: 1px solid var(--border-color);
color: var(--text-color);
}
.parts-table .btn-view {
background: var(--secondary-color);
color: white;
border: none;
padding: 0.4rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
transition: all 0.3s;
}
.parts-table .btn-view:hover {
background: #2980b9;
}
/* Botón Ver Partes */
.btn-parts {
background: var(--accent-color);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s;
width: 100%;
margin-top: 1rem;
}
.btn-parts:hover {
background: #c0392b;
color: white;
}
/* Modal de detalle de parte */
.part-detail-header {
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
color: white;
padding: 1.5rem;
border-radius: 0;
}
.part-detail-body {
padding: 1.5rem;
background-color: var(--card-bg);
}
.part-detail-row {
display: flex;
justify-content: space-between;
padding: 0.8rem 0;
border-bottom: 1px solid var(--border-color);
}
.part-detail-row:last-child {
border-bottom: none;
}
.part-detail-label {
font-weight: 600;
color: var(--secondary-color);
}
.part-detail-value {
color: var(--text-color);
}
.modal-content {
background-color: var(--card-bg);
color: var(--text-color);
}
.modal-footer {
background-color: var(--card-bg);
border-top-color: var(--border-color);
}
.part-oem-badge {
background: var(--secondary-color);
color: white;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.9rem;
}
/* Estados */
.loading-state, .empty-state {
text-align: center;
padding: 4rem 2rem;
color: #7f8c8d;
color: var(--muted-text);
}
.loading-state i, .empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
color: #bdc3c7;
color: var(--muted-text);
}
/* Botón volver */
@@ -288,15 +487,464 @@
grid-template-columns: 1fr;
}
}
/* FASE 2: Search bar styles */
.search-bar {
max-width: 500px;
}
.search-bar .form-control {
border-radius: 25px 0 0 25px;
padding: 0.6rem 1.2rem;
border: 2px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
color: white;
}
.search-bar .form-control::placeholder {
color: rgba(255,255,255,0.7);
}
.search-bar .form-control:focus {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.5);
box-shadow: none;
color: white;
}
.search-bar .btn {
border-radius: 0 25px 25px 0;
padding: 0.6rem 1.2rem;
border: 2px solid rgba(255,255,255,0.3);
border-left: none;
}
/* FASE 2: Quality tier badges */
.quality-badge {
display: inline-block;
padding: 0.25rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: white;
text-transform: uppercase;
}
.quality-economy { background-color: #f39c12; }
.quality-standard { background-color: #3498db; }
.quality-premium { background-color: #27ae60; }
.quality-oem { background-color: #9b59b6; }
/* FASE 2: Search results modal */
.search-results-list {
max-height: 400px;
overflow-y: auto;
}
.search-result-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-part-number {
font-weight: 600;
color: var(--primary-color);
}
.search-result-name {
color: #666;
font-size: 0.9rem;
}
/* FASE 2: Alternatives table */
.alternatives-section, .crossref-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 2px solid #eee;
}
.alternatives-section h5, .crossref-section h5 {
color: var(--primary-color);
margin-bottom: 1rem;
font-weight: 600;
}
.alternatives-table {
width: 100%;
font-size: 0.9rem;
}
.alternatives-table thead {
background-color: #f8f9fa;
}
.alternatives-table th, .alternatives-table td {
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.alternatives-table tbody tr:hover {
background-color: #f8f9fa;
}
.crossref-badge {
display: inline-block;
background-color: #e9ecef;
color: var(--primary-color);
padding: 0.3rem 0.6rem;
border-radius: 6px;
margin: 0.2rem;
font-size: 0.85rem;
}
.price-tag {
font-weight: 600;
color: #27ae60;
}
/* FASE 3: Diagram viewer styles */
.diagram-viewer {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
transition: background-color 0.3s ease;
}
.diagram-header {
background: linear-gradient(135deg, var(--header-gradient-start), var(--header-gradient-end));
color: white;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.diagram-container {
position: relative;
padding: 1rem;
min-height: 400px;
display: flex;
justify-content: center;
align-items: center;
}
.diagram-svg-wrapper {
position: relative;
display: inline-block;
}
.diagram-svg-wrapper svg {
max-width: 100%;
height: auto;
}
.hotspot {
position: absolute;
cursor: pointer;
transition: all 0.2s ease;
}
.hotspot:hover {
transform: scale(1.1);
filter: brightness(1.2);
}
.hotspot-label {
position: absolute;
background: var(--primary-color);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: bold;
pointer-events: none;
white-space: nowrap;
}
.diagram-thumbnails {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
padding: 1rem;
}
.diagram-thumbnail {
background: var(--card-bg);
border-radius: 8px;
padding: 0.5rem;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.diagram-thumbnail:hover, .diagram-thumbnail.active {
border-color: var(--accent-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.diagram-thumbnail img, .diagram-thumbnail svg {
width: 100%;
height: 100px;
object-fit: contain;
}
.diagram-thumbnail-name {
text-align: center;
font-size: 0.85rem;
margin-top: 0.5rem;
color: var(--text-color);
}
.zoom-controls {
display: flex;
gap: 0.5rem;
}
.zoom-controls button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
}
.zoom-controls button:hover {
background: rgba(255,255,255,0.3);
}
/* FASE 4: VIN Decoder styles */
.vin-input-group {
max-width: 400px;
}
.vin-result {
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
margin-top: 1rem;
transition: background-color 0.3s ease;
}
.vin-result-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.vin-badge {
background: var(--header-gradient-start);
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
font-family: monospace;
font-size: 1.1rem;
letter-spacing: 1px;
}
.vehicle-match-card {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
border-radius: 10px;
padding: 1rem;
margin-top: 1rem;
}
.vehicle-no-match {
background: #f39c12;
}
.search-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.search-tab {
padding: 0.5rem 1rem;
border: none;
background: #e9ecef;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.search-tab.active {
background: var(--secondary-color);
color: white;
}
.search-results-section {
margin-top: 1.5rem;
}
.search-results-section h5 {
color: var(--primary-color);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #eee;
}
/* FASE 4: Enhanced search bar */
.search-bar .btn-vin {
border-radius: 0;
border: 2px solid rgba(255,255,255,0.3);
border-left: none;
background: rgba(255,255,255,0.1);
color: white;
}
.search-bar .btn-vin:hover {
background: rgba(255,255,255,0.2);
}
.search-bar .btn-search {
border-radius: 0 25px 25px 0;
}
/* FASE 5: Dark mode toggle button */
#darkModeToggle {
background: rgba(255,255,255,0.1);
border: 2px solid rgba(255,255,255,0.3);
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
#darkModeToggle:hover {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.5);
}
#darkModeToggle i {
font-size: 1.1rem;
}
/* FASE 5: Keyboard shortcut hint */
.keyboard-hint {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 0.1rem 0.4rem;
border-radius: 4px;
font-size: 0.75rem;
margin-left: 0.5rem;
font-family: monospace;
}
[data-theme="dark"] .keyboard-hint {
background: rgba(0,0,0,0.3);
}
/* FASE 5: Breadcrumb year display */
.breadcrumb-year {
font-weight: 600;
color: var(--secondary-color);
}
/* FASE 5: Accessibility styles */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 1000;
text-decoration: none;
font-weight: 600;
border-radius: 0 0 4px 0;
}
.skip-link:focus {
top: 0;
outline: 2px solid #fff;
outline-offset: 2px;
}
/* Focus styles for interactive elements */
.brand-card:focus,
.model-card:focus,
.category-card:focus,
.diagram-thumbnail:focus,
.search-result-item:focus {
outline: 3px solid var(--secondary-color);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.3);
}
.brand-card:focus-visible,
.model-card:focus-visible,
.category-card:focus-visible,
.diagram-thumbnail:focus-visible,
.search-result-item:focus-visible {
outline: 3px solid var(--secondary-color);
outline-offset: 2px;
}
/* Ensure buttons and links have visible focus */
.btn:focus-visible,
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 3px solid var(--secondary-color);
outline-offset: 2px;
}
/* High contrast focus for dark backgrounds */
.dashboard-header .btn:focus-visible,
.dashboard-header input:focus-visible {
outline-color: #fff;
}
/* Screen reader only class for hidden labels */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<div class="dashboard-header">
<!-- FASE 5: Skip to content link for accessibility -->
<a href="#mainContent" class="skip-link">Saltar al contenido</a>
<header class="dashboard-header" role="banner">
<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 class="d-flex align-items-center gap-3">
<h1 class="mb-0"><i class="fas fa-car-side"></i> Catalogo de Autopartes</h1>
<button class="btn" id="darkModeToggle" title="Alternar modo oscuro (Ctrl+D)">
<i class="fas fa-moon"></i>
</button>
</div>
<p class="lead mb-0 mt-2">Explora vehiculos y partes por marca, modelo y especificaciones</p>
</div>
<div class="col-md-6">
<div class="stats-bar justify-content-md-end">
@@ -312,9 +960,25 @@
<div class="stat-number" id="totalVehicles">0</div>
<div class="stat-label">Vehículos</div>
</div>
<div class="stat-item">
<div class="stat-number" id="totalParts">0</div>
<div class="stat-label">Partes</div>
</div>
</div>
</div>
</div>
<!-- FASE 4: Enhanced Search bar with VIN support -->
<div class="search-bar mt-3">
<div class="input-group">
<input type="text" id="partNumberSearch" class="form-control" placeholder="Buscar por numero de parte o texto... (presiona / para enfocar)" aria-label="Buscar partes" onkeypress="if(event.key==='Enter') dashboard.searchPartNumber()">
<button class="btn btn-vin" onclick="dashboard.openVinDecoder()" title="Decodificar VIN">
<i class="fas fa-barcode"></i>
</button>
<button class="btn btn-light btn-search" onclick="dashboard.searchPartNumber()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
@@ -330,34 +994,124 @@
</nav>
</div>
<!-- Barra de filtros (solo visible en vista de vehículos) -->
<div class="filters-bar" id="filtersBar" style="display: none;">
<!-- Barra de filtros (solo visible en vista de vehiculos) -->
<div class="filters-bar" id="filtersBar" style="display: none;" role="search" aria-label="Filtros de vehiculos">
<div class="row g-3 align-items-center">
<div class="col-auto">
<label class="col-form-label fw-bold">Filtrar por:</label>
<span class="col-form-label fw-bold" id="filterLabel">Filtrar por:</span>
</div>
<div class="col-md-3">
<select id="yearFilter" class="form-select">
<label for="yearFilter" class="sr-only">Filtrar por año</label>
<select id="yearFilter" class="form-select" aria-label="Filtrar por año">
<option value="">Todos los años</option>
</select>
</div>
<div class="col-md-3">
<select id="engineFilter" class="form-select">
<label for="engineFilter" class="sr-only">Filtrar por motor</label>
<select id="engineFilter" class="form-select" aria-label="Filtrar por motor">
<option value="">Todos los motores</option>
</select>
</div>
<div class="col-auto">
<span id="resultCount" class="badge bg-secondary fs-6">0 resultados</span>
<span id="resultCount" class="badge bg-secondary fs-6" role="status" aria-live="polite">0 resultados</span>
</div>
</div>
</div>
<!-- Contenedor principal -->
<div id="mainContent">
<div class="loading-state">
<i class="fas fa-spinner fa-spin"></i>
<main id="mainContent" role="main" tabindex="-1" aria-live="polite">
<div class="loading-state" role="status" aria-label="Cargando contenido">
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
<h4>Cargando...</h4>
</div>
</main>
</div>
<!-- Modal de Detalle de Parte -->
<div class="modal fade" id="partDetailModal" tabindex="-1" aria-labelledby="partDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header part-detail-header">
<h5 class="modal-title" id="partDetailModalLabel">
<i class="fas fa-cog"></i> Detalle de Parte
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body part-detail-body" id="partDetailContent">
<!-- Contenido dinámico -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<!-- FASE 2: Modal de Resultados de Búsqueda -->
<div class="modal fade" id="searchResultsModal" tabindex="-1" aria-labelledby="searchResultsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header part-detail-header">
<h5 class="modal-title" id="searchResultsModalLabel">
<i class="fas fa-search"></i> Resultados de Búsqueda
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body" id="searchResultsContent">
<!-- Contenido dinámico -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<!-- FASE 3: Modal de Diagrama -->
<div class="modal fade" id="diagramModal" tabindex="-1" role="dialog" aria-labelledby="diagramModalLabel" aria-modal="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header part-detail-header">
<h5 class="modal-title" id="diagramModalLabel">
<i class="fas fa-project-diagram" aria-hidden="true"></i> Diagrama
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body p-0" id="diagramModalContent">
<!-- Dynamic content -->
</div>
</div>
</div>
</div>
<!-- FASE 4: Modal de VIN Decoder -->
<div class="modal fade" id="vinDecoderModal" tabindex="-1" role="dialog" aria-labelledby="vinDecoderModalLabel" aria-modal="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header part-detail-header">
<h5 class="modal-title" id="vinDecoderModalLabel">
<i class="fas fa-barcode" aria-hidden="true"></i> Decodificador de VIN
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body" id="vinDecoderContent">
<div class="vin-input-group mb-4">
<label for="vinInput" class="form-label">Ingresa el VIN (17 caracteres)</label>
<div class="input-group">
<input type="text" id="vinInput" class="form-control"
maxlength="17" placeholder="Ej: 4T1BF1FK5CU123456"
style="text-transform: uppercase; font-family: monospace;"
aria-describedby="vinHelp"
onkeypress="if(event.key==='Enter') dashboard.decodeVin()">
<button class="btn btn-primary" onclick="dashboard.decodeVin()" aria-label="Decodificar VIN">
<i class="fas fa-search" aria-hidden="true"></i> Decodificar
</button>
</div>
<small id="vinHelp" class="text-muted">El VIN se encuentra en la placa del tablero o en la puerta del conductor</small>
</div>
<div id="vinResult" role="region" aria-live="polite"></div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="metalGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#b0b0b0"/>
<stop offset="50%" style="stop-color:#808080"/>
<stop offset="100%" style="stop-color:#606060"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Front Brake Assembly / Ensamble de Freno Delantero
</text>
<!-- Brake Rotor (large circle) -->
<circle cx="250" cy="200" r="120" fill="url(#metalGrad)" stroke="#333" stroke-width="3" id="rotor"/>
<circle cx="250" cy="200" r="100" fill="none" stroke="#555" stroke-width="2"/>
<circle cx="250" cy="200" r="40" fill="#444" stroke="#333" stroke-width="2"/>
<!-- Rotor ventilation slots -->
<line x1="250" y1="60" x2="250" y2="80" stroke="#666" stroke-width="3"/>
<line x1="250" y1="320" x2="250" y2="340" stroke="#666" stroke-width="3"/>
<line x1="130" y1="200" x2="150" y2="200" stroke="#666" stroke-width="3"/>
<line x1="350" y1="200" x2="370" y2="200" stroke="#666" stroke-width="3"/>
<!-- Brake Caliper -->
<rect x="320" y="140" width="80" height="120" rx="10" ry="10" fill="#c0392b" stroke="#922b21" stroke-width="3" id="caliper"/>
<rect x="330" y="155" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
<rect x="330" y="210" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
<!-- Caliper bolts -->
<circle cx="340" cy="150" r="6" fill="#333"/>
<circle cx="380" cy="150" r="6" fill="#333"/>
<circle cx="340" cy="250" r="6" fill="#333"/>
<circle cx="380" cy="250" r="6" fill="#333"/>
<!-- Brake Pads (visible through caliper) -->
<rect x="300" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-inner"/>
<rect x="405" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-outer"/>
<!-- Callout lines and numbers -->
<!-- Callout 1: Brake Rotor -->
<line x1="170" y1="120" x2="100" y2="60" stroke="#333" stroke-width="1.5"/>
<circle cx="100" cy="60" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="100" y="65" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Callout 2: Brake Caliper -->
<line x1="400" y1="140" x2="480" y2="80" stroke="#333" stroke-width="1.5"/>
<circle cx="480" cy="80" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="480" y="85" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
<!-- Callout 3: Brake Pads -->
<line x1="307" y1="250" x2="250" y2="320" stroke="#333" stroke-width="1.5"/>
<circle cx="250" cy="320" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="250" y="325" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
<!-- Legend -->
<rect x="440" y="300" width="150" height="90" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="515" y="320" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="450" y="340" font-family="Arial" font-size="11" fill="#333">1. Brake Rotor / Disco</text>
<text x="450" y="358" font-family="Arial" font-size="11" fill="#333">2. Brake Caliper / Caliper</text>
<text x="450" y="376" font-family="Arial" font-size="11" fill="#333">3. Brake Pads / Balatas</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="oilGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#8B4513"/>
<stop offset="100%" style="stop-color:#654321"/>
</linearGradient>
<linearGradient id="filterGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#2c3e50"/>
<stop offset="50%" style="stop-color:#34495e"/>
<stop offset="100%" style="stop-color:#2c3e50"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Oil Filter System / Sistema de Filtro de Aceite
</text>
<!-- Engine Block (simplified) -->
<rect x="50" y="100" width="200" height="200" fill="#555" stroke="#333" stroke-width="3" rx="10"/>
<text x="150" y="200" text-anchor="middle" font-family="Arial" font-size="14" fill="#ccc">ENGINE</text>
<text x="150" y="220" text-anchor="middle" font-family="Arial" font-size="12" fill="#999">MOTOR</text>
<!-- Oil passage from engine -->
<rect x="250" y="180" width="60" height="20" fill="url(#oilGrad)"/>
<path d="M250,190 L230,190" stroke="#8B4513" stroke-width="8" fill="none"/>
<!-- Oil Filter Housing -->
<rect x="310" y="120" width="100" height="160" fill="#777" stroke="#555" stroke-width="3" rx="5"/>
<!-- Oil Filter (canister type) -->
<rect x="320" y="140" width="80" height="120" fill="url(#filterGrad)" stroke="#1a252f" stroke-width="3" rx="8" id="oil-filter"/>
<!-- Filter ridges -->
<line x1="320" y1="160" x2="400" y2="160" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="180" x2="400" y2="180" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="200" x2="400" y2="200" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="220" x2="400" y2="220" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="240" x2="400" y2="240" stroke="#1a252f" stroke-width="2"/>
<!-- Filter label area -->
<rect x="335" y="175" width="50" height="50" fill="#2980b9" rx="3"/>
<text x="360" y="195" text-anchor="middle" font-family="Arial" font-size="10" fill="white">OIL</text>
<text x="360" y="210" text-anchor="middle" font-family="Arial" font-size="10" fill="white">FILTER</text>
<!-- Oil return passage -->
<rect x="410" y="180" width="60" height="20" fill="url(#oilGrad)"/>
<!-- Oil Pan (simplified) -->
<path d="M470,170 L530,170 L550,300 L450,300 Z" fill="#666" stroke="#444" stroke-width="3"/>
<text x="500" y="250" text-anchor="middle" font-family="Arial" font-size="12" fill="#ccc">OIL PAN</text>
<text x="500" y="265" text-anchor="middle" font-family="Arial" font-size="10" fill="#999">CARTER</text>
<!-- Flow arrows -->
<polygon points="275,185 285,190 275,195" fill="#8B4513"/>
<polygon points="435,185 445,190 435,195" fill="#8B4513"/>
<!-- Callout for Oil Filter -->
<line x1="360" y1="140" x2="360" y2="70" stroke="#333" stroke-width="1.5"/>
<line x1="360" y1="70" x2="420" y2="70" stroke="#333" stroke-width="1.5"/>
<circle cx="420" cy="70" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="420" y="75" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Legend -->
<rect x="50" y="320" width="200" height="60" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="150" y="340" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="60" y="360" font-family="Arial" font-size="11" fill="#333">1. Oil Filter / Filtro de Aceite</text>
<!-- Oil flow label -->
<text x="300" y="380" text-anchor="middle" font-family="Arial" font-size="10" fill="#666">Oil Flow Direction / Direccion del Flujo de Aceite</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="strutGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#444"/>
<stop offset="50%" style="stop-color:#666"/>
<stop offset="100%" style="stop-color:#444"/>
</linearGradient>
<linearGradient id="springGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#27ae60"/>
<stop offset="50%" style="stop-color:#2ecc71"/>
<stop offset="100%" style="stop-color:#27ae60"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Front Suspension / Suspension Delantera
</text>
<!-- Vehicle body mounting point -->
<rect x="180" y="50" width="240" height="30" fill="#555" stroke="#333" stroke-width="2"/>
<text x="300" y="70" text-anchor="middle" font-family="Arial" font-size="10" fill="#ccc">BODY / CARROCERIA</text>
<!-- Strut Mount (top) -->
<ellipse cx="300" cy="95" rx="35" ry="15" fill="#888" stroke="#555" stroke-width="2"/>
<!-- Strut Assembly -->
<rect x="285" y="95" width="30" height="150" fill="url(#strutGrad)" stroke="#333" stroke-width="2" id="strut"/>
<!-- Strut piston rod -->
<rect x="293" y="95" width="14" height="60" fill="#999" stroke="#777" stroke-width="1"/>
<!-- Coil Spring around strut -->
<path d="M275,120 Q310,130 275,140 Q240,150 275,160 Q310,170 275,180 Q240,190 275,200 Q310,210 275,220"
fill="none" stroke="url(#springGrad)" stroke-width="8" stroke-linecap="round"/>
<!-- Lower Control Arm -->
<path d="M150,320 L300,280 L450,320" fill="none" stroke="#444" stroke-width="12" stroke-linecap="round"/>
<rect x="140" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
<rect x="430" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
<!-- Ball Joint (connecting strut to control arm) -->
<circle cx="300" cy="280" r="20" fill="#c0392b" stroke="#922b21" stroke-width="3" id="ball-joint"/>
<circle cx="300" cy="280" r="8" fill="#333"/>
<!-- Steering Knuckle (simplified) -->
<rect x="280" y="250" width="40" height="25" fill="#777" stroke="#555" stroke-width="2"/>
<!-- Wheel hub representation -->
<circle cx="300" cy="340" r="40" fill="#444" stroke="#333" stroke-width="3"/>
<circle cx="300" cy="340" r="15" fill="#333"/>
<text x="300" y="345" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">HUB</text>
<!-- Sway Bar Link -->
<line x1="350" y1="300" x2="420" y2="250" stroke="#555" stroke-width="6"/>
<circle cx="350" cy="300" r="6" fill="#777" stroke="#555" stroke-width="2"/>
<circle cx="420" cy="250" r="6" fill="#777" stroke="#555" stroke-width="2"/>
<!-- Callout 1: Strut Assembly -->
<line x1="320" y1="150" x2="420" y2="100" stroke="#333" stroke-width="1.5"/>
<circle cx="420" cy="100" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="420" y="105" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Callout 2: Ball Joint -->
<line x1="280" y1="280" x2="180" y2="280" stroke="#333" stroke-width="1.5"/>
<line x1="180" y1="280" x2="150" y2="250" stroke="#333" stroke-width="1.5"/>
<circle cx="150" cy="250" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="150" y="255" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
<!-- Callout 3: Control Arm -->
<line x1="400" y1="320" x2="500" y2="350" stroke="#333" stroke-width="1.5"/>
<circle cx="500" cy="350" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="500" y="355" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
<!-- Legend -->
<rect x="440" y="50" width="150" height="100" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="515" y="70" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="450" y="90" font-family="Arial" font-size="10" fill="#333">1. Strut / Amortiguador</text>
<text x="450" y="108" font-family="Arial" font-size="10" fill="#333">2. Ball Joint / Rotula</text>
<text x="450" y="126" font-family="Arial" font-size="10" fill="#333">3. Control Arm / Brazo</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,527 @@
-- ============================================================================
-- EJEMPLO COMPLETO DE BASE DE DATOS DE AUTOPARTES
-- Muestra cómo quedará la base de datos al completar todas las fases
-- ============================================================================
-- ============================================================================
-- TABLAS EXISTENTES (Ya implementadas)
-- ============================================================================
-- Marcas de vehículos
INSERT INTO brands (id, name, country, founded_year) VALUES
(1, 'Toyota', 'Japan', 1937),
(2, 'Honda', 'Japan', 1948),
(3, 'Ford', 'USA', 1903),
(4, 'Chevrolet', 'USA', 1911),
(5, 'Volkswagen', 'Germany', 1937);
-- Años
INSERT INTO years (id, year) VALUES
(1, 2020), (2, 2021), (3, 2022), (4, 2023), (5, 2024);
-- Motores
INSERT INTO engines (id, name, displacement_cc, cylinders, fuel_type, power_hp, torque_nm, engine_code) VALUES
(1, '2.5L 4-Cyl Dynamic Force', 2487, 4, 'gasoline', 203, 250, 'A25A-FKS'),
(2, '2.5L Hybrid', 2487, 4, 'hybrid', 215, 221, 'A25A-FXS'),
(3, '3.5L V6', 3456, 6, 'gasoline', 301, 362, '2GR-FKS'),
(4, '1.5L Turbo', 1498, 4, 'gasoline', 192, 260, 'L15CA'),
(5, '2.0L Turbo EcoBoost', 1999, 4, 'gasoline', 250, 373, 'EcoBoost');
-- Modelos
INSERT INTO models (id, brand_id, name, body_type, generation, production_start_year, production_end_year) VALUES
(1, 1, 'Camry', 'sedan', 'XV70', 2018, NULL),
(2, 1, 'Corolla', 'sedan', 'E210', 2019, NULL),
(3, 1, 'RAV4', 'suv', 'XA50', 2019, NULL),
(4, 2, 'Civic', 'sedan', '11th Gen', 2022, NULL),
(5, 2, 'CR-V', 'suv', '6th Gen', 2023, NULL),
(6, 3, 'F-150', 'truck', '14th Gen', 2021, NULL);
-- Configuraciones Modelo-Año-Motor (model_year_engine)
INSERT INTO model_year_engine (id, model_id, year_id, engine_id, trim_level, drivetrain, transmission) VALUES
-- Toyota Camry 2023
(1, 1, 4, 1, 'LE', 'FWD', 'automatic'),
(2, 1, 4, 1, 'SE', 'FWD', 'automatic'),
(3, 1, 4, 1, 'XLE', 'AWD', 'automatic'),
(4, 1, 4, 2, 'SE Hybrid', 'FWD', 'CVT'),
(5, 1, 4, 3, 'XSE V6', 'FWD', 'automatic'),
-- Toyota Camry 2024
(6, 1, 5, 1, 'LE', 'FWD', 'automatic'),
(7, 1, 5, 2, 'SE Hybrid', 'FWD', 'CVT'),
-- Honda Civic 2023
(8, 4, 4, 4, 'Sport', 'FWD', 'CVT'),
(9, 4, 4, 4, 'Touring', 'FWD', 'CVT');
-- ============================================================================
-- FASE 1: CATÁLOGO DE PARTES (Implementado)
-- ============================================================================
-- Categorías principales
INSERT INTO part_categories (id, name, name_es, slug, icon_name, display_order) VALUES
(1, 'Body & Lamp Assembly', 'Carrocería y Lámparas', 'body-lamp', 'fa-car-side', 1),
(2, 'Brake & Wheel Hub', 'Frenos y Mazas', 'brake-wheel', 'fa-compact-disc', 2),
(3, 'Cooling System', 'Sistema de Enfriamiento', 'cooling', 'fa-temperature-low', 3),
(4, 'Drivetrain', 'Tren Motriz', 'drivetrain', 'fa-cogs', 4),
(5, 'Electrical & Lighting', 'Eléctrico e Iluminación', 'electrical', 'fa-bolt', 5),
(6, 'Engine', 'Motor', 'engine', 'fa-cog', 6),
(7, 'Exhaust', 'Escape', 'exhaust', 'fa-wind', 7),
(8, 'Fuel & Air', 'Combustible y Aire', 'fuel-air', 'fa-gas-pump', 8),
(9, 'Heat & Air Conditioning', 'Calefacción y A/C', 'hvac', 'fa-snowflake', 9),
(10, 'Steering', 'Dirección', 'steering', 'fa-dharmachakra', 10),
(11, 'Suspension', 'Suspensión', 'suspension', 'fa-truck-monster', 11),
(12, 'Transmission', 'Transmisión', 'transmission', 'fa-gears', 12);
-- Grupos dentro de categorías (ejemplos)
INSERT INTO part_groups (id, category_id, name, name_es, slug, display_order) VALUES
-- Engine groups
(1, 6, 'Oil Filters', 'Filtros de Aceite', 'oil-filters', 1),
(2, 6, 'Air Filters', 'Filtros de Aire', 'air-filters', 2),
(3, 6, 'Spark Plugs', 'Bujías', 'spark-plugs', 3),
(4, 6, 'Timing Belt & Chain', 'Banda/Cadena de Tiempo', 'timing', 4),
(5, 6, 'Gaskets & Seals', 'Juntas y Sellos', 'gaskets', 5),
(6, 6, 'Engine Mounts', 'Soportes de Motor', 'mounts', 6),
-- Brake groups
(10, 2, 'Brake Pads', 'Balatas/Pastillas', 'brake-pads', 1),
(11, 2, 'Brake Rotors', 'Discos de Freno', 'brake-rotors', 2),
(12, 2, 'Brake Calipers', 'Calipers', 'brake-calipers', 3),
(13, 2, 'Brake Lines', 'Líneas de Freno', 'brake-lines', 4),
(14, 2, 'Wheel Bearings', 'Baleros de Rueda', 'wheel-bearings', 5),
-- Suspension groups
(20, 11, 'Shocks & Struts', 'Amortiguadores', 'shocks-struts', 1),
(21, 11, 'Control Arms', 'Brazos de Control', 'control-arms', 2),
(22, 11, 'Ball Joints', 'Rótulas', 'ball-joints', 3),
(23, 11, 'Tie Rod Ends', 'Terminales', 'tie-rods', 4),
(24, 11, 'Sway Bar Links', 'Ligas de Barra Estabilizadora', 'sway-bar', 5),
-- Electrical groups
(30, 5, 'Batteries', 'Baterías', 'batteries', 1),
(31, 5, 'Alternators', 'Alternadores', 'alternators', 2),
(32, 5, 'Starters', 'Marchas', 'starters', 3),
(33, 5, 'Ignition Coils', 'Bobinas de Ignición', 'ignition-coils', 4),
(34, 5, 'Sensors', 'Sensores', 'sensors', 5);
-- Partes OEM (catálogo maestro)
INSERT INTO parts (id, oem_part_number, name, name_es, group_id, description, description_es, weight_kg, material) VALUES
-- Filtros de aceite Toyota
(1, '04152-YZZA1', 'Oil Filter Element', 'Elemento Filtro de Aceite', 1,
'Genuine Toyota oil filter for 2.5L engines', 'Filtro de aceite genuino Toyota para motores 2.5L', 0.3, 'Paper/Metal'),
(2, '04152-YZZA5', 'Oil Filter Element', 'Elemento Filtro de Aceite', 1,
'Genuine Toyota oil filter for 3.5L V6 engines', 'Filtro de aceite genuino Toyota para motores 3.5L V6', 0.35, 'Paper/Metal'),
-- Filtros de aire
(3, '17801-0V020', 'Air Filter Element', 'Elemento Filtro de Aire', 2,
'Engine air filter for Camry 2.5L', 'Filtro de aire motor para Camry 2.5L', 0.4, 'Paper'),
(4, '17801-38051', 'Air Filter Element', 'Elemento Filtro de Aire', 2,
'Engine air filter for Camry V6', 'Filtro de aire motor para Camry V6', 0.45, 'Paper'),
-- Bujías
(5, '90919-01275', 'Spark Plug - Iridium', 'Bujía - Iridio', 3,
'Denso Iridium TT spark plug', 'Bujía Denso Iridium TT', 0.05, 'Iridium/Nickel'),
(6, '90919-01253', 'Spark Plug - Standard', 'Bujía - Estándar', 3,
'NGK Standard spark plug', 'Bujía NGK Estándar', 0.05, 'Nickel'),
-- Pastillas de freno
(10, '04465-06200', 'Front Brake Pads', 'Pastillas de Freno Delanteras', 10,
'Genuine Toyota front brake pad set', 'Juego de pastillas delanteras genuinas Toyota', 1.2, 'Ceramic'),
(11, '04466-06200', 'Rear Brake Pads', 'Pastillas de Freno Traseras', 10,
'Genuine Toyota rear brake pad set', 'Juego de pastillas traseras genuinas Toyota', 0.9, 'Ceramic'),
-- Discos de freno
(12, '43512-06190', 'Front Brake Rotor', 'Disco de Freno Delantero', 11,
'Genuine Toyota front brake rotor', 'Disco de freno delantero genuino Toyota', 8.5, 'Cast Iron'),
(13, '42431-06190', 'Rear Brake Rotor', 'Disco de Freno Trasero', 11,
'Genuine Toyota rear brake rotor', 'Disco de freno trasero genuino Toyota', 5.2, 'Cast Iron'),
-- Amortiguadores
(20, '48510-06780', 'Front Strut Assembly', 'Amortiguador Delantero Completo', 20,
'Front strut assembly with spring', 'Amortiguador delantero con resorte', 12.5, 'Steel'),
(21, '48530-06400', 'Rear Shock Absorber', 'Amortiguador Trasero', 20,
'Rear shock absorber', 'Amortiguador trasero', 3.8, 'Steel'),
-- Rótulas y terminales
(22, '43330-09510', 'Lower Ball Joint', 'Rótula Inferior', 22,
'Front lower ball joint', 'Rótula inferior delantera', 0.8, 'Steel'),
(23, '45046-09631', 'Outer Tie Rod End', 'Terminal Exterior', 23,
'Steering outer tie rod end', 'Terminal exterior de dirección', 0.5, 'Steel'),
-- Sensores
(30, '89467-06150', 'Oxygen Sensor - Upstream', 'Sensor de Oxígeno - Arriba', 34,
'Primary oxygen sensor (Bank 1)', 'Sensor de oxígeno primario (Banco 1)', 0.15, 'Ceramic/Metal'),
(31, '89467-06160', 'Oxygen Sensor - Downstream', 'Sensor de Oxígeno - Abajo', 34,
'Secondary oxygen sensor (Bank 1)', 'Sensor de oxígeno secundario (Banco 1)', 0.15, 'Ceramic/Metal');
-- Fitment: Qué partes van en qué vehículos
INSERT INTO vehicle_parts (id, model_year_engine_id, part_id, quantity_required, position, fitment_notes) VALUES
-- Toyota Camry 2023 2.5L LE (mye_id = 1)
(1, 1, 1, 1, NULL, 'Use with 2.5L 4-Cyl engine only'),
(2, 1, 3, 1, NULL, NULL),
(3, 1, 5, 4, NULL, 'Gap: 0.043 inch'),
(4, 1, 10, 1, 'front', NULL),
(5, 1, 11, 1, 'rear', NULL),
(6, 1, 12, 2, 'front', 'Left and Right'),
(7, 1, 13, 2, 'rear', 'Left and Right'),
(8, 1, 20, 2, 'front', 'Left and Right'),
(9, 1, 21, 2, 'rear', 'Left and Right'),
(10, 1, 22, 2, 'front-lower', 'Left and Right'),
(11, 1, 23, 2, 'front', 'Left and Right'),
(12, 1, 30, 1, 'upstream', 'Bank 1 Sensor 1'),
(13, 1, 31, 1, 'downstream', 'Bank 1 Sensor 2'),
-- Toyota Camry 2023 V6 XSE (mye_id = 5)
(20, 5, 2, 1, NULL, 'Use with 3.5L V6 engine only'),
(21, 5, 4, 1, NULL, NULL),
(22, 5, 6, 6, NULL, 'V6 requires 6 spark plugs'),
(23, 5, 10, 1, 'front', NULL),
(24, 5, 11, 1, 'rear', NULL);
-- ============================================================================
-- FASE 2: CROSS-REFERENCES Y AFTERMARKET (Por implementar)
-- ============================================================================
-- Fabricantes (OEM y aftermarket)
CREATE TABLE IF NOT EXISTS manufacturers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT CHECK(type IN ('oem', 'aftermarket', 'remanufactured')),
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium', 'oem')),
country TEXT,
logo_url TEXT,
website TEXT
);
INSERT INTO manufacturers (id, name, type, quality_tier, country, website) VALUES
(1, 'Toyota', 'oem', 'oem', 'Japan', 'https://parts.toyota.com'),
(2, 'Honda', 'oem', 'oem', 'Japan', 'https://parts.honda.com'),
(3, 'Bosch', 'aftermarket', 'premium', 'Germany', 'https://www.boschparts.com'),
(4, 'Denso', 'aftermarket', 'premium', 'Japan', 'https://www.denso.com'),
(5, 'NGK', 'aftermarket', 'premium', 'Japan', 'https://www.ngk.com'),
(6, 'Akebono', 'aftermarket', 'premium', 'Japan', 'https://www.akebono.com'),
(7, 'Brembo', 'aftermarket', 'premium', 'Italy', 'https://www.brembo.com'),
(8, 'Monroe', 'aftermarket', 'standard', 'USA', 'https://www.monroe.com'),
(9, 'KYB', 'aftermarket', 'premium', 'Japan', 'https://www.kyb.com'),
(10, 'Moog', 'aftermarket', 'premium', 'USA', 'https://www.moogparts.com'),
(11, 'Fram', 'aftermarket', 'economy', 'USA', 'https://www.fram.com'),
(12, 'WIX', 'aftermarket', 'standard', 'USA', 'https://www.wixfilters.com'),
(13, 'K&N', 'aftermarket', 'premium', 'USA', 'https://www.knfilters.com'),
(14, 'Motorcraft', 'oem', 'oem', 'USA', 'https://www.motorcraft.com'),
(15, 'ACDelco', 'oem', 'oem', 'USA', 'https://www.acdelco.com');
-- Partes aftermarket vinculadas a OEM
CREATE TABLE IF NOT EXISTS aftermarket_parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
oem_part_id INTEGER NOT NULL,
manufacturer_id INTEGER NOT NULL,
part_number TEXT NOT NULL,
name TEXT,
name_es TEXT,
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium')),
price_usd REAL,
warranty_months INTEGER,
FOREIGN KEY (oem_part_id) REFERENCES parts(id),
FOREIGN KEY (manufacturer_id) REFERENCES manufacturers(id)
);
INSERT INTO aftermarket_parts (id, oem_part_id, manufacturer_id, part_number, name, name_es, quality_tier, price_usd, warranty_months) VALUES
-- Alternativas para filtro de aceite Toyota 04152-YZZA1
(1, 1, 3, '3311', 'Premium Oil Filter', 'Filtro Aceite Premium', 'premium', 12.99, 12),
(2, 1, 11, 'XG9972', 'Ultra Synthetic Oil Filter', 'Filtro Aceite Sintético', 'standard', 8.49, 12),
(3, 1, 12, '57047', 'Oil Filter', 'Filtro de Aceite', 'standard', 7.99, 6),
-- Alternativas para filtro de aire Toyota 17801-0V020
(4, 3, 13, '33-5057', 'High-Flow Air Filter', 'Filtro Aire Alto Flujo', 'premium', 54.99, 120), -- K&N lifetime
(5, 3, 11, 'CA11476', 'Extra Guard Air Filter', 'Filtro Aire Extra Guard', 'economy', 18.99, 12),
(6, 3, 3, 'F00E164749', 'Workshop Air Filter', 'Filtro Aire Taller', 'premium', 24.99, 24),
-- Alternativas para bujías
(7, 5, 4, 'IK20TT', 'Iridium TT Spark Plug', 'Bujía Iridium TT', 'premium', 11.99, 60),
(8, 5, 5, 'ILKAR7B11', 'Laser Iridium Spark Plug', 'Bujía Laser Iridium', 'premium', 13.99, 60),
(9, 6, 3, 'FR7DC+', 'Super Plus Spark Plug', 'Bujía Super Plus', 'standard', 4.99, 24),
-- Alternativas para pastillas de freno
(10, 10, 6, 'ACT1293', 'ProACT Ultra-Premium Ceramic', 'Cerámica Ultra-Premium', 'premium', 89.99, 36),
(11, 10, 7, 'P83124N', 'Premium NAO Ceramic Pads', 'Pastillas Cerámicas NAO', 'premium', 129.99, 24),
(12, 10, 3, 'BC1293', 'QuietCast Ceramic Pads', 'Pastillas Cerámicas QuietCast', 'standard', 54.99, 24),
-- Alternativas para amortiguadores
(13, 20, 8, '72389', 'OESpectrum Strut Assembly', 'Ensamble Amortiguador OE', 'standard', 189.99, 24),
(14, 20, 9, '339407', 'Excel-G Strut Assembly', 'Ensamble Amortiguador Excel-G', 'premium', 229.99, 36),
(15, 21, 8, '37324', 'OESpectrum Shock', 'Amortiguador OESpectrum', 'standard', 54.99, 24),
(16, 21, 9, '341461', 'Excel-G Shock', 'Amortiguador Excel-G', 'premium', 69.99, 36);
-- Cross-references (números alternativos)
CREATE TABLE IF NOT EXISTS part_cross_references (
id INTEGER PRIMARY KEY AUTOINCREMENT,
part_id INTEGER NOT NULL,
cross_reference_number TEXT NOT NULL,
reference_type TEXT CHECK(reference_type IN ('oem_alternate', 'supersession', 'interchange', 'competitor')),
source TEXT,
notes TEXT,
FOREIGN KEY (part_id) REFERENCES parts(id)
);
INSERT INTO part_cross_references (id, part_id, cross_reference_number, reference_type, source, notes) VALUES
-- Oil filter cross-refs
(1, 1, '04152-YZZA3', 'oem_alternate', 'Toyota', 'Earlier part number'),
(2, 1, '04152-31090', 'oem_alternate', 'Toyota', 'Lexus equivalent'),
(3, 1, 'L14476', 'interchange', 'Purolator', NULL),
(4, 1, 'CH10358', 'interchange', 'Champion', NULL),
-- Spark plug cross-refs
(5, 5, 'SK20R11', 'oem_alternate', 'Denso', 'Denso part number'),
(6, 5, '3297', 'interchange', 'NGK', 'ILKAR7B11 equivalent'),
-- Brake pad cross-refs
(7, 10, '04465-06201', 'supersession', 'Toyota', 'Superseded from'),
(8, 10, '04465-33450', 'oem_alternate', 'Lexus', 'Lexus ES equivalent');
-- ============================================================================
-- FASE 3: DIAGRAMAS EXPLOSIONADOS (Por implementar)
-- ============================================================================
-- Diagramas
CREATE TABLE IF NOT EXISTS diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER NOT NULL,
image_path TEXT NOT NULL,
thumbnail_path TEXT,
display_order INTEGER DEFAULT 0,
source TEXT,
FOREIGN KEY (group_id) REFERENCES part_groups(id)
);
INSERT INTO diagrams (id, name, name_es, group_id, image_path, thumbnail_path, display_order, source) VALUES
(1, 'Front Brake Assembly', 'Ensamble Freno Delantero', 10,
'/static/diagrams/toyota/camry/2023/front_brake_assembly.svg',
'/static/diagrams/toyota/camry/2023/front_brake_assembly_thumb.png', 1, 'Toyota TIS'),
(2, 'Rear Brake Assembly', 'Ensamble Freno Trasero', 10,
'/static/diagrams/toyota/camry/2023/rear_brake_assembly.svg',
'/static/diagrams/toyota/camry/2023/rear_brake_assembly_thumb.png', 2, 'Toyota TIS'),
(3, 'Front Suspension', 'Suspensión Delantera', 20,
'/static/diagrams/toyota/camry/2023/front_suspension.svg',
'/static/diagrams/toyota/camry/2023/front_suspension_thumb.png', 1, 'Toyota TIS'),
(4, 'Engine Oil System', 'Sistema de Aceite Motor', 1,
'/static/diagrams/toyota/camry/2023/engine_oil_system.svg',
'/static/diagrams/toyota/camry/2023/engine_oil_system_thumb.png', 1, 'Toyota TIS'),
(5, 'Ignition System', 'Sistema de Ignición', 3,
'/static/diagrams/toyota/camry/2023/ignition_system.svg',
'/static/diagrams/toyota/camry/2023/ignition_system_thumb.png', 1, 'Toyota TIS');
-- Diagramas específicos por vehículo
CREATE TABLE IF NOT EXISTS vehicle_diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diagram_id INTEGER NOT NULL,
model_year_engine_id INTEGER NOT NULL,
notes TEXT,
FOREIGN KEY (diagram_id) REFERENCES diagrams(id),
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id),
UNIQUE(diagram_id, model_year_engine_id)
);
INSERT INTO vehicle_diagrams (id, diagram_id, model_year_engine_id, notes) VALUES
(1, 1, 1, NULL), -- Front brake diagram for Camry 2023 2.5L LE
(2, 2, 1, NULL), -- Rear brake diagram
(3, 3, 1, NULL), -- Front suspension diagram
(4, 4, 1, 'For 2.5L 4-Cyl engines'),
(5, 5, 1, NULL),
(6, 1, 5, 'V6 uses same brakes'), -- Same brake for V6
(7, 4, 5, 'For 3.5L V6 engines');
-- Hotspots clickeables en diagramas
CREATE TABLE IF NOT EXISTS diagram_hotspots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diagram_id INTEGER NOT NULL,
part_id INTEGER NOT NULL,
callout_number INTEGER,
shape TEXT DEFAULT 'rect' CHECK(shape IN ('rect', 'circle', 'poly')),
coords TEXT NOT NULL, -- x1,y1,x2,y2 for rect; cx,cy,r for circle; x1,y1,x2,y2,... for poly
FOREIGN KEY (diagram_id) REFERENCES diagrams(id),
FOREIGN KEY (part_id) REFERENCES parts(id)
);
INSERT INTO diagram_hotspots (id, diagram_id, part_id, callout_number, shape, coords) VALUES
-- Front Brake Assembly hotspots
(1, 1, 10, 1, 'rect', '150,200,250,280'), -- Brake Pads
(2, 1, 12, 2, 'rect', '100,150,300,350'), -- Brake Rotor
(3, 1, NULL, 3, 'rect', '280,180,380,300'), -- Caliper (not in our parts yet)
-- Front Suspension hotspots
(4, 3, 20, 1, 'rect', '200,50,300,250'), -- Strut Assembly
(5, 3, 22, 2, 'circle', '250,400,30'), -- Ball Joint
(6, 3, 23, 3, 'circle', '150,350,25'), -- Tie Rod End
(7, 3, NULL, 4, 'rect', '100,200,180,380'), -- Control Arm
-- Engine Oil System hotspots
(8, 4, 1, 1, 'rect', '300,250,400,350'), -- Oil Filter
-- Ignition System hotspots
(9, 5, 5, 1, 'rect', '150,100,200,180'), -- Spark Plug 1
(10, 5, 5, 2, 'rect', '220,100,270,180'), -- Spark Plug 2
(11, 5, 5, 3, 'rect', '290,100,340,180'), -- Spark Plug 3
(12, 5, 5, 4, 'rect', '360,100,410,180'); -- Spark Plug 4
-- ============================================================================
-- FASE 4: BÚSQUEDA FULL-TEXT Y VIN DECODER (Por implementar)
-- ============================================================================
-- Full-Text Search (SQLite FTS5)
CREATE VIRTUAL TABLE IF NOT EXISTS parts_fts USING fts5(
oem_part_number,
name,
name_es,
description,
description_es,
content='parts',
content_rowid='id'
);
-- Triggers para sincronización automática
CREATE TRIGGER IF NOT EXISTS parts_ai AFTER INSERT ON parts BEGIN
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
VALUES (new.id, new.oem_part_number, new.name, new.name_es, new.description, new.description_es);
END;
CREATE TRIGGER IF NOT EXISTS parts_ad AFTER DELETE ON parts BEGIN
INSERT INTO parts_fts(parts_fts, rowid, oem_part_number, name, name_es, description, description_es)
VALUES ('delete', old.id, old.oem_part_number, old.name, old.name_es, old.description, old.description_es);
END;
CREATE TRIGGER IF NOT EXISTS parts_au AFTER UPDATE ON parts BEGIN
INSERT INTO parts_fts(parts_fts, rowid, oem_part_number, name, name_es, description, description_es)
VALUES ('delete', old.id, old.oem_part_number, old.name, old.name_es, old.description, old.description_es);
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
VALUES (new.id, new.oem_part_number, new.name, new.name_es, new.description, new.description_es);
END;
-- Cache de VINs decodificados
CREATE TABLE IF NOT EXISTS vin_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vin TEXT NOT NULL UNIQUE,
decoded_data TEXT NOT NULL, -- JSON from NHTSA API
model_year_engine_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id)
);
INSERT INTO vin_cache (id, vin, decoded_data, model_year_engine_id, expires_at) VALUES
(1, '4T1BF1FK5CU123456', '{
"Make": "TOYOTA",
"Model": "Camry",
"ModelYear": "2023",
"BodyClass": "Sedan/Saloon",
"DriveType": "FWD/Front-Wheel Drive",
"EngineConfiguration": "In-Line",
"EngineCylinders": "4",
"DisplacementL": "2.5",
"FuelTypePrimary": "Gasoline",
"TransmissionStyle": "Automatic"
}', 1, '2026-03-05'),
(2, '4T1K61AK5PU234567', '{
"Make": "TOYOTA",
"Model": "Camry",
"ModelYear": "2023",
"BodyClass": "Sedan/Saloon",
"DriveType": "FWD/Front-Wheel Drive",
"EngineConfiguration": "V-Type",
"EngineCylinders": "6",
"DisplacementL": "3.5",
"FuelTypePrimary": "Gasoline",
"TransmissionStyle": "Automatic"
}', 5, '2026-03-05');
-- ============================================================================
-- FASE 5: OPTIMIZACIÓN - ÍNDICES ADICIONALES
-- ============================================================================
-- Índices compuestos para queries frecuentes
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_mye_part ON vehicle_parts(model_year_engine_id, part_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_oem ON aftermarket_parts(oem_part_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_manufacturer ON aftermarket_parts(manufacturer_id);
CREATE INDEX IF NOT EXISTS idx_cross_ref_part ON part_cross_references(part_id);
CREATE INDEX IF NOT EXISTS idx_cross_ref_number ON part_cross_references(cross_reference_number);
CREATE INDEX IF NOT EXISTS idx_diagrams_group ON diagrams(group_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_diagram ON diagram_hotspots(diagram_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_part ON diagram_hotspots(part_id);
CREATE INDEX IF NOT EXISTS idx_vin_cache_vin ON vin_cache(vin);
-- ============================================================================
-- QUERIES DE EJEMPLO
-- ============================================================================
-- 1. Buscar todas las partes para un vehículo específico
/*
SELECT
pc.name_es AS categoria,
pg.name_es AS grupo,
p.oem_part_number,
p.name_es AS nombre,
vp.quantity_required AS cantidad,
vp.position AS posicion
FROM vehicle_parts vp
JOIN parts p ON vp.part_id = p.id
JOIN part_groups pg ON p.group_id = pg.id
JOIN part_categories pc ON pg.category_id = pc.id
WHERE vp.model_year_engine_id = 1
ORDER BY pc.display_order, pg.display_order;
*/
-- 2. Buscar alternativas aftermarket para una parte OEM
/*
SELECT
m.name AS fabricante,
m.quality_tier AS calidad,
ap.part_number,
ap.name_es AS nombre,
ap.price_usd AS precio,
ap.warranty_months AS garantia_meses
FROM aftermarket_parts ap
JOIN manufacturers m ON ap.manufacturer_id = m.id
WHERE ap.oem_part_id = 1
ORDER BY m.quality_tier DESC, ap.price_usd;
*/
-- 3. Búsqueda full-text de partes
/*
SELECT p.*
FROM parts p
JOIN parts_fts ON p.id = parts_fts.rowid
WHERE parts_fts MATCH 'filtro aceite'
ORDER BY rank;
*/
-- 4. Obtener hotspots de un diagrama con info de partes
/*
SELECT
dh.callout_number,
dh.shape,
dh.coords,
p.oem_part_number,
p.name_es AS nombre
FROM diagram_hotspots dh
LEFT JOIN parts p ON dh.part_id = p.id
WHERE dh.diagram_id = 1
ORDER BY dh.callout_number;
*/
-- 5. Buscar por número de parte (incluye cross-references)
/*
SELECT DISTINCT
p.id,
p.oem_part_number,
p.name_es,
'OEM' AS source
FROM parts p
WHERE p.oem_part_number LIKE '%04152%'
UNION
SELECT DISTINCT
p.id,
p.oem_part_number,
p.name_es,
'Cross-Ref: ' || pcr.cross_reference_number AS source
FROM parts p
JOIN part_cross_references pcr ON p.id = pcr.part_id
WHERE pcr.cross_reference_number LIKE '%04152%';
*/

View File

@@ -0,0 +1,208 @@
# Diagrama de Base de Datos - Catálogo de Autopartes
## Diagrama de Relaciones (ERD)
```
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ VEHÍCULOS (Existente) │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ brands │ │ models │ │ years │ │ engines │
├──────────────┤ ├──────────────┤ ├──────────────┤ ├──────────────┤
│ id (PK) │◄────│ brand_id(FK) │ │ id (PK) │ │ id (PK) │
│ name │ │ id (PK) │ │ year │ │ name │
│ country │ │ name │ └──────┬───────┘ │ displacement │
│ founded_year │ │ body_type │ │ │ cylinders │
└──────────────┘ │ generation │ │ │ fuel_type │
└──────┬───────┘ │ │ power_hp │
│ │ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ model_year_engine (MYE) │
├─────────────────────────────────────────────────────┤
│ id (PK) ◄─────── Identificador único de │
│ model_id (FK) configuración vehículo │
│ year_id (FK) │
│ engine_id (FK) │
│ trim_level │
│ drivetrain │
│ transmission │
└────────────────────────┬────────────────────────────┘
│ (1:N)
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ FASE 1: CATÁLOGO DE PARTES │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ part_categories │ │ part_groups │ │ parts │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │◄────────│ category_id (FK) │◄────────│ group_id (FK) │
│ name │ │ id (PK) │ │ id (PK) │
│ name_es │ │ name │ │ oem_part_number │
│ parent_id (FK)───┼─┐ │ name_es │ │ name │
│ slug │ │ │ slug │ │ name_es │
│ icon_name │ │ │ display_order │ │ description │
│ display_order │◄┘ └──────────────────┘ │ weight_kg │
└──────────────────┘ │ material │
│ │ is_discontinued │
│ (Ej: 12 categorías) │ superseded_by_id │
│ - Engine └────────┬─────────┘
│ - Brakes │
│ - Suspension │
│ - etc. │
┌────────────────────────────────────────────┼────────────────┐
│ │ │
▼ ▼ │
┌──────────────────┐ ┌──────────────────┐ │
│ vehicle_parts │ │ (FASE 2) │ │
├──────────────────┤ │ aftermarket_parts│ │
│ id (PK) │ ├──────────────────┤ │
│ mye_id (FK) ─────┼──► model_year_engine │ oem_part_id (FK)─┼─────────┤
│ part_id (FK) ────┼──► parts │ manufacturer_id │ │
│ quantity_required│ │ part_number │ │
│ position │ │ quality_tier │ │
│ fitment_notes │ │ price_usd │ │
└──────────────────┘ └──────────────────┘ │
┌──────────────────┐ │
│ part_cross_refs │ │
├──────────────────┤ │
│ part_id (FK) ────┼─────────┘
│ cross_ref_number │
│ reference_type │
└──────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ FASE 2: AFTERMARKET Y FABRICANTES │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ manufacturers │◄────────│ aftermarket_parts│
├──────────────────┤ ├──────────────────┤
│ id (PK) │ │ manufacturer_id │
│ name │ │ oem_part_id (FK) │──► parts
│ type │ │ part_number │
│ quality_tier │ │ name │
│ country │ │ price_usd │
│ logo_url │ │ warranty_months │
└──────────────────┘ └──────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ FASE 3: DIAGRAMAS EXPLOSIONADOS │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ diagrams │ │ vehicle_diagrams │ │diagram_hotspots │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │◄────────│ diagram_id (FK) │ │ diagram_id (FK)──┼──► diagrams
│ name │ │ mye_id (FK) ─────┼──► MYE │ part_id (FK) ────┼──► parts
│ name_es │ └──────────────────┘ │ callout_number │
│ group_id (FK)────┼──► part_groups │ shape │
│ image_path │ │ coords │
│ thumbnail_path │ └──────────────────┘
└──────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ FASE 4: BÚSQUEDA Y VIN │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ parts_fts │ │ vin_cache │
│ (FTS5 Virtual) │ ├──────────────────┤
├──────────────────┤ │ vin │
│ oem_part_number │ │ decoded_data │──► JSON from NHTSA
│ name │ │ mye_id (FK) ─────┼──► model_year_engine
│ name_es │ │ expires_at │
│ description │ └──────────────────┘
└──────────────────┘
```
## Flujo de Navegación del Usuario
```
┌─────────┐ ┌─────────┐ ┌─────────────────┐ ┌────────────────┐
│ Marcas │───►│ Modelos │───►│ Vehículos │───►│ Categorías │
│ (brands)│ │(models) │ │(model_year_eng) │ │(part_categories│
└─────────┘ └─────────┘ └─────────────────┘ └───────┬────────┘
┌────────────────────────────────────────────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐
│ Grupos │───►│ Partes │───►│ Detalle Parte │
│(part_groups)│ │ (parts) │ │ + Alternativas Aftermarket │
└─────────────┘ └─────────────┘ │ + Cross-References │
│ │ + Diagrama con Hotspots │
│ └─────────────────────────────┘
┌─────────────────────┐
│ Filtrar por MYE │
│ (vehicle_parts) │
│ Cantidad, Posición │
└─────────────────────┘
```
## Resumen de Tablas por Fase
| Fase | Tabla | Registros Esperados | Descripción |
|------|-------|---------------------|-------------|
| Base | brands | ~50 | Marcas de vehículos |
| Base | models | ~2,000 | Modelos de vehículos |
| Base | years | ~30 | Años (1995-2025) |
| Base | engines | ~500 | Especificaciones de motores |
| Base | model_year_engine | ~50,000 | Configuraciones únicas |
| **F1** | part_categories | 12 | Categorías principales |
| **F1** | part_groups | ~200 | Subcategorías |
| **F1** | parts | ~100,000 | Catálogo maestro OEM |
| **F1** | vehicle_parts | ~500,000 | Fitment por vehículo |
| **F2** | manufacturers | ~50 | OEM y aftermarket |
| **F2** | aftermarket_parts | ~300,000 | Alternativas aftermarket |
| **F2** | part_cross_references | ~200,000 | Números alternativos |
| **F3** | diagrams | ~5,000 | Diagramas explosionados |
| **F3** | vehicle_diagrams | ~20,000 | Asignación a vehículos |
| **F3** | diagram_hotspots | ~50,000 | Áreas clickeables |
| **F4** | parts_fts | Virtual | Índice full-text |
| **F4** | vin_cache | Variable | Cache de VINs |
## Ejemplo de Datos Relacionados
### Un Toyota Camry 2023 2.5L LE completo:
```sql
-- Vehículo
brands.id = 1 (Toyota)
models.id = 1 (Camry)
years.id = 4 (2023)
engines.id = 1 (2.5L 4-Cyl)
model_year_engine.id = 1
-- Sus partes (via vehicle_parts)
OEM # Parte Cant Posición
04152-YZZA1 Filtro de Aceite 1 -
17801-0V020 Filtro de Aire 1 -
90919-01275 Bujía Iridium 4 -
04465-06200 Pastillas Freno Delanteras 1 front
04466-06200 Pastillas Freno Traseras 1 rear
43512-06190 Disco Freno Delantero 2 front
42431-06190 Disco Freno Trasero 2 rear
48510-06780 Amortiguador Delantero 2 front
48530-06400 Amortiguador Trasero 2 rear
-- Alternativas para el filtro de aceite (via aftermarket_parts)
Fabricante Número Nombre Precio
Bosch 3311 Premium Oil Filter $12.99
Fram XG9972 Ultra Synthetic Oil Filter $8.49
WIX 57047 Oil Filter $7.99
```

View File

@@ -0,0 +1,548 @@
#!/usr/bin/env python3
"""
Populate Part Categories and Groups
This script populates the SQLite database with initial part categories
and groups following the RockAuto style organization.
The script is idempotent - it can be run multiple times safely.
"""
import sqlite3
import os
import re
from pathlib import Path
def create_slug(name: str) -> str:
"""
Generate a URL-friendly slug from a name.
Args:
name: The name to convert to a slug
Returns:
Lowercase string with spaces replaced by hyphens
"""
# Convert to lowercase
slug = name.lower()
# Remove special characters except spaces and hyphens
slug = re.sub(r'[^a-z0-9\s-]', '', slug)
# Replace spaces with hyphens
slug = re.sub(r'\s+', '-', slug)
# Remove multiple consecutive hyphens
slug = re.sub(r'-+', '-', slug)
# Strip leading/trailing hyphens
slug = slug.strip('-')
return slug
def get_database_path() -> str:
"""Get the path to the SQLite database."""
return "/home/Autopartes/vehicle_database/vehicle_database.db"
def get_schema_path() -> str:
"""Get the path to the schema SQL file."""
return "/home/Autopartes/vehicle_database/sql/schema.sql"
def create_tables_if_not_exist(conn: sqlite3.Connection) -> None:
"""
Create the necessary tables if they don't exist.
Reads and executes relevant CREATE TABLE statements from schema.sql.
Args:
conn: SQLite database connection
"""
schema_path = get_schema_path()
if not os.path.exists(schema_path):
print(f"Warning: Schema file not found at {schema_path}")
print("Creating tables with embedded schema...")
# Fallback embedded schema for the parts catalog tables
embedded_schema = """
CREATE TABLE IF NOT EXISTS part_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
name_es TEXT,
parent_id INTEGER,
slug TEXT UNIQUE,
icon_name TEXT,
display_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES part_categories(id)
);
CREATE TABLE IF NOT EXISTS part_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
name TEXT NOT NULL,
name_es TEXT,
slug TEXT,
display_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES part_categories(id)
);
CREATE TABLE IF NOT EXISTS parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
oem_part_number TEXT NOT NULL,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER,
description TEXT,
description_es TEXT,
weight_kg REAL,
material TEXT,
is_discontinued BOOLEAN DEFAULT 0,
superseded_by_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES part_groups(id),
FOREIGN KEY (superseded_by_id) REFERENCES parts(id)
);
CREATE TABLE IF NOT EXISTS vehicle_parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_year_engine_id INTEGER NOT NULL,
part_id INTEGER NOT NULL,
quantity_required INTEGER DEFAULT 1,
position TEXT,
fitment_notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id),
FOREIGN KEY (part_id) REFERENCES parts(id),
UNIQUE(model_year_engine_id, part_id, position)
);
CREATE INDEX IF NOT EXISTS idx_part_categories_parent ON part_categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_part_categories_slug ON part_categories(slug);
CREATE INDEX IF NOT EXISTS idx_part_groups_category ON part_groups(category_id);
CREATE INDEX IF NOT EXISTS idx_parts_oem ON parts(oem_part_number);
CREATE INDEX IF NOT EXISTS idx_parts_group ON parts(group_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_mye ON vehicle_parts(model_year_engine_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_part ON vehicle_parts(part_id);
"""
conn.executescript(embedded_schema)
else:
# Read the schema file
with open(schema_path, 'r') as f:
schema_sql = f.read()
# Execute the entire schema (CREATE TABLE IF NOT EXISTS is safe to re-run)
conn.executescript(schema_sql)
conn.commit()
print("Tables created/verified successfully.")
def get_categories_data() -> list:
"""
Return the list of categories to insert.
Returns:
List of tuples: (name, name_es, icon_name, display_order)
"""
return [
("Body & Lamp Assembly", "Carroceria y Lamparas", "fa-car-side", 1),
("Brake & Wheel Hub", "Frenos y Mazas", "fa-compact-disc", 2),
("Cooling System", "Sistema de Enfriamiento", "fa-temperature-low", 3),
("Drivetrain", "Tren Motriz", "fa-cogs", 4),
("Electrical & Lighting", "Electrico e Iluminacion", "fa-bolt", 5),
("Engine", "Motor", "fa-cog", 6),
("Exhaust", "Escape", "fa-wind", 7),
("Fuel & Air", "Combustible y Aire", "fa-gas-pump", 8),
("Heat & Air Conditioning", "Calefaccion y AC", "fa-snowflake", 9),
("Steering", "Direccion", "fa-dharmachakra", 10),
("Suspension", "Suspension", "fa-truck-monster", 11),
("Transmission", "Transmision", "fa-gears", 12),
]
def get_groups_data() -> dict:
"""
Return the groups for each category.
Returns:
Dictionary mapping category name to list of groups.
Each group is a tuple: (name, name_es, display_order)
"""
return {
"Body & Lamp Assembly": [
("Body Panels", "Paneles de Carroceria", 1),
("Bumpers", "Defensas", 2),
("Doors", "Puertas", 3),
("Fenders", "Salpicaderas", 4),
("Hoods", "Cofres", 5),
("Trunk Lids", "Tapas de Cajuela", 6),
("Grilles", "Parrillas", 7),
("Mirrors", "Espejos", 8),
("Headlights", "Faros Delanteros", 9),
("Taillights", "Calaveras", 10),
("Fog Lights", "Faros de Niebla", 11),
("Turn Signal Lights", "Luces Direccionales", 12),
("Windshield", "Parabrisas", 13),
("Window Glass", "Cristales de Ventana", 14),
("Moldings & Trim", "Molduras y Acabados", 15),
],
"Brake & Wheel Hub": [
("Brake Pads", "Balatas", 1),
("Brake Rotors", "Discos de Freno", 2),
("Brake Drums", "Tambores de Freno", 3),
("Brake Shoes", "Zapatas de Freno", 4),
("Brake Calipers", "Calibradores de Freno", 5),
("Brake Lines", "Lineas de Freno", 6),
("Brake Hoses", "Mangueras de Freno", 7),
("Master Cylinder", "Cilindro Maestro", 8),
("Wheel Cylinders", "Cilindros de Rueda", 9),
("Brake Boosters", "Boosters de Freno", 10),
("ABS Components", "Componentes ABS", 11),
("Wheel Bearings", "Baleros de Rueda", 12),
("Wheel Hubs", "Mazas de Rueda", 13),
("Wheel Studs", "Birlos", 14),
("Parking Brake", "Freno de Mano", 15),
],
"Cooling System": [
("Radiators", "Radiadores", 1),
("Radiator Hoses", "Mangueras de Radiador", 2),
("Water Pumps", "Bombas de Agua", 3),
("Thermostats", "Termostatos", 4),
("Cooling Fans", "Ventiladores", 5),
("Fan Clutches", "Clutch de Ventilador", 6),
("Coolant Reservoirs", "Depositos de Anticongelante", 7),
("Radiator Caps", "Tapones de Radiador", 8),
("Heater Cores", "Nucleos de Calefaccion", 9),
("Heater Hoses", "Mangueras de Calefaccion", 10),
("Coolant Sensors", "Sensores de Temperatura", 11),
("Oil Coolers", "Enfriadores de Aceite", 12),
("Intercoolers", "Intercoolers", 13),
],
"Drivetrain": [
("CV Axles", "Flechas CV", 1),
("CV Joints", "Juntas CV", 2),
("CV Boots", "Guardapolvos CV", 3),
("U-Joints", "Crucetas", 4),
("Drive Shafts", "Flechas Cardanes", 5),
("Differentials", "Diferenciales", 6),
("Axle Shafts", "Ejes", 7),
("Transfer Cases", "Cajas de Transferencia", 8),
("Wheel Bearings", "Baleros de Rueda", 9),
("Hub Assemblies", "Ensambles de Maza", 10),
],
"Electrical & Lighting": [
("Batteries", "Baterias", 1),
("Alternators", "Alternadores", 2),
("Starters", "Marchas", 3),
("Ignition Coils", "Bobinas de Ignicion", 4),
("Spark Plug Wires", "Cables de Bujia", 5),
("Distributors", "Distribuidores", 6),
("Ignition Switches", "Switches de Encendido", 7),
("Relays", "Relevadores", 8),
("Fuses", "Fusibles", 9),
("Switches", "Interruptores", 10),
("Wiring Harnesses", "Arneses Electricos", 11),
("Sensors", "Sensores", 12),
("Headlight Bulbs", "Focos de Faros", 13),
("Taillight Bulbs", "Focos de Calaveras", 14),
("Interior Lights", "Luces Interiores", 15),
("Horn", "Claxon", 16),
],
"Engine": [
("Oil Filters", "Filtros de Aceite", 1),
("Air Filters", "Filtros de Aire", 2),
("Spark Plugs", "Bujias", 3),
("Belts", "Bandas", 4),
("Timing Belts", "Bandas de Tiempo", 5),
("Timing Chains", "Cadenas de Tiempo", 6),
("Timing Components", "Componentes de Tiempo", 7),
("Gaskets", "Juntas", 8),
("Head Gaskets", "Juntas de Cabeza", 9),
("Valve Cover Gaskets", "Juntas de Tapa de Punterias", 10),
("Oil Pan Gaskets", "Juntas de Carter", 11),
("Pistons", "Pistones", 12),
("Piston Rings", "Anillos de Piston", 13),
("Connecting Rods", "Bielas", 14),
("Crankshafts", "Cigueñales", 15),
("Camshafts", "Arboles de Levas", 16),
("Valves", "Valvulas", 17),
("Valve Springs", "Resortes de Valvula", 18),
("Rocker Arms", "Balancines", 19),
("Lifters", "Buzos", 20),
("Oil Pumps", "Bombas de Aceite", 21),
("Engine Mounts", "Soportes de Motor", 22),
("Cylinder Heads", "Cabezas de Motor", 23),
("Engine Blocks", "Bloques de Motor", 24),
("Harmonic Balancers", "Dampers", 25),
("Pulleys", "Poleas", 26),
("Tensioners", "Tensores", 27),
("Idler Pulleys", "Poleas Locas", 28),
],
"Exhaust": [
("Exhaust Manifolds", "Multiples de Escape", 1),
("Catalytic Converters", "Convertidores Cataliticos", 2),
("Mufflers", "Mofles", 3),
("Resonators", "Resonadores", 4),
("Exhaust Pipes", "Tubos de Escape", 5),
("Exhaust Tips", "Terminales de Escape", 6),
("Exhaust Gaskets", "Juntas de Escape", 7),
("Exhaust Hangers", "Soportes de Escape", 8),
("O2 Sensors", "Sensores de Oxigeno", 9),
("EGR Valves", "Valvulas EGR", 10),
("Headers", "Headers", 11),
("Flex Pipes", "Flexibles", 12),
],
"Fuel & Air": [
("Fuel Pumps", "Bombas de Gasolina", 1),
("Fuel Filters", "Filtros de Gasolina", 2),
("Fuel Injectors", "Inyectores", 3),
("Fuel Lines", "Lineas de Combustible", 4),
("Fuel Tanks", "Tanques de Gasolina", 5),
("Fuel Caps", "Tapones de Gasolina", 6),
("Carburetors", "Carburadores", 7),
("Throttle Bodies", "Cuerpos de Aceleracion", 8),
("Intake Manifolds", "Multiples de Admision", 9),
("Air Intake Hoses", "Mangueras de Admision", 10),
("Mass Air Flow Sensors", "Sensores MAF", 11),
("Throttle Position Sensors", "Sensores TPS", 12),
("Fuel Pressure Regulators", "Reguladores de Presion", 13),
("PCV Valves", "Valvulas PCV", 14),
("Air Intake Systems", "Sistemas de Admision", 15),
("Turbochargers", "Turbocargadores", 16),
("Superchargers", "Supercargadores", 17),
],
"Heat & Air Conditioning": [
("AC Compressors", "Compresores de AC", 1),
("AC Condensers", "Condensadores de AC", 2),
("AC Evaporators", "Evaporadores de AC", 3),
("AC Hoses", "Mangueras de AC", 4),
("AC Accumulators", "Acumuladores de AC", 5),
("AC Receiver Driers", "Filtros Deshidratadores", 6),
("AC Expansion Valves", "Valvulas de Expansion", 7),
("AC Clutches", "Clutch de AC", 8),
("Blower Motors", "Motores de Ventilador", 9),
("Blower Resistors", "Resistencias de Ventilador", 10),
("Heater Control Valves", "Valvulas de Calefaccion", 11),
("Cabin Air Filters", "Filtros de Cabina", 12),
("AC Pressure Switches", "Switches de Presion AC", 13),
("Climate Control Units", "Unidades de Control Climatico", 14),
],
"Steering": [
("Power Steering Pumps", "Bombas de Direccion Hidraulica", 1),
("Power Steering Hoses", "Mangueras de Direccion", 2),
("Power Steering Fluid Reservoirs", "Depositos de Direccion", 3),
("Steering Racks", "Cremalleras de Direccion", 4),
("Steering Gearboxes", "Cajas de Direccion", 5),
("Tie Rods", "Terminales de Direccion", 6),
("Tie Rod Ends", "Rotulas de Direccion", 7),
("Inner Tie Rods", "Terminales Interiores", 8),
("Steering Columns", "Columnas de Direccion", 9),
("Steering Wheels", "Volantes", 10),
("Pitman Arms", "Brazos Pitman", 11),
("Idler Arms", "Brazos Locos", 12),
("Center Links", "Barras Centrales", 13),
("Drag Links", "Barras de Arrastre", 14),
("Steering Knuckles", "Muñones", 15),
],
"Suspension": [
("Shocks", "Amortiguadores", 1),
("Struts", "Struts", 2),
("Strut Mounts", "Bases de Strut", 3),
("Coil Springs", "Resortes", 4),
("Leaf Springs", "Muelles", 5),
("Control Arms", "Brazos de Control", 6),
("Upper Control Arms", "Brazos Superiores", 7),
("Lower Control Arms", "Brazos Inferiores", 8),
("Ball Joints", "Rotulas", 9),
("Bushings", "Bujes", 10),
("Sway Bars", "Barras Estabilizadoras", 11),
("Sway Bar Links", "Links de Barra Estabilizadora", 12),
("Sway Bar Bushings", "Bujes de Barra Estabilizadora", 13),
("Torsion Bars", "Barras de Torsion", 14),
("Trailing Arms", "Brazos Traseros", 15),
("Track Bars", "Barras Track", 16),
("Radius Arms", "Brazos de Radio", 17),
("Air Suspension", "Suspension Neumatica", 18),
("Bump Stops", "Topes", 19),
],
"Transmission": [
("Transmission Filters", "Filtros de Transmision", 1),
("Clutch Kits", "Kits de Clutch", 2),
("Clutch Discs", "Discos de Clutch", 3),
("Pressure Plates", "Platos de Presion", 4),
("Throw-out Bearings", "Collares de Clutch", 5),
("Clutch Masters", "Cilindros Maestros de Clutch", 6),
("Clutch Slaves", "Cilindros Esclavos de Clutch", 7),
("Flywheels", "Volantes de Motor", 8),
("Flexplates", "Flexplates", 9),
("Torque Converters", "Convertidores de Torque", 10),
("Transmission Mounts", "Soportes de Transmision", 11),
("Shift Cables", "Cables de Cambios", 12),
("Shift Linkages", "Varillajes de Cambios", 13),
("Speedometer Gears", "Engranes de Velocimetro", 14),
("Transmission Gaskets", "Juntas de Transmision", 15),
("Transmission Seals", "Sellos de Transmision", 16),
],
}
def insert_categories(conn: sqlite3.Connection) -> dict:
"""
Insert categories into the database.
Args:
conn: SQLite database connection
Returns:
Dictionary mapping category name to category id
"""
cursor = conn.cursor()
categories = get_categories_data()
category_ids = {}
for name, name_es, icon_name, display_order in categories:
slug = create_slug(name)
# Use INSERT OR IGNORE to make the script idempotent
cursor.execute("""
INSERT OR IGNORE INTO part_categories
(name, name_es, slug, icon_name, display_order)
VALUES (?, ?, ?, ?, ?)
""", (name, name_es, slug, icon_name, display_order))
# Get the id (whether it was just inserted or already existed)
cursor.execute("SELECT id FROM part_categories WHERE slug = ?", (slug,))
result = cursor.fetchone()
if result:
category_ids[name] = result[0]
print(f" Category: {name} (ID: {result[0]})")
conn.commit()
return category_ids
def insert_groups(conn: sqlite3.Connection, category_ids: dict) -> None:
"""
Insert groups into the database.
Args:
conn: SQLite database connection
category_ids: Dictionary mapping category name to category id
"""
cursor = conn.cursor()
groups_data = get_groups_data()
for category_name, groups in groups_data.items():
if category_name not in category_ids:
print(f" Warning: Category '{category_name}' not found, skipping groups")
continue
category_id = category_ids[category_name]
for name, name_es, display_order in groups:
slug = create_slug(name)
# Check if group already exists for this category
cursor.execute("""
SELECT id FROM part_groups
WHERE category_id = ? AND slug = ?
""", (category_id, slug))
if cursor.fetchone() is None:
cursor.execute("""
INSERT INTO part_groups
(category_id, name, name_es, slug, display_order)
VALUES (?, ?, ?, ?, ?)
""", (category_id, name, name_es, slug, display_order))
print(f" Group: {name}")
conn.commit()
def print_summary(conn: sqlite3.Connection) -> None:
"""
Print a summary of the data in the database.
Args:
conn: SQLite database connection
"""
cursor = conn.cursor()
# Count categories
cursor.execute("SELECT COUNT(*) FROM part_categories")
category_count = cursor.fetchone()[0]
# Count groups
cursor.execute("SELECT COUNT(*) FROM part_groups")
group_count = cursor.fetchone()[0]
print("\n" + "=" * 50)
print("SUMMARY")
print("=" * 50)
print(f"Total Categories: {category_count}")
print(f"Total Groups: {group_count}")
print()
# Show categories with group counts
cursor.execute("""
SELECT pc.name, pc.name_es, pc.icon_name, COUNT(pg.id) as group_count
FROM part_categories pc
LEFT JOIN part_groups pg ON pc.id = pg.category_id
GROUP BY pc.id
ORDER BY pc.display_order
""")
print("Categories and Group Counts:")
print("-" * 50)
for row in cursor.fetchall():
name, name_es, icon, count = row
print(f" {icon:20} {name:30} ({count} groups)")
def main():
"""Main function to populate categories and groups."""
db_path = get_database_path()
print("=" * 50)
print("POPULATE PART CATEGORIES AND GROUPS")
print("=" * 50)
print(f"Database: {db_path}")
print()
# Check if database exists
if not os.path.exists(db_path):
print(f"Warning: Database does not exist at {db_path}")
print("Creating new database...")
# Connect to database
conn = sqlite3.connect(db_path)
try:
# Create tables if they don't exist
print("Creating/verifying tables...")
create_tables_if_not_exist(conn)
print()
# Insert categories
print("Inserting categories...")
category_ids = insert_categories(conn)
print()
# Insert groups
print("Inserting groups...")
insert_groups(conn, category_ids)
# Print summary
print_summary(conn)
print("\nDone! Categories and groups populated successfully.")
except sqlite3.Error as e:
print(f"Database error: {e}")
raise
finally:
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,574 @@
#!/usr/bin/env python3
"""
FASE 2: Populate cross-references and aftermarket parts
This script creates FASE 2 tables and populates them with manufacturers,
aftermarket part alternatives, and cross-references.
"""
import sqlite3
import os
import random
import string
from typing import List, Dict, Tuple, Optional
# Database path configuration
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
class Fase2Manager:
"""Manager for FASE 2 tables: manufacturers, aftermarket_parts, and cross-references"""
def __init__(self, db_path: str = DB_PATH):
self.db_path = db_path
self.connection = None
def connect(self):
"""Connect to the SQLite database"""
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row
print(f"Connected to database: {self.db_path}")
def disconnect(self):
"""Close the database connection"""
if self.connection:
self.connection.close()
print("Disconnected from database")
def create_fase2_tables(self):
"""Create FASE 2 tables from schema file"""
if not os.path.exists(SCHEMA_PATH):
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
with open(SCHEMA_PATH, 'r') as f:
schema = f.read()
if self.connection:
cursor = self.connection.cursor()
cursor.executescript(schema)
self.connection.commit()
print("FASE 2 tables created successfully")
def get_manufacturer_by_name(self, name: str) -> Optional[int]:
"""Get manufacturer ID by name, returns None if not found"""
cursor = self.connection.cursor()
cursor.execute("SELECT id FROM manufacturers WHERE name = ?", (name,))
result = cursor.fetchone()
return result[0] if result else None
def insert_manufacturer(self, name: str, type_: str, quality_tier: str,
country: str = None, logo_url: str = None,
website: str = None) -> int:
"""Insert a manufacturer if it doesn't exist, return its ID"""
existing_id = self.get_manufacturer_by_name(name)
if existing_id:
print(f" Manufacturer '{name}' already exists (ID: {existing_id})")
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO manufacturers (name, type, quality_tier, country, logo_url, website)
VALUES (?, ?, ?, ?, ?, ?)""",
(name, type_, quality_tier, country, logo_url, website)
)
self.connection.commit()
manufacturer_id = cursor.lastrowid
print(f" Inserted manufacturer: {name} (ID: {manufacturer_id})")
return manufacturer_id
def get_all_parts(self) -> List[Dict]:
"""Get all parts from the parts table"""
cursor = self.connection.cursor()
cursor.execute("""
SELECT p.id, p.oem_part_number, p.name, p.name_es, p.group_id,
pg.name as group_name, pc.name as category_name
FROM parts p
LEFT JOIN part_groups pg ON p.group_id = pg.id
LEFT JOIN part_categories pc ON pg.category_id = pc.id
""")
return [dict(row) for row in cursor.fetchall()]
def get_aftermarket_part(self, oem_part_id: int, manufacturer_id: int) -> Optional[int]:
"""Check if an aftermarket part already exists"""
cursor = self.connection.cursor()
cursor.execute(
"""SELECT id FROM aftermarket_parts
WHERE oem_part_id = ? AND manufacturer_id = ?""",
(oem_part_id, manufacturer_id)
)
result = cursor.fetchone()
return result[0] if result else None
def insert_aftermarket_part(self, oem_part_id: int, manufacturer_id: int,
part_number: str, name: str = None, name_es: str = None,
quality_tier: str = 'standard', price_usd: float = None,
warranty_months: int = 12, in_stock: bool = True) -> int:
"""Insert an aftermarket part if it doesn't exist"""
existing_id = self.get_aftermarket_part(oem_part_id, manufacturer_id)
if existing_id:
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO aftermarket_parts
(oem_part_id, manufacturer_id, part_number, name, name_es,
quality_tier, price_usd, warranty_months, in_stock)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(oem_part_id, manufacturer_id, part_number, name, name_es,
quality_tier, price_usd, warranty_months, in_stock)
)
self.connection.commit()
return cursor.lastrowid
def get_cross_reference(self, part_id: int, cross_reference_number: str) -> Optional[int]:
"""Check if a cross-reference already exists"""
cursor = self.connection.cursor()
cursor.execute(
"""SELECT id FROM part_cross_references
WHERE part_id = ? AND cross_reference_number = ?""",
(part_id, cross_reference_number)
)
result = cursor.fetchone()
return result[0] if result else None
def insert_cross_reference(self, part_id: int, cross_reference_number: str,
reference_type: str, source: str = None,
notes: str = None) -> int:
"""Insert a cross-reference if it doesn't exist"""
existing_id = self.get_cross_reference(part_id, cross_reference_number)
if existing_id:
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO part_cross_references
(part_id, cross_reference_number, reference_type, source, notes)
VALUES (?, ?, ?, ?, ?)""",
(part_id, cross_reference_number, reference_type, source, notes)
)
self.connection.commit()
return cursor.lastrowid
def get_manufacturers_by_tier(self, quality_tier: str) -> List[Dict]:
"""Get all manufacturers of a specific quality tier"""
cursor = self.connection.cursor()
cursor.execute(
"SELECT * FROM manufacturers WHERE quality_tier = ?",
(quality_tier,)
)
return [dict(row) for row in cursor.fetchall()]
# Manufacturer data
MANUFACTURERS_DATA = {
# OEM manufacturers
'oem': [
{'name': 'Toyota', 'country': 'Japan', 'website': 'https://www.toyota.com'},
{'name': 'Honda', 'country': 'Japan', 'website': 'https://www.honda.com'},
{'name': 'Ford', 'country': 'USA', 'website': 'https://www.ford.com'},
{'name': 'GM/ACDelco', 'country': 'USA', 'website': 'https://www.acdelco.com'},
{'name': 'Volkswagen', 'country': 'Germany', 'website': 'https://www.vw.com'},
{'name': 'Nissan', 'country': 'Japan', 'website': 'https://www.nissan.com'},
{'name': 'Hyundai/Kia', 'country': 'South Korea', 'website': 'https://www.hyundai.com'},
],
# Premium aftermarket
'premium': [
{'name': 'Bosch', 'country': 'Germany', 'website': 'https://www.bosch.com'},
{'name': 'Denso', 'country': 'Japan', 'website': 'https://www.denso.com'},
{'name': 'NGK', 'country': 'Japan', 'website': 'https://www.ngk.com'},
{'name': 'Akebono', 'country': 'Japan', 'website': 'https://www.akebono.com'},
{'name': 'Brembo', 'country': 'Italy', 'website': 'https://www.brembo.com'},
{'name': 'KYB', 'country': 'Japan', 'website': 'https://www.kyb.com'},
{'name': 'Moog', 'country': 'USA', 'website': 'https://www.moogparts.com'},
{'name': 'Continental', 'country': 'Germany', 'website': 'https://www.continental.com'},
],
# Standard aftermarket
'standard': [
{'name': 'Monroe', 'country': 'USA', 'website': 'https://www.monroe.com'},
{'name': 'Raybestos', 'country': 'USA', 'website': 'https://www.raybestos.com'},
{'name': 'Wagner', 'country': 'USA', 'website': 'https://www.wagnerbrake.com'},
{'name': 'Cardone', 'country': 'USA', 'website': 'https://www.cardone.com'},
{'name': 'Standard Motor Products', 'country': 'USA', 'website': 'https://www.smpcorp.com'},
],
# Economy aftermarket
'economy': [
{'name': 'Fram', 'country': 'USA', 'website': 'https://www.fram.com'},
{'name': 'WIX', 'country': 'USA', 'website': 'https://www.wixfilters.com'},
{'name': 'Duralast', 'country': 'USA', 'website': 'https://www.autozone.com'},
{'name': 'AutoZone Valucraft', 'country': 'USA', 'website': 'https://www.autozone.com'},
],
}
# Part number prefixes by manufacturer for realistic generation
MANUFACTURER_PREFIXES = {
'Bosch': ['0 280', '0 986', '1 457', 'F 00M'],
'Denso': ['234-', '471-', '210-', '950-'],
'NGK': ['ZFR', 'BKR', 'LFR', 'TR'],
'Akebono': ['ACT', 'ASP', 'EUR', 'PRO'],
'Brembo': ['P 85', 'P 06', 'P 23', 'P 50'],
'KYB': ['332', '334', '343', '344'],
'Moog': ['K', 'ES', 'RK', 'CK'],
'Continental': ['49', '50', '51', 'A1'],
'Monroe': ['32', '33', '34', '37'],
'Raybestos': ['FRC', 'SGD', 'ATD', 'PGD'],
'Wagner': ['QC', 'OEX', 'TQ', 'ZD'],
'Cardone': ['18-', '19-', '20-', '21-'],
'Standard Motor Products': ['FD', 'TM', 'AC', 'JH'],
'Fram': ['PH', 'CA', 'TG', 'XG'],
'WIX': ['51', '57', '46', '33'],
'Duralast': ['DL', 'BP', 'AF', 'OF'],
'AutoZone Valucraft': ['VC', 'VB', 'VA', 'VP'],
}
# Price multipliers by quality tier (relative to a base OEM price)
PRICE_MULTIPLIERS = {
'premium': (0.75, 1.10), # 75-110% of OEM price
'standard': (0.50, 0.75), # 50-75% of OEM price
'economy': (0.25, 0.50), # 25-50% of OEM price
}
# Warranty months by quality tier
WARRANTY_MONTHS = {
'premium': [24, 36, 48],
'standard': [12, 18, 24],
'economy': [6, 12],
}
def generate_part_number(manufacturer_name: str, oem_number: str) -> str:
"""Generate a realistic aftermarket part number"""
prefixes = MANUFACTURER_PREFIXES.get(manufacturer_name, ['XX'])
prefix = random.choice(prefixes)
# Extract numeric portion from OEM number or generate random
numeric_part = ''.join(filter(str.isdigit, oem_number))
if len(numeric_part) < 4:
numeric_part = ''.join(random.choices(string.digits, k=5))
else:
# Modify slightly to make it different
numeric_part = numeric_part[:4] + str(random.randint(0, 99)).zfill(2)
return f"{prefix}{numeric_part}"
def generate_base_price(part_name: str, category_name: str = None) -> float:
"""Generate a realistic base price for a part based on category"""
# Base price ranges by category/keyword
price_ranges = {
'spark plug': (5, 25),
'filter': (8, 45),
'oil filter': (5, 20),
'air filter': (12, 35),
'brake pad': (25, 80),
'brake rotor': (40, 150),
'shock': (50, 200),
'strut': (80, 250),
'sensor': (20, 120),
'alternator': (100, 350),
'starter': (80, 300),
'water pump': (30, 120),
'radiator': (100, 400),
'thermostat': (10, 40),
'belt': (15, 60),
'hose': (10, 50),
'gasket': (5, 80),
'bearing': (15, 100),
'cv joint': (40, 150),
'tie rod': (25, 80),
'ball joint': (30, 100),
'control arm': (60, 200),
'default': (20, 100),
}
# Find matching price range
part_name_lower = part_name.lower() if part_name else ''
category_lower = (category_name or '').lower()
for keyword, (min_price, max_price) in price_ranges.items():
if keyword in part_name_lower or keyword in category_lower:
return round(random.uniform(min_price, max_price), 2)
return round(random.uniform(*price_ranges['default']), 2)
def generate_cross_reference_number(oem_number: str, ref_type: str) -> str:
"""Generate a cross-reference number based on type"""
if ref_type == 'oem_alternate':
# Slight variation of OEM number
chars = list(oem_number)
if len(chars) > 2:
idx = random.randint(0, len(chars) - 1)
if chars[idx].isdigit():
chars[idx] = str((int(chars[idx]) + 1) % 10)
elif chars[idx].isalpha():
chars[idx] = random.choice(string.ascii_uppercase)
return ''.join(chars)
elif ref_type == 'supersession':
# New part number format
return f"SUP-{oem_number[-6:]}" if len(oem_number) > 6 else f"SUP-{oem_number}"
elif ref_type == 'interchange':
# Generic interchange format
numeric = ''.join(filter(str.isdigit, oem_number))
return f"INT-{numeric[:6] if len(numeric) > 6 else numeric}"
elif ref_type == 'competitor':
# Competitor format
return f"CMP-{random.choice(string.ascii_uppercase)}{random.randint(1000, 9999)}"
return oem_number
def populate_manufacturers(manager: Fase2Manager) -> Dict[str, int]:
"""Populate the manufacturers table and return a mapping of name to ID"""
print("\n=== Populating Manufacturers ===")
manufacturer_ids = {}
# Insert OEM manufacturers
print("\nOEM Manufacturers:")
for mfr in MANUFACTURERS_DATA['oem']:
mfr_id = manager.insert_manufacturer(
name=mfr['name'],
type_='oem',
quality_tier='oem',
country=mfr['country'],
website=mfr['website']
)
manufacturer_ids[mfr['name']] = mfr_id
# Insert Premium aftermarket
print("\nPremium Aftermarket Manufacturers:")
for mfr in MANUFACTURERS_DATA['premium']:
mfr_id = manager.insert_manufacturer(
name=mfr['name'],
type_='aftermarket',
quality_tier='premium',
country=mfr['country'],
website=mfr['website']
)
manufacturer_ids[mfr['name']] = mfr_id
# Insert Standard aftermarket
print("\nStandard Aftermarket Manufacturers:")
for mfr in MANUFACTURERS_DATA['standard']:
mfr_id = manager.insert_manufacturer(
name=mfr['name'],
type_='aftermarket',
quality_tier='standard',
country=mfr['country'],
website=mfr['website']
)
manufacturer_ids[mfr['name']] = mfr_id
# Insert Economy aftermarket
print("\nEconomy Aftermarket Manufacturers:")
for mfr in MANUFACTURERS_DATA['economy']:
mfr_id = manager.insert_manufacturer(
name=mfr['name'],
type_='aftermarket',
quality_tier='economy',
country=mfr['country'],
website=mfr['website']
)
manufacturer_ids[mfr['name']] = mfr_id
print(f"\nTotal manufacturers: {len(manufacturer_ids)}")
return manufacturer_ids
def populate_aftermarket_parts(manager: Fase2Manager, manufacturer_ids: Dict[str, int]):
"""Generate aftermarket parts for each OEM part in the database"""
print("\n=== Generating Aftermarket Parts ===")
parts = manager.get_all_parts()
if not parts:
print("No parts found in the database. Aftermarket parts will be generated when parts are added.")
return
total_aftermarket = 0
for part in parts:
oem_part_id = part['id']
oem_number = part['oem_part_number']
part_name = part['name']
category_name = part.get('category_name', '')
# Generate base price for this part
base_price = generate_base_price(part_name, category_name)
# Determine how many aftermarket alternatives (2-4)
num_alternatives = random.randint(2, 4)
# Select manufacturers from different tiers
tiers_to_use = ['premium', 'standard', 'economy']
random.shuffle(tiers_to_use)
alternatives_created = 0
for tier in tiers_to_use:
if alternatives_created >= num_alternatives:
break
# Get manufacturers for this tier
tier_manufacturers = [
name for name, data in
[(m['name'], m) for m in (
MANUFACTURERS_DATA.get(tier, [])
)]
]
if not tier_manufacturers:
continue
# Pick 1-2 manufacturers from this tier
selected = random.sample(
tier_manufacturers,
min(2, len(tier_manufacturers), num_alternatives - alternatives_created)
)
for mfr_name in selected:
if alternatives_created >= num_alternatives:
break
mfr_id = manufacturer_ids.get(mfr_name)
if not mfr_id:
continue
# Generate aftermarket part number
am_part_number = generate_part_number(mfr_name, oem_number)
# Calculate price based on tier
price_range = PRICE_MULTIPLIERS.get(tier, (0.5, 0.8))
price_multiplier = random.uniform(*price_range)
am_price = round(base_price * price_multiplier, 2)
# Get warranty for tier
warranty = random.choice(WARRANTY_MONTHS.get(tier, [12]))
# Determine quality tier for the part
quality_tier = tier
# Insert aftermarket part
am_id = manager.insert_aftermarket_part(
oem_part_id=oem_part_id,
manufacturer_id=mfr_id,
part_number=am_part_number,
name=f"{mfr_name} {part_name}",
name_es=part.get('name_es'),
quality_tier=quality_tier,
price_usd=am_price,
warranty_months=warranty,
in_stock=random.random() > 0.1 # 90% in stock
)
if am_id:
alternatives_created += 1
total_aftermarket += 1
print(f" Part {oem_number}: {alternatives_created} aftermarket alternatives created")
print(f"\nTotal aftermarket parts created: {total_aftermarket}")
def populate_cross_references(manager: Fase2Manager):
"""Generate cross-references for OEM parts"""
print("\n=== Generating Cross-References ===")
parts = manager.get_all_parts()
if not parts:
print("No parts found in the database. Cross-references will be generated when parts are added.")
return
total_refs = 0
reference_types = ['oem_alternate', 'supersession', 'interchange', 'competitor']
sources = ['RockAuto', 'PartsGeek', 'AutoZone', 'OReilly', 'NAPA', 'Manufacturer']
for part in parts:
part_id = part['id']
oem_number = part['oem_part_number']
# Generate 1-3 cross-references per part
num_refs = random.randint(1, 3)
used_types = random.sample(reference_types, min(num_refs, len(reference_types)))
for ref_type in used_types:
cross_ref_number = generate_cross_reference_number(oem_number, ref_type)
source = random.choice(sources)
notes = None
if ref_type == 'supersession':
notes = "New part number supersedes original"
elif ref_type == 'interchange':
notes = "Interchangeable with original"
ref_id = manager.insert_cross_reference(
part_id=part_id,
cross_reference_number=cross_ref_number,
reference_type=ref_type,
source=source,
notes=notes
)
if ref_id:
total_refs += 1
print(f" Part {oem_number}: {len(used_types)} cross-references created")
print(f"\nTotal cross-references created: {total_refs}")
def main():
"""Main entry point for FASE 2 population"""
print("=" * 60)
print("FASE 2: Cross-References and Aftermarket Parts Population")
print("=" * 60)
manager = Fase2Manager()
try:
# Connect to database
manager.connect()
# Create FASE 2 tables (idempotent)
manager.create_fase2_tables()
# Populate manufacturers
manufacturer_ids = populate_manufacturers(manager)
# Generate aftermarket parts
populate_aftermarket_parts(manager, manufacturer_ids)
# Generate cross-references
populate_cross_references(manager)
print("\n" + "=" * 60)
print("FASE 2 population completed successfully!")
print("=" * 60)
# Print summary
cursor = manager.connection.cursor()
cursor.execute("SELECT COUNT(*) FROM manufacturers")
mfr_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM aftermarket_parts")
am_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM part_cross_references")
xref_count = cursor.fetchone()[0]
print(f"\nSummary:")
print(f" Manufacturers: {mfr_count}")
print(f" Aftermarket Parts: {am_count}")
print(f" Cross-References: {xref_count}")
except Exception as e:
print(f"\nError: {e}")
raise
finally:
manager.disconnect()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,697 @@
#!/usr/bin/env python3
"""
FASE 3: Populate exploded diagrams and hotspots
This script creates FASE 3 tables and populates them with sample diagrams,
vehicle-diagram relationships, and clickable hotspots linked to parts.
"""
import sqlite3
import os
from typing import List, Dict, Optional
# Database path configuration
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
DIAGRAMS_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'dashboard', 'static', 'diagrams')
class Fase3Manager:
"""Manager for FASE 3 tables: diagrams, vehicle_diagrams, and diagram_hotspots"""
def __init__(self, db_path: str = DB_PATH):
self.db_path = db_path
self.connection = None
def connect(self):
"""Connect to the SQLite database"""
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row
print(f"Connected to database: {self.db_path}")
def disconnect(self):
"""Close the database connection"""
if self.connection:
self.connection.close()
print("Disconnected from database")
def create_fase3_tables(self):
"""Create FASE 3 tables from schema file"""
if not os.path.exists(SCHEMA_PATH):
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
with open(SCHEMA_PATH, 'r') as f:
schema = f.read()
if self.connection:
cursor = self.connection.cursor()
cursor.executescript(schema)
self.connection.commit()
print("FASE 3 tables created successfully")
def create_diagrams_directory(self):
"""Create the diagrams directory structure"""
if not os.path.exists(DIAGRAMS_DIR):
os.makedirs(DIAGRAMS_DIR)
print(f"Created diagrams directory: {DIAGRAMS_DIR}")
else:
print(f"Diagrams directory already exists: {DIAGRAMS_DIR}")
def get_diagram_by_name(self, name: str) -> Optional[int]:
"""Get diagram ID by name, returns None if not found"""
cursor = self.connection.cursor()
cursor.execute("SELECT id FROM diagrams WHERE name = ?", (name,))
result = cursor.fetchone()
return result[0] if result else None
def insert_diagram(self, name: str, name_es: str, group_id: int, image_path: str,
thumbnail_path: str = None, svg_content: str = None,
width: int = 600, height: int = 400, display_order: int = 0,
source: str = None) -> int:
"""Insert a diagram if it doesn't exist, return its ID"""
existing_id = self.get_diagram_by_name(name)
if existing_id:
print(f" Diagram '{name}' already exists (ID: {existing_id})")
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO diagrams (name, name_es, group_id, image_path, thumbnail_path,
svg_content, width, height, display_order, source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(name, name_es, group_id, image_path, thumbnail_path, svg_content,
width, height, display_order, source)
)
self.connection.commit()
diagram_id = cursor.lastrowid
print(f" Inserted diagram: {name} (ID: {diagram_id})")
return diagram_id
def get_vehicle_diagram(self, diagram_id: int, model_year_engine_id: int) -> Optional[int]:
"""Check if a vehicle-diagram link already exists"""
cursor = self.connection.cursor()
cursor.execute(
"""SELECT id FROM vehicle_diagrams
WHERE diagram_id = ? AND model_year_engine_id = ?""",
(diagram_id, model_year_engine_id)
)
result = cursor.fetchone()
return result[0] if result else None
def insert_vehicle_diagram(self, diagram_id: int, model_year_engine_id: int,
notes: str = None) -> int:
"""Link a diagram to a vehicle configuration"""
existing_id = self.get_vehicle_diagram(diagram_id, model_year_engine_id)
if existing_id:
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO vehicle_diagrams (diagram_id, model_year_engine_id, notes)
VALUES (?, ?, ?)""",
(diagram_id, model_year_engine_id, notes)
)
self.connection.commit()
return cursor.lastrowid
def get_hotspot(self, diagram_id: int, callout_number: int) -> Optional[int]:
"""Check if a hotspot already exists for this diagram and callout"""
cursor = self.connection.cursor()
cursor.execute(
"""SELECT id FROM diagram_hotspots
WHERE diagram_id = ? AND callout_number = ?""",
(diagram_id, callout_number)
)
result = cursor.fetchone()
return result[0] if result else None
def insert_hotspot(self, diagram_id: int, part_id: int = None, callout_number: int = None,
label: str = None, shape: str = 'rect', coords: str = '',
color: str = '#e74c3c') -> int:
"""Insert a hotspot for a diagram"""
if callout_number:
existing_id = self.get_hotspot(diagram_id, callout_number)
if existing_id:
return existing_id
cursor = self.connection.cursor()
cursor.execute(
"""INSERT INTO diagram_hotspots (diagram_id, part_id, callout_number, label,
shape, coords, color)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(diagram_id, part_id, callout_number, label, shape, coords, color)
)
self.connection.commit()
return cursor.lastrowid
def get_part_by_name_pattern(self, pattern: str) -> Optional[Dict]:
"""Get a part by name pattern"""
cursor = self.connection.cursor()
cursor.execute(
"SELECT id, oem_part_number, name FROM parts WHERE name LIKE ?",
(f"%{pattern}%",)
)
result = cursor.fetchone()
return dict(result) if result else None
def get_group_by_name(self, name: str) -> Optional[int]:
"""Get group ID by name"""
cursor = self.connection.cursor()
cursor.execute("SELECT id FROM part_groups WHERE name LIKE ?", (f"%{name}%",))
result = cursor.fetchone()
return result[0] if result else None
def get_all_model_year_engines(self) -> List[int]:
"""Get all model_year_engine IDs"""
cursor = self.connection.cursor()
cursor.execute("SELECT id FROM model_year_engine")
return [row[0] for row in cursor.fetchall()]
# SVG Templates for diagrams
def generate_brake_assembly_svg() -> str:
"""Generate SVG for front brake assembly diagram"""
return '''<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="metalGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#b0b0b0"/>
<stop offset="50%" style="stop-color:#808080"/>
<stop offset="100%" style="stop-color:#606060"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Front Brake Assembly / Ensamble de Freno Delantero
</text>
<!-- Brake Rotor (large circle) -->
<circle cx="250" cy="200" r="120" fill="url(#metalGrad)" stroke="#333" stroke-width="3" id="rotor"/>
<circle cx="250" cy="200" r="100" fill="none" stroke="#555" stroke-width="2"/>
<circle cx="250" cy="200" r="40" fill="#444" stroke="#333" stroke-width="2"/>
<!-- Rotor ventilation slots -->
<line x1="250" y1="60" x2="250" y2="80" stroke="#666" stroke-width="3"/>
<line x1="250" y1="320" x2="250" y2="340" stroke="#666" stroke-width="3"/>
<line x1="130" y1="200" x2="150" y2="200" stroke="#666" stroke-width="3"/>
<line x1="350" y1="200" x2="370" y2="200" stroke="#666" stroke-width="3"/>
<!-- Brake Caliper -->
<rect x="320" y="140" width="80" height="120" rx="10" ry="10" fill="#c0392b" stroke="#922b21" stroke-width="3" id="caliper"/>
<rect x="330" y="155" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
<rect x="330" y="210" width="60" height="35" rx="5" ry="5" fill="#e74c3c"/>
<!-- Caliper bolts -->
<circle cx="340" cy="150" r="6" fill="#333"/>
<circle cx="380" cy="150" r="6" fill="#333"/>
<circle cx="340" cy="250" r="6" fill="#333"/>
<circle cx="380" cy="250" r="6" fill="#333"/>
<!-- Brake Pads (visible through caliper) -->
<rect x="300" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-inner"/>
<rect x="405" y="160" width="15" height="80" fill="#8b7355" stroke="#5d4e37" stroke-width="2" id="pad-outer"/>
<!-- Callout lines and numbers -->
<!-- Callout 1: Brake Rotor -->
<line x1="170" y1="120" x2="100" y2="60" stroke="#333" stroke-width="1.5"/>
<circle cx="100" cy="60" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="100" y="65" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Callout 2: Brake Caliper -->
<line x1="400" y1="140" x2="480" y2="80" stroke="#333" stroke-width="1.5"/>
<circle cx="480" cy="80" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="480" y="85" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
<!-- Callout 3: Brake Pads -->
<line x1="307" y1="250" x2="250" y2="320" stroke="#333" stroke-width="1.5"/>
<circle cx="250" cy="320" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="250" y="325" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
<!-- Legend -->
<rect x="440" y="300" width="150" height="90" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="515" y="320" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="450" y="340" font-family="Arial" font-size="11" fill="#333">1. Brake Rotor / Disco</text>
<text x="450" y="358" font-family="Arial" font-size="11" fill="#333">2. Brake Caliper / Caliper</text>
<text x="450" y="376" font-family="Arial" font-size="11" fill="#333">3. Brake Pads / Balatas</text>
</svg>'''
def generate_oil_filter_system_svg() -> str:
"""Generate SVG for oil filter system diagram"""
return '''<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="oilGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#8B4513"/>
<stop offset="100%" style="stop-color:#654321"/>
</linearGradient>
<linearGradient id="filterGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#2c3e50"/>
<stop offset="50%" style="stop-color:#34495e"/>
<stop offset="100%" style="stop-color:#2c3e50"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Oil Filter System / Sistema de Filtro de Aceite
</text>
<!-- Engine Block (simplified) -->
<rect x="50" y="100" width="200" height="200" fill="#555" stroke="#333" stroke-width="3" rx="10"/>
<text x="150" y="200" text-anchor="middle" font-family="Arial" font-size="14" fill="#ccc">ENGINE</text>
<text x="150" y="220" text-anchor="middle" font-family="Arial" font-size="12" fill="#999">MOTOR</text>
<!-- Oil passage from engine -->
<rect x="250" y="180" width="60" height="20" fill="url(#oilGrad)"/>
<path d="M250,190 L230,190" stroke="#8B4513" stroke-width="8" fill="none"/>
<!-- Oil Filter Housing -->
<rect x="310" y="120" width="100" height="160" fill="#777" stroke="#555" stroke-width="3" rx="5"/>
<!-- Oil Filter (canister type) -->
<rect x="320" y="140" width="80" height="120" fill="url(#filterGrad)" stroke="#1a252f" stroke-width="3" rx="8" id="oil-filter"/>
<!-- Filter ridges -->
<line x1="320" y1="160" x2="400" y2="160" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="180" x2="400" y2="180" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="200" x2="400" y2="200" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="220" x2="400" y2="220" stroke="#1a252f" stroke-width="2"/>
<line x1="320" y1="240" x2="400" y2="240" stroke="#1a252f" stroke-width="2"/>
<!-- Filter label area -->
<rect x="335" y="175" width="50" height="50" fill="#2980b9" rx="3"/>
<text x="360" y="195" text-anchor="middle" font-family="Arial" font-size="10" fill="white">OIL</text>
<text x="360" y="210" text-anchor="middle" font-family="Arial" font-size="10" fill="white">FILTER</text>
<!-- Oil return passage -->
<rect x="410" y="180" width="60" height="20" fill="url(#oilGrad)"/>
<!-- Oil Pan (simplified) -->
<path d="M470,170 L530,170 L550,300 L450,300 Z" fill="#666" stroke="#444" stroke-width="3"/>
<text x="500" y="250" text-anchor="middle" font-family="Arial" font-size="12" fill="#ccc">OIL PAN</text>
<text x="500" y="265" text-anchor="middle" font-family="Arial" font-size="10" fill="#999">CARTER</text>
<!-- Flow arrows -->
<polygon points="275,185 285,190 275,195" fill="#8B4513"/>
<polygon points="435,185 445,190 435,195" fill="#8B4513"/>
<!-- Callout for Oil Filter -->
<line x1="360" y1="140" x2="360" y2="70" stroke="#333" stroke-width="1.5"/>
<line x1="360" y1="70" x2="420" y2="70" stroke="#333" stroke-width="1.5"/>
<circle cx="420" cy="70" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="420" y="75" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Legend -->
<rect x="50" y="320" width="200" height="60" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="150" y="340" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="60" y="360" font-family="Arial" font-size="11" fill="#333">1. Oil Filter / Filtro de Aceite</text>
<!-- Oil flow label -->
<text x="300" y="380" text-anchor="middle" font-family="Arial" font-size="10" fill="#666">Oil Flow Direction / Direccion del Flujo de Aceite</text>
</svg>'''
def generate_suspension_diagram_svg() -> str:
"""Generate SVG for front suspension diagram"""
return '''<?xml version="1.0" encoding="UTF-8"?>
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="strutGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#444"/>
<stop offset="50%" style="stop-color:#666"/>
<stop offset="100%" style="stop-color:#444"/>
</linearGradient>
<linearGradient id="springGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#27ae60"/>
<stop offset="50%" style="stop-color:#2ecc71"/>
<stop offset="100%" style="stop-color:#27ae60"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="600" height="400" fill="#f5f5f5"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#333">
Front Suspension / Suspension Delantera
</text>
<!-- Vehicle body mounting point -->
<rect x="180" y="50" width="240" height="30" fill="#555" stroke="#333" stroke-width="2"/>
<text x="300" y="70" text-anchor="middle" font-family="Arial" font-size="10" fill="#ccc">BODY / CARROCERIA</text>
<!-- Strut Mount (top) -->
<ellipse cx="300" cy="95" rx="35" ry="15" fill="#888" stroke="#555" stroke-width="2"/>
<!-- Strut Assembly -->
<rect x="285" y="95" width="30" height="150" fill="url(#strutGrad)" stroke="#333" stroke-width="2" id="strut"/>
<!-- Strut piston rod -->
<rect x="293" y="95" width="14" height="60" fill="#999" stroke="#777" stroke-width="1"/>
<!-- Coil Spring around strut -->
<path d="M275,120 Q310,130 275,140 Q240,150 275,160 Q310,170 275,180 Q240,190 275,200 Q310,210 275,220"
fill="none" stroke="url(#springGrad)" stroke-width="8" stroke-linecap="round"/>
<!-- Lower Control Arm -->
<path d="M150,320 L300,280 L450,320" fill="none" stroke="#444" stroke-width="12" stroke-linecap="round"/>
<rect x="140" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
<rect x="430" y="310" width="30" height="30" fill="#666" stroke="#444" stroke-width="2" rx="5"/>
<!-- Ball Joint (connecting strut to control arm) -->
<circle cx="300" cy="280" r="20" fill="#c0392b" stroke="#922b21" stroke-width="3" id="ball-joint"/>
<circle cx="300" cy="280" r="8" fill="#333"/>
<!-- Steering Knuckle (simplified) -->
<rect x="280" y="250" width="40" height="25" fill="#777" stroke="#555" stroke-width="2"/>
<!-- Wheel hub representation -->
<circle cx="300" cy="340" r="40" fill="#444" stroke="#333" stroke-width="3"/>
<circle cx="300" cy="340" r="15" fill="#333"/>
<text x="300" y="345" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">HUB</text>
<!-- Sway Bar Link -->
<line x1="350" y1="300" x2="420" y2="250" stroke="#555" stroke-width="6"/>
<circle cx="350" cy="300" r="6" fill="#777" stroke="#555" stroke-width="2"/>
<circle cx="420" cy="250" r="6" fill="#777" stroke="#555" stroke-width="2"/>
<!-- Callout 1: Strut Assembly -->
<line x1="320" y1="150" x2="420" y2="100" stroke="#333" stroke-width="1.5"/>
<circle cx="420" cy="100" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="420" y="105" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">1</text>
<!-- Callout 2: Ball Joint -->
<line x1="280" y1="280" x2="180" y2="280" stroke="#333" stroke-width="1.5"/>
<line x1="180" y1="280" x2="150" y2="250" stroke="#333" stroke-width="1.5"/>
<circle cx="150" cy="250" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="150" y="255" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">2</text>
<!-- Callout 3: Control Arm -->
<line x1="400" y1="320" x2="500" y2="350" stroke="#333" stroke-width="1.5"/>
<circle cx="500" cy="350" r="15" fill="#3498db" stroke="#2980b9" stroke-width="2"/>
<text x="500" y="355" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="white">3</text>
<!-- Legend -->
<rect x="440" y="50" width="150" height="100" fill="white" stroke="#ccc" stroke-width="1" rx="5"/>
<text x="515" y="70" text-anchor="middle" font-family="Arial" font-size="12" font-weight="bold" fill="#333">Parts / Partes</text>
<text x="450" y="90" font-family="Arial" font-size="10" fill="#333">1. Strut / Amortiguador</text>
<text x="450" y="108" font-family="Arial" font-size="10" fill="#333">2. Ball Joint / Rotula</text>
<text x="450" y="126" font-family="Arial" font-size="10" fill="#333">3. Control Arm / Brazo</text>
</svg>'''
def save_svg_file(filename: str, content: str):
"""Save SVG content to file"""
filepath = os.path.join(DIAGRAMS_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f" Saved SVG file: {filepath}")
return filepath
def populate_diagrams(manager: Fase3Manager) -> Dict[str, int]:
"""Populate the diagrams table with sample diagrams"""
print("\n=== Populating Diagrams ===")
diagram_ids = {}
# Get group IDs for different diagram types
brake_rotor_group = manager.get_group_by_name("Brake Rotors")
oil_filter_group = manager.get_group_by_name("Oil Filters")
struts_group = manager.get_group_by_name("Struts")
# Use fallback group IDs if not found
if not brake_rotor_group:
brake_rotor_group = 17 # Default Brake Rotors group
if not oil_filter_group:
oil_filter_group = 70 # Default Oil Filters group
if not struts_group:
struts_group = 157 # Default Struts group
diagrams_data = [
{
'name': 'Front Brake Assembly',
'name_es': 'Ensamble de Freno Delantero',
'group_id': brake_rotor_group,
'image_path': 'diagrams/brake_assembly.svg',
'svg_generator': generate_brake_assembly_svg,
'svg_filename': 'brake_assembly.svg',
'source': 'System Generated'
},
{
'name': 'Oil Filter System',
'name_es': 'Sistema de Filtro de Aceite',
'group_id': oil_filter_group,
'image_path': 'diagrams/oil_filter_system.svg',
'svg_generator': generate_oil_filter_system_svg,
'svg_filename': 'oil_filter_system.svg',
'source': 'System Generated'
},
{
'name': 'Front Suspension Assembly',
'name_es': 'Ensamble de Suspension Delantera',
'group_id': struts_group,
'image_path': 'diagrams/suspension_assembly.svg',
'svg_generator': generate_suspension_diagram_svg,
'svg_filename': 'suspension_assembly.svg',
'source': 'System Generated'
}
]
for diagram_data in diagrams_data:
# Generate SVG content
svg_content = diagram_data['svg_generator']()
# Save SVG file
save_svg_file(diagram_data['svg_filename'], svg_content)
# Insert diagram record
diagram_id = manager.insert_diagram(
name=diagram_data['name'],
name_es=diagram_data['name_es'],
group_id=diagram_data['group_id'],
image_path=diagram_data['image_path'],
thumbnail_path=None,
svg_content=svg_content,
width=600,
height=400,
display_order=0,
source=diagram_data['source']
)
diagram_ids[diagram_data['name']] = diagram_id
print(f"\nTotal diagrams created: {len(diagram_ids)}")
return diagram_ids
def populate_vehicle_diagrams(manager: Fase3Manager, diagram_ids: Dict[str, int]):
"""Link diagrams to vehicle configurations"""
print("\n=== Linking Diagrams to Vehicles ===")
# Get all model_year_engine entries
mye_ids = manager.get_all_model_year_engines()
if not mye_ids:
print("No vehicle configurations found. Skipping vehicle-diagram links.")
return
total_links = 0
# Link each diagram to all vehicle configurations (diagrams are generic)
for diagram_name, diagram_id in diagram_ids.items():
for mye_id in mye_ids:
manager.insert_vehicle_diagram(
diagram_id=diagram_id,
model_year_engine_id=mye_id,
notes=f"Standard {diagram_name.lower()} diagram"
)
total_links += 1
print(f" Created {total_links} vehicle-diagram links")
def populate_hotspots(manager: Fase3Manager, diagram_ids: Dict[str, int]):
"""Create hotspots for each diagram linking to actual parts"""
print("\n=== Creating Diagram Hotspots ===")
total_hotspots = 0
# Hotspots for Brake Assembly diagram
if 'Front Brake Assembly' in diagram_ids:
diagram_id = diagram_ids['Front Brake Assembly']
print(f"\n Creating hotspots for Front Brake Assembly (ID: {diagram_id})")
# Find parts to link
brake_rotor = manager.get_part_by_name_pattern("Brake Rotor")
brake_pads = manager.get_part_by_name_pattern("Brake Pads")
brake_caliper = manager.get_part_by_name_pattern("Caliper")
# Hotspot 1: Brake Rotor - rectangle around the rotor area
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=brake_rotor['id'] if brake_rotor else None,
callout_number=1,
label="Brake Rotor / Disco de Freno",
shape='circle',
coords='250,200,120', # cx,cy,r for circle
color='#3498db'
)
total_hotspots += 1
print(f" Added hotspot 1: Brake Rotor")
# Hotspot 2: Brake Caliper - rectangle around caliper
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=brake_caliper['id'] if brake_caliper else None,
callout_number=2,
label="Brake Caliper / Calibrador",
shape='rect',
coords='320,140,80,120', # x,y,width,height for rect
color='#e74c3c'
)
total_hotspots += 1
print(f" Added hotspot 2: Brake Caliper")
# Hotspot 3: Brake Pads - rectangle around pad area
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=brake_pads['id'] if brake_pads else None,
callout_number=3,
label="Brake Pads / Balatas",
shape='rect',
coords='300,160,120,80', # x,y,width,height
color='#8b7355'
)
total_hotspots += 1
print(f" Added hotspot 3: Brake Pads")
# Hotspots for Oil Filter System diagram
if 'Oil Filter System' in diagram_ids:
diagram_id = diagram_ids['Oil Filter System']
print(f"\n Creating hotspots for Oil Filter System (ID: {diagram_id})")
# Find oil filter part
oil_filter = manager.get_part_by_name_pattern("Oil Filter")
# Hotspot 1: Oil Filter
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=oil_filter['id'] if oil_filter else None,
callout_number=1,
label="Oil Filter / Filtro de Aceite",
shape='rect',
coords='320,140,80,120', # x,y,width,height
color='#2980b9'
)
total_hotspots += 1
print(f" Added hotspot 1: Oil Filter")
# Hotspots for Suspension Assembly diagram
if 'Front Suspension Assembly' in diagram_ids:
diagram_id = diagram_ids['Front Suspension Assembly']
print(f"\n Creating hotspots for Front Suspension Assembly (ID: {diagram_id})")
# Find parts
strut = manager.get_part_by_name_pattern("Strut")
ball_joint = manager.get_part_by_name_pattern("Ball Joint")
control_arm = manager.get_part_by_name_pattern("Control Arm")
# Hotspot 1: Strut Assembly
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=strut['id'] if strut else None,
callout_number=1,
label="Strut Assembly / Amortiguador",
shape='rect',
coords='275,95,50,150', # x,y,width,height
color='#27ae60'
)
total_hotspots += 1
print(f" Added hotspot 1: Strut Assembly")
# Hotspot 2: Ball Joint
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=ball_joint['id'] if ball_joint else None,
callout_number=2,
label="Ball Joint / Rotula",
shape='circle',
coords='300,280,20', # cx,cy,r
color='#c0392b'
)
total_hotspots += 1
print(f" Added hotspot 2: Ball Joint")
# Hotspot 3: Control Arm
manager.insert_hotspot(
diagram_id=diagram_id,
part_id=control_arm['id'] if control_arm else None,
callout_number=3,
label="Control Arm / Brazo de Control",
shape='poly',
coords='150,320,300,280,450,320,430,340,300,310,170,340', # polygon points
color='#444'
)
total_hotspots += 1
print(f" Added hotspot 3: Control Arm")
print(f"\nTotal hotspots created: {total_hotspots}")
def main():
"""Main entry point for FASE 3 population"""
print("=" * 60)
print("FASE 3: Exploded Diagrams and Hotspots Population")
print("=" * 60)
manager = Fase3Manager()
try:
# Connect to database
manager.connect()
# Create FASE 3 tables (idempotent)
manager.create_fase3_tables()
# Create diagrams directory
manager.create_diagrams_directory()
# Populate diagrams
diagram_ids = populate_diagrams(manager)
# Link diagrams to vehicles
populate_vehicle_diagrams(manager, diagram_ids)
# Create hotspots
populate_hotspots(manager, diagram_ids)
print("\n" + "=" * 60)
print("FASE 3 population completed successfully!")
print("=" * 60)
# Print summary
cursor = manager.connection.cursor()
cursor.execute("SELECT COUNT(*) FROM diagrams")
diagram_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM vehicle_diagrams")
vd_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM diagram_hotspots")
hotspot_count = cursor.fetchone()[0]
print(f"\nSummary:")
print(f" Diagrams: {diagram_count}")
print(f" Vehicle-Diagram Links: {vd_count}")
print(f" Hotspots: {hotspot_count}")
print(f"\nSVG files saved to: {DIAGRAMS_DIR}")
except Exception as e:
print(f"\nError: {e}")
raise
finally:
manager.disconnect()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
FASE 4: Full-Text Search and VIN Decoder
This script creates FASE 4 tables (FTS5, triggers, vin_cache) and populates
the parts_fts table with existing parts data.
"""
import sqlite3
import os
import json
from typing import Optional
from datetime import datetime, timedelta
# Database path configuration
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
class Fase4Manager:
"""Manager for FASE 4 tables: parts_fts, vin_cache, and related triggers"""
def __init__(self, db_path: str = DB_PATH):
self.db_path = db_path
self.connection = None
def connect(self):
"""Connect to the SQLite database"""
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row
print(f"Connected to database: {self.db_path}")
def disconnect(self):
"""Close the database connection"""
if self.connection:
self.connection.close()
print("Disconnected from database")
def create_fase4_tables(self):
"""Create FASE 4 tables from schema file"""
if not os.path.exists(SCHEMA_PATH):
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
with open(SCHEMA_PATH, 'r') as f:
schema = f.read()
if self.connection:
cursor = self.connection.cursor()
cursor.executescript(schema)
self.connection.commit()
print("FASE 4 tables created successfully")
def check_fts_table_exists(self) -> bool:
"""Check if the parts_fts FTS5 table exists"""
cursor = self.connection.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
)
return cursor.fetchone() is not None
def check_fts_populated(self) -> bool:
"""Check if the FTS table has any data"""
cursor = self.connection.cursor()
try:
cursor.execute("SELECT COUNT(*) FROM parts_fts")
count = cursor.fetchone()[0]
return count > 0
except sqlite3.OperationalError:
return False
def populate_fts_from_parts(self):
"""Populate the parts_fts table with existing parts data"""
if not self.check_fts_table_exists():
print("FTS table does not exist, creating tables first...")
self.create_fase4_tables()
# Check if already populated
if self.check_fts_populated():
print("parts_fts table already has data, skipping population")
return
cursor = self.connection.cursor()
# Get count of parts
cursor.execute("SELECT COUNT(*) FROM parts")
parts_count = cursor.fetchone()[0]
if parts_count == 0:
print("No parts found in parts table, nothing to populate")
return
# Populate FTS table from parts
cursor.execute("""
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
SELECT id, oem_part_number, name, name_es, description, description_es FROM parts
""")
self.connection.commit()
# Rebuild FTS index for proper search functionality
cursor.execute("INSERT INTO parts_fts(parts_fts) VALUES('rebuild')")
self.connection.commit()
# Verify population
cursor.execute("SELECT COUNT(*) FROM parts_fts")
fts_count = cursor.fetchone()[0]
print(f"Populated parts_fts with {fts_count} entries from {parts_count} parts")
def get_vin_by_vin(self, vin: str) -> Optional[int]:
"""Get VIN cache entry ID by VIN, returns None if not found"""
cursor = self.connection.cursor()
cursor.execute("SELECT id FROM vin_cache WHERE vin = ?", (vin,))
result = cursor.fetchone()
return result[0] if result else None
def insert_vin_cache(self, vin: str, decoded_data: dict, make: str, model: str,
year: int, engine_info: str = None, body_class: str = None,
drive_type: str = None, model_year_engine_id: int = None,
expires_days: int = 30) -> int:
"""Insert a VIN cache entry if it doesn't exist, return its ID"""
existing_id = self.get_vin_by_vin(vin)
if existing_id:
print(f" VIN '{vin}' already exists in cache (ID: {existing_id})")
return existing_id
cursor = self.connection.cursor()
expires_at = datetime.now() + timedelta(days=expires_days)
cursor.execute(
"""INSERT INTO vin_cache
(vin, decoded_data, make, model, year, engine_info, body_class,
drive_type, model_year_engine_id, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(vin, json.dumps(decoded_data), make, model, year, engine_info,
body_class, drive_type, model_year_engine_id, expires_at.isoformat())
)
self.connection.commit()
vin_id = cursor.lastrowid
print(f" Inserted VIN cache: {vin} -> {make} {model} {year} (ID: {vin_id})")
return vin_id
def populate_sample_vins(self):
"""Populate sample VIN cache entries for testing"""
print("\nPopulating sample VIN cache entries...")
sample_vins = [
{
'vin': '4T1BF1FK5CU123456',
'decoded_data': {
'Make': 'TOYOTA',
'Model': 'Camry',
'ModelYear': '2023',
'EngineModel': '2.5L I4',
'BodyClass': 'Sedan/Saloon',
'DriveType': 'FWD',
'PlantCountry': 'UNITED STATES (USA)',
'VehicleType': 'PASSENGER CAR'
},
'make': 'Toyota',
'model': 'Camry',
'year': 2023,
'engine_info': '2.5L I4 DOHC 16V',
'body_class': 'Sedan',
'drive_type': 'FWD'
},
{
'vin': '1HGBH41JXMN109186',
'decoded_data': {
'Make': 'HONDA',
'Model': 'Civic',
'ModelYear': '2023',
'EngineModel': '2.0L I4',
'BodyClass': 'Sedan/Saloon',
'DriveType': 'FWD',
'PlantCountry': 'UNITED STATES (USA)',
'VehicleType': 'PASSENGER CAR'
},
'make': 'Honda',
'model': 'Civic',
'year': 2023,
'engine_info': '2.0L I4 DOHC 16V',
'body_class': 'Sedan',
'drive_type': 'FWD'
},
{
'vin': '1FA6P8CF5L5123456',
'decoded_data': {
'Make': 'FORD',
'Model': 'Mustang',
'ModelYear': '2020',
'EngineModel': '5.0L V8',
'BodyClass': 'Coupe',
'DriveType': 'RWD',
'PlantCountry': 'UNITED STATES (USA)',
'VehicleType': 'PASSENGER CAR'
},
'make': 'Ford',
'model': 'Mustang',
'year': 2020,
'engine_info': '5.0L V8 Coyote',
'body_class': 'Coupe',
'drive_type': 'RWD'
}
]
for vin_data in sample_vins:
self.insert_vin_cache(
vin=vin_data['vin'],
decoded_data=vin_data['decoded_data'],
make=vin_data['make'],
model=vin_data['model'],
year=vin_data['year'],
engine_info=vin_data['engine_info'],
body_class=vin_data['body_class'],
drive_type=vin_data['drive_type']
)
def verify_installation(self):
"""Verify FASE 4 installation"""
cursor = self.connection.cursor()
print("\n" + "=" * 50)
print("FASE 4 Installation Verification")
print("=" * 50)
# Check FTS table
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
)
fts_exists = cursor.fetchone() is not None
print(f"parts_fts table: {'OK' if fts_exists else 'MISSING'}")
if fts_exists:
cursor.execute("SELECT COUNT(*) FROM parts_fts")
fts_count = cursor.fetchone()[0]
print(f" - FTS entries: {fts_count}")
# Check triggers
triggers = ['parts_fts_insert', 'parts_fts_delete', 'parts_fts_update']
for trigger_name in triggers:
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='trigger' AND name=?",
(trigger_name,)
)
trigger_exists = cursor.fetchone() is not None
print(f"Trigger {trigger_name}: {'OK' if trigger_exists else 'MISSING'}")
# Check vin_cache table
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='vin_cache'"
)
vin_exists = cursor.fetchone() is not None
print(f"vin_cache table: {'OK' if vin_exists else 'MISSING'}")
if vin_exists:
cursor.execute("SELECT COUNT(*) FROM vin_cache")
vin_count = cursor.fetchone()[0]
print(f" - VIN cache entries: {vin_count}")
cursor.execute("SELECT vin, make, model, year FROM vin_cache")
for row in cursor.fetchall():
print(f" - {row['vin']}: {row['make']} {row['model']} {row['year']}")
# Check indexes
indexes = ['idx_vin_cache_vin', 'idx_vin_cache_make_model']
for index_name in indexes:
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
(index_name,)
)
index_exists = cursor.fetchone() is not None
print(f"Index {index_name}: {'OK' if index_exists else 'MISSING'}")
print("=" * 50)
def main():
"""Main function to populate FASE 4 tables"""
print("=" * 60)
print("FASE 4: Full-Text Search and VIN Decoder Population")
print("=" * 60)
manager = Fase4Manager()
try:
manager.connect()
# Step 1: Create FASE 4 tables (FTS5, triggers, vin_cache)
print("\n[1/4] Creating FASE 4 tables...")
manager.create_fase4_tables()
# Step 2: Populate FTS table with existing parts
print("\n[2/4] Populating Full-Text Search index...")
manager.populate_fts_from_parts()
# Step 3: Add sample VIN cache entries
print("\n[3/4] Adding sample VIN cache entries...")
manager.populate_sample_vins()
# Step 4: Verify installation
print("\n[4/4] Verifying FASE 4 installation...")
manager.verify_installation()
print("\nFASE 4 population completed successfully!")
except Exception as e:
print(f"\nError during FASE 4 population: {e}")
raise
finally:
manager.disconnect()
if __name__ == '__main__':
main()

View File

@@ -64,3 +64,234 @@ CREATE INDEX IF NOT EXISTS idx_models_brand ON models(brand_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_model ON model_year_engine(model_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_year ON model_year_engine(year_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_engine ON model_year_engine(engine_id);
-- =====================================================
-- PARTS CATALOG SCHEMA (FASE 1)
-- =====================================================
-- Categorías de partes (Engine, Brakes, Suspension, etc.)
CREATE TABLE IF NOT EXISTS part_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
name_es TEXT,
parent_id INTEGER,
slug TEXT UNIQUE,
icon_name TEXT,
display_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES part_categories(id)
);
-- Grupos dentro de categorías (subcategorías más específicas)
CREATE TABLE IF NOT EXISTS part_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
name TEXT NOT NULL,
name_es TEXT,
slug TEXT,
display_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES part_categories(id)
);
-- Catálogo maestro de partes
CREATE TABLE IF NOT EXISTS parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
oem_part_number TEXT NOT NULL,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER,
description TEXT,
description_es TEXT,
weight_kg REAL,
material TEXT,
is_discontinued BOOLEAN DEFAULT 0,
superseded_by_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES part_groups(id),
FOREIGN KEY (superseded_by_id) REFERENCES parts(id)
);
-- Fitment: qué partes van en qué vehículos
CREATE TABLE IF NOT EXISTS vehicle_parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_year_engine_id INTEGER NOT NULL,
part_id INTEGER NOT NULL,
quantity_required INTEGER DEFAULT 1,
position TEXT, -- e.g., 'front', 'rear', 'left', 'right'
fitment_notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id),
FOREIGN KEY (part_id) REFERENCES parts(id),
UNIQUE(model_year_engine_id, part_id, position)
);
-- Índices para el catálogo de partes
CREATE INDEX IF NOT EXISTS idx_part_categories_parent ON part_categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_part_categories_slug ON part_categories(slug);
CREATE INDEX IF NOT EXISTS idx_part_groups_category ON part_groups(category_id);
CREATE INDEX IF NOT EXISTS idx_parts_oem ON parts(oem_part_number);
CREATE INDEX IF NOT EXISTS idx_parts_group ON parts(group_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_mye ON vehicle_parts(model_year_engine_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_part ON vehicle_parts(part_id);
-- =====================================================
-- FASE 2: CROSS-REFERENCES Y AFTERMARKET
-- =====================================================
-- Fabricantes (OEM y aftermarket)
CREATE TABLE IF NOT EXISTS manufacturers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT CHECK(type IN ('oem', 'aftermarket', 'remanufactured')),
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium', 'oem')),
country TEXT,
logo_url TEXT,
website TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Partes aftermarket vinculadas a OEM
CREATE TABLE IF NOT EXISTS aftermarket_parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
oem_part_id INTEGER NOT NULL,
manufacturer_id INTEGER NOT NULL,
part_number TEXT NOT NULL,
name TEXT,
name_es TEXT,
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium')),
price_usd REAL,
warranty_months INTEGER,
in_stock BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (oem_part_id) REFERENCES parts(id),
FOREIGN KEY (manufacturer_id) REFERENCES manufacturers(id)
);
-- Cross-references (números alternativos)
CREATE TABLE IF NOT EXISTS part_cross_references (
id INTEGER PRIMARY KEY AUTOINCREMENT,
part_id INTEGER NOT NULL,
cross_reference_number TEXT NOT NULL,
reference_type TEXT CHECK(reference_type IN ('oem_alternate', 'supersession', 'interchange', 'competitor')),
source TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (part_id) REFERENCES parts(id)
);
-- Índices para FASE 2
CREATE INDEX IF NOT EXISTS idx_aftermarket_oem ON aftermarket_parts(oem_part_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_manufacturer ON aftermarket_parts(manufacturer_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_part_number ON aftermarket_parts(part_number);
CREATE INDEX IF NOT EXISTS idx_cross_ref_part ON part_cross_references(part_id);
CREATE INDEX IF NOT EXISTS idx_cross_ref_number ON part_cross_references(cross_reference_number);
-- =====================================================
-- FASE 3: DIAGRAMAS EXPLOSIONADOS
-- =====================================================
-- Diagramas de partes
CREATE TABLE IF NOT EXISTS diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER NOT NULL,
image_path TEXT NOT NULL,
thumbnail_path TEXT,
svg_content TEXT,
width INTEGER,
height INTEGER,
display_order INTEGER DEFAULT 0,
source TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES part_groups(id)
);
-- Diagramas específicos por vehículo (qué diagramas aplican a qué vehículos)
CREATE TABLE IF NOT EXISTS vehicle_diagrams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diagram_id INTEGER NOT NULL,
model_year_engine_id INTEGER NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (diagram_id) REFERENCES diagrams(id),
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id),
UNIQUE(diagram_id, model_year_engine_id)
);
-- Hotspots clickeables en diagramas
CREATE TABLE IF NOT EXISTS diagram_hotspots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diagram_id INTEGER NOT NULL,
part_id INTEGER,
callout_number INTEGER,
label TEXT,
shape TEXT DEFAULT 'rect' CHECK(shape IN ('rect', 'circle', 'poly')),
coords TEXT NOT NULL,
color TEXT DEFAULT '#e74c3c',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (diagram_id) REFERENCES diagrams(id),
FOREIGN KEY (part_id) REFERENCES parts(id)
);
-- Índices para FASE 3
CREATE INDEX IF NOT EXISTS idx_diagrams_group ON diagrams(group_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_diagrams_diagram ON vehicle_diagrams(diagram_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_diagrams_mye ON vehicle_diagrams(model_year_engine_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_diagram ON diagram_hotspots(diagram_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_part ON diagram_hotspots(part_id);
-- =====================================================
-- FASE 4: BÚSQUEDA FULL-TEXT Y VIN DECODER
-- =====================================================
-- Full-Text Search virtual table (SQLite FTS5)
CREATE VIRTUAL TABLE IF NOT EXISTS parts_fts USING fts5(
oem_part_number,
name,
name_es,
description,
description_es,
content='parts',
content_rowid='id'
);
-- Triggers para sincronización automática con parts table
CREATE TRIGGER IF NOT EXISTS parts_fts_insert AFTER INSERT ON parts BEGIN
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
VALUES (new.id, new.oem_part_number, new.name, new.name_es, new.description, new.description_es);
END;
CREATE TRIGGER IF NOT EXISTS parts_fts_delete AFTER DELETE ON parts BEGIN
INSERT INTO parts_fts(parts_fts, rowid, oem_part_number, name, name_es, description, description_es)
VALUES ('delete', old.id, old.oem_part_number, old.name, old.name_es, old.description, old.description_es);
END;
CREATE TRIGGER IF NOT EXISTS parts_fts_update AFTER UPDATE ON parts BEGIN
INSERT INTO parts_fts(parts_fts, rowid, oem_part_number, name, name_es, description, description_es)
VALUES ('delete', old.id, old.oem_part_number, old.name, old.name_es, old.description, old.description_es);
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
VALUES (new.id, new.oem_part_number, new.name, new.name_es, new.description, new.description_es);
END;
-- Cache de VINs decodificados
CREATE TABLE IF NOT EXISTS vin_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vin TEXT NOT NULL UNIQUE,
decoded_data TEXT NOT NULL,
make TEXT,
model TEXT,
year INTEGER,
engine_info TEXT,
body_class TEXT,
drive_type TEXT,
model_year_engine_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
FOREIGN KEY (model_year_engine_id) REFERENCES model_year_engine(id)
);
-- Índices para FASE 4
CREATE INDEX IF NOT EXISTS idx_vin_cache_vin ON vin_cache(vin);
CREATE INDEX IF NOT EXISTS idx_vin_cache_make_model ON vin_cache(make, model, year);

Binary file not shown.