feat(pos): selector de vehiculo con dropdowns — Ano > Marca > Modelo > Motor

Barra de 4 dropdowns arriba del catalogo que se habilitan en cascada.
Al completar los 4, muestra categorias y partes para ese vehiculo.
Boton de limpiar para resetear. Endpoint /years-all para cargar anos.
Estilos con design system tokens (ambos temas).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 07:59:50 +00:00
parent 1a770999f5
commit 10d5b62e00
3 changed files with 243 additions and 0 deletions

View File

@@ -92,6 +92,19 @@ def years():
return _master_only(_do)
@catalog_bp.route('/years-all', methods=['GET'])
@require_auth('catalog.view')
def years_all():
"""Get all available years (for vehicle selector dropdown)."""
def _do(master):
cur = master.cursor()
cur.execute("SELECT DISTINCT id_year, year_car FROM years ORDER BY year_car DESC")
rows = cur.fetchall()
cur.close()
return jsonify({'data': [{'id_year': r[0], 'year_car': r[1]} for r in rows]})
return _master_only(_do)
@catalog_bp.route('/engines', methods=['GET'])
@require_auth('catalog.view')
def engines():

View File

@@ -816,6 +816,138 @@
}
});
// ─── VEHICLE SELECTOR (dropdown bar) ───
var vsYear = document.getElementById('vsYear');
var vsBrand = document.getElementById('vsBrand');
var vsModel = document.getElementById('vsModel');
var vsEngine = document.getElementById('vsEngine');
var vsClear = document.getElementById('vsClear');
// Load years on init
function vsLoadYears() {
apiFetch(API + '/years-all').then(function (data) {
if (!data) return;
var years = data.data || data;
// If endpoint doesn't exist, generate from 1990-2026
if (!years || !years.length) {
years = [];
for (var y = 2026; y >= 1990; y--) years.push({ id_year: y, year_car: y });
}
vsYear.innerHTML = '<option value="">Año...</option>';
years.forEach(function (y) {
vsYear.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
});
}).catch(function () {
// Fallback: generate years statically
vsYear.innerHTML = '<option value="">Año...</option>';
for (var y = 2026; y >= 1990; y--) {
vsYear.innerHTML += '<option value="' + y + '">' + y + '</option>';
}
});
}
function vsYearChanged() {
var yearId = vsYear.value;
vsBrand.innerHTML = '<option value="">Marca...</option>';
vsModel.innerHTML = '<option value="">Modelo...</option>';
vsEngine.innerHTML = '<option value="">Motor...</option>';
vsBrand.disabled = true;
vsModel.disabled = true;
vsEngine.disabled = true;
vsClear.style.display = yearId ? '' : 'none';
if (!yearId) return;
// Load brands (reuse existing endpoint)
vsBrand.disabled = false;
apiFetch(API + '/brands').then(function (data) {
var brands = data.data || data;
if (!brands) return;
vsBrand.innerHTML = '<option value="">Marca...</option>';
brands.forEach(function (b) {
vsBrand.innerHTML += '<option value="' + b.id_brand + '">' + esc(b.name_brand) + '</option>';
});
});
}
function vsBrandChanged() {
var brandId = vsBrand.value;
vsModel.innerHTML = '<option value="">Modelo...</option>';
vsEngine.innerHTML = '<option value="">Motor...</option>';
vsModel.disabled = true;
vsEngine.disabled = true;
if (!brandId) return;
vsModel.disabled = false;
apiFetch(API + '/models?brand_id=' + brandId).then(function (data) {
var models = data.data || data;
if (!models) return;
vsModel.innerHTML = '<option value="">Modelo...</option>';
models.forEach(function (m) {
vsModel.innerHTML += '<option value="' + m.id_model + '">' + esc(m.name_model) + '</option>';
});
});
}
function vsModelChanged() {
var modelId = vsModel.value;
var yearVal = vsYear.value;
vsEngine.innerHTML = '<option value="">Motor...</option>';
vsEngine.disabled = true;
if (!modelId || !yearVal) return;
vsEngine.disabled = false;
apiFetch(API + '/engines?model_id=' + modelId + '&year_id=' + yearVal).then(function (data) {
var engines = data.data || data;
if (!engines) return;
vsEngine.innerHTML = '<option value="">Motor...</option>';
engines.forEach(function (e) {
var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
vsEngine.innerHTML += '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
});
// If only 1 engine, auto-select
if (engines.length === 1) {
vsEngine.value = engines[0].id_mye;
vsEngineChanged();
}
});
}
function vsEngineChanged() {
var myeId = vsEngine.value;
if (!myeId) return;
// Update state and load categories
var yearText = vsYear.options[vsYear.selectedIndex].text;
var brandText = vsBrand.options[vsBrand.selectedIndex].text;
var modelText = vsModel.options[vsModel.selectedIndex].text;
var engineText = vsEngine.options[vsEngine.selectedIndex].text;
state.brand = { id: parseInt(vsBrand.value), name: brandText };
state.model = { id: parseInt(vsModel.value), name: modelText };
state.year = { id: parseInt(vsYear.value), value: yearText };
state.engine = { id_mye: parseInt(myeId), name: engineText };
state.level = 'categories';
loadCategories();
}
function vsClearAll() {
vsYear.value = '';
vsBrand.innerHTML = '<option value="">Marca...</option>';
vsModel.innerHTML = '<option value="">Modelo...</option>';
vsEngine.innerHTML = '<option value="">Motor...</option>';
vsBrand.disabled = true;
vsModel.disabled = true;
vsEngine.disabled = true;
vsClear.style.display = 'none';
state = { level: 'brands', brand: null, model: null, year: null, engine: null, category: null, group: null, page: 1, totalPages: 1 };
loadBrands();
}
// ─── EXPOSE GLOBALS (for backward compat) ───
window.CatalogApp = {
toggleCart: toggleCart,
@@ -825,10 +957,16 @@
updateQty: updateQuantity,
clearCart: clearCart,
loadPage: function (p) { loadParts(p); },
vsYearChanged: vsYearChanged,
vsBrandChanged: vsBrandChanged,
vsModelChanged: vsModelChanged,
vsEngineChanged: vsEngineChanged,
vsClear: vsClearAll,
};
// ─── INIT ───
renderCart();
vsLoadYears();
loadBrands();
})();

View File

@@ -433,12 +433,70 @@
RESPONSIVE
========================================================================= */
/* ── Vehicle Selector ── */
.vehicle-selector {
background: var(--color-bg-elevated);
border-bottom: 2px solid var(--color-primary-muted, rgba(245,166,35,0.2));
padding: var(--space-3) var(--space-5);
flex-shrink: 0;
}
.vehicle-selector__inner {
display: flex;
align-items: flex-end;
gap: var(--space-3);
flex-wrap: wrap;
}
.vs-group { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 140px; }
.vs-label {
font-size: var(--text-caption, 0.75rem);
font-weight: var(--font-weight-semibold, 600);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wider, 0.04em);
}
.vs-select {
padding: var(--space-2) var(--space-3);
background: var(--color-bg-base);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
font-family: var(--font-body);
font-size: var(--text-body-sm, 0.875rem);
cursor: pointer;
transition: var(--transition-fast);
appearance: auto;
}
[data-theme="industrial"] .vs-select { border-radius: 0; }
[data-theme="modern"] .vs-select { border-radius: var(--radius-md); }
.vs-select:focus { border-color: var(--color-primary); outline: none; box-shadow: var(--shadow-focus); }
.vs-select:disabled { opacity: 0.4; cursor: not-allowed; }
.vs-arrow {
color: var(--color-primary);
font-size: 1.4rem;
font-weight: 700;
padding-bottom: 6px;
flex-shrink: 0;
}
.vs-clear {
background: none;
border: 1px solid var(--color-border);
color: var(--color-text-muted);
padding: var(--space-2) var(--space-3);
cursor: pointer;
font-size: 1rem;
transition: var(--transition-fast);
}
[data-theme="industrial"] .vs-clear { border-radius: 0; }
[data-theme="modern"] .vs-clear { border-radius: var(--radius-md); }
.vs-clear:hover { color: var(--color-error); border-color: var(--color-error); }
@media (max-width: 768px) {
.sidebar { position: fixed; left: -260px; z-index: var(--z-modal); transition: left var(--duration-normal) var(--ease-in-out); height: 100vh; }
.sidebar.is-open { left: 0; }
.search-bar { width: 200px; }
.detail-panel { width: 100%; }
.nav-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.vehicle-selector__inner { flex-direction: column; }
.vs-arrow { display: none; }
}
</style>
</head>
@@ -540,6 +598,40 @@
</div>
</header>
<!-- Vehicle selector bar -->
<div class="vehicle-selector" id="vehicleSelector">
<div class="vehicle-selector__inner">
<div class="vs-group">
<label class="vs-label">Año</label>
<select class="vs-select" id="vsYear" onchange="CatalogApp.vsYearChanged()">
<option value="">Seleccionar...</option>
</select>
</div>
<div class="vs-arrow"></div>
<div class="vs-group">
<label class="vs-label">Marca</label>
<select class="vs-select" id="vsBrand" disabled onchange="CatalogApp.vsBrandChanged()">
<option value="">Seleccionar...</option>
</select>
</div>
<div class="vs-arrow"></div>
<div class="vs-group">
<label class="vs-label">Modelo</label>
<select class="vs-select" id="vsModel" disabled onchange="CatalogApp.vsModelChanged()">
<option value="">Seleccionar...</option>
</select>
</div>
<div class="vs-arrow"></div>
<div class="vs-group">
<label class="vs-label">Motor</label>
<select class="vs-select" id="vsEngine" disabled onchange="CatalogApp.vsEngineChanged()">
<option value="">Seleccionar...</option>
</select>
</div>
<button class="vs-clear" id="vsClear" onclick="CatalogApp.vsClear()" title="Limpiar seleccion" style="display:none;"></button>
</div>
</div>
<!-- Scrollable page body -->
<div class="page-body" id="pageBody">