From acf7c8a4daf6a7b8b5f3d16bf297c47f15674ca6 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 9 Feb 2026 15:31:53 +0300 Subject: [PATCH] fix: load component prices via API instead of removed current_price field After the recent refactor that removed CurrentPrice from local_components, the configurator's autocomplete was filtering out all components because it checked for the now-removed current_price field. Instead, now load prices from the API when the user starts typing in a component search field: - Added ensurePricesLoaded() to fetch prices via /api/quote/price-levels - Added componentPricesCache to store loaded prices - Updated all 3 autocomplete modes (single, multi, section) to load prices - Changed price checks from c.current_price to hasComponentPrice() - Updated cart item creation to use cached prices Components without prices are still filtered out as required, but the check now uses API data rather than a removed database field. Co-Authored-By: Claude Haiku 4.5 --- web/templates/index.html | 78 +++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/web/templates/index.html b/web/templates/index.html index 7d1d645..6938929 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -351,6 +351,8 @@ let priceLevelsRefreshTimer = null; let warehouseStockLotsByPricelist = new Map(); let warehouseStockLoadSeq = 0; let warehouseStockLoadsByPricelist = new Map(); +let componentPricesCache = {}; // { lot_name: price } - caches prices loaded via API +let componentPricesCacheLoading = new Map(); // { category: Promise } - tracks ongoing price loads // Autocomplete state let autocompleteInput = null; @@ -1201,12 +1203,54 @@ function renderMultiSelectTabWithSections(sections) { document.getElementById('tab-content').innerHTML = html; } +// Load prices for components in a category/tab via API +async function ensurePricesLoaded(components) { + if (!components || components.length === 0) return; + + // Filter out components that already have prices cached + const toLoad = components.filter(c => !(c.lot_name in componentPricesCache)); + if (toLoad.length === 0) return; + + try { + // Use quote/price-levels API to get prices for these components + const resp = await fetch('/api/quote/price-levels', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + items: toLoad.map(c => ({ lot_name: c.lot_name, quantity: 1 })), + pricelist_ids: Object.fromEntries( + Object.entries(selectedPricelistIds) + .filter(([, id]) => typeof id === 'number' && id > 0) + ) + }) + }); + + if (resp.ok) { + const data = await resp.json(); + if (data.items) { + data.items.forEach(item => { + // Cache the estimate price (or 0 if not found) + componentPricesCache[item.lot_name] = item.estimate_price || 0; + }); + } + } + } catch (e) { + console.error('Failed to load component prices', e); + } +} + +function hasComponentPrice(lotName) { + return lotName in componentPricesCache && componentPricesCache[lotName] > 0; +} + // Autocomplete for single select (Base tab) -function showAutocomplete(category, input) { +async function showAutocomplete(category, input) { autocompleteInput = input; autocompleteCategory = category; autocompleteMode = 'single'; autocompleteIndex = -1; + const components = getComponentsForCategory(category); + await ensurePricesLoaded(components); filterAutocomplete(category, input.value); } @@ -1215,7 +1259,7 @@ function filterAutocomplete(category, search) { const searchLower = search.toLowerCase(); autocompleteFiltered = components.filter(c => { - if (!c.current_price) return false; + if (!hasComponentPrice(c.lot_name)) return false; if (!isComponentAllowedByStockFilter(c)) return false; const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase(); return text.includes(searchLower); @@ -1298,12 +1342,13 @@ function selectAutocompleteItem(index) { const qtyInput = document.getElementById('qty-' + autocompleteCategory); const qty = parseInt(qtyInput?.value) || 1; + const price = componentPricesCache[comp.lot_name] || 0; cart.push({ lot_name: comp.lot_name, quantity: qty, - unit_price: comp.current_price, - estimate_price: comp.current_price, + unit_price: price, + estimate_price: price, warehouse_price: null, competitor_price: null, delta_wh_estimate_abs: null, @@ -1333,11 +1378,13 @@ function hideAutocomplete() { } // Autocomplete for multi select tabs -function showAutocompleteMulti(input) { +async function showAutocompleteMulti(input) { autocompleteInput = input; autocompleteCategory = null; autocompleteMode = 'multi'; autocompleteIndex = -1; + const components = getComponentsForTab(currentTab); + await ensurePricesLoaded(components); filterAutocompleteMulti(input.value); } @@ -1349,7 +1396,7 @@ function filterAutocompleteMulti(search) { const addedLots = new Set(cart.map(i => i.lot_name)); autocompleteFiltered = components.filter(c => { - if (!c.current_price) return false; + if (!hasComponentPrice(c.lot_name)) return false; if (addedLots.has(c.lot_name)) return false; if (!isComponentAllowedByStockFilter(c)) return false; const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase(); @@ -1390,12 +1437,13 @@ function selectAutocompleteItemMulti(index) { const qtyInput = document.getElementById('new-qty'); const qty = parseInt(qtyInput?.value) || 1; + const price = componentPricesCache[comp.lot_name] || 0; cart.push({ lot_name: comp.lot_name, quantity: qty, - unit_price: comp.current_price, - estimate_price: comp.current_price, + unit_price: price, + estimate_price: price, warehouse_price: null, competitor_price: null, delta_wh_estimate_abs: null, @@ -1417,11 +1465,16 @@ function selectAutocompleteItemMulti(index) { } // Autocomplete for sectioned tabs (like storage with RAID and Disks sections) -function showAutocompleteSection(sectionId, input) { +async function showAutocompleteSection(sectionId, input) { autocompleteInput = input; autocompleteCategory = sectionId; // Store section ID autocompleteMode = 'section'; autocompleteIndex = -1; + + // Load prices for tab components + const components = getComponentsForTab(currentTab); + await ensurePricesLoaded(components); + filterAutocompleteSection(sectionId, input.value, input); } @@ -1448,7 +1501,7 @@ function filterAutocompleteSection(sectionId, search, inputElement) { const addedLots = new Set(cart.map(i => i.lot_name)); autocompleteFiltered = sectionComponents.filter(c => { - if (!c.current_price) return false; + if (!hasComponentPrice(c.lot_name)) return false; if (addedLots.has(c.lot_name)) return false; if (!isComponentAllowedByStockFilter(c)) return false; const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase(); @@ -1489,12 +1542,13 @@ function selectAutocompleteItemSection(index, sectionId) { const qtyInput = document.getElementById('new-qty-' + sectionId); const qty = parseInt(qtyInput?.value) || 1; + const price = componentPricesCache[comp.lot_name] || 0; cart.push({ lot_name: comp.lot_name, quantity: qty, - unit_price: comp.current_price, - estimate_price: comp.current_price, + unit_price: price, + estimate_price: price, warehouse_price: null, competitor_price: null, delta_wh_estimate_abs: null,