diff --git a/pos/blueprints/catalog_bp.py b/pos/blueprints/catalog_bp.py index 412215d..fad36cc 100644 --- a/pos/blueprints/catalog_bp.py +++ b/pos/blueprints/catalog_bp.py @@ -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(): diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js index 3aa4cab..4b6e54f 100644 --- a/pos/static/js/catalog.js +++ b/pos/static/js/catalog.js @@ -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 = ''; + years.forEach(function (y) { + vsYear.innerHTML += ''; + }); + }).catch(function () { + // Fallback: generate years statically + vsYear.innerHTML = ''; + for (var y = 2026; y >= 1990; y--) { + vsYear.innerHTML += ''; + } + }); + } + + function vsYearChanged() { + var yearId = vsYear.value; + vsBrand.innerHTML = ''; + vsModel.innerHTML = ''; + vsEngine.innerHTML = ''; + 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 = ''; + brands.forEach(function (b) { + vsBrand.innerHTML += ''; + }); + }); + } + + function vsBrandChanged() { + var brandId = vsBrand.value; + vsModel.innerHTML = ''; + vsEngine.innerHTML = ''; + 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 = ''; + models.forEach(function (m) { + vsModel.innerHTML += ''; + }); + }); + } + + function vsModelChanged() { + var modelId = vsModel.value; + var yearVal = vsYear.value; + vsEngine.innerHTML = ''; + 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 = ''; + engines.forEach(function (e) { + var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : ''); + vsEngine.innerHTML += ''; + }); + // 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 = ''; + vsModel.innerHTML = ''; + vsEngine.innerHTML = ''; + 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(); })(); diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html index 45c002f..1283f1c 100644 --- a/pos/templates/catalog.html +++ b/pos/templates/catalog.html @@ -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; } } @@ -540,6 +598,40 @@ + +