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:
2026-02-17 03:09:22 +00:00
parent 3ea2de61e2
commit 7ecf1295a5
17 changed files with 6605 additions and 848 deletions

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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)">

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View 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; }