fix: performance improvements, shared UI, and cross-reference data quality
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>
This commit is contained in:
@@ -50,14 +50,15 @@ class VehicleDashboard {
|
||||
|
||||
if (brandsRes.ok && vehiclesRes.ok) {
|
||||
const brands = await brandsRes.json();
|
||||
const vehicles = await vehiclesRes.json();
|
||||
const vehiclesData = await vehiclesRes.json();
|
||||
const vehicles = vehiclesData.data || vehiclesData;
|
||||
|
||||
// Contar modelos únicos
|
||||
const uniqueModels = new Set(vehicles.map(v => `${v.brand}-${v.model}`));
|
||||
|
||||
this.stats.brands = brands.length;
|
||||
this.stats.models = uniqueModels.size;
|
||||
this.stats.vehicles = vehicles.length;
|
||||
this.stats.vehicles = vehiclesData.pagination ? vehiclesData.pagination.total : vehicles.length;
|
||||
|
||||
const brandsEl = document.getElementById('totalBrands');
|
||||
const modelsEl = document.getElementById('totalModels');
|
||||
@@ -300,29 +301,18 @@ class VehicleDashboard {
|
||||
`;
|
||||
|
||||
try {
|
||||
const [brandsRes, vehiclesRes] = await Promise.all([
|
||||
fetch('/api/brands'),
|
||||
fetch('/api/vehicles')
|
||||
]);
|
||||
const brandsRes = await fetch('/api/brands?detailed=true');
|
||||
|
||||
if (!brandsRes.ok || !vehiclesRes.ok) {
|
||||
if (!brandsRes.ok) {
|
||||
throw new Error('Error al cargar datos');
|
||||
}
|
||||
|
||||
const brands = await brandsRes.json();
|
||||
const vehicles = await vehiclesRes.json();
|
||||
|
||||
// Contar modelos y vehículos por marca
|
||||
// Build brandStats from detailed response
|
||||
const brandStats = {};
|
||||
brands.forEach(brand => {
|
||||
brandStats[brand] = { models: new Set(), vehicles: 0 };
|
||||
});
|
||||
|
||||
vehicles.forEach(v => {
|
||||
if (brandStats[v.brand]) {
|
||||
brandStats[v.brand].models.add(v.model);
|
||||
brandStats[v.brand].vehicles++;
|
||||
}
|
||||
brands.forEach(b => {
|
||||
brandStats[b.name] = { models: { size: b.model_count }, vehicles: b.vehicle_count };
|
||||
});
|
||||
|
||||
if (brands.length === 0) {
|
||||
@@ -337,17 +327,17 @@ class VehicleDashboard {
|
||||
}
|
||||
|
||||
container.innerHTML = `<div class="content-grid brands-grid">
|
||||
${brands.map(brand => `
|
||||
<div class="brand-card" onclick="dashboard.goToModels('${brand}')">
|
||||
${brands.map(b => `
|
||||
<div class="brand-card" onclick="dashboard.goToModels('${b.name}')">
|
||||
<div class="brand-icon">
|
||||
<i class="fas fa-car"></i>
|
||||
</div>
|
||||
<div class="brand-name">${brand}</div>
|
||||
<div class="brand-name">${b.name}</div>
|
||||
<div class="brand-count">
|
||||
${brandStats[brand].models.size} modelos
|
||||
${b.model_count} modelos
|
||||
</div>
|
||||
<div class="brand-count">
|
||||
${brandStats[brand].vehicles} vehículos
|
||||
${b.vehicle_count} vehículos
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
@@ -386,31 +376,13 @@ class VehicleDashboard {
|
||||
`;
|
||||
|
||||
try {
|
||||
const [modelsRes, vehiclesRes] = await Promise.all([
|
||||
fetch(`/api/models?brand=${encodeURIComponent(brand)}`),
|
||||
fetch(`/api/vehicles?brand=${encodeURIComponent(brand)}`)
|
||||
]);
|
||||
const modelsRes = await fetch(`/api/models?brand=${encodeURIComponent(brand)}&detailed=true`);
|
||||
|
||||
if (!modelsRes.ok || !vehiclesRes.ok) {
|
||||
if (!modelsRes.ok) {
|
||||
throw new Error('Error al cargar datos');
|
||||
}
|
||||
|
||||
const models = await modelsRes.json();
|
||||
const vehicles = await vehiclesRes.json();
|
||||
|
||||
// Contar vehículos y años por modelo
|
||||
const modelStats = {};
|
||||
models.forEach(model => {
|
||||
modelStats[model] = { years: new Set(), vehicles: 0, engines: new Set() };
|
||||
});
|
||||
|
||||
vehicles.forEach(v => {
|
||||
if (modelStats[v.model]) {
|
||||
modelStats[v.model].years.add(v.year);
|
||||
modelStats[v.model].vehicles++;
|
||||
modelStats[v.model].engines.add(v.engine);
|
||||
}
|
||||
});
|
||||
|
||||
if (models.length === 0) {
|
||||
container.innerHTML = `
|
||||
@@ -427,26 +399,22 @@ class VehicleDashboard {
|
||||
}
|
||||
|
||||
container.innerHTML = `<div class="content-grid models-grid">
|
||||
${models.map(model => {
|
||||
const stats = modelStats[model];
|
||||
const yearsArray = Array.from(stats.years).sort((a, b) => b - a);
|
||||
const yearRange = yearsArray.length > 0
|
||||
? (yearsArray.length > 1
|
||||
? `${yearsArray[yearsArray.length - 1]} - ${yearsArray[0]}`
|
||||
: `${yearsArray[0]}`)
|
||||
: 'N/A';
|
||||
${models.map(m => {
|
||||
const yearRange = m.year_count > 1
|
||||
? `${m.year_min} - ${m.year_max}`
|
||||
: `${m.year_min}`;
|
||||
|
||||
return `
|
||||
<div class="model-card" onclick="dashboard.goToVehicles('${brand}', '${model}')">
|
||||
<div class="model-name">${model}</div>
|
||||
<div class="model-card" onclick="dashboard.goToVehicles('${brand}', '${m.name}')">
|
||||
<div class="model-name">${m.name}</div>
|
||||
<div class="model-info">
|
||||
<i class="fas fa-calendar-alt"></i> ${yearRange}
|
||||
</div>
|
||||
<div class="model-info">
|
||||
<i class="fas fa-cogs"></i> ${stats.engines.size} motores
|
||||
<i class="fas fa-cogs"></i> ${m.engine_count} motores
|
||||
</div>
|
||||
<div class="model-info">
|
||||
<i class="fas fa-list"></i> ${stats.vehicles} variantes
|
||||
<i class="fas fa-list"></i> ${m.vehicle_count} variantes
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -491,16 +459,18 @@ class VehicleDashboard {
|
||||
try {
|
||||
// Fetch both vehicles info and model_year_engine IDs
|
||||
const [vehiclesRes, myeRes] = await Promise.all([
|
||||
fetch(`/api/vehicles?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`),
|
||||
fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`)
|
||||
fetch(`/api/vehicles?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&per_page=100`),
|
||||
fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&per_page=100`)
|
||||
]);
|
||||
|
||||
if (!vehiclesRes.ok || !myeRes.ok) {
|
||||
throw new Error('Error al cargar vehículos');
|
||||
}
|
||||
|
||||
const vehicles = await vehiclesRes.json();
|
||||
const myeRecords = await myeRes.json();
|
||||
const vehiclesData = await vehiclesRes.json();
|
||||
const myeData = await myeRes.json();
|
||||
const vehicles = vehiclesData.data || vehiclesData;
|
||||
const myeRecords = myeData.data || myeData;
|
||||
|
||||
// Merge mye_id into vehicles based on matching fields
|
||||
// Only keep vehicles that have a matching mye_id (i.e., have parts)
|
||||
@@ -911,7 +881,24 @@ class VehicleDashboard {
|
||||
}
|
||||
|
||||
const groups = await response.json();
|
||||
this.displayGroups(groups, categoryId);
|
||||
|
||||
// Fetch diagrams for Suspension (11) or Steering (10) when vehicle is selected
|
||||
let vehicleDiagrams = [];
|
||||
if (this.selectedVehicleId && (categoryId === 10 || categoryId === 11)) {
|
||||
try {
|
||||
const diagRes = await fetch(`/api/vehicles/${this.selectedVehicleId}/diagrams/by-category?category_id=${categoryId}`);
|
||||
if (diagRes.ok) {
|
||||
const catGroups = await diagRes.json();
|
||||
for (const cg of catGroups) {
|
||||
vehicleDiagrams.push(...cg.diagrams);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading diagrams for strip:', e);
|
||||
}
|
||||
}
|
||||
|
||||
this.displayGroups(groups, categoryId, vehicleDiagrams);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@@ -928,10 +915,10 @@ class VehicleDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
displayGroups(groups, categoryId) {
|
||||
displayGroups(groups, categoryId, vehicleDiagrams = []) {
|
||||
const container = document.getElementById('mainContent');
|
||||
|
||||
if (groups.length === 0) {
|
||||
if (groups.length === 0 && vehicleDiagrams.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
@@ -944,8 +931,42 @@ class VehicleDashboard {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build diagram strip HTML if diagrams are available
|
||||
let diagramStripHtml = '';
|
||||
if (vehicleDiagrams.length > 0) {
|
||||
// Store diagram list for the viewer
|
||||
this._currentDiagramList = vehicleDiagrams;
|
||||
|
||||
diagramStripHtml = `
|
||||
<div class="diagrams-strip">
|
||||
<div class="diagrams-strip-header">
|
||||
<h5><i class="fas fa-drafting-compass"></i> Diagramas MOOG para tu vehículo</h5>
|
||||
<span class="strip-badge">${vehicleDiagrams.length} diagrama${vehicleDiagrams.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="diagrams-strip-scroll">
|
||||
${vehicleDiagrams.map((d, idx) => {
|
||||
const type = (d.name || '')[0];
|
||||
const typeLabel = type === 'F' ? 'Delantera' : type === 'S' ? 'Dirección' : type === 'R' ? 'Trasera' : '';
|
||||
const imgSrc = d.image_url || '/static/diagrams/moog/' + d.name + '.jpg';
|
||||
return `
|
||||
<div class="strip-card" onclick="dashboard.openDiagramViewer(${d.id}, ${idx})"
|
||||
title="${d.name_es || d.name}">
|
||||
<img class="strip-card-img" src="${imgSrc}" alt="${d.name}"
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none';this.parentElement.querySelector('.strip-card-body').style.paddingTop='3rem'">
|
||||
<div class="strip-card-body">
|
||||
<div class="strip-card-title">${d.name}</div>
|
||||
<div class="strip-card-type">${typeLabel}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<h4 class="mb-3">${this.selectedCategory.name_es || this.selectedCategory.name}</h4>
|
||||
${diagramStripHtml}
|
||||
<div class="content-grid categories-grid">
|
||||
${groups.map(group => `
|
||||
<div class="category-card">
|
||||
@@ -1602,6 +1623,305 @@ class VehicleDashboard {
|
||||
wrapper.style.transform = `scale(${this.currentDiagramZoom})`;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// FASE 6: Full-screen Diagram Viewer (split layout)
|
||||
// ================================================================
|
||||
|
||||
openDiagramViewer(diagramId, indexInList) {
|
||||
this._dvCurrentIndex = typeof indexInList === 'number' ? indexInList : -1;
|
||||
this._dvDiagramList = this._currentDiagramList || [];
|
||||
this._dvZoom = 1;
|
||||
this._dvDragging = false;
|
||||
|
||||
const overlay = document.getElementById('diagramViewerOverlay');
|
||||
overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
this._loadDiagramInViewer(diagramId);
|
||||
this._bindDiagramViewerEvents();
|
||||
}
|
||||
|
||||
closeDiagramViewer() {
|
||||
const overlay = document.getElementById('diagramViewerOverlay');
|
||||
overlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
this._unbindDiagramViewerEvents();
|
||||
}
|
||||
|
||||
async _loadDiagramInViewer(diagramId) {
|
||||
const titleEl = document.getElementById('dvTitle');
|
||||
const subtitleEl = document.getElementById('dvSubtitle');
|
||||
const imgWrapper = document.getElementById('dvImgWrapper');
|
||||
const img = document.getElementById('dvImg');
|
||||
const partsList = document.getElementById('dvPartsList');
|
||||
const partsCount = document.getElementById('dvPartsCount');
|
||||
|
||||
// Show loading in parts
|
||||
partsList.innerHTML = '<div style="text-align:center;padding:3rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin" style="font-size:1.5rem"></i><p style="margin-top:0.5rem">Cargando...</p></div>';
|
||||
partsCount.textContent = '...';
|
||||
|
||||
try {
|
||||
// Fetch diagram detail + parts in parallel
|
||||
const [diagRes, partsRes] = await Promise.all([
|
||||
fetch(`/api/diagrams/${diagramId}`),
|
||||
fetch(`/api/diagrams/${diagramId}/parts${this.selectedVehicleId ? '?mye_id=' + this.selectedVehicleId : ''}`)
|
||||
]);
|
||||
|
||||
const diagram = await diagRes.json();
|
||||
const parts = await partsRes.json();
|
||||
|
||||
// Update title
|
||||
const type = (diagram.name || '')[0];
|
||||
const typeLabel = type === 'F' ? 'Suspensión Delantera' : type === 'S' ? 'Dirección' : type === 'R' ? 'Suspensión Trasera' : diagram.group_name || '';
|
||||
titleEl.textContent = diagram.name || 'Diagrama';
|
||||
subtitleEl.textContent = diagram.name_es || typeLabel;
|
||||
|
||||
// Update image
|
||||
const imgSrc = diagram.image_url || (diagram.image_path ? '/' + diagram.image_path : '');
|
||||
img.src = imgSrc;
|
||||
img.alt = diagram.name_es || diagram.name;
|
||||
this._dvZoom = 1;
|
||||
imgWrapper.style.transform = '';
|
||||
imgWrapper.classList.remove('zoomed');
|
||||
document.getElementById('dvZoomLevel').textContent = '100%';
|
||||
|
||||
// Render hotspots on image
|
||||
this._renderViewerHotspots(diagram.hotspots || [], imgWrapper);
|
||||
|
||||
// Render parts list
|
||||
this._renderViewerParts(parts, diagram.hotspots || []);
|
||||
|
||||
// Update nav button states
|
||||
const prevBtn = document.getElementById('dvPrevBtn');
|
||||
const nextBtn = document.getElementById('dvNextBtn');
|
||||
prevBtn.disabled = this._dvCurrentIndex <= 0;
|
||||
nextBtn.disabled = this._dvCurrentIndex < 0 || this._dvCurrentIndex >= this._dvDiagramList.length - 1;
|
||||
prevBtn.style.opacity = prevBtn.disabled ? '0.3' : '1';
|
||||
nextBtn.style.opacity = nextBtn.disabled ? '0.3' : '1';
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error loading diagram in viewer:', e);
|
||||
partsList.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-exclamation-triangle" style="font-size:1.5rem;color:#f59e0b"></i><p style="margin-top:0.5rem">Error cargando diagrama</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
_renderViewerHotspots(hotspots, wrapper) {
|
||||
// Remove existing hotspot markers
|
||||
wrapper.querySelectorAll('.hotspot-marker').forEach(el => el.remove());
|
||||
|
||||
if (!hotspots || hotspots.length === 0) return;
|
||||
|
||||
hotspots.forEach((hs, idx) => {
|
||||
// coords stored as "x%,y%" (percentage-based)
|
||||
const coords = (hs.coords || '').split(',');
|
||||
if (coords.length < 2) return;
|
||||
|
||||
const xPct = parseFloat(coords[0]);
|
||||
const yPct = parseFloat(coords[1]);
|
||||
if (isNaN(xPct) || isNaN(yPct)) return;
|
||||
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'hotspot-marker pulse';
|
||||
marker.style.left = xPct + '%';
|
||||
marker.style.top = yPct + '%';
|
||||
marker.dataset.partId = hs.part_id || '';
|
||||
marker.dataset.callout = hs.callout_number || (idx + 1);
|
||||
marker.title = hs.part_name || hs.label || 'Parte ' + (idx + 1);
|
||||
marker.innerHTML = `<span class="hotspot-number">${hs.callout_number || (idx + 1)}</span>`;
|
||||
|
||||
marker.addEventListener('click', () => {
|
||||
this._highlightPartInList(hs.part_id);
|
||||
// Highlight this marker
|
||||
wrapper.querySelectorAll('.hotspot-marker').forEach(m => m.classList.remove('active'));
|
||||
marker.classList.add('active');
|
||||
});
|
||||
|
||||
wrapper.appendChild(marker);
|
||||
});
|
||||
}
|
||||
|
||||
_renderViewerParts(parts, hotspots) {
|
||||
const listEl = document.getElementById('dvPartsList');
|
||||
const countEl = document.getElementById('dvPartsCount');
|
||||
|
||||
countEl.textContent = parts.length;
|
||||
|
||||
if (!parts || parts.length === 0) {
|
||||
listEl.innerHTML = '<div style="text-align:center;padding:3rem;color:var(--text-secondary)"><i class="fas fa-box-open" style="font-size:2rem;margin-bottom:0.5rem"></i><p>No hay partes vinculadas</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a hotspot lookup by part_id
|
||||
const hotspotMap = {};
|
||||
(hotspots || []).forEach((hs, idx) => {
|
||||
if (hs.part_id) hotspotMap[hs.part_id] = hs.callout_number || (idx + 1);
|
||||
});
|
||||
|
||||
// Group by group_name
|
||||
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 class="dv-group-label">${group}</div>`;
|
||||
for (const p of groupParts) {
|
||||
const callout = hotspotMap[p.id];
|
||||
let xrefHtml = '';
|
||||
if (p.cross_references && p.cross_references.length > 0) {
|
||||
xrefHtml = `<div class="dv-xref-list">${p.cross_references.map(x => `<span class="dv-xref-tag">${x.number}</span>`).join('')}</div>`;
|
||||
}
|
||||
html += `
|
||||
<div class="dv-part-item" data-part-id="${p.id}" onclick="dashboard._onViewerPartClick(${p.id})">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem">
|
||||
${callout ? `<span style="background:var(--accent);color:white;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.65rem;font-weight:700;flex-shrink:0">${callout}</span>` : ''}
|
||||
<div class="dv-part-number">${p.part_number || p.oem_part_number}</div>
|
||||
</div>
|
||||
<div class="dv-part-name">${p.name_es || p.name || ''}</div>
|
||||
${xrefHtml}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
listEl.innerHTML = html;
|
||||
}
|
||||
|
||||
_highlightPartInList(partId) {
|
||||
if (!partId) return;
|
||||
const listEl = document.getElementById('dvPartsList');
|
||||
listEl.querySelectorAll('.dv-part-item').forEach(el => el.classList.remove('highlighted'));
|
||||
const target = listEl.querySelector(`.dv-part-item[data-part-id="${partId}"]`);
|
||||
if (target) {
|
||||
target.classList.add('highlighted');
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
_onViewerPartClick(partId) {
|
||||
// Highlight in list
|
||||
this._highlightPartInList(partId);
|
||||
|
||||
// Highlight matching hotspot on image
|
||||
const wrapper = document.getElementById('dvImgWrapper');
|
||||
wrapper.querySelectorAll('.hotspot-marker').forEach(m => {
|
||||
m.classList.remove('active');
|
||||
if (m.dataset.partId == partId) {
|
||||
m.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_dvNavigate(delta) {
|
||||
const newIdx = this._dvCurrentIndex + delta;
|
||||
if (newIdx < 0 || newIdx >= this._dvDiagramList.length) return;
|
||||
this._dvCurrentIndex = newIdx;
|
||||
const d = this._dvDiagramList[newIdx];
|
||||
if (d) this._loadDiagramInViewer(d.id);
|
||||
}
|
||||
|
||||
_dvSetZoom(level) {
|
||||
this._dvZoom = Math.max(0.25, Math.min(4, level));
|
||||
const wrapper = document.getElementById('dvImgWrapper');
|
||||
if (this._dvZoom !== 1) {
|
||||
wrapper.classList.add('zoomed');
|
||||
wrapper.style.transform = `scale(${this._dvZoom})`;
|
||||
} else {
|
||||
wrapper.classList.remove('zoomed');
|
||||
wrapper.style.transform = '';
|
||||
}
|
||||
document.getElementById('dvZoomLevel').textContent = `${Math.round(this._dvZoom * 100)}%`;
|
||||
}
|
||||
|
||||
_bindDiagramViewerEvents() {
|
||||
// Avoid duplicate bindings
|
||||
if (this._dvBound) return;
|
||||
this._dvBound = true;
|
||||
|
||||
this._dvHandlers = {
|
||||
close: () => this.closeDiagramViewer(),
|
||||
prev: () => this._dvNavigate(-1),
|
||||
next: () => this._dvNavigate(1),
|
||||
zoomIn: () => this._dvSetZoom(this._dvZoom + 0.25),
|
||||
zoomOut: () => this._dvSetZoom(this._dvZoom - 0.25),
|
||||
zoomFit: () => this._dvSetZoom(1),
|
||||
keydown: (e) => {
|
||||
const overlay = document.getElementById('diagramViewerOverlay');
|
||||
if (!overlay.classList.contains('active')) return;
|
||||
if (e.key === 'Escape') this.closeDiagramViewer();
|
||||
if (e.key === 'ArrowLeft') this._dvNavigate(-1);
|
||||
if (e.key === 'ArrowRight') this._dvNavigate(1);
|
||||
if (e.key === '+' || e.key === '=') this._dvSetZoom(this._dvZoom + 0.25);
|
||||
if (e.key === '-') this._dvSetZoom(this._dvZoom - 0.25);
|
||||
},
|
||||
wheel: (e) => {
|
||||
const overlay = document.getElementById('diagramViewerOverlay');
|
||||
if (!overlay.classList.contains('active')) return;
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.15 : 0.15;
|
||||
this._dvSetZoom(this._dvZoom + delta);
|
||||
},
|
||||
partsFilter: (e) => {
|
||||
const q = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('#dvPartsList .dv-part-item').forEach(el => {
|
||||
el.style.display = el.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||||
});
|
||||
},
|
||||
mousedown: (e) => {
|
||||
if (this._dvZoom <= 1) return;
|
||||
this._dvDragging = true;
|
||||
this._dvDragStart = { x: e.clientX, y: e.clientY };
|
||||
const container = document.getElementById('dvImgContainer');
|
||||
this._dvScrollStart = { x: container.scrollLeft, y: container.scrollTop };
|
||||
container.style.cursor = 'grabbing';
|
||||
},
|
||||
mousemove: (e) => {
|
||||
if (!this._dvDragging) return;
|
||||
const container = document.getElementById('dvImgContainer');
|
||||
container.scrollLeft = this._dvScrollStart.x - (e.clientX - this._dvDragStart.x);
|
||||
container.scrollTop = this._dvScrollStart.y - (e.clientY - this._dvDragStart.y);
|
||||
},
|
||||
mouseup: () => {
|
||||
this._dvDragging = false;
|
||||
const container = document.getElementById('dvImgContainer');
|
||||
if (container) container.style.cursor = '';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('dvCloseBtn').addEventListener('click', this._dvHandlers.close);
|
||||
document.getElementById('dvPrevBtn').addEventListener('click', this._dvHandlers.prev);
|
||||
document.getElementById('dvNextBtn').addEventListener('click', this._dvHandlers.next);
|
||||
document.getElementById('dvZoomIn').addEventListener('click', this._dvHandlers.zoomIn);
|
||||
document.getElementById('dvZoomOut').addEventListener('click', this._dvHandlers.zoomOut);
|
||||
document.getElementById('dvZoomFit').addEventListener('click', this._dvHandlers.zoomFit);
|
||||
document.getElementById('dvPartsFilter').addEventListener('input', this._dvHandlers.partsFilter);
|
||||
document.addEventListener('keydown', this._dvHandlers.keydown);
|
||||
document.getElementById('dvImgContainer').addEventListener('wheel', this._dvHandlers.wheel, { passive: false });
|
||||
document.getElementById('dvImgContainer').addEventListener('mousedown', this._dvHandlers.mousedown);
|
||||
window.addEventListener('mousemove', this._dvHandlers.mousemove);
|
||||
window.addEventListener('mouseup', this._dvHandlers.mouseup);
|
||||
}
|
||||
|
||||
_unbindDiagramViewerEvents() {
|
||||
if (!this._dvBound) return;
|
||||
this._dvBound = false;
|
||||
|
||||
document.getElementById('dvCloseBtn')?.removeEventListener('click', this._dvHandlers.close);
|
||||
document.getElementById('dvPrevBtn')?.removeEventListener('click', this._dvHandlers.prev);
|
||||
document.getElementById('dvNextBtn')?.removeEventListener('click', this._dvHandlers.next);
|
||||
document.getElementById('dvZoomIn')?.removeEventListener('click', this._dvHandlers.zoomIn);
|
||||
document.getElementById('dvZoomOut')?.removeEventListener('click', this._dvHandlers.zoomOut);
|
||||
document.getElementById('dvZoomFit')?.removeEventListener('click', this._dvHandlers.zoomFit);
|
||||
document.getElementById('dvPartsFilter')?.removeEventListener('input', this._dvHandlers.partsFilter);
|
||||
document.removeEventListener('keydown', this._dvHandlers.keydown);
|
||||
document.getElementById('dvImgContainer')?.removeEventListener('wheel', this._dvHandlers.wheel);
|
||||
document.getElementById('dvImgContainer')?.removeEventListener('mousedown', this._dvHandlers.mousedown);
|
||||
window.removeEventListener('mousemove', this._dvHandlers.mousemove);
|
||||
window.removeEventListener('mouseup', this._dvHandlers.mouseup);
|
||||
}
|
||||
|
||||
// FASE 4: Open VIN decoder modal
|
||||
openVinDecoder() {
|
||||
// Clear previous results
|
||||
|
||||
Reference in New Issue
Block a user