// /home/Autopartes/pos/static/js/diagrams.js // Interactive exploded-view diagrams with zoom/pan and clickable hotspots. (function () { 'use strict'; var API = '/pos/api/diagrams'; var CATALOG_API = '/pos/api/catalog'; var token = localStorage.getItem('pos_token'); if (!token) { window.location.href = '/pos/login'; return; } var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; // ---- State ---- var state = { view: 'list', // 'list' | 'viewer' diagrams: [], categories: [], // grouped by category currentDiagram: null, hotspots: [], selectedHotspot: null, // zoom/pan scale: 1, panX: 0, panY: 0, isPanning: false, lastPointer: null, }; // ---- DOM refs ---- var diagramList = document.getElementById('diagramList'); var diagramViewer = document.getElementById('diagramViewer'); var svgContainer = document.getElementById('svgContainer'); var svgWrapper = document.getElementById('svgWrapper'); var backBtn = document.getElementById('backBtn'); var zoomInBtn = document.getElementById('zoomInBtn'); var zoomOutBtn = document.getElementById('zoomOutBtn'); var zoomResetBtn = document.getElementById('zoomResetBtn'); var diagramTitle = document.getElementById('diagramTitle'); var hotspotPanel = document.getElementById('hotspotPanel'); var hotspotBody = document.getElementById('hotspotBody'); var hotspotClose = document.getElementById('hotspotClose'); var partsListEl = document.getElementById('partsList'); var loadingEl = document.getElementById('loading'); var emptyEl = document.getElementById('emptyState'); var filterInput = document.getElementById('diagramFilter'); var profileAvatar = document.getElementById('profileAvatar'); var profileName = document.getElementById('profileName'); var profileRole = document.getElementById('profileRole'); // ---- Init ---- function init() { loadProfile(); loadAllDiagrams(); bindEvents(); } function loadProfile() { try { var u = JSON.parse(localStorage.getItem('pos_user') || '{}'); if (profileName) profileName.textContent = u.name || '--'; if (profileRole) profileRole.textContent = u.role || '--'; if (profileAvatar) { var n = (u.name || '--'); profileAvatar.textContent = n.split(' ').map(function(w){return w[0]||'';}).join('').substring(0,2).toUpperCase(); } } catch(e) {} } function bindEvents() { if (backBtn) backBtn.addEventListener('click', showList); if (zoomInBtn) zoomInBtn.addEventListener('click', function() { zoom(1.25); }); if (zoomOutBtn) zoomOutBtn.addEventListener('click', function() { zoom(0.8); }); if (zoomResetBtn) zoomResetBtn.addEventListener('click', resetZoom); if (hotspotClose) hotspotClose.addEventListener('click', closeHotspotPanel); if (filterInput) filterInput.addEventListener('input', renderList); // Zoom with mouse wheel on SVG container if (svgContainer) { svgContainer.addEventListener('wheel', function(e) { e.preventDefault(); var factor = e.deltaY < 0 ? 1.1 : 0.9; zoom(factor); }, { passive: false }); // Pan with mouse drag svgContainer.addEventListener('mousedown', startPan); svgContainer.addEventListener('mousemove', doPan); svgContainer.addEventListener('mouseup', endPan); svgContainer.addEventListener('mouseleave', endPan); // Touch support svgContainer.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { startPan({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY, preventDefault: function(){} }); } }); svgContainer.addEventListener('touchmove', function(e) { if (e.touches.length === 1) { e.preventDefault(); doPan({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }); } }, { passive: false }); svgContainer.addEventListener('touchend', endPan); } // Keyboard shortcuts document.addEventListener('keydown', function(e) { if (state.view !== 'viewer') return; if (e.key === 'Escape') { if (state.selectedHotspot) closeHotspotPanel(); else showList(); } if (e.key === '+' || e.key === '=') zoom(1.15); if (e.key === '-') zoom(0.87); if (e.key === '0') resetZoom(); }); } // ---- API calls ---- function apiFetch(url, cb) { loadingEl.style.display = 'flex'; emptyEl.style.display = 'none'; fetch(url, { headers: headers }) .then(function(r) { return r.json(); }) .then(function(data) { loadingEl.style.display = 'none'; cb(null, data); }) .catch(function(err) { loadingEl.style.display = 'none'; cb(err); }); } function loadAllDiagrams() { apiFetch(API + '/', function(err, data) { if (err || !data || !data.data) { emptyEl.style.display = 'flex'; return; } state.diagrams = data.data; // Group by category var cats = {}; data.data.forEach(function(d) { var key = d.category_name || 'Other'; if (!cats[key]) cats[key] = { name: key, name_es: d.category_name_es || key, diagrams: [] }; cats[key].diagrams.push(d); }); state.categories = Object.values(cats); renderList(); }); } function loadDiagram(id) { apiFetch(API + '/' + id, function(err, data) { if (err || !data || data.error) return; state.currentDiagram = data; state.hotspots = data.hotspots || []; showViewer(); }); } // ---- Render list view ---- function renderList() { state.view = 'list'; diagramViewer.style.display = 'none'; diagramList.style.display = 'block'; var filter = (filterInput && filterInput.value || '').toLowerCase(); var html = ''; state.categories.forEach(function(cat) { var filtered = cat.diagrams.filter(function(d) { if (!filter) return true; return (d.name_es || d.name || '').toLowerCase().indexOf(filter) !== -1 || (d.category_name_es || '').toLowerCase().indexOf(filter) !== -1 || (d.group_name_es || '').toLowerCase().indexOf(filter) !== -1; }); if (filtered.length === 0) return; html += '
'; html += '

' + esc(cat.name_es || cat.name) + '

'; html += '
'; filtered.forEach(function(d) { html += '
'; html += '
'; html += '' + esc(d.name_es || d.name) + ''; html += '
'; html += '
'; html += '
' + esc(d.name_es || d.name) + '
'; html += '
' + esc(d.group_name_es || d.group_name || '') + '
'; html += '
'; html += '
'; }); html += '
'; }); if (!html) { emptyEl.style.display = 'flex'; } else { emptyEl.style.display = 'none'; } diagramList.innerHTML = html; } // ---- Viewer ---- function showViewer() { state.view = 'viewer'; diagramList.style.display = 'none'; diagramViewer.style.display = 'flex'; closeHotspotPanel(); var d = state.currentDiagram; if (diagramTitle) diagramTitle.textContent = d.name_es || d.name; // Load SVG inline for interactivity resetZoom(); loadSVGInline(d.id_diagram); renderPartsList(); } function showList() { state.view = 'list'; state.currentDiagram = null; state.selectedHotspot = null; diagramViewer.style.display = 'none'; diagramList.style.display = 'block'; closeHotspotPanel(); } function loadSVGInline(diagramId) { svgWrapper.innerHTML = '
Cargando diagrama...
'; fetch(API + '/' + diagramId + '/svg', { headers: headers }) .then(function(r) { return r.text(); }) .then(function(svgText) { svgWrapper.innerHTML = svgText; var svg = svgWrapper.querySelector('svg'); if (svg) { svg.style.width = '100%'; svg.style.height = '100%'; svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); attachHotspotListeners(svg); } }) .catch(function() { svgWrapper.innerHTML = '
Error al cargar el diagrama
'; }); } function attachHotspotListeners(svg) { var areas = svg.querySelectorAll('[data-hotspot]'); areas.forEach(function(area) { var callout = parseInt(area.getAttribute('data-hotspot')); area.style.cursor = 'pointer'; area.addEventListener('click', function(e) { e.stopPropagation(); selectHotspot(callout); }); area.addEventListener('mouseenter', function() { highlightHotspot(callout, true); }); area.addEventListener('mouseleave', function() { highlightHotspot(callout, false); }); }); } function highlightHotspot(callout, on) { // Highlight in parts list var items = partsListEl.querySelectorAll('.part-item'); items.forEach(function(item) { if (parseInt(item.dataset.callout) === callout) { item.classList.toggle('is-highlighted', on); } }); } function selectHotspot(callout) { var hotspot = state.hotspots.find(function(h) { return h.callout_number === callout; }); if (!hotspot) return; state.selectedHotspot = hotspot; // Highlight in SVG var svg = svgWrapper.querySelector('svg'); if (svg) { svg.querySelectorAll('[data-hotspot]').forEach(function(area) { var num = parseInt(area.getAttribute('data-hotspot')); if (num === callout) { area.style.fill = 'rgba(245, 166, 35, 0.25)'; area.style.stroke = '#F5A623'; area.style.strokeWidth = '3'; } else { area.style.fill = 'transparent'; area.style.stroke = 'none'; } }); } // Highlight in parts list var items = partsListEl.querySelectorAll('.part-item'); items.forEach(function(item) { item.classList.toggle('is-active', parseInt(item.dataset.callout) === callout); }); // Show hotspot detail panel showHotspotDetail(hotspot); } function showHotspotDetail(hotspot) { hotspotPanel.classList.add('is-open'); var partName = hotspot.part_name_es || hotspot.part_name || 'Parte #' + hotspot.callout_number; var partNum = hotspot.part_number || ''; var desc = hotspot.description_es || hotspot.description || ''; // Determine search term for catalog link var searchTerm = partNum || partName; // Map callout to friendly part names for placeholder diagrams var placeholderNames = getPlaceholderPartInfo(hotspot.callout_number); var displayName = hotspot.part_name_es || hotspot.part_name || placeholderNames.name; var displayDesc = desc || placeholderNames.desc; var html = ''; html += '
'; html += '
' + hotspot.callout_number + '
'; html += '

' + esc(displayName) + '

'; if (partNum) { html += '
No. Parte: ' + esc(partNum) + '
'; } if (displayDesc) { html += '

' + esc(displayDesc) + '

'; } // Action buttons html += '
'; if (hotspot.part_id) { html += ''; html += ''; } else { html += ''; } html += '
'; html += '
'; hotspotBody.innerHTML = html; } function getPlaceholderPartInfo(callout) { // For built-in placeholder diagrams, provide friendly names var d = state.currentDiagram; if (!d) return { name: 'Parte ' + callout, desc: '' }; var brakeNames = { 1: { name: 'Disco de freno', desc: 'Disco ventilado de freno delantero. Se recomienda cambiar en pares.' }, 2: { name: 'Caliper de freno', desc: 'Caliper con pistones, incluye purga. Verificar compatibilidad con tipo de pastilla.' }, 3: { name: 'Pastillas de freno', desc: 'Juego de pastillas con indicador de desgaste. Material ceramico o semi-metalico.' }, 4: { name: 'Manguera de freno', desc: 'Manguera flexible de alta presion. Revisar por grietas cada 40,000 km.' }, 5: { name: 'Cilindro maestro', desc: 'Cilindro maestro con deposito de liquido de frenos. Incluye empaques.' }, }; var suspNames = { 1: { name: 'Amortiguador', desc: 'Amortiguador delantero de gas. Se recomienda cambiar en pares.' }, 2: { name: 'Resorte helicoidal', desc: 'Resorte de suspension delantera. Verificar altura libre.' }, 3: { name: 'Brazo de control', desc: 'Brazo inferior de control con bujes. Incluye herraje de montaje.' }, 4: { name: 'Rotula', desc: 'Rotula inferior de suspension. Incluye guardapolvo y seguros.' }, 5: { name: 'Barra de acoplamiento', desc: 'Barra de acoplamiento de direccion con terminales. Requiere alineacion.' }, }; var engineNames = { 1: { name: 'Filtro de aire', desc: 'Filtro de aire del motor. Cambiar cada 15,000-20,000 km.' }, 2: { name: 'Bujias', desc: 'Juego de bujias. Verificar tipo (platino, iridio) segun especificacion del motor.' }, 3: { name: 'Banda serpentina', desc: 'Banda de accesorios. Revisar tension y desgaste. Incluye alternador, A/C y direccion.' }, 4: { name: 'Junta de culata', desc: 'Junta de cabeza de cilindros. Material MLS multicapa. Requiere torque especifico.' }, 5: { name: 'Filtro de aceite', desc: 'Filtro de aceite del motor. Cambiar en cada servicio de aceite.' }, }; var name = (d.name || '').toLowerCase(); if (name.indexOf('brak') !== -1 || name.indexOf('freno') !== -1) return brakeNames[callout] || { name: 'Parte ' + callout, desc: '' }; if (name.indexOf('susp') !== -1) return suspNames[callout] || { name: 'Parte ' + callout, desc: '' }; if (name.indexOf('engine') !== -1 || name.indexOf('motor') !== -1) return engineNames[callout] || { name: 'Parte ' + callout, desc: '' }; return { name: 'Parte ' + callout, desc: '' }; } function closeHotspotPanel() { hotspotPanel.classList.remove('is-open'); state.selectedHotspot = null; // Clear SVG highlights var svg = svgWrapper.querySelector('svg'); if (svg) { svg.querySelectorAll('[data-hotspot]').forEach(function(area) { area.style.fill = 'transparent'; area.style.stroke = 'none'; }); } // Clear list highlights if (partsListEl) { partsListEl.querySelectorAll('.part-item').forEach(function(item) { item.classList.remove('is-active'); }); } } // ---- Parts list sidebar ---- function renderPartsList() { if (!partsListEl) return; var html = '

Partes en diagrama

'; state.hotspots.forEach(function(h) { var info = getPlaceholderPartInfo(h.callout_number); var name = h.part_name_es || h.part_name || info.name; var num = h.part_number || ''; html += '
'; html += '' + h.callout_number + ''; html += '
'; html += '
' + esc(name) + '
'; if (num) html += '
' + esc(num) + '
'; html += '
'; html += '
'; }); if (state.hotspots.length === 0) { html += '
Sin partes definidas
'; } partsListEl.innerHTML = html; } // ---- Zoom / Pan ---- function zoom(factor) { state.scale = Math.max(0.3, Math.min(5, state.scale * factor)); applyTransform(); } function resetZoom() { state.scale = 1; state.panX = 0; state.panY = 0; applyTransform(); } function applyTransform() { if (!svgWrapper) return; svgWrapper.style.transform = 'translate(' + state.panX + 'px, ' + state.panY + 'px) scale(' + state.scale + ')'; } function startPan(e) { // Only pan with left button and not on a hotspot if (e.button && e.button !== 0) return; state.isPanning = true; state.lastPointer = { x: e.clientX, y: e.clientY }; } function doPan(e) { if (!state.isPanning || !state.lastPointer) return; var dx = e.clientX - state.lastPointer.x; var dy = e.clientY - state.lastPointer.y; state.panX += dx; state.panY += dy; state.lastPointer = { x: e.clientX, y: e.clientY }; applyTransform(); } function endPan() { state.isPanning = false; state.lastPointer = null; } // ---- Actions ---- function viewPart(partId) { window.open('/pos/catalog?part=' + partId, '_blank'); } function searchPart(query) { window.open('/pos/catalog?search=' + encodeURIComponent(query), '_blank'); } function addToCart(partId) { // Reuse the catalog cart logic if available alert('Funcion disponible desde el catalogo. Busca la parte para agregarla al carrito.'); } // ---- Helpers ---- function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.appendChild(document.createTextNode(s)); return d.innerHTML; } // ---- Public API ---- window.DiagramsApp = { openDiagram: loadDiagram, selectHotspot: selectHotspot, viewPart: viewPart, searchPart: searchPart, addToCart: addToCart, }; // Init on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();