Files
Autoparts-DB/dashboard/demo.html
2026-03-18 22:25:27 +00:00

1222 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NEXUS AUTOPARTS — Cat&aacute;logo OEM</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=Outfit:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0a0f;
--bg2: #12121a;
--card: #1a1a24;
--hover: #252532;
--border: #2a2a3a;
--accent: #ff6b35;
--accent-glow: rgba(255, 107, 53, 0.25);
--text: #ffffff;
--text2: #a0a0b0;
--success: #22c55e;
--info: #3b82f6;
}
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* --- Header --- */
.header {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background: rgba(18, 18, 26, 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid var(--border);
padding: 0.7rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
gap: 0.7rem;
}
.logo-mark {
width: 40px; height: 40px;
background: linear-gradient(135deg, var(--accent), #ff4500);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem;
box-shadow: 0 3px 16px var(--accent-glow);
}
.logo-text {
font-family: 'Outfit', sans-serif;
font-weight: 800;
font-size: 1.3rem;
background: linear-gradient(135deg, #fff, var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-sub {
font-size: 0.6rem;
color: var(--text2);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.header-search {
position: relative;
width: 340px;
}
.header-search input {
width: 100%;
padding: 0.5rem 0.8rem 0.5rem 2.2rem;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: 'DM Sans', sans-serif;
font-size: 0.85rem;
outline: none;
transition: border-color 0.2s;
}
.header-search input:focus { border-color: var(--accent); }
.header-search input::placeholder { color: var(--text2); }
.header-search svg {
position: absolute;
left: 0.7rem; top: 50%;
transform: translateY(-50%);
width: 16px; height: 16px;
color: var(--text2);
pointer-events: none;
}
.search-drop {
position: absolute;
top: calc(100% + 4px);
left: 0; right: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
max-height: 320px;
overflow-y: auto;
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
display: none;
z-index: 200;
}
.search-drop.open { display: block; }
.search-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.55rem 0.8rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
}
.search-item:last-child { border-bottom: none; }
.search-item:hover { background: var(--hover); }
.search-item .si-oem {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
font-size: 0.85rem;
color: var(--accent);
}
.search-item .si-name {
font-size: 0.8rem;
color: var(--text2);
margin-left: 0.5rem;
}
.search-item .si-cat {
font-size: 0.7rem;
color: var(--text2);
}
.badge-demo {
font-size: 0.65rem;
font-weight: 700;
background: var(--accent);
color: #fff;
padding: 0.2rem 0.6rem;
border-radius: 5px;
letter-spacing: 0.08em;
}
/* --- Main --- */
.main {
max-width: 1200px;
margin: 0 auto;
padding: 4.5rem 1.5rem 2rem;
}
/* --- Breadcrumb --- */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 1rem;
font-size: 0.8rem;
flex-wrap: wrap;
}
.breadcrumb span {
color: var(--text2);
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
cursor: pointer;
}
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb .sep {
color: var(--border);
}
/* --- Section title --- */
.section-title {
font-family: 'Outfit', sans-serif;
font-weight: 700;
font-size: 1.4rem;
margin-bottom: 1rem;
}
.section-subtitle {
font-size: 0.85rem;
color: var(--text2);
margin-top: -0.6rem;
margin-bottom: 1rem;
}
/* --- Grid --- */
.grid {
display: grid;
gap: 0.6rem;
}
.grid-brands {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.grid-models {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.grid-vehicles {
grid-template-columns: 1fr;
}
.grid-categories {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.grid-parts {
grid-template-columns: 1fr;
}
/* --- Card --- */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.8rem 1rem;
cursor: pointer;
transition: border-color 0.2s, transform 0.15s, background 0.2s;
}
.card:hover {
border-color: var(--accent);
background: var(--hover);
}
.card:active {
transform: scale(0.98);
}
/* Brand card */
.card-brand {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-brand .cb-name {
font-weight: 700;
font-size: 0.95rem;
}
.card-brand .cb-count {
font-size: 0.7rem;
color: var(--text2);
font-family: 'JetBrains Mono', monospace;
}
/* Model card */
.card-model {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.card-model .cm-name {
font-weight: 700;
font-size: 1rem;
}
.card-model .cm-years {
font-size: 0.75rem;
color: var(--text2);
}
.card-model .cm-count {
font-size: 0.7rem;
color: var(--text2);
font-family: 'JetBrains Mono', monospace;
}
/* Vehicle row */
.card-vehicle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.cv-main {
display: flex;
align-items: center;
gap: 1rem;
}
.cv-year {
font-family: 'Outfit', sans-serif;
font-weight: 700;
font-size: 1.1rem;
color: var(--accent);
min-width: 50px;
}
.cv-engine {
font-weight: 600;
font-size: 0.9rem;
}
.cv-details {
font-size: 0.75rem;
color: var(--text2);
}
.cv-trim {
font-size: 0.7rem;
color: var(--info);
background: rgba(59, 130, 246, 0.1);
padding: 0.15rem 0.5rem;
border-radius: 4px;
}
/* Category card */
.card-category {
display: flex;
align-items: center;
justify-content: space-between;
}
.cc-name {
font-weight: 600;
font-size: 0.9rem;
}
.cc-count {
font-size: 0.7rem;
color: var(--text2);
font-family: 'JetBrains Mono', monospace;
}
/* Group header */
.group-header {
padding: 0.6rem 0;
margin-top: 0.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.group-header .gh-name {
font-weight: 700;
font-size: 0.9rem;
color: var(--accent);
}
.group-header .gh-count {
font-size: 0.7rem;
color: var(--text2);
}
/* Part row */
.part-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 0.6rem;
border-bottom: 1px solid rgba(42, 42, 58, 0.4);
transition: background 0.15s;
}
.part-row:hover {
background: var(--hover);
border-radius: 6px;
}
.part-oem {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 0.9rem;
color: var(--accent);
min-width: 160px;
}
.part-name {
font-size: 0.85rem;
flex: 1;
}
.part-pos {
font-size: 0.7rem;
color: var(--text2);
}
.part-qty {
font-size: 0.7rem;
color: var(--text2);
font-family: 'JetBrains Mono', monospace;
min-width: 40px;
text-align: right;
}
/* --- Loading / Empty --- */
.loading, .empty {
text-align: center;
padding: 3rem 1rem;
color: var(--text2);
font-size: 0.9rem;
}
/* --- Responsive --- */
@media (max-width: 768px) {
.header-search { display: none; }
.grid-brands { grid-template-columns: repeat(2, 1fr); }
.main { padding: 4rem 0.8rem 1.5rem; }
.part-oem { min-width: 120px; font-size: 0.8rem; }
}
/* --- Part row clickable --- */
.part-row {
cursor: pointer;
}
/* --- Modal overlay --- */
.modal-bg {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 500;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
}
.modal-bg.open {
opacity: 1;
pointer-events: auto;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
width: 600px;
max-width: 95vw;
max-height: 85vh;
overflow-y: auto;
transform: translateY(12px) scale(0.97);
transition: transform 0.25s;
}
.modal-bg.open .modal {
transform: translateY(0) scale(1);
}
/* Modal header */
.modal-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1.2rem 1.2rem 0.8rem;
border-bottom: 1px solid var(--border);
}
.modal-head-info {
flex: 1;
min-width: 0;
}
.modal-oem {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 1.2rem;
color: var(--accent);
word-break: break-all;
}
.modal-part-name {
font-size: 0.95rem;
margin-top: 0.2rem;
}
.modal-close {
background: none;
border: none;
color: var(--text2);
font-size: 1.4rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
margin-left: 0.5rem;
}
.modal-close:hover {
background: var(--hover);
color: var(--text);
}
/* Modal detail fields */
.modal-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
padding: 0.8rem 1.2rem;
}
.mf {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.mf-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text2);
font-weight: 600;
}
.mf-value {
font-size: 0.85rem;
font-weight: 500;
}
/* Modal sections */
.modal-section {
padding: 0.8rem 1.2rem;
border-top: 1px solid var(--border);
}
.modal-section-title {
font-family: 'DM Sans', sans-serif;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent);
margin-bottom: 0.6rem;
}
/* Alternatives table */
.alt-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.alt-table th {
text-align: left;
padding: 0.35rem 0.5rem;
border-bottom: 1px solid var(--border);
color: var(--text2);
font-size: 0.7rem;
text-transform: uppercase;
font-weight: 600;
}
.alt-table td {
padding: 0.4rem 0.5rem;
border-bottom: 1px solid rgba(42, 42, 58, 0.4);
}
.alt-table .alt-pn {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
color: var(--info);
}
.alt-table .alt-mfr {
font-weight: 600;
}
.alt-quality {
font-size: 0.65rem;
font-weight: 600;
padding: 0.12rem 0.4rem;
border-radius: 4px;
text-transform: uppercase;
}
.alt-quality.premium { background: rgba(34, 197, 94, 0.15); color: var(--success); }
.alt-quality.economy { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
.alt-quality.oem { background: rgba(59, 130, 246, 0.15); color: var(--info); }
/* Cross-ref rows */
.xref-row {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 0.35rem 0;
border-bottom: 1px solid rgba(42, 42, 58, 0.3);
}
.xref-row:last-child { border-bottom: none; }
.xref-number {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
font-size: 0.85rem;
color: var(--text);
}
.xref-type {
font-size: 0.65rem;
font-weight: 600;
padding: 0.1rem 0.35rem;
border-radius: 3px;
background: rgba(255, 107, 53, 0.12);
color: var(--accent);
text-transform: uppercase;
}
.xref-source {
font-size: 0.75rem;
color: var(--text2);
}
.modal-empty {
text-align: center;
padding: 1rem;
color: var(--text2);
font-size: 0.8rem;
}
.modal-loading {
text-align: center;
padding: 0.8rem;
color: var(--text2);
font-size: 0.8rem;
}
/* --- Region Filter --- */
.region-bar {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.region-label {
font-size: 0.75rem;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
margin-right: 0.3rem;
}
.region-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--card);
color: var(--text2);
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.region-chip:hover {
border-color: var(--accent);
color: var(--text);
}
.region-chip.active {
background: rgba(255, 107, 53, 0.15);
border-color: var(--accent);
color: var(--accent);
}
.region-chip .rc-flag {
font-size: 1rem;
line-height: 1;
}
/* --- Animations --- */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.card, .part-row, .group-header {
animation: fadeUp 0.3s ease both;
}
</style>
</head>
<body>
<header class="header">
<div class="logo">
<div class="logo-mark">&#9881;&#65039;</div>
<div>
<div class="logo-text">NEXUS AUTOPARTS</div>
<div class="logo-sub">Cat&aacute;logo de partes OEM</div>
</div>
</div>
<div class="header-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input id="search-input" type="text" placeholder="Buscar por n&uacute;mero OEM o nombre..." autocomplete="off">
<div id="search-drop" class="search-drop"></div>
</div>
<span class="badge-demo">DEMO</span>
</header>
<main class="main">
<div id="breadcrumb" class="breadcrumb"></div>
<h1 id="title" class="section-title"></h1>
<p id="subtitle" class="section-subtitle"></p>
<div id="content"></div>
</main>
<!-- Part Detail Modal -->
<div id="modal-bg" class="modal-bg">
<div class="modal">
<div class="modal-head">
<div class="modal-head-info">
<div class="modal-oem" id="modal-oem"></div>
<div class="modal-part-name" id="modal-name"></div>
</div>
<button class="modal-close" id="modal-close">&times;</button>
</div>
<div id="modal-image" style="text-align:center;margin-bottom:1rem;display:none;">
<img id="modal-image-img" style="max-width:100%;max-height:300px;border-radius:8px;object-fit:contain;" />
</div>
<div class="modal-fields" id="modal-fields"></div>
<div class="modal-section" id="section-alts">
<div class="modal-section-title">Intercambios / Alternativas</div>
<div id="modal-alts"><div class="modal-loading">Cargando...</div></div>
</div>
<div class="modal-section" id="section-xrefs">
<div class="modal-section-title">Referencias cruzadas</div>
<div id="modal-xrefs"><div class="modal-loading">Cargando...</div></div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var API = '';
var state = { brand: null, model: null, mye_id: null, mye_label: null, category: null, category_name: null };
// Region filter: bitmask 1=MX, 2=US, 4=CA, 8=RW
var REGIONS = [
{ bit: 1, code: 'MX', label: 'M\u00e9xico', flag: '\uD83C\uDDF2\uD83C\uDDFD' },
{ bit: 2, code: 'US', label: 'USA', flag: '\uD83C\uDDFA\uD83C\uDDF8' },
{ bit: 4, code: 'CA', label: 'Canad\u00e1', flag: '\uD83C\uDDE8\uD83C\uDDE6' },
{ bit: 8, code: 'RW', label: 'Resto', flag: '\uD83C\uDF0E' }
];
// Load from localStorage or default MX+US (bitmask = 3)
var regionMask = parseInt(localStorage.getItem('nexus_region') || '3');
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ============================================================
// Navigation
// ============================================================
function go(level, data) {
if (level === 'brands') {
state = { brand: null, model: null, mye_id: null, mye_label: null, category: null, category_name: null };
loadBrands();
} else if (level === 'models') {
state.brand = data;
state.model = null; state.mye_id = null; state.mye_label = null; state.category = null;
loadModels(data);
} else if (level === 'vehicles') {
state.model = data;
state.mye_id = null; state.mye_label = null; state.category = null;
loadVehicles(state.brand, data);
} else if (level === 'categories') {
state.mye_id = data.id;
state.mye_label = data.year + ' ' + data.engine;
state.category = null;
loadCategories(data.id);
} else if (level === 'parts') {
state.category = data.id;
state.category_name = data.name;
loadParts(state.mye_id, data.id, data.name);
}
updateBreadcrumb();
}
function updateBreadcrumb() {
var bc = document.getElementById('breadcrumb');
var parts = ['<a onclick="go(\'brands\')">Marcas</a>'];
if (state.brand) {
parts.push('<span class="sep">&rsaquo;</span>');
parts.push('<a onclick="go(\'models\',\'' + esc(state.brand) + '\')">' + esc(state.brand) + '</a>');
}
if (state.model) {
parts.push('<span class="sep">&rsaquo;</span>');
parts.push('<a onclick="go(\'vehicles\',\'' + esc(state.model) + '\')">' + esc(state.model) + '</a>');
}
if (state.mye_id) {
parts.push('<span class="sep">&rsaquo;</span>');
parts.push('<a onclick="go(\'categories\',{id:' + state.mye_id + ',year:\'\',engine:\'\'})">' + esc(state.mye_label) + '</a>');
}
if (state.category_name) {
parts.push('<span class="sep">&rsaquo;</span>');
parts.push('<span>' + esc(state.category_name) + '</span>');
}
bc.innerHTML = parts.join('');
}
// expose go to inline onclick
window.go = go;
// ============================================================
// Brands
// ============================================================
function buildRegionBar() {
var html = '<div class="region-bar">'
+ '<span class="region-label">Mercado:</span>';
REGIONS.forEach(function (r) {
var active = (regionMask & r.bit) ? ' active' : '';
html += '<div class="region-chip' + active + '" data-bit="' + r.bit + '" onclick="toggleRegion(' + r.bit + ')">'
+ '<span class="rc-flag">' + r.flag + '</span>'
+ r.label
+ '</div>';
});
html += '</div>';
return html;
}
window.toggleRegion = function (bit) {
regionMask ^= bit; // Toggle the bit
if (regionMask === 0) regionMask = bit; // Prevent all-off
localStorage.setItem('nexus_region', regionMask);
// Update chip styles
document.querySelectorAll('.region-chip').forEach(function (chip) {
var b = parseInt(chip.getAttribute('data-bit'));
if (regionMask & b) {
chip.classList.add('active');
} else {
chip.classList.remove('active');
}
});
// Reload brands
fetchBrands();
};
function loadBrands() {
var el = document.getElementById('content');
document.getElementById('title').textContent = 'Selecciona una marca';
document.getElementById('subtitle').innerHTML = buildRegionBar();
el.innerHTML = '<div class="loading">Cargando marcas...</div>';
fetchBrands();
}
function fetchBrands() {
var el = document.getElementById('content');
el.innerHTML = '<div class="loading">Cargando marcas...</div>';
fetch(API + '/api/brands?detailed=true&region=' + regionMask)
.then(function (r) { return r.json(); })
.then(function (brands) {
var sub = document.getElementById('subtitle');
// Keep region bar, update count text after it
var countEl = sub.querySelector('.region-count');
if (!countEl) {
var span = document.createElement('div');
span.className = 'region-count';
span.style.cssText = 'font-size:0.85rem; color:var(--text2); margin-top:0.3rem;';
sub.appendChild(span);
countEl = span;
}
countEl.textContent = brands.length + ' marcas disponibles';
el.className = 'grid grid-brands';
el.innerHTML = brands.map(function (b) {
return '<div class="card card-brand" onclick="go(\'models\',\'' + esc(b.name) + '\')">'
+ '<span class="cb-name">' + esc(b.name) + '</span>'
+ '<span class="cb-count">' + b.model_count + ' modelos</span>'
+ '</div>';
}).join('');
});
}
// ============================================================
// Models
// ============================================================
function loadModels(brand) {
var el = document.getElementById('content');
document.getElementById('title').textContent = brand;
el.innerHTML = '<div class="loading">Cargando modelos...</div>';
fetch(API + '/api/models?brand=' + encodeURIComponent(brand) + '&detailed=true')
.then(function (r) { return r.json(); })
.then(function (models) {
document.getElementById('subtitle').textContent = models.length + ' modelos';
el.className = 'grid grid-models';
el.innerHTML = models.map(function (m) {
var years = m.year_min === m.year_max ? m.year_min : m.year_min + ' - ' + m.year_max;
return '<div class="card card-model" onclick="go(\'vehicles\',\'' + esc(m.name) + '\')">'
+ '<span class="cm-name">' + esc(m.name) + '</span>'
+ '<span class="cm-years">' + years + '</span>'
+ '<span class="cm-count">' + m.vehicle_count + ' variantes</span>'
+ '</div>';
}).join('');
});
}
// ============================================================
// Vehicles (year + engine combos)
// ============================================================
function loadVehicles(brand, model) {
var el = document.getElementById('content');
document.getElementById('title').textContent = brand + ' ' + model;
el.innerHTML = '<div class="loading">Cargando variantes...</div>';
fetch(API + '/api/model-year-engine?brand=' + encodeURIComponent(brand) + '&model=' + encodeURIComponent(model) + '&per_page=100')
.then(function (r) { return r.json(); })
.then(function (res) {
var data = res.data || [];
document.getElementById('subtitle').textContent = data.length + ' variantes';
el.className = 'grid grid-vehicles';
if (data.length === 0) {
el.innerHTML = '<div class="empty">No se encontraron variantes para este modelo</div>';
return;
}
el.innerHTML = data.map(function (v) {
return '<div class="card card-vehicle" onclick="go(\'categories\',' + JSON.stringify(v).replace(/"/g, '&quot;') + ')">'
+ '<div class="cv-main">'
+ '<span class="cv-year">' + v.year + '</span>'
+ '<div>'
+ '<div class="cv-engine">' + esc(v.engine) + '</div>'
+ '<div class="cv-details">' + esc(v.drivetrain || '') + (v.transmission ? ' &middot; ' + esc(v.transmission) : '') + '</div>'
+ '</div>'
+ '</div>'
+ (v.trim_level ? '<span class="cv-trim">' + esc(v.trim_level) + '</span>' : '')
+ '</div>';
}).join('');
});
}
// ============================================================
// Categories
// ============================================================
function loadCategories(myeId) {
var el = document.getElementById('content');
document.getElementById('title').textContent = state.brand + ' ' + state.model + ' ' + state.mye_label;
el.innerHTML = '<div class="loading">Cargando categor&iacute;as...</div>';
fetch(API + '/api/vehicles/' + myeId + '/categories')
.then(function (r) { return r.json(); })
.then(function (cats) {
if (cats.length === 0) {
document.getElementById('subtitle').textContent = '';
el.innerHTML = '<div class="empty">No hay partes registradas para este veh&iacute;culo</div>';
return;
}
document.getElementById('subtitle').textContent = cats.length + ' categor&iacute;as con partes';
el.className = 'grid grid-categories';
el.innerHTML = cats.map(function (c) {
return '<div class="card card-category" onclick="go(\'parts\',' + JSON.stringify({id: c.id, name: c.name}).replace(/"/g, '&quot;') + ')">'
+ '<span class="cc-name">' + esc(c.name) + '</span>'
+ '</div>';
}).join('');
});
}
// ============================================================
// Parts
// ============================================================
function loadParts(myeId, categoryId, categoryName) {
var el = document.getElementById('content');
document.getElementById('title').textContent = categoryName;
el.innerHTML = '<div class="loading">Cargando partes...</div>';
fetch(API + '/api/vehicles/' + myeId + '/parts?category_id=' + categoryId + '&per_page=100')
.then(function (r) { return r.json(); })
.then(function (res) {
var parts = res.data || res;
if (parts.length === 0) {
document.getElementById('subtitle').textContent = '';
el.innerHTML = '<div class="empty">No hay partes en esta categor&iacute;a</div>';
return;
}
document.getElementById('subtitle').textContent = parts.length + ' partes OEM';
el.className = 'grid grid-parts';
// Group by group_name
var groups = {};
var groupOrder = [];
parts.forEach(function (p) {
var g = p.group_name || 'Otros';
if (!groups[g]) { groups[g] = []; groupOrder.push(g); }
groups[g].push(p);
});
var html = '';
groupOrder.forEach(function (g) {
html += '<div class="group-header"><span class="gh-name">' + esc(g) + '</span>'
+ '<span class="gh-count">' + groups[g].length + '</span></div>';
groups[g].forEach(function (p) {
html += '<div class="part-row" data-part-id="' + p.id + '">'
+ '<span class="part-oem">' + esc(p.oem_part_number) + '</span>'
+ '<span class="part-name">' + esc(p.name || p.name_es || '') + '</span>'
+ (p.position ? '<span class="part-pos">' + esc(p.position) + '</span>' : '')
+ '<span class="part-qty">x' + (p.quantity_required || 1) + '</span>'
+ '</div>';
});
});
el.innerHTML = html;
// Attach click handlers to part rows
el.querySelectorAll('.part-row[data-part-id]').forEach(function (row) {
row.addEventListener('click', function () {
openPartModal(parseInt(row.getAttribute('data-part-id')));
});
});
});
}
// ============================================================
// Part Detail Modal
// ============================================================
var modalBg = document.getElementById('modal-bg');
document.getElementById('modal-close').addEventListener('click', closeModal);
modalBg.addEventListener('click', function (e) {
if (e.target === modalBg) closeModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modalBg.classList.contains('open')) closeModal();
});
function closeModal() {
modalBg.classList.remove('open');
}
function openPartModal(partId) {
// Show modal immediately with loading state
document.getElementById('modal-oem').textContent = '...';
document.getElementById('modal-name').textContent = 'Cargando...';
document.getElementById('modal-fields').innerHTML = '';
document.getElementById('modal-image').style.display = 'none';
document.getElementById('modal-alts').innerHTML = '<div class="modal-loading">Cargando intercambios...</div>';
document.getElementById('modal-xrefs').innerHTML = '<div class="modal-loading">Cargando referencias...</div>';
modalBg.classList.add('open');
// Fetch part detail
fetch(API + '/api/parts/' + partId)
.then(function (r) { return r.json(); })
.then(function (p) {
document.getElementById('modal-oem').textContent = p.oem_part_number || '';
document.getElementById('modal-name').textContent = p.name || p.name_es || '';
if (p.image_url) {
document.getElementById('modal-image-img').src = p.image_url;
document.getElementById('modal-image').style.display = 'block';
} else {
document.getElementById('modal-image').style.display = 'none';
}
var fields = '';
if (p.name_es) fields += '<div class="mf"><span class="mf-label">Nombre (ES)</span><span class="mf-value">' + esc(p.name_es) + '</span></div>';
if (p.category_name) fields += '<div class="mf"><span class="mf-label">Categor\u00eda</span><span class="mf-value">' + esc(p.category_name) + '</span></div>';
if (p.group_name) fields += '<div class="mf"><span class="mf-label">Grupo</span><span class="mf-value">' + esc(p.group_name) + '</span></div>';
if (p.description) fields += '<div class="mf" style="grid-column:1/-1"><span class="mf-label">Descripci\u00f3n</span><span class="mf-value">' + esc(p.description) + '</span></div>';
if (p.description_es) fields += '<div class="mf" style="grid-column:1/-1"><span class="mf-label">Descripci\u00f3n (ES)</span><span class="mf-value">' + esc(p.description_es) + '</span></div>';
document.getElementById('modal-fields').innerHTML = fields || '<div class="mf"><span class="mf-label">Categor\u00eda</span><span class="mf-value">' + esc(p.category_name || '') + '</span></div><div class="mf"><span class="mf-label">Grupo</span><span class="mf-value">' + esc(p.group_name || '') + '</span></div>';
});
// Fetch alternatives
fetch(API + '/api/parts/' + partId + '/alternatives')
.then(function (r) { return r.json(); })
.then(function (alts) {
var el = document.getElementById('modal-alts');
if (alts.length === 0) {
el.innerHTML = '<div class="modal-empty">No hay intercambios registrados para esta parte</div>';
return;
}
var html = '<table class="alt-table"><thead><tr>'
+ '<th>N\u00famero</th><th>Fabricante</th><th>Calidad</th><th>Precio</th><th>Garant\u00eda</th>'
+ '</tr></thead><tbody>';
alts.forEach(function (a) {
var qClass = (a.quality_tier || '').toLowerCase().indexOf('premium') >= 0 ? 'premium'
: (a.quality_tier || '').toLowerCase().indexOf('economy') >= 0 ? 'economy' : 'oem';
html += '<tr>'
+ '<td class="alt-pn">' + esc(a.part_number) + '</td>'
+ '<td class="alt-mfr">' + esc(a.manufacturer_name) + '</td>'
+ '<td>' + (a.quality_tier ? '<span class="alt-quality ' + qClass + '">' + esc(a.quality_tier) + '</span>' : '-') + '</td>'
+ '<td>' + (a.price_usd ? '$' + parseFloat(a.price_usd).toFixed(2) : '-') + '</td>'
+ '<td>' + (a.warranty_months ? a.warranty_months + ' meses' : '-') + '</td>'
+ '</tr>';
});
html += '</tbody></table>';
el.innerHTML = html;
});
// Fetch cross-references
fetch(API + '/api/parts/' + partId + '/cross-references')
.then(function (r) { return r.json(); })
.then(function (xrefs) {
var el = document.getElementById('modal-xrefs');
if (xrefs.length === 0) {
el.innerHTML = '<div class="modal-empty">No hay referencias cruzadas registradas</div>';
return;
}
el.innerHTML = xrefs.map(function (x) {
return '<div class="xref-row">'
+ '<span class="xref-number">' + esc(x.cross_reference_number) + '</span>'
+ (x.reference_type ? '<span class="xref-type">' + esc(x.reference_type) + '</span>' : '')
+ (x.source ? '<span class="xref-source">' + esc(x.source) + '</span>' : '')
+ '</div>';
}).join('');
});
}
// Expose for search results
window.openPartModal = openPartModal;
// ============================================================
// Global Search
// ============================================================
var searchTimer = null;
var searchInput = document.getElementById('search-input');
var searchDrop = document.getElementById('search-drop');
searchInput.addEventListener('input', function () {
clearTimeout(searchTimer);
var q = this.value.trim();
if (q.length < 2) { searchDrop.classList.remove('open'); return; }
searchTimer = setTimeout(function () {
fetch(API + '/api/search?q=' + encodeURIComponent(q) + '&limit=12')
.then(function (r) { return r.json(); })
.then(function (results) {
var items = results.parts || [];
if (!items.length) {
searchDrop.innerHTML = '<div style="padding:0.8rem;color:var(--text2);font-size:0.85rem">Sin resultados para "' + esc(q) + '"</div>';
} else {
searchDrop.innerHTML = items.slice(0, 10).map(function (p) {
return '<div class="search-item" data-part-id="' + p.id + '">'
+ '<div>'
+ '<span class="si-oem">' + esc(p.oem_part_number) + '</span>'
+ '<span class="si-name">' + esc(p.name || '') + '</span>'
+ '</div>'
+ '<span class="si-cat">' + esc(p.category_name || '') + '</span>'
+ '</div>';
}).join('');
searchDrop.querySelectorAll('.search-item[data-part-id]').forEach(function (item) {
item.addEventListener('mousedown', function (e) {
e.preventDefault();
var pid = parseInt(item.getAttribute('data-part-id'));
searchDrop.classList.remove('open');
searchInput.value = '';
openPartModal(pid);
});
});
}
searchDrop.classList.add('open');
});
}, 200);
});
searchInput.addEventListener('blur', function () {
setTimeout(function () { searchDrop.classList.remove('open'); }, 200);
});
searchInput.addEventListener('focus', function () {
if (searchDrop.innerHTML.trim()) searchDrop.classList.add('open');
});
// ============================================================
// Init
// ============================================================
go('brands');
})();
</script>
</body>
</html>