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)
|
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'])
|
@catalog_bp.route('/engines', methods=['GET'])
|
||||||
@require_auth('catalog.view')
|
@require_auth('catalog.view')
|
||||||
def engines():
|
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) ───
|
// ─── EXPOSE GLOBALS (for backward compat) ───
|
||||||
window.CatalogApp = {
|
window.CatalogApp = {
|
||||||
toggleCart: toggleCart,
|
toggleCart: toggleCart,
|
||||||
@@ -825,10 +957,16 @@
|
|||||||
updateQty: updateQuantity,
|
updateQty: updateQuantity,
|
||||||
clearCart: clearCart,
|
clearCart: clearCart,
|
||||||
loadPage: function (p) { loadParts(p); },
|
loadPage: function (p) { loadParts(p); },
|
||||||
|
vsYearChanged: vsYearChanged,
|
||||||
|
vsBrandChanged: vsBrandChanged,
|
||||||
|
vsModelChanged: vsModelChanged,
|
||||||
|
vsEngineChanged: vsEngineChanged,
|
||||||
|
vsClear: vsClearAll,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── INIT ───
|
// ─── INIT ───
|
||||||
renderCart();
|
renderCart();
|
||||||
|
vsLoadYears();
|
||||||
loadBrands();
|
loadBrands();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -433,12 +433,70 @@
|
|||||||
RESPONSIVE
|
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) {
|
@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 { 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; }
|
.sidebar.is-open { left: 0; }
|
||||||
.search-bar { width: 200px; }
|
.search-bar { width: 200px; }
|
||||||
.detail-panel { width: 100%; }
|
.detail-panel { width: 100%; }
|
||||||
.nav-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
.nav-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
||||||
|
.vehicle-selector__inner { flex-direction: column; }
|
||||||
|
.vs-arrow { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -540,6 +598,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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 -->
|
<!-- Scrollable page body -->
|
||||||
<div class="page-body" id="pageBody">
|
<div class="page-body" id="pageBody">
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user