Backend (server.py): - Fix N+1 query in /api/diagrams/<id>/parts with batch cross-ref query - Add LIMIT safety nets to 15 endpoints (50-5000 per data type) - Add pagination to /api/vehicles, /api/model-year-engine, /api/vehicles/<id>/parts, /api/admin/export - Optimize search_vehicles() EXISTS subquery to JOIN - Restrict static route to /static/* subdir (security fix) - Add detailed=true support to /api/brands and /api/models Frontend: - Extract shared CSS into shared.css (variables, reset, buttons, forms, scrollbar) - Create shared nav.js component (logo + navigation links, auto-highlights) - Update all 4 HTML pages to use shared CSS and nav - Update JS to handle paginated API responses Data quality: - Fix cross-reference source field: map 72K records from catalog names to actual brands - Fix aftermarket_parts manufacturer_id: correct 8K records with wrong brand attribution - Delete 98MB backup file, orphan records, and garbage cross-references - Add import scripts for DAR, FRAM, WIX, MOOG, Cartek catalogs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1090 lines
40 KiB
HTML
1090 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Diagramas de Suspensión - AutoParts DB</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<link rel="stylesheet" href="/shared.css">
|
|
<style>
|
|
/* Main container */
|
|
.main-container {
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 5rem 2rem 2rem;
|
|
}
|
|
|
|
/* Search bar */
|
|
.search-section {
|
|
display: flex; gap: 1rem; margin-bottom: 2rem;
|
|
flex-wrap: wrap; align-items: center;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1; min-width: 250px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
|
color: var(--text-primary);
|
|
font-size: 0.95rem;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.search-input:focus { border-color: var(--accent); }
|
|
|
|
.search-wrapper {
|
|
position: relative; flex: 1; min-width: 250px;
|
|
}
|
|
|
|
.search-wrapper i {
|
|
position: absolute; left: 0.85rem; top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.filter-select {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 0.75rem 1rem;
|
|
color: var(--text-primary);
|
|
font-size: 0.9rem;
|
|
outline: none;
|
|
min-width: 160px;
|
|
}
|
|
|
|
.filter-select option { background: var(--bg-card); }
|
|
|
|
.type-filters {
|
|
display: flex; gap: 0.5rem;
|
|
}
|
|
|
|
.type-btn {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 1rem;
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.type-btn:hover { border-color: var(--accent); color: var(--text-primary); }
|
|
.type-btn.active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
/* Stats bar */
|
|
.stats-bar {
|
|
display: flex; gap: 2rem; margin-bottom: 1.5rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.stats-bar span { color: var(--accent); font-weight: 600; }
|
|
|
|
/* Diagram grid */
|
|
.diagram-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.diagram-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.diagram-card:hover {
|
|
border-color: var(--accent);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 30px rgba(0,0,0,0.4);
|
|
}
|
|
|
|
.diagram-card-img {
|
|
width: 100%;
|
|
aspect-ratio: 16/10;
|
|
object-fit: contain;
|
|
background: #f0f0f0;
|
|
display: block;
|
|
}
|
|
|
|
.diagram-card-body {
|
|
padding: 0.85rem 1rem;
|
|
}
|
|
|
|
.diagram-card-title {
|
|
font-family: 'Orbitron', monospace;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.diagram-card-sub {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.diagram-card-badge {
|
|
display: inline-block;
|
|
font-size: 0.7rem;
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 4px;
|
|
margin-top: 0.4rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-front { background: rgba(59,130,246,0.2); color: #60a5fa; }
|
|
.badge-steering { background: rgba(245,158,11,0.2); color: #fbbf24; }
|
|
.badge-rear { background: rgba(34,197,94,0.2); color: #4ade80; }
|
|
|
|
/* Viewer overlay */
|
|
.viewer-overlay {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.85);
|
|
z-index: 2000;
|
|
display: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.viewer-overlay.active {
|
|
display: flex;
|
|
opacity: 1;
|
|
}
|
|
|
|
.viewer-layout {
|
|
display: flex;
|
|
width: 100%; height: 100%;
|
|
}
|
|
|
|
/* Diagram panel (left) */
|
|
.viewer-diagram {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.viewer-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.75rem 1.25rem;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.viewer-toolbar .title {
|
|
font-family: 'Orbitron', monospace;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
flex: 1;
|
|
}
|
|
|
|
.viewer-toolbar .subtitle {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.toolbar-btn {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 0.75rem;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.toolbar-btn:hover {
|
|
border-color: var(--accent);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.viewer-img-container {
|
|
flex: 1;
|
|
overflow: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
background: #e8e8e8;
|
|
}
|
|
|
|
.viewer-img-container img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
transition: transform 0.3s ease;
|
|
cursor: grab;
|
|
}
|
|
|
|
.viewer-img-container img.zoomed {
|
|
max-width: none;
|
|
max-height: none;
|
|
cursor: move;
|
|
}
|
|
|
|
/* Parts panel (right) */
|
|
.viewer-parts {
|
|
width: 420px;
|
|
background: var(--bg-secondary);
|
|
border-left: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.parts-header {
|
|
padding: 0.85rem 1.25rem;
|
|
background: var(--bg-card);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.parts-header h3 {
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
flex: 1;
|
|
}
|
|
|
|
.parts-header .count {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
background: var(--bg-hover);
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.parts-search {
|
|
padding: 0.65rem 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.parts-search input {
|
|
width: 100%;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 0.75rem;
|
|
color: var(--text-primary);
|
|
font-size: 0.85rem;
|
|
outline: none;
|
|
}
|
|
|
|
.parts-search input:focus { border-color: var(--accent); }
|
|
|
|
.parts-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.part-item {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.part-item:hover, .part-item.highlighted {
|
|
border-color: var(--accent);
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.part-item .part-number {
|
|
font-family: 'Orbitron', monospace;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.part-item .part-name {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
margin: 0.2rem 0;
|
|
}
|
|
|
|
.part-item .part-group {
|
|
font-size: 0.7rem;
|
|
color: var(--text-secondary);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.xref-list {
|
|
margin-top: 0.4rem;
|
|
padding-top: 0.4rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.xref-item {
|
|
display: inline-block;
|
|
font-size: 0.7rem;
|
|
padding: 0.1rem 0.4rem;
|
|
background: rgba(59,130,246,0.15);
|
|
color: #60a5fa;
|
|
border-radius: 3px;
|
|
margin: 0.1rem;
|
|
}
|
|
|
|
.xref-label {
|
|
font-size: 0.65rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.2rem;
|
|
}
|
|
|
|
/* Loading / Empty states */
|
|
.loading-state, .empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 3rem;
|
|
color: var(--text-secondary);
|
|
text-align: center;
|
|
}
|
|
|
|
.loading-state i, .empty-state i {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.spinner {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
/* Pagination */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-top: 2rem;
|
|
padding-bottom: 2rem;
|
|
}
|
|
|
|
.page-btn {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 1rem;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.page-btn:hover { border-color: var(--accent); color: var(--text-primary); }
|
|
.page-btn.active { background: var(--accent); border-color: var(--accent); color: white; }
|
|
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.viewer-layout { flex-direction: column; }
|
|
.viewer-parts { width: 100%; height: 50%; }
|
|
.diagram-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
|
|
.search-section { flex-direction: column; }
|
|
}
|
|
|
|
/* Close button */
|
|
.close-btn {
|
|
position: absolute;
|
|
top: 0.75rem; right: 0.75rem;
|
|
background: rgba(0,0,0,0.6);
|
|
border: none;
|
|
color: white;
|
|
width: 36px; height: 36px;
|
|
border-radius: 50%;
|
|
font-size: 1.1rem;
|
|
cursor: pointer;
|
|
z-index: 10;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.close-btn:hover { background: var(--accent); }
|
|
|
|
/* Zoom controls */
|
|
.zoom-controls {
|
|
position: absolute;
|
|
bottom: 1rem; left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex; gap: 0.5rem;
|
|
background: rgba(0,0,0,0.6);
|
|
padding: 0.4rem;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.zoom-btn {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.4rem 0.75rem;
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.zoom-btn:hover { background: var(--accent); }
|
|
|
|
.zoom-level {
|
|
display: flex;
|
|
align-items: center;
|
|
color: var(--text-secondary);
|
|
font-size: 0.8rem;
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
/* Nav buttons in viewer */
|
|
.viewer-nav-btn {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: rgba(0,0,0,0.5);
|
|
border: none;
|
|
color: white;
|
|
width: 44px; height: 44px;
|
|
border-radius: 50%;
|
|
font-size: 1.2rem;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
z-index: 5;
|
|
}
|
|
|
|
.viewer-nav-btn:hover { background: var(--accent); }
|
|
.viewer-nav-btn.prev { left: 0.75rem; }
|
|
.viewer-nav-btn.next { right: 430px; }
|
|
|
|
@media (max-width: 768px) {
|
|
.viewer-nav-btn.next { right: 0.75rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Shared Navigation -->
|
|
<div id="shared-nav"></div>
|
|
<script src="/nav.js"></script>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-container">
|
|
|
|
<!-- Vehicle Selection -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h2 style="font-family: 'Orbitron', monospace; font-size: 1.3rem; margin-bottom: 1rem; color: var(--accent);">
|
|
<i class="fas fa-drafting-compass"></i> Diagramas de Suspensión y Dirección
|
|
</h2>
|
|
<p style="color: var(--text-secondary); margin-bottom: 1.25rem; font-size: 0.9rem;">
|
|
Selecciona tu vehículo para ver los diagramas MOOG disponibles, o usa "Ver Todos" para navegar la galería completa.
|
|
</p>
|
|
|
|
<div class="search-section">
|
|
<select class="filter-select" id="brandSelect" onchange="onBrandChange()">
|
|
<option value="">Marca</option>
|
|
</select>
|
|
<select class="filter-select" id="modelSelect" onchange="onModelChange()" disabled>
|
|
<option value="">Modelo</option>
|
|
</select>
|
|
<select class="filter-select" id="yearSelect" onchange="onYearChange()" disabled>
|
|
<option value="">Año</option>
|
|
</select>
|
|
<select class="filter-select" id="engineSelect" onchange="onEngineChange()" disabled>
|
|
<option value="">Motor</option>
|
|
</select>
|
|
<button class="type-btn" id="browseAllBtn" onclick="toggleBrowseAll()">
|
|
<i class="fas fa-th"></i> Ver Todos
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vehicle info bar -->
|
|
<div id="vehicleInfoBar" style="display: none;"></div>
|
|
|
|
<!-- Stats (shown in browse all mode) -->
|
|
<div class="stats-bar" id="statsBar" style="display: none;">
|
|
<div>Diagramas: <span id="diagramCount">0</span></div>
|
|
<div>Frontal: <span id="frontCount">0</span></div>
|
|
<div>Direccion: <span id="steeringCount">0</span></div>
|
|
<div>Trasera: <span id="rearCount">0</span></div>
|
|
</div>
|
|
|
|
<!-- Browse All search/filter (shown in browse mode) -->
|
|
<div id="browseAllControls" style="display: none;">
|
|
<div class="search-section">
|
|
<div class="search-wrapper">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" class="search-input" id="searchInput"
|
|
placeholder="Buscar por código (F200, S341, R004...)">
|
|
</div>
|
|
<div class="type-filters">
|
|
<button class="type-btn active" data-type="all">Todos</button>
|
|
<button class="type-btn" data-type="F"><i class="fas fa-car"></i> Delantera</button>
|
|
<button class="type-btn" data-type="S"><i class="fas fa-cog"></i> Dirección</button>
|
|
<button class="type-btn" data-type="R"><i class="fas fa-car-rear"></i> Trasera</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Diagram Grid -->
|
|
<div class="diagram-grid" id="diagramGrid">
|
|
<div class="empty-state" style="grid-column: 1/-1;">
|
|
<i class="fas fa-car" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
|
|
<p>Selecciona un vehículo arriba para ver sus diagramas</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="pagination" id="pagination"></div>
|
|
</div>
|
|
|
|
<!-- Viewer Overlay -->
|
|
<div class="viewer-overlay" id="viewerOverlay">
|
|
<button class="close-btn" id="closeViewer"><i class="fas fa-times"></i></button>
|
|
<button class="viewer-nav-btn prev" id="prevDiagram"><i class="fas fa-chevron-left"></i></button>
|
|
<button class="viewer-nav-btn next" id="nextDiagram"><i class="fas fa-chevron-right"></i></button>
|
|
|
|
<div class="viewer-layout">
|
|
<!-- Diagram Image -->
|
|
<div class="viewer-diagram" style="position: relative;">
|
|
<div class="viewer-toolbar">
|
|
<div>
|
|
<div class="title" id="viewerTitle">F200</div>
|
|
<div class="subtitle" id="viewerSubtitle">Suspension Delantera</div>
|
|
</div>
|
|
</div>
|
|
<div class="viewer-img-container" id="imgContainer">
|
|
<img id="viewerImg" src="" alt="Diagram">
|
|
</div>
|
|
<div class="zoom-controls">
|
|
<button class="zoom-btn" id="zoomOut"><i class="fas fa-minus"></i></button>
|
|
<span class="zoom-level" id="zoomLevel">100%</span>
|
|
<button class="zoom-btn" id="zoomIn"><i class="fas fa-plus"></i></button>
|
|
<button class="zoom-btn" id="zoomFit"><i class="fas fa-expand"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Parts Panel -->
|
|
<div class="viewer-parts">
|
|
<div class="parts-header">
|
|
<i class="fas fa-list-ul" style="color: var(--accent)"></i>
|
|
<h3>Partes del Diagrama</h3>
|
|
<span class="count" id="partsCount">0</span>
|
|
</div>
|
|
<div class="parts-search">
|
|
<input type="text" id="partsFilter" placeholder="Filtrar partes...">
|
|
</div>
|
|
<div class="parts-list" id="partsList">
|
|
<div class="loading-state">
|
|
<i class="fas fa-spinner spinner"></i>
|
|
<p>Selecciona un diagrama</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '';
|
|
let allDiagrams = [];
|
|
let filteredDiagrams = [];
|
|
let currentPage = 1;
|
|
const perPage = 48;
|
|
let currentViewerIdx = -1;
|
|
let currentZoom = 1;
|
|
let isDragging = false;
|
|
let dragStart = { x: 0, y: 0 };
|
|
let scrollStart = { x: 0, y: 0 };
|
|
let browseAllMode = false;
|
|
let selectedMYEId = null;
|
|
|
|
// --- Vehicle-first navigation ---
|
|
async function loadBrands() {
|
|
try {
|
|
const res = await fetch(`${API}/api/brands`);
|
|
const brands = await res.json();
|
|
const sel = document.getElementById('brandSelect');
|
|
brands.forEach(b => {
|
|
const opt = document.createElement('option');
|
|
opt.value = b;
|
|
opt.textContent = b;
|
|
sel.appendChild(opt);
|
|
});
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function onBrandChange() {
|
|
const brand = document.getElementById('brandSelect').value;
|
|
const modelSel = document.getElementById('modelSelect');
|
|
const yearSel = document.getElementById('yearSelect');
|
|
const engineSel = document.getElementById('engineSelect');
|
|
|
|
modelSel.innerHTML = '<option value="">Modelo</option>';
|
|
yearSel.innerHTML = '<option value="">Año</option>';
|
|
engineSel.innerHTML = '<option value="">Motor</option>';
|
|
modelSel.disabled = true;
|
|
yearSel.disabled = true;
|
|
engineSel.disabled = true;
|
|
selectedMYEId = null;
|
|
|
|
if (!brand) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/models?brand=${encodeURIComponent(brand)}`);
|
|
const models = await res.json();
|
|
models.forEach(m => {
|
|
const opt = document.createElement('option');
|
|
opt.value = m;
|
|
opt.textContent = m;
|
|
modelSel.appendChild(opt);
|
|
});
|
|
modelSel.disabled = false;
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function onModelChange() {
|
|
const brand = document.getElementById('brandSelect').value;
|
|
const model = document.getElementById('modelSelect').value;
|
|
const yearSel = document.getElementById('yearSelect');
|
|
const engineSel = document.getElementById('engineSelect');
|
|
|
|
yearSel.innerHTML = '<option value="">Año</option>';
|
|
engineSel.innerHTML = '<option value="">Motor</option>';
|
|
yearSel.disabled = true;
|
|
engineSel.disabled = true;
|
|
selectedMYEId = null;
|
|
|
|
if (!model) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/years?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`);
|
|
const years = await res.json();
|
|
years.forEach(y => {
|
|
const opt = document.createElement('option');
|
|
opt.value = y;
|
|
opt.textContent = y;
|
|
yearSel.appendChild(opt);
|
|
});
|
|
yearSel.disabled = false;
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function onYearChange() {
|
|
const brand = document.getElementById('brandSelect').value;
|
|
const model = document.getElementById('modelSelect').value;
|
|
const year = document.getElementById('yearSelect').value;
|
|
const engineSel = document.getElementById('engineSelect');
|
|
|
|
engineSel.innerHTML = '<option value="">Motor</option>';
|
|
engineSel.disabled = true;
|
|
selectedMYEId = null;
|
|
|
|
if (!year) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/engines?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}`);
|
|
const engines = await res.json();
|
|
engines.forEach(e => {
|
|
const opt = document.createElement('option');
|
|
opt.value = e;
|
|
opt.textContent = e;
|
|
engineSel.appendChild(opt);
|
|
});
|
|
engineSel.disabled = false;
|
|
|
|
// If only one engine, auto-select
|
|
if (engines.length === 1) {
|
|
engineSel.value = engines[0];
|
|
onEngineChange();
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function onEngineChange() {
|
|
const brand = document.getElementById('brandSelect').value;
|
|
const model = document.getElementById('modelSelect').value;
|
|
const year = document.getElementById('yearSelect').value;
|
|
const engine = document.getElementById('engineSelect').value;
|
|
|
|
if (!engine) { selectedMYEId = null; return; }
|
|
|
|
// Get mye_id
|
|
try {
|
|
const res = await fetch(`${API}/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}&with_parts=false&per_page=100`);
|
|
const result = await res.json();
|
|
const records = result.data || result;
|
|
const match = records.find(r => r.engine === engine);
|
|
|
|
if (match) {
|
|
selectedMYEId = match.id;
|
|
loadVehicleDiagrams(match.id, `${brand} ${model} ${year} - ${engine}`);
|
|
}
|
|
} catch(e) {
|
|
console.error('Error finding MYE:', e);
|
|
}
|
|
}
|
|
|
|
async function loadVehicleDiagrams(myeId, vehicleLabel) {
|
|
const grid = document.getElementById('diagramGrid');
|
|
grid.innerHTML = '<div class="loading-state" style="grid-column:1/-1"><i class="fas fa-spinner spinner" style="font-size:2rem"></i><p>Cargando diagramas...</p></div>';
|
|
|
|
// Hide browse-all controls
|
|
document.getElementById('browseAllControls').style.display = 'none';
|
|
document.getElementById('statsBar').style.display = 'none';
|
|
document.getElementById('pagination').innerHTML = '';
|
|
|
|
// Show vehicle info
|
|
const infoBar = document.getElementById('vehicleInfoBar');
|
|
infoBar.style.display = 'flex';
|
|
infoBar.className = 'stats-bar';
|
|
infoBar.innerHTML = `<div><i class="fas fa-car" style="color:var(--accent)"></i> <span>${vehicleLabel}</span></div>`;
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/vehicles/${myeId}/diagrams`);
|
|
const diagrams = await res.json();
|
|
|
|
filteredDiagrams = diagrams.map(d => ({
|
|
...d,
|
|
image_path: d.image_url ? d.image_url.replace(/^\//, '') : `static/diagrams/moog/${d.name}.jpg`
|
|
}));
|
|
|
|
if (filteredDiagrams.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><i class="fas fa-drafting-compass" style="font-size:2.5rem;margin-bottom:0.75rem;opacity:0.3"></i><p>No hay diagramas disponibles para este vehículo</p></div>';
|
|
return;
|
|
}
|
|
|
|
infoBar.innerHTML += `<div>Diagramas: <span>${filteredDiagrams.length}</span></div>`;
|
|
|
|
renderGrid();
|
|
} catch (e) {
|
|
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><i class="fas fa-exclamation-triangle"></i><p>Error cargando diagramas</p></div>';
|
|
}
|
|
}
|
|
|
|
function toggleBrowseAll() {
|
|
browseAllMode = !browseAllMode;
|
|
const btn = document.getElementById('browseAllBtn');
|
|
|
|
if (browseAllMode) {
|
|
btn.classList.add('active');
|
|
btn.innerHTML = '<i class="fas fa-car"></i> Por Vehículo';
|
|
document.getElementById('browseAllControls').style.display = 'block';
|
|
document.getElementById('statsBar').style.display = 'flex';
|
|
document.getElementById('vehicleInfoBar').style.display = 'none';
|
|
loadAllDiagrams();
|
|
} else {
|
|
btn.classList.remove('active');
|
|
btn.innerHTML = '<i class="fas fa-th"></i> Ver Todos';
|
|
document.getElementById('browseAllControls').style.display = 'none';
|
|
document.getElementById('statsBar').style.display = 'none';
|
|
document.getElementById('pagination').innerHTML = '';
|
|
|
|
const grid = document.getElementById('diagramGrid');
|
|
if (selectedMYEId) {
|
|
const brand = document.getElementById('brandSelect').value;
|
|
const model = document.getElementById('modelSelect').value;
|
|
const year = document.getElementById('yearSelect').value;
|
|
const engine = document.getElementById('engineSelect').value;
|
|
loadVehicleDiagrams(selectedMYEId, `${brand} ${model} ${year} - ${engine}`);
|
|
} else {
|
|
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><i class="fas fa-car" style="font-size:3rem;margin-bottom:1rem;opacity:0.3"></i><p>Selecciona un vehículo arriba para ver sus diagramas</p></div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadAllDiagrams() {
|
|
const grid = document.getElementById('diagramGrid');
|
|
grid.innerHTML = '<div class="loading-state" style="grid-column:1/-1"><i class="fas fa-spinner spinner" style="font-size:2rem"></i><p>Cargando todos los diagramas...</p></div>';
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/diagrams`);
|
|
const data = await res.json();
|
|
allDiagrams = data.filter(d => d.name && /^[FSR]\d{3}$/.test(d.name)).map(d => ({
|
|
...d,
|
|
image_path: `static/diagrams/moog/${d.name}.jpg`,
|
|
}));
|
|
applyBrowseFilters();
|
|
} catch(e) {
|
|
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Error cargando diagramas</p></div>';
|
|
}
|
|
}
|
|
|
|
function applyBrowseFilters() {
|
|
const searchEl = document.getElementById('searchInput');
|
|
const query = searchEl ? searchEl.value.toUpperCase().trim() : '';
|
|
const typeBtn = document.querySelector('#browseAllControls .type-btn.active');
|
|
const typeFilter = typeBtn ? typeBtn.dataset.type : 'all';
|
|
|
|
filteredDiagrams = allDiagrams.filter(d => {
|
|
const name = (d.name || '').toUpperCase();
|
|
if (typeFilter !== 'all' && !name.startsWith(typeFilter)) return false;
|
|
if (query && !name.includes(query) && !(d.name_es || '').toUpperCase().includes(query)) return false;
|
|
return true;
|
|
});
|
|
|
|
filteredDiagrams.sort((a, b) => {
|
|
const order = { F: 0, S: 1, R: 2 };
|
|
return (order[a.name[0]] ?? 3) - (order[b.name[0]] ?? 3) || a.name.localeCompare(b.name);
|
|
});
|
|
|
|
const front = filteredDiagrams.filter(d => d.name.startsWith('F')).length;
|
|
const steer = filteredDiagrams.filter(d => d.name.startsWith('S')).length;
|
|
const rear = filteredDiagrams.filter(d => d.name.startsWith('R')).length;
|
|
document.getElementById('diagramCount').textContent = filteredDiagrams.length;
|
|
document.getElementById('frontCount').textContent = front;
|
|
document.getElementById('steeringCount').textContent = steer;
|
|
document.getElementById('rearCount').textContent = rear;
|
|
|
|
currentPage = 1;
|
|
renderGrid();
|
|
}
|
|
|
|
// --- Render grid ---
|
|
function renderGrid() {
|
|
const grid = document.getElementById('diagramGrid');
|
|
const start = (currentPage - 1) * perPage;
|
|
const pageItems = filteredDiagrams.slice(start, start + perPage);
|
|
|
|
if (pageItems.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><i class="fas fa-search" style="font-size:2rem;margin-bottom:0.5rem;opacity:0.3"></i><p>No se encontraron diagramas</p></div>';
|
|
document.getElementById('pagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = pageItems.map((d, i) => {
|
|
const globalIdx = start + i;
|
|
const type = (d.name || '')[0];
|
|
const badgeClass = type === 'F' ? 'badge-front' : type === 'S' ? 'badge-steering' : 'badge-rear';
|
|
const badgeText = type === 'F' ? 'Delantera' : type === 'S' ? 'Dirección' : 'Trasera';
|
|
const imgSrc = d.image_url ? d.image_url : (d.image_path || `static/diagrams/moog/${d.name}.jpg`);
|
|
|
|
return `
|
|
<div class="diagram-card" onclick="openViewer(${globalIdx})">
|
|
<img class="diagram-card-img" src="${imgSrc}" alt="${d.name}" loading="lazy"
|
|
onerror="this.style.background='#333';this.style.minHeight='150px'">
|
|
<div class="diagram-card-body">
|
|
<div class="diagram-card-title">${d.name}</div>
|
|
<div class="diagram-card-sub">${d.name_es || d.group_name || ''}</div>
|
|
<span class="diagram-card-badge ${badgeClass}">${badgeText}</span>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
renderPagination();
|
|
}
|
|
|
|
function renderPagination() {
|
|
const totalPages = Math.ceil(filteredDiagrams.length / perPage);
|
|
const pag = document.getElementById('pagination');
|
|
if (totalPages <= 1) { pag.innerHTML = ''; return; }
|
|
|
|
let html = `<button class="page-btn" onclick="goPage(${currentPage-1})" ${currentPage<=1?'disabled':''}><i class="fas fa-chevron-left"></i></button>`;
|
|
const start = Math.max(1, currentPage - 3);
|
|
const end = Math.min(totalPages, currentPage + 3);
|
|
if (start > 1) {
|
|
html += `<button class="page-btn" onclick="goPage(1)">1</button>`;
|
|
if (start > 2) html += `<span style="color:var(--text-secondary);padding:0 0.5rem">...</span>`;
|
|
}
|
|
for (let p = start; p <= end; p++) {
|
|
html += `<button class="page-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`;
|
|
}
|
|
if (end < totalPages) {
|
|
if (end < totalPages-1) html += `<span style="color:var(--text-secondary);padding:0 0.5rem">...</span>`;
|
|
html += `<button class="page-btn" onclick="goPage(${totalPages})">${totalPages}</button>`;
|
|
}
|
|
html += `<button class="page-btn" onclick="goPage(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}><i class="fas fa-chevron-right"></i></button>`;
|
|
pag.innerHTML = html;
|
|
}
|
|
|
|
function goPage(p) {
|
|
const totalPages = Math.ceil(filteredDiagrams.length / perPage);
|
|
if (p < 1 || p > totalPages) return;
|
|
currentPage = p;
|
|
renderGrid();
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
// --- Viewer ---
|
|
function openViewer(idx) {
|
|
currentViewerIdx = idx;
|
|
const d = filteredDiagrams[idx];
|
|
if (!d) return;
|
|
|
|
const overlay = document.getElementById('viewerOverlay');
|
|
overlay.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
loadDiagramInViewer(d);
|
|
}
|
|
|
|
function loadDiagramInViewer(d) {
|
|
const type = (d.name || '')[0];
|
|
const typeLabel = type === 'F' ? 'Suspensión Delantera' : type === 'S' ? 'Dirección' : 'Suspensión Trasera';
|
|
|
|
document.getElementById('viewerTitle').textContent = d.name;
|
|
document.getElementById('viewerSubtitle').textContent = d.name_es || typeLabel;
|
|
|
|
const img = document.getElementById('viewerImg');
|
|
img.src = d.image_url ? d.image_url : (d.image_path || `static/diagrams/moog/${d.name}.jpg`);
|
|
img.classList.remove('zoomed');
|
|
currentZoom = 1;
|
|
img.style.transform = '';
|
|
document.getElementById('zoomLevel').textContent = '100%';
|
|
|
|
const myeParam = selectedMYEId ? `?mye_id=${selectedMYEId}` : '';
|
|
loadDiagramParts(d.id, myeParam);
|
|
}
|
|
|
|
async function loadDiagramParts(diagramId, queryParams) {
|
|
const list = document.getElementById('partsList');
|
|
list.innerHTML = '<div class="loading-state"><i class="fas fa-spinner spinner"></i><p>Cargando partes...</p></div>';
|
|
document.getElementById('partsCount').textContent = '...';
|
|
|
|
try {
|
|
const res = await fetch(`${API}/api/diagrams/${diagramId}/parts${queryParams || ''}`);
|
|
const parts = await res.json();
|
|
|
|
document.getElementById('partsCount').textContent = parts.length;
|
|
|
|
if (parts.length === 0) {
|
|
list.innerHTML = '<div class="empty-state"><i class="fas fa-box-open"></i><p>No hay partes vinculadas</p></div>';
|
|
return;
|
|
}
|
|
|
|
const grouped = {};
|
|
parts.forEach(p => {
|
|
const g = p.group_name_es || p.group_name || 'Otros';
|
|
if (!grouped[g]) grouped[g] = [];
|
|
grouped[g].push(p);
|
|
});
|
|
|
|
let html = '';
|
|
for (const [group, groupParts] of Object.entries(grouped)) {
|
|
html += `<div style="font-size:0.75rem;color:var(--accent);padding:0.5rem 0.25rem 0.25rem;font-weight:600;">${group}</div>`;
|
|
for (const p of groupParts) {
|
|
let xrefHtml = '';
|
|
if (p.cross_references && p.cross_references.length > 0) {
|
|
xrefHtml = `<div class="xref-list"><div class="xref-label">Refs:</div>${p.cross_references.map(x => `<span class="xref-item">${x.number}</span>`).join('')}</div>`;
|
|
}
|
|
html += `<div class="part-item" data-part-id="${p.id}"><div class="part-number">${p.part_number}</div><div class="part-name">${p.name_es || p.name || ''}</div>${xrefHtml}</div>`;
|
|
}
|
|
}
|
|
list.innerHTML = html;
|
|
} catch (e) {
|
|
list.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>Error cargando partes</p></div>';
|
|
}
|
|
}
|
|
|
|
function closeViewer() {
|
|
document.getElementById('viewerOverlay').classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
currentViewerIdx = -1;
|
|
}
|
|
|
|
function navigateViewer(delta) {
|
|
const newIdx = currentViewerIdx + delta;
|
|
if (newIdx < 0 || newIdx >= filteredDiagrams.length) return;
|
|
currentViewerIdx = newIdx;
|
|
loadDiagramInViewer(filteredDiagrams[newIdx]);
|
|
}
|
|
|
|
function setZoom(level) {
|
|
currentZoom = Math.max(0.25, Math.min(4, level));
|
|
const img = document.getElementById('viewerImg');
|
|
if (currentZoom !== 1) {
|
|
img.classList.add('zoomed');
|
|
img.style.transform = `scale(${currentZoom})`;
|
|
} else {
|
|
img.classList.remove('zoomed');
|
|
img.style.transform = '';
|
|
}
|
|
document.getElementById('zoomLevel').textContent = `${Math.round(currentZoom * 100)}%`;
|
|
}
|
|
|
|
// --- Pan (drag) ---
|
|
const imgContainer = document.getElementById('imgContainer');
|
|
imgContainer.addEventListener('mousedown', (e) => {
|
|
if (currentZoom <= 1) return;
|
|
isDragging = true;
|
|
dragStart = { x: e.clientX, y: e.clientY };
|
|
scrollStart = { x: imgContainer.scrollLeft, y: imgContainer.scrollTop };
|
|
imgContainer.style.cursor = 'grabbing';
|
|
});
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
imgContainer.scrollLeft = scrollStart.x - (e.clientX - dragStart.x);
|
|
imgContainer.scrollTop = scrollStart.y - (e.clientY - dragStart.y);
|
|
});
|
|
window.addEventListener('mouseup', () => { isDragging = false; imgContainer.style.cursor = ''; });
|
|
|
|
// Parts filter
|
|
document.getElementById('partsFilter').addEventListener('input', (e) => {
|
|
const q = e.target.value.toLowerCase();
|
|
document.querySelectorAll('.part-item').forEach(el => {
|
|
el.style.display = el.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
// Browse-all search & type filter listeners
|
|
document.getElementById('searchInput')?.addEventListener('input', () => { if (browseAllMode) applyBrowseFilters(); });
|
|
document.querySelectorAll('#browseAllControls .type-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('#browseAllControls .type-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
if (browseAllMode) applyBrowseFilters();
|
|
});
|
|
});
|
|
|
|
// Viewer controls
|
|
document.getElementById('closeViewer').addEventListener('click', closeViewer);
|
|
document.getElementById('prevDiagram').addEventListener('click', () => navigateViewer(-1));
|
|
document.getElementById('nextDiagram').addEventListener('click', () => navigateViewer(1));
|
|
document.getElementById('zoomIn').addEventListener('click', () => setZoom(currentZoom + 0.25));
|
|
document.getElementById('zoomOut').addEventListener('click', () => setZoom(currentZoom - 0.25));
|
|
document.getElementById('zoomFit').addEventListener('click', () => setZoom(1));
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
const overlay = document.getElementById('viewerOverlay');
|
|
if (overlay.classList.contains('active')) {
|
|
if (e.key === 'Escape') closeViewer();
|
|
if (e.key === 'ArrowLeft') navigateViewer(-1);
|
|
if (e.key === 'ArrowRight') navigateViewer(1);
|
|
if (e.key === '+' || e.key === '=') setZoom(currentZoom + 0.25);
|
|
if (e.key === '-') setZoom(currentZoom - 0.25);
|
|
}
|
|
});
|
|
|
|
imgContainer.addEventListener('wheel', (e) => {
|
|
if (!document.getElementById('viewerOverlay').classList.contains('active')) return;
|
|
e.preventDefault();
|
|
setZoom(currentZoom + (e.deltaY > 0 ? -0.15 : 0.15));
|
|
});
|
|
|
|
// Initialize
|
|
loadBrands();
|
|
</script>
|
|
</body>
|
|
</html>
|