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:
@@ -5,76 +5,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Panel - Autopartes DB</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/shared.css">
|
||||
<style>
|
||||
/* Admin-specific variable overrides */
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-tertiary: #1a1a25;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #8888aa;
|
||||
--accent: #ff6b35;
|
||||
--accent-hover: #ff8555;
|
||||
--success: #00d68f;
|
||||
--warning: #ffaa00;
|
||||
--danger: #ff4444;
|
||||
--border: #2a2a3a;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-nav a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-nav a:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
@@ -257,39 +201,7 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Forms - admin-specific */
|
||||
select.form-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -451,28 +363,6 @@
|
||||
.badge-premium { background: #5a5a2a; color: #ffff7f; }
|
||||
.badge-oem { background: #2a2a5a; color: #7f7fff; }
|
||||
|
||||
/* Alert */
|
||||
.alert {
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(0, 214, 143, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(255, 68, 68, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
@@ -701,21 +591,12 @@
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<a href="/" class="logo">AUTOPARTES DB</a>
|
||||
<nav class="header-nav">
|
||||
<a href="/">Catálogo</a>
|
||||
<a href="/customer-landing.html">Landing</a>
|
||||
<a href="/admin.html" style="color: var(--accent);">Admin</a>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- Shared Navigation -->
|
||||
<div id="shared-nav"></div>
|
||||
<script src="/nav.js"></script>
|
||||
|
||||
<div class="container">
|
||||
<!-- Sidebar -->
|
||||
@@ -768,6 +649,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>Diagramas</h3>
|
||||
<div class="sidebar-item" data-section="diagrams">
|
||||
<span class="icon">📐</span>
|
||||
<span>Hotspot Editor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>Importar/Exportar</h3>
|
||||
<div class="sidebar-item" data-section="import">
|
||||
@@ -1237,6 +1126,87 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Diagrams / Hotspot Editor Section -->
|
||||
<section id="section-diagrams" class="admin-section">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Editor de Hotspots</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Busca un diagrama por código y haz clic en la imagen para agregar hotspots vinculados a partes.
|
||||
</p>
|
||||
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
|
||||
<input type="text" class="form-input" id="diagramSearchInput"
|
||||
placeholder="Buscar diagrama (ej: F200, S341...)"
|
||||
style="max-width: 300px;"
|
||||
onkeypress="if(event.key==='Enter') searchDiagramsAdmin()">
|
||||
<button class="btn btn-primary" onclick="searchDiagramsAdmin()">
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagram search results grid -->
|
||||
<div id="diagramSearchResults" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;"></div>
|
||||
|
||||
<!-- Hotspot Editor Area (shown when a diagram is selected) -->
|
||||
<div id="hotspotEditorArea" style="display: none;">
|
||||
<div class="card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h2 id="hotspotEditorTitle" style="margin: 0; font-size: 1.1rem;">Diagrama</h2>
|
||||
<button class="btn btn-secondary" onclick="closeHotspotEditor()">Cerrar Editor</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<!-- Image with click-to-place -->
|
||||
<div style="flex: 1; min-width: 400px; position: relative; background: #f0f0f0; border-radius: 8px; overflow: hidden; cursor: crosshair;" id="hotspotImageContainer">
|
||||
<img id="hotspotEditorImg" src="" alt="Diagram"
|
||||
style="width: 100%; display: block;"
|
||||
onclick="onHotspotImageClick(event)">
|
||||
<div id="hotspotMarkersContainer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hotspot form + list -->
|
||||
<div style="width: 320px; flex-shrink: 0;">
|
||||
<h3 style="font-size: 0.95rem; margin-bottom: 0.75rem;">Agregar / Editar Hotspot</h3>
|
||||
<form id="hotspotForm" style="margin-bottom: 1rem;">
|
||||
<input type="hidden" id="hsEditId">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Posición (x%, y%)</label>
|
||||
<input type="text" class="form-input" id="hsCoords" placeholder="Clic en imagen..." readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label"># Callout</label>
|
||||
<input type="number" class="form-input" id="hsCallout" min="1" value="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Parte OEM (buscar)</label>
|
||||
<input type="text" class="form-input" id="hsPartSearch"
|
||||
placeholder="Buscar parte por nombre o #..."
|
||||
oninput="searchPartsForHotspot(this.value)">
|
||||
<select class="form-input" id="hsPartSelect" size="4" style="margin-top: 0.25rem; display: none;">
|
||||
</select>
|
||||
<input type="hidden" id="hsPartId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Etiqueta</label>
|
||||
<input type="text" class="form-input" id="hsLabel" placeholder="Opcional">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="button" class="btn btn-primary" onclick="saveHotspot()">Guardar</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="clearHotspotForm()">Limpiar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3 style="font-size: 0.95rem; margin-bottom: 0.5rem;">Hotspots Existentes</h3>
|
||||
<div id="hotspotsList" style="max-height: 300px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -115,6 +115,9 @@ function showSection(sectionId) {
|
||||
case 'fitment':
|
||||
loadFitment();
|
||||
break;
|
||||
case 'diagrams':
|
||||
// Just show section, user uses search
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,11 +1178,12 @@ async function loadVehiclesForSelect(selectId) {
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/model-year-engine');
|
||||
const vehicles = await response.json();
|
||||
const response = await fetch('/api/model-year-engine?per_page=100');
|
||||
const result = await response.json();
|
||||
const vehicles = result.data || result;
|
||||
|
||||
select.innerHTML = '<option value="">Selecciona vehículo...</option>' +
|
||||
vehicles.slice(0, 100).map(v =>
|
||||
vehicles.map(v =>
|
||||
`<option value="${v.id}">${v.brand} ${v.model} ${v.year} - ${v.engine}</option>`
|
||||
).join('');
|
||||
} catch (e) {
|
||||
@@ -1558,8 +1562,9 @@ async function loadBulkEngines() {
|
||||
const engines = await response.json();
|
||||
|
||||
// Get MYE IDs for each engine
|
||||
const myeResponse = await fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}`);
|
||||
const myeData = await myeResponse.json();
|
||||
const myeResponse = await fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}&per_page=100`);
|
||||
const myeResult = await myeResponse.json();
|
||||
const myeData = myeResult.data || myeResult;
|
||||
|
||||
engineSelect.innerHTML = '<option value="">Selecciona motor...</option>' +
|
||||
myeData.map(mye => `<option value="${mye.id}">${mye.engine}</option>`).join('');
|
||||
@@ -1707,3 +1712,258 @@ showSection = function(sectionId) {
|
||||
initBulkEditor();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Diagram Hotspot Editor
|
||||
// ============================================================================
|
||||
|
||||
let currentEditorDiagramId = null;
|
||||
let currentEditorHotspots = [];
|
||||
let partSearchTimeout = null;
|
||||
|
||||
async function searchDiagramsAdmin() {
|
||||
const q = document.getElementById('diagramSearchInput').value.trim();
|
||||
const container = document.getElementById('diagramSearchResults');
|
||||
|
||||
if (!q) {
|
||||
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1">Ingresa un código de diagrama para buscar</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1"><i class="fas fa-spinner fa-spin"></i> Buscando...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/diagrams/search?q=${encodeURIComponent(q)}`);
|
||||
const diagrams = await res.json();
|
||||
|
||||
if (diagrams.length === 0) {
|
||||
container.innerHTML = '<p style="color:var(--text-secondary);grid-column:1/-1">No se encontraron diagramas</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = diagrams.map(d => {
|
||||
const imgSrc = d.image_path ? '/' + d.image_path : `/static/diagrams/moog/${d.name}.jpg`;
|
||||
return `
|
||||
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color 0.2s"
|
||||
onclick="openHotspotEditor(${d.id})"
|
||||
onmouseover="this.style.borderColor='var(--accent)'"
|
||||
onmouseout="this.style.borderColor='var(--border)'">
|
||||
<img src="${imgSrc}" alt="${d.name}" style="width:100%;height:120px;object-fit:contain;background:#f0f0f0;display:block"
|
||||
onerror="this.style.display='none'">
|
||||
<div style="padding:0.5rem 0.65rem">
|
||||
<div style="font-weight:600;color:var(--accent)">${d.name}</div>
|
||||
<div style="font-size:0.8rem;color:var(--text-secondary)">${d.name_es || d.source || ''}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<p style="color:#e74c3c;grid-column:1/-1">Error al buscar diagramas</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function openHotspotEditor(diagramId) {
|
||||
currentEditorDiagramId = diagramId;
|
||||
document.getElementById('hotspotEditorArea').style.display = 'block';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/diagrams/${diagramId}`);
|
||||
const diagram = await res.json();
|
||||
|
||||
document.getElementById('hotspotEditorTitle').textContent = `${diagram.name} - ${diagram.name_es || diagram.group_name || ''}`;
|
||||
|
||||
const imgSrc = diagram.image_url || (diagram.image_path ? '/' + diagram.image_path : '');
|
||||
document.getElementById('hotspotEditorImg').src = imgSrc;
|
||||
|
||||
currentEditorHotspots = diagram.hotspots || [];
|
||||
renderEditorHotspots();
|
||||
clearHotspotForm();
|
||||
|
||||
// Auto-set next callout number
|
||||
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
|
||||
document.getElementById('hsCallout').value = maxCallout + 1;
|
||||
|
||||
// Scroll to editor
|
||||
document.getElementById('hotspotEditorArea').scrollIntoView({ behavior: 'smooth' });
|
||||
} catch (e) {
|
||||
showAlert('Error al cargar diagrama', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeHotspotEditor() {
|
||||
document.getElementById('hotspotEditorArea').style.display = 'none';
|
||||
currentEditorDiagramId = null;
|
||||
currentEditorHotspots = [];
|
||||
}
|
||||
|
||||
function onHotspotImageClick(event) {
|
||||
const img = event.target;
|
||||
const rect = img.getBoundingClientRect();
|
||||
const xPct = ((event.clientX - rect.left) / rect.width * 100).toFixed(2);
|
||||
const yPct = ((event.clientY - rect.top) / rect.height * 100).toFixed(2);
|
||||
|
||||
document.getElementById('hsCoords').value = `${xPct},${yPct}`;
|
||||
|
||||
// Show temporary marker
|
||||
renderEditorHotspots();
|
||||
const container = document.getElementById('hotspotMarkersContainer');
|
||||
const tempMarker = document.createElement('div');
|
||||
tempMarker.style.cssText = `position:absolute;left:${xPct}%;top:${yPct}%;width:24px;height:24px;border-radius:50%;background:rgba(46,204,113,0.5);border:2px solid #2ecc71;transform:translate(-50%,-50%);pointer-events:none;z-index:10`;
|
||||
container.appendChild(tempMarker);
|
||||
}
|
||||
|
||||
function renderEditorHotspots() {
|
||||
const container = document.getElementById('hotspotMarkersContainer');
|
||||
const list = document.getElementById('hotspotsList');
|
||||
|
||||
// Markers on image
|
||||
container.innerHTML = currentEditorHotspots.map(h => {
|
||||
const coords = (h.coords || '').split(',');
|
||||
if (coords.length < 2) return '';
|
||||
return `<div style="position:absolute;left:${coords[0]}%;top:${coords[1]}%;width:24px;height:24px;border-radius:50%;background:rgba(231,76,60,0.4);border:2px solid #e74c3c;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;font-size:0.6rem;font-weight:700;color:white;pointer-events:auto;cursor:pointer" onclick="editHotspot(${h.id})" title="${h.label || h.part_name || ''}">${h.callout_number || ''}</div>`;
|
||||
}).join('');
|
||||
|
||||
// List
|
||||
if (currentEditorHotspots.length === 0) {
|
||||
list.innerHTML = '<p style="color:var(--text-secondary);font-size:0.85rem">No hay hotspots</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = currentEditorHotspots.map(h => `
|
||||
<div style="background:var(--bg-hover);border:1px solid var(--border);border-radius:6px;padding:0.5rem;margin-bottom:0.4rem;display:flex;align-items:center;gap:0.5rem">
|
||||
<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">${h.callout_number || '?'}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:0.82rem;font-weight:500">${h.part_name || h.label || 'Sin parte'}</div>
|
||||
<div style="font-size:0.72rem;color:var(--text-secondary)">${h.part_number || ''} | ${h.coords}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" style="padding:0.2rem 0.5rem;font-size:0.75rem" onclick="editHotspot(${h.id})">Editar</button>
|
||||
<button class="btn" style="padding:0.2rem 0.5rem;font-size:0.75rem;background:#e74c3c;color:white;border:none;border-radius:4px;cursor:pointer" onclick="deleteHotspot(${h.id})">Borrar</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function editHotspot(hotspotId) {
|
||||
const hs = currentEditorHotspots.find(h => h.id === hotspotId);
|
||||
if (!hs) return;
|
||||
|
||||
document.getElementById('hsEditId').value = hs.id;
|
||||
document.getElementById('hsCoords').value = hs.coords || '';
|
||||
document.getElementById('hsCallout').value = hs.callout_number || '';
|
||||
document.getElementById('hsLabel').value = hs.label || '';
|
||||
document.getElementById('hsPartId').value = hs.part_id || '';
|
||||
document.getElementById('hsPartSearch').value = hs.part_name ? `${hs.part_number} - ${hs.part_name}` : '';
|
||||
}
|
||||
|
||||
function clearHotspotForm() {
|
||||
document.getElementById('hsEditId').value = '';
|
||||
document.getElementById('hsCoords').value = '';
|
||||
document.getElementById('hsLabel').value = '';
|
||||
document.getElementById('hsPartId').value = '';
|
||||
document.getElementById('hsPartSearch').value = '';
|
||||
document.getElementById('hsPartSelect').style.display = 'none';
|
||||
|
||||
// Keep callout at next number
|
||||
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
|
||||
document.getElementById('hsCallout').value = maxCallout + 1;
|
||||
}
|
||||
|
||||
async function searchPartsForHotspot(query) {
|
||||
clearTimeout(partSearchTimeout);
|
||||
const select = document.getElementById('hsPartSelect');
|
||||
|
||||
if (!query || query.length < 2) {
|
||||
select.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
partSearchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/parts?search=${encodeURIComponent(query)}&per_page=20`);
|
||||
const data = await res.json();
|
||||
const parts = data.data || data;
|
||||
|
||||
if (parts.length === 0) {
|
||||
select.innerHTML = '<option disabled>Sin resultados</option>';
|
||||
} else {
|
||||
select.innerHTML = parts.map(p =>
|
||||
`<option value="${p.id}">${p.oem_part_number} - ${p.name_es || p.name}</option>`
|
||||
).join('');
|
||||
}
|
||||
select.style.display = 'block';
|
||||
|
||||
select.onchange = function() {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
document.getElementById('hsPartId').value = opt.value;
|
||||
document.getElementById('hsPartSearch').value = opt.textContent;
|
||||
select.style.display = 'none';
|
||||
};
|
||||
} catch (e) {
|
||||
select.innerHTML = '<option disabled>Error buscando</option>';
|
||||
select.style.display = 'block';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function saveHotspot() {
|
||||
const editId = document.getElementById('hsEditId').value;
|
||||
const coords = document.getElementById('hsCoords').value.trim();
|
||||
const callout = parseInt(document.getElementById('hsCallout').value) || null;
|
||||
const partId = parseInt(document.getElementById('hsPartId').value) || null;
|
||||
const label = document.getElementById('hsLabel').value.trim();
|
||||
|
||||
if (!coords) {
|
||||
showAlert('Haz clic en la imagen para seleccionar posición', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
diagram_id: currentEditorDiagramId,
|
||||
coords: coords,
|
||||
callout_number: callout,
|
||||
part_id: partId,
|
||||
label: label,
|
||||
shape: 'circle',
|
||||
color: '#e74c3c'
|
||||
};
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (editId) {
|
||||
res = await fetch(`/api/admin/hotspots/${editId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/admin/hotspots', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
if (!res.ok) throw new Error(result.error || 'Error al guardar');
|
||||
|
||||
showAlert(editId ? 'Hotspot actualizado' : 'Hotspot creado');
|
||||
|
||||
// Reload diagram to refresh hotspots
|
||||
await openHotspotEditor(currentEditorDiagramId);
|
||||
} catch (e) {
|
||||
showAlert(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHotspot(hotspotId) {
|
||||
if (!confirm('Eliminar este hotspot?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/hotspots/${hotspotId}`, { method: 'DELETE' });
|
||||
const result = await res.json();
|
||||
if (!res.ok) throw new Error(result.error || 'Error al eliminar');
|
||||
|
||||
showAlert('Hotspot eliminado');
|
||||
await openHotspotEditor(currentEditorDiagramId);
|
||||
} catch (e) {
|
||||
showAlert(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,125 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AutoParts DB - Tienda de Autopartes</title>
|
||||
<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="/shared.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-card: #1a1a24;
|
||||
--bg-hover: #252532;
|
||||
--accent: #ff6b35;
|
||||
--accent-hover: #ff8555;
|
||||
--accent-glow: rgba(255, 107, 53, 0.3);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a0b0;
|
||||
--border: #2a2a3a;
|
||||
--success: #22c55e;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: rgba(18, 18, 26, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
transition: color 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-links a:hover,
|
||||
.nav-links a.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-links a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.nav-links a:hover::after,
|
||||
.nav-links a.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-links a.admin-link {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.nav-links a.admin-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Landing page-specific header extras */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -165,29 +49,34 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.7rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.9rem;
|
||||
/* Footer logo (reuses .logo classes) */
|
||||
.footer .logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.footer .logo-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px var(--accent-glow);
|
||||
.footer .logo-text {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
@@ -1060,30 +949,23 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<a href="customer-landing.html" class="logo">
|
||||
<div class="logo-icon">⚙️</div>
|
||||
<div class="logo-text">AUTOPARTS DB</div>
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="customer-landing.html" class="active">Inicio</a>
|
||||
<a href="index.html">Catálogo</a>
|
||||
<a href="#brands-section">Marcas</a>
|
||||
<a href="#featured-section">Productos</a>
|
||||
<a href="#cta-section">Contacto</a>
|
||||
<a href="admin.html" class="admin-link">⚡ Admin</a>
|
||||
</nav>
|
||||
<div class="header-actions">
|
||||
<button class="search-btn" onclick="openSearchModal()">🔍</button>
|
||||
<button class="cart-btn">
|
||||
🛒
|
||||
<span class="cart-badge" id="cart-count">0</span>
|
||||
</button>
|
||||
<a href="index.html" class="btn btn-primary">Dashboard</a>
|
||||
<button class="mobile-menu-btn">☰</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Shared Navigation -->
|
||||
<div id="shared-nav"></div>
|
||||
<script src="/nav.js"></script>
|
||||
<script>
|
||||
// Inject landing-page-specific header extras (search, cart, dashboard btn)
|
||||
(function() {
|
||||
var extra = document.getElementById('shared-nav-extra');
|
||||
if (!extra) return;
|
||||
extra.innerHTML = ''
|
||||
+ '<div class="header-actions">'
|
||||
+ '<button class="search-btn" onclick="openSearchModal()">\uD83D\uDD0D</button>'
|
||||
+ '<button class="cart-btn">\uD83D\uDED2<span class="cart-badge" id="cart-count">0</span></button>'
|
||||
+ '<a href="index.html" class="btn btn-primary">Dashboard</a>'
|
||||
+ '<button class="mobile-menu-btn">\u2630</button>'
|
||||
+ '</div>';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div class="search-modal" id="searchModal" onclick="closeSearchModal(event)">
|
||||
|
||||
@@ -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
|
||||
|
||||
1089
dashboard/diagrams.html
Normal file
1089
dashboard/diagrams.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,88 +7,9 @@
|
||||
<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>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-card: #1a1a24;
|
||||
--bg-hover: #252532;
|
||||
--accent: #ff6b35;
|
||||
--accent-hover: #ff8555;
|
||||
--accent-glow: rgba(255, 107, 53, 0.3);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a0b0;
|
||||
--border: #2a2a3a;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: rgba(18, 18, 26, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 2rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Search & Header extras (page-specific) */
|
||||
.search-container {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
@@ -637,43 +558,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.7rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
@@ -1168,40 +1052,7 @@
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Quality Badges */
|
||||
.quality-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.quality-economy { background: var(--warning); color: #000; }
|
||||
.quality-standard { background: var(--info); color: white; }
|
||||
.quality-premium { background: var(--success); color: white; }
|
||||
.quality-oem { background: #9b59b6; color: white; }
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 2000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
@@ -1539,45 +1390,6 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Loading & Empty States */
|
||||
.state-container {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.state-container i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.state-container h4 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Back Button */
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.header-stats {
|
||||
@@ -1652,167 +1464,532 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Skip link */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 0;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
z-index: 3000;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
/* ========== Diagram Strip (horizontal scroll above groups) ========== */
|
||||
.diagrams-strip {
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.diagrams-strip-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.diagrams-strip-header h5 {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.diagrams-strip-header .strip-badge {
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 107, 53, 0.2);
|
||||
color: var(--accent);
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.diagrams-strip-scroll {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
overflow-x: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent) var(--bg-hover);
|
||||
}
|
||||
|
||||
.diagrams-strip-scroll::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.diagrams-strip-scroll::-webkit-scrollbar-track {
|
||||
background: var(--bg-hover);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.diagrams-strip-scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--accent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.strip-card {
|
||||
flex: 0 0 180px;
|
||||
background: var(--bg-hover);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.strip-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
.strip-card-img {
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
object-fit: contain;
|
||||
background: #e8e8e8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.strip-card-body {
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
|
||||
.strip-card-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.strip-card-type {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* ========== Diagram Viewer Overlay (full-screen split) ========== */
|
||||
.diagram-viewer-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
z-index: 3000;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.diagram-viewer-overlay.active {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dv-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Left: Diagram image panel */
|
||||
.dv-image-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dv-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.65rem 1.25rem;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dv-toolbar .dv-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dv-toolbar .dv-subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dv-toolbar-btn {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dv-toolbar-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dv-close-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dv-close-btn:hover { background: var(--accent); }
|
||||
|
||||
.dv-img-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background: #e0e0e0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dv-img-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.dv-img-wrapper img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.dv-img-wrapper.zoomed img {
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.dv-zoom-controls {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.35rem;
|
||||
border-radius: 8px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.dv-zoom-btn {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
padding: 0.35rem 0.65rem;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dv-zoom-btn:hover { background: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.dv-zoom-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
/* Nav arrows */
|
||||
.dv-nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.dv-nav-btn:hover { background: var(--accent); }
|
||||
.dv-nav-btn.prev { left: 0.75rem; }
|
||||
.dv-nav-btn.next { right: 0.75rem; }
|
||||
|
||||
/* Right: Parts panel */
|
||||
.dv-parts-panel {
|
||||
width: 400px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dv-parts-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.dv-parts-header h3 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dv-parts-header .dv-parts-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-hover);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dv-parts-search {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dv-parts-search input {
|
||||
width: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.65rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.82rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dv-parts-search input:focus { border-color: var(--accent); }
|
||||
|
||||
.dv-parts-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.dv-group-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent);
|
||||
padding: 0.5rem 0.25rem 0.2rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.dv-part-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dv-part-item:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.dv-part-item.highlighted {
|
||||
border-color: var(--accent);
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
|
||||
.dv-part-number {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dv-part-name {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.dv-xref-list {
|
||||
margin-top: 0.35rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dv-xref-tag {
|
||||
display: inline-block;
|
||||
font-size: 0.68rem;
|
||||
padding: 0.08rem 0.4rem;
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #60a5fa;
|
||||
border-radius: 3px;
|
||||
margin: 0.08rem;
|
||||
}
|
||||
|
||||
/* ========== Hotspot markers ========== */
|
||||
.hotspot-marker {
|
||||
position: absolute;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 107, 53, 0.35);
|
||||
border: 2px solid var(--accent);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hotspot-marker:hover,
|
||||
.hotspot-marker.active {
|
||||
background: rgba(255, 107, 53, 0.6);
|
||||
transform: translate(-50%, -50%) scale(1.25);
|
||||
box-shadow: 0 0 12px rgba(255, 107, 53, 0.5);
|
||||
}
|
||||
|
||||
.hotspot-marker .hotspot-number {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
@keyframes hotspot-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(255, 107, 53, 0); }
|
||||
}
|
||||
|
||||
.hotspot-marker.pulse {
|
||||
animation: hotspot-pulse 1.5s ease-in-out 3;
|
||||
}
|
||||
|
||||
/* ========== Responsive ========== */
|
||||
@media (max-width: 768px) {
|
||||
.dv-layout { flex-direction: column; }
|
||||
.dv-parts-panel { width: 100%; height: 45%; }
|
||||
.strip-card { flex: 0 0 150px; }
|
||||
.strip-card-img { height: 90px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#mainContent" class="skip-link">Saltar al contenido</a>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<a href="customer-landing.html" class="logo">
|
||||
<div class="logo-icon">⚙️</div>
|
||||
<div class="logo-text">AUTOPARTS DB</div>
|
||||
</a>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-box-enhanced">
|
||||
<div class="search-input-wrapper">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" class="search-input" id="searchInput"
|
||||
placeholder="Buscar partes, números OEM, vehículos... (presiona /)"
|
||||
aria-label="Buscar partes"
|
||||
autocomplete="off"
|
||||
oninput="enhancedSearch.onInput(this.value)"
|
||||
onkeydown="enhancedSearch.onKeydown(event)"
|
||||
onfocus="enhancedSearch.onFocus()">
|
||||
<div class="search-filters-toggle" onclick="enhancedSearch.toggleFilters()">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</div>
|
||||
<button class="vin-btn" onclick="dashboard.openVinDecoder()" title="Decodificar VIN">
|
||||
<i class="fas fa-barcode"></i>
|
||||
</button>
|
||||
<div class="search-loading" id="searchLoading" style="display: none;">
|
||||
<div class="search-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown de resultados -->
|
||||
<div class="search-dropdown" id="searchDropdown">
|
||||
<!-- Filtros -->
|
||||
<div class="search-filters" id="searchFilters" style="display: none;">
|
||||
<div class="filter-group">
|
||||
<label>Categoría</label>
|
||||
<select id="searchCategoryFilter" onchange="enhancedSearch.applyFilters()">
|
||||
<option value="">Todas</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Buscar en</label>
|
||||
<select id="searchTypeFilter" onchange="enhancedSearch.applyFilters()">
|
||||
<option value="all">Todo</option>
|
||||
<option value="parts">Solo Partes</option>
|
||||
<option value="vehicles">Solo Vehículos</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Búsquedas recientes -->
|
||||
<div class="search-recent" id="searchRecent">
|
||||
<div class="search-section-title">
|
||||
<i class="fas fa-history"></i> Búsquedas recientes
|
||||
<span class="clear-recent" onclick="enhancedSearch.clearRecent()">Limpiar</span>
|
||||
</div>
|
||||
<div class="search-recent-items" id="searchRecentItems"></div>
|
||||
</div>
|
||||
|
||||
<!-- Resultados -->
|
||||
<div class="search-results-container" id="searchResultsContainer">
|
||||
<!-- Parts results -->
|
||||
<div class="search-results-section" id="partsResults" style="display: none;">
|
||||
<div class="search-section-title"><i class="fas fa-cog"></i> Partes</div>
|
||||
<div class="search-results-list" id="partsResultsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicles results -->
|
||||
<div class="search-results-section" id="vehiclesResults" style="display: none;">
|
||||
<div class="search-section-title"><i class="fas fa-car"></i> Vehículos</div>
|
||||
<div class="search-results-list" id="vehiclesResultsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- No results -->
|
||||
<div class="search-no-results" id="searchNoResults" style="display: none;">
|
||||
<i class="fas fa-search"></i>
|
||||
<p>No se encontraron resultados</p>
|
||||
<span>Intenta con otros términos de búsqueda</span>
|
||||
<div class="search-suggestions" style="margin-top: 1rem;">
|
||||
<span style="display: block; margin-bottom: 0.5rem; font-size: 0.8rem;">Búsquedas populares:</span>
|
||||
<div class="search-suggestion-tags">
|
||||
<span class="search-tag" onclick="enhancedSearch.searchRecent('brake')">brake</span>
|
||||
<span class="search-tag" onclick="enhancedSearch.searchRecent('filter')">filter</span>
|
||||
<span class="search-tag" onclick="enhancedSearch.searchRecent('spark plug')">spark plug</span>
|
||||
<span class="search-tag" onclick="enhancedSearch.searchRecent('camry')">camry</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer con acciones -->
|
||||
<div class="search-dropdown-footer" id="searchFooter" style="display: none;">
|
||||
<span class="search-hint">
|
||||
<kbd>↑↓</kbd> navegar <kbd>Enter</kbd> seleccionar <kbd>Esc</kbd> cerrar
|
||||
</span>
|
||||
<button class="search-view-all" onclick="enhancedSearch.viewAllResults()">
|
||||
Ver todos los resultados <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="header-stats">
|
||||
<div class="header-stat">
|
||||
<div class="header-stat-value" id="totalBrands">0</div>
|
||||
<div class="header-stat-label">Marcas</div>
|
||||
</div>
|
||||
<div class="header-stat">
|
||||
<div class="header-stat-value" id="totalModels">0</div>
|
||||
<div class="header-stat-label">Modelos</div>
|
||||
</div>
|
||||
<div class="header-stat">
|
||||
<div class="header-stat-value" id="totalParts">0</div>
|
||||
<div class="header-stat-label">Partes</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="customer-landing.html" class="btn btn-secondary btn-icon" title="Ir a inicio">
|
||||
<i class="fas fa-home"></i>
|
||||
</a>
|
||||
<a href="admin.html" class="btn btn-primary btn-icon" title="Panel de administración">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Shared Navigation -->
|
||||
<div id="shared-nav"></div>
|
||||
<script src="/nav.js"></script>
|
||||
<script>
|
||||
// Inject page-specific search bar and stats into the shared nav header
|
||||
(function() {
|
||||
var extra = document.getElementById('shared-nav-extra');
|
||||
if (!extra) return;
|
||||
extra.innerHTML = ''
|
||||
+ '<div class="search-container">'
|
||||
+ '<div class="search-box-enhanced">'
|
||||
+ '<div class="search-input-wrapper">'
|
||||
+ '<i class="fas fa-search search-icon"></i>'
|
||||
+ '<input type="text" class="search-input" id="searchInput"'
|
||||
+ ' placeholder="Buscar partes, n\u00fameros OEM, veh\u00edculos... (presiona /)"'
|
||||
+ ' aria-label="Buscar partes"'
|
||||
+ ' autocomplete="off"'
|
||||
+ ' oninput="enhancedSearch.onInput(this.value)"'
|
||||
+ ' onkeydown="enhancedSearch.onKeydown(event)"'
|
||||
+ ' onfocus="enhancedSearch.onFocus()">'
|
||||
+ '<div class="search-filters-toggle" onclick="enhancedSearch.toggleFilters()">'
|
||||
+ '<i class="fas fa-sliders-h"></i>'
|
||||
+ '</div>'
|
||||
+ '<button class="vin-btn" onclick="dashboard.openVinDecoder()" title="Decodificar VIN">'
|
||||
+ '<i class="fas fa-barcode"></i>'
|
||||
+ '</button>'
|
||||
+ '<div class="search-loading" id="searchLoading" style="display: none;">'
|
||||
+ '<div class="search-spinner"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-dropdown" id="searchDropdown">'
|
||||
+ '<div class="search-filters" id="searchFilters" style="display: none;">'
|
||||
+ '<div class="filter-group"><label>Categor\u00eda</label>'
|
||||
+ '<select id="searchCategoryFilter" onchange="enhancedSearch.applyFilters()"><option value="">Todas</option></select>'
|
||||
+ '</div>'
|
||||
+ '<div class="filter-group"><label>Buscar en</label>'
|
||||
+ '<select id="searchTypeFilter" onchange="enhancedSearch.applyFilters()">'
|
||||
+ '<option value="all">Todo</option><option value="parts">Solo Partes</option><option value="vehicles">Solo Veh\u00edculos</option>'
|
||||
+ '</select>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-recent" id="searchRecent">'
|
||||
+ '<div class="search-section-title"><i class="fas fa-history"></i> B\u00fasquedas recientes '
|
||||
+ '<span class="clear-recent" onclick="enhancedSearch.clearRecent()">Limpiar</span></div>'
|
||||
+ '<div class="search-recent-items" id="searchRecentItems"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-results-container" id="searchResultsContainer">'
|
||||
+ '<div class="search-results-section" id="partsResults" style="display: none;">'
|
||||
+ '<div class="search-section-title"><i class="fas fa-cog"></i> Partes</div>'
|
||||
+ '<div class="search-results-list" id="partsResultsList"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-results-section" id="vehiclesResults" style="display: none;">'
|
||||
+ '<div class="search-section-title"><i class="fas fa-car"></i> Veh\u00edculos</div>'
|
||||
+ '<div class="search-results-list" id="vehiclesResultsList"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-no-results" id="searchNoResults" style="display: none;">'
|
||||
+ '<i class="fas fa-search"></i><p>No se encontraron resultados</p>'
|
||||
+ '<span>Intenta con otros t\u00e9rminos de b\u00fasqueda</span>'
|
||||
+ '<div class="search-suggestions" style="margin-top: 1rem;">'
|
||||
+ '<span style="display: block; margin-bottom: 0.5rem; font-size: 0.8rem;">B\u00fasquedas populares:</span>'
|
||||
+ '<div class="search-suggestion-tags">'
|
||||
+ '<span class="search-tag" onclick="enhancedSearch.searchRecent(\'brake\')">brake</span>'
|
||||
+ '<span class="search-tag" onclick="enhancedSearch.searchRecent(\'filter\')">filter</span>'
|
||||
+ '<span class="search-tag" onclick="enhancedSearch.searchRecent(\'spark plug\')">spark plug</span>'
|
||||
+ '<span class="search-tag" onclick="enhancedSearch.searchRecent(\'camry\')">camry</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="search-dropdown-footer" id="searchFooter" style="display: none;">'
|
||||
+ '<span class="search-hint"><kbd>\u2191\u2193</kbd> navegar <kbd>Enter</kbd> seleccionar <kbd>Esc</kbd> cerrar</span>'
|
||||
+ '<button class="search-view-all" onclick="enhancedSearch.viewAllResults()">Ver todos los resultados <i class="fas fa-arrow-right"></i></button>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="header-actions">'
|
||||
+ '<div class="header-stats">'
|
||||
+ '<div class="header-stat"><div class="header-stat-value" id="totalBrands">0</div><div class="header-stat-label">Marcas</div></div>'
|
||||
+ '<div class="header-stat"><div class="header-stat-value" id="totalModels">0</div><div class="header-stat-label">Modelos</div></div>'
|
||||
+ '<div class="header-stat"><div class="header-stat-value" id="totalParts">0</div><div class="header-stat-label">Partes</div></div>'
|
||||
+ '</div>'
|
||||
+ '<a href="customer-landing.html" class="btn btn-secondary btn-icon" title="Ir a inicio"><i class="fas fa-home"></i></a>'
|
||||
+ '<a href="admin.html" class="btn btn-primary btn-icon" title="Panel de administraci\u00f3n"><i class="fas fa-cog"></i></a>'
|
||||
+ '</div>';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="main-container">
|
||||
@@ -1924,6 +2101,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagram Viewer Overlay (split layout) -->
|
||||
<div class="diagram-viewer-overlay" id="diagramViewerOverlay">
|
||||
<div class="dv-layout">
|
||||
<!-- Left: Diagram image -->
|
||||
<div class="dv-image-panel">
|
||||
<div class="dv-toolbar">
|
||||
<div style="flex:1">
|
||||
<div class="dv-title" id="dvTitle">F200</div>
|
||||
<div class="dv-subtitle" id="dvSubtitle">Suspension Delantera</div>
|
||||
</div>
|
||||
<button class="dv-toolbar-btn" id="dvPrevBtn" title="Anterior"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="dv-toolbar-btn" id="dvNextBtn" title="Siguiente"><i class="fas fa-chevron-right"></i></button>
|
||||
<button class="dv-close-btn" id="dvCloseBtn" title="Cerrar"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="dv-img-container" id="dvImgContainer">
|
||||
<div class="dv-img-wrapper" id="dvImgWrapper">
|
||||
<img id="dvImg" src="" alt="Diagram">
|
||||
<!-- Hotspot markers rendered here -->
|
||||
</div>
|
||||
<div class="dv-zoom-controls">
|
||||
<button class="dv-zoom-btn" id="dvZoomOut"><i class="fas fa-minus"></i></button>
|
||||
<span class="dv-zoom-level" id="dvZoomLevel">100%</span>
|
||||
<button class="dv-zoom-btn" id="dvZoomIn"><i class="fas fa-plus"></i></button>
|
||||
<button class="dv-zoom-btn" id="dvZoomFit"><i class="fas fa-expand"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Parts panel -->
|
||||
<div class="dv-parts-panel">
|
||||
<div class="dv-parts-header">
|
||||
<i class="fas fa-list-ul" style="color: var(--accent)"></i>
|
||||
<h3>Partes del Diagrama</h3>
|
||||
<span class="dv-parts-count" id="dvPartsCount">0</span>
|
||||
</div>
|
||||
<div class="dv-parts-search">
|
||||
<input type="text" id="dvPartsFilter" placeholder="Filtrar partes...">
|
||||
</div>
|
||||
<div class="dv-parts-list" id="dvPartsList">
|
||||
<div style="text-align:center;padding:3rem;color:var(--text-secondary)">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size:1.5rem;margin-bottom:0.5rem"></i>
|
||||
<p>Cargando partes...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
<script src="enhanced-search.js"></script>
|
||||
</body>
|
||||
|
||||
109
dashboard/nav.js
Normal file
109
dashboard/nav.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* nav.js -- Shared navigation component for AutoParts DB
|
||||
*
|
||||
* Injects a consistent header/nav bar into <div id="shared-nav"></div>.
|
||||
* Auto-highlights the current page link based on window.location.pathname.
|
||||
*
|
||||
* The injected header includes a <div id="shared-nav-extra"></div> slot
|
||||
* that pages can populate with additional header content (search bars, stats, etc.)
|
||||
* after this script runs.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var path = window.location.pathname;
|
||||
|
||||
function isActive(href) {
|
||||
var h = href.replace(/\/+$/, '') || '/';
|
||||
var p = path.replace(/\/+$/, '') || '/';
|
||||
if (h === p) return true;
|
||||
if ((h === '/' || h === '/index.html') && (p === '/' || p === '/index.html')) return true;
|
||||
if ((h === '/admin.html' || h === '/admin') && (p === '/admin.html' || p === '/admin')) return true;
|
||||
if ((h === '/diagramas' || h === '/diagrams.html') && (p === '/diagramas' || p === '/diagrams.html')) return true;
|
||||
if ((h === '/customer-landing.html') && (p === '/customer-landing.html')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
var navLinks = [
|
||||
{ label: 'Cat\u00e1logo', href: '/' },
|
||||
{ label: 'Diagramas', href: '/diagramas' },
|
||||
{ label: 'Admin', href: '/admin' }
|
||||
];
|
||||
|
||||
var linksHTML = navLinks.map(function (link) {
|
||||
var baseStyle = 'text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: color 0.2s;';
|
||||
if (isActive(link.href)) {
|
||||
baseStyle += ' color: var(--accent);';
|
||||
} else {
|
||||
baseStyle += ' color: var(--text-secondary);';
|
||||
}
|
||||
return '<a href="' + link.href + '" style="' + baseStyle + '"'
|
||||
+ ' onmouseover="this.style.color=\'var(--accent)\'"'
|
||||
+ ' onmouseout="' + (isActive(link.href) ? '' : 'this.style.color=\'var(--text-secondary)\'') + '"'
|
||||
+ '>' + link.label + '</a>';
|
||||
}).join('');
|
||||
|
||||
var html = ''
|
||||
+ '<header id="shared-nav-header" style="'
|
||||
+ 'background: rgba(18, 18, 26, 0.95);'
|
||||
+ 'backdrop-filter: blur(20px);'
|
||||
+ '-webkit-backdrop-filter: blur(20px);'
|
||||
+ 'border-bottom: 1px solid var(--border);'
|
||||
+ 'padding: 1rem 2rem;'
|
||||
+ 'position: fixed;'
|
||||
+ 'top: 0; left: 0; right: 0;'
|
||||
+ 'z-index: 1000;'
|
||||
+ '">'
|
||||
+ '<div style="'
|
||||
+ 'max-width: 1600px;'
|
||||
+ 'margin: 0 auto;'
|
||||
+ 'display: flex;'
|
||||
+ 'justify-content: space-between;'
|
||||
+ 'align-items: center;'
|
||||
+ 'gap: 2rem;'
|
||||
+ '">'
|
||||
// Logo
|
||||
+ '<a href="/" style="'
|
||||
+ 'display: flex;'
|
||||
+ 'align-items: center;'
|
||||
+ 'gap: 0.75rem;'
|
||||
+ 'text-decoration: none;'
|
||||
+ 'flex-shrink: 0;'
|
||||
+ '">'
|
||||
+ '<div style="'
|
||||
+ 'width: 42px; height: 42px;'
|
||||
+ 'background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);'
|
||||
+ 'border-radius: 10px;'
|
||||
+ 'display: flex; align-items: center; justify-content: center;'
|
||||
+ 'font-size: 1.5rem;'
|
||||
+ 'box-shadow: 0 4px 20px var(--accent-glow);'
|
||||
+ '">\u2699\uFE0F</div>'
|
||||
+ '<span style="'
|
||||
+ 'font-family: Orbitron, sans-serif;'
|
||||
+ 'font-size: 1.3rem;'
|
||||
+ 'font-weight: 700;'
|
||||
+ 'background: linear-gradient(135deg, #fff 0%, var(--accent) 100%);'
|
||||
+ '-webkit-background-clip: text;'
|
||||
+ '-webkit-text-fill-color: transparent;'
|
||||
+ 'background-clip: text;'
|
||||
+ '">AUTOPARTS DB</span>'
|
||||
+ '</a>'
|
||||
// Slot for extra page-specific content (search bars, stats, etc.)
|
||||
+ '<div id="shared-nav-extra" style="display: contents;"></div>'
|
||||
// Nav links
|
||||
+ '<nav id="shared-nav-links" style="'
|
||||
+ 'display: flex;'
|
||||
+ 'gap: 1.5rem;'
|
||||
+ 'align-items: center;'
|
||||
+ 'flex-shrink: 0;'
|
||||
+ '">'
|
||||
+ linksHTML
|
||||
+ '</nav>'
|
||||
+ '</div>'
|
||||
+ '</header>';
|
||||
|
||||
var target = document.getElementById('shared-nav');
|
||||
if (target) {
|
||||
target.innerHTML = html;
|
||||
}
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
262
dashboard/shared.css
Normal file
262
dashboard/shared.css
Normal file
@@ -0,0 +1,262 @@
|
||||
/* ============================================================
|
||||
shared.css -- Common styles for all AutoParts DB pages
|
||||
============================================================ */
|
||||
|
||||
/* --- Reset --- */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- CSS Variables (union of all pages) --- */
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-card: #1a1a24;
|
||||
--bg-hover: #252532;
|
||||
--bg-tertiary: #1a1a25;
|
||||
--accent: #ff6b35;
|
||||
--accent-hover: #ff8555;
|
||||
--accent-glow: rgba(255, 107, 53, 0.3);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a0b0;
|
||||
--border: #2a2a3a;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
--danger: #ff4444;
|
||||
}
|
||||
|
||||
/* --- Base body --- */
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* --- Shared Button Styles --- */
|
||||
.btn {
|
||||
padding: 0.7rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* --- Shared Animations --- */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* --- Loading & Empty States --- */
|
||||
.state-container {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.state-container i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.state-container h4 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* --- Scrollbar Styling --- */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* --- Skip Link (accessibility) --- */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 0;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
z-index: 3000;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* --- Screen Reader Only --- */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* --- Alert / Toast Styles --- */
|
||||
.alert {
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(0, 214, 143, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(255, 68, 68, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* --- Modal Base Styles --- */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 2000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* --- Form Styles --- */
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* --- Quality Badges --- */
|
||||
.quality-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.quality-economy { background: var(--warning); color: #000; }
|
||||
.quality-standard { background: var(--info); color: white; }
|
||||
.quality-premium { background: var(--success); color: white; }
|
||||
.quality-oem { background: #9b59b6; color: white; }
|
||||
Reference in New Issue
Block a user