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:
@@ -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():
|
||||
|
||||
@@ -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();
|
||||
|
||||
})();
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user