Files
Autoparts-DB/dashboard/diagrams.html
consultoria-as 7b2a904498 feat: migrate to PostgreSQL + SQLAlchemy ORM, rebrand to Nexus Autoparts
- Migrate from SQLite to PostgreSQL with normalized schema
- Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission,
  materials, position_part, manufacture_type, quality_tier, countries,
  reference_type, shapes)
- Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries
- Rewrite console/db.py (27 methods) using SQLAlchemy ORM
- Add models.py with 27 SQLAlchemy model definitions
- Add config.py for centralized DB_URL configuration
- Add migrate_to_postgres.py migration script
- Add docs/METABASE_GUIDE.md with complete data entry guide
- Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS"
- Fill vehicle data gaps via NHTSA API + heuristics:
  engines (cylinders, power, torque), brands (country, founded_year),
  models (body_type, production years), MYE (drivetrain, transmission, trim)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 05:24:47 +00:00

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 - Nexus Autoparts</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>