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:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
|
||||
1686
dashboard/server.py
1686
dashboard/server.py
File diff suppressed because it is too large
Load Diff
65
dashboard/static/diagrams/brake_assembly.svg
Normal file
65
dashboard/static/diagrams/brake_assembly.svg
Normal 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 |
73
dashboard/static/diagrams/oil_filter_system.svg
Normal file
73
dashboard/static/diagrams/oil_filter_system.svg
Normal 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 |
84
dashboard/static/diagrams/suspension_assembly.svg
Normal file
84
dashboard/static/diagrams/suspension_assembly.svg
Normal 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 |
527
vehicle_database/docs/database_example_complete.sql
Normal file
527
vehicle_database/docs/database_example_complete.sql
Normal 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%';
|
||||
*/
|
||||
208
vehicle_database/docs/database_schema_diagram.md
Normal file
208
vehicle_database/docs/database_schema_diagram.md
Normal 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 │
|
||||
└────────────────┴─────────┴─────────────────────────────┴─────────┘
|
||||
```
|
||||
548
vehicle_database/scripts/populate_categories.py
Normal file
548
vehicle_database/scripts/populate_categories.py
Normal 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()
|
||||
574
vehicle_database/scripts/populate_fase2.py
Normal file
574
vehicle_database/scripts/populate_fase2.py
Normal 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()
|
||||
697
vehicle_database/scripts/populate_fase3.py
Normal file
697
vehicle_database/scripts/populate_fase3.py
Normal 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()
|
||||
312
vehicle_database/scripts/populate_fase4.py
Executable file
312
vehicle_database/scripts/populate_fase4.py
Executable 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()
|
||||
@@ -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.
Reference in New Issue
Block a user