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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user