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 warehouseStockLotsByPricelist = new Map();
|
||||||
let warehouseStockLoadSeq = 0;
|
let warehouseStockLoadSeq = 0;
|
||||||
let warehouseStockLoadsByPricelist = new Map();
|
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
|
// Autocomplete state
|
||||||
let autocompleteInput = null;
|
let autocompleteInput = null;
|
||||||
@@ -1201,12 +1203,54 @@ function renderMultiSelectTabWithSections(sections) {
|
|||||||
document.getElementById('tab-content').innerHTML = html;
|
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)
|
// Autocomplete for single select (Base tab)
|
||||||
function showAutocomplete(category, input) {
|
async function showAutocomplete(category, input) {
|
||||||
autocompleteInput = input;
|
autocompleteInput = input;
|
||||||
autocompleteCategory = category;
|
autocompleteCategory = category;
|
||||||
autocompleteMode = 'single';
|
autocompleteMode = 'single';
|
||||||
autocompleteIndex = -1;
|
autocompleteIndex = -1;
|
||||||
|
const components = getComponentsForCategory(category);
|
||||||
|
await ensurePricesLoaded(components);
|
||||||
filterAutocomplete(category, input.value);
|
filterAutocomplete(category, input.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,7 +1259,7 @@ function filterAutocomplete(category, search) {
|
|||||||
const searchLower = search.toLowerCase();
|
const searchLower = search.toLowerCase();
|
||||||
|
|
||||||
autocompleteFiltered = components.filter(c => {
|
autocompleteFiltered = components.filter(c => {
|
||||||
if (!c.current_price) return false;
|
if (!hasComponentPrice(c.lot_name)) return false;
|
||||||
if (!isComponentAllowedByStockFilter(c)) return false;
|
if (!isComponentAllowedByStockFilter(c)) return false;
|
||||||
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
||||||
return text.includes(searchLower);
|
return text.includes(searchLower);
|
||||||
@@ -1298,12 +1342,13 @@ function selectAutocompleteItem(index) {
|
|||||||
|
|
||||||
const qtyInput = document.getElementById('qty-' + autocompleteCategory);
|
const qtyInput = document.getElementById('qty-' + autocompleteCategory);
|
||||||
const qty = parseInt(qtyInput?.value) || 1;
|
const qty = parseInt(qtyInput?.value) || 1;
|
||||||
|
const price = componentPricesCache[comp.lot_name] || 0;
|
||||||
|
|
||||||
cart.push({
|
cart.push({
|
||||||
lot_name: comp.lot_name,
|
lot_name: comp.lot_name,
|
||||||
quantity: qty,
|
quantity: qty,
|
||||||
unit_price: comp.current_price,
|
unit_price: price,
|
||||||
estimate_price: comp.current_price,
|
estimate_price: price,
|
||||||
warehouse_price: null,
|
warehouse_price: null,
|
||||||
competitor_price: null,
|
competitor_price: null,
|
||||||
delta_wh_estimate_abs: null,
|
delta_wh_estimate_abs: null,
|
||||||
@@ -1333,11 +1378,13 @@ function hideAutocomplete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Autocomplete for multi select tabs
|
// Autocomplete for multi select tabs
|
||||||
function showAutocompleteMulti(input) {
|
async function showAutocompleteMulti(input) {
|
||||||
autocompleteInput = input;
|
autocompleteInput = input;
|
||||||
autocompleteCategory = null;
|
autocompleteCategory = null;
|
||||||
autocompleteMode = 'multi';
|
autocompleteMode = 'multi';
|
||||||
autocompleteIndex = -1;
|
autocompleteIndex = -1;
|
||||||
|
const components = getComponentsForTab(currentTab);
|
||||||
|
await ensurePricesLoaded(components);
|
||||||
filterAutocompleteMulti(input.value);
|
filterAutocompleteMulti(input.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1349,7 +1396,7 @@ function filterAutocompleteMulti(search) {
|
|||||||
const addedLots = new Set(cart.map(i => i.lot_name));
|
const addedLots = new Set(cart.map(i => i.lot_name));
|
||||||
|
|
||||||
autocompleteFiltered = components.filter(c => {
|
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 (addedLots.has(c.lot_name)) return false;
|
||||||
if (!isComponentAllowedByStockFilter(c)) return false;
|
if (!isComponentAllowedByStockFilter(c)) return false;
|
||||||
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
||||||
@@ -1390,12 +1437,13 @@ function selectAutocompleteItemMulti(index) {
|
|||||||
|
|
||||||
const qtyInput = document.getElementById('new-qty');
|
const qtyInput = document.getElementById('new-qty');
|
||||||
const qty = parseInt(qtyInput?.value) || 1;
|
const qty = parseInt(qtyInput?.value) || 1;
|
||||||
|
const price = componentPricesCache[comp.lot_name] || 0;
|
||||||
|
|
||||||
cart.push({
|
cart.push({
|
||||||
lot_name: comp.lot_name,
|
lot_name: comp.lot_name,
|
||||||
quantity: qty,
|
quantity: qty,
|
||||||
unit_price: comp.current_price,
|
unit_price: price,
|
||||||
estimate_price: comp.current_price,
|
estimate_price: price,
|
||||||
warehouse_price: null,
|
warehouse_price: null,
|
||||||
competitor_price: null,
|
competitor_price: null,
|
||||||
delta_wh_estimate_abs: null,
|
delta_wh_estimate_abs: null,
|
||||||
@@ -1417,11 +1465,16 @@ function selectAutocompleteItemMulti(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Autocomplete for sectioned tabs (like storage with RAID and Disks sections)
|
// Autocomplete for sectioned tabs (like storage with RAID and Disks sections)
|
||||||
function showAutocompleteSection(sectionId, input) {
|
async function showAutocompleteSection(sectionId, input) {
|
||||||
autocompleteInput = input;
|
autocompleteInput = input;
|
||||||
autocompleteCategory = sectionId; // Store section ID
|
autocompleteCategory = sectionId; // Store section ID
|
||||||
autocompleteMode = 'section';
|
autocompleteMode = 'section';
|
||||||
autocompleteIndex = -1;
|
autocompleteIndex = -1;
|
||||||
|
|
||||||
|
// Load prices for tab components
|
||||||
|
const components = getComponentsForTab(currentTab);
|
||||||
|
await ensurePricesLoaded(components);
|
||||||
|
|
||||||
filterAutocompleteSection(sectionId, input.value, input);
|
filterAutocompleteSection(sectionId, input.value, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1448,7 +1501,7 @@ function filterAutocompleteSection(sectionId, search, inputElement) {
|
|||||||
const addedLots = new Set(cart.map(i => i.lot_name));
|
const addedLots = new Set(cart.map(i => i.lot_name));
|
||||||
|
|
||||||
autocompleteFiltered = sectionComponents.filter(c => {
|
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 (addedLots.has(c.lot_name)) return false;
|
||||||
if (!isComponentAllowedByStockFilter(c)) return false;
|
if (!isComponentAllowedByStockFilter(c)) return false;
|
||||||
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase();
|
||||||
@@ -1489,12 +1542,13 @@ function selectAutocompleteItemSection(index, sectionId) {
|
|||||||
|
|
||||||
const qtyInput = document.getElementById('new-qty-' + sectionId);
|
const qtyInput = document.getElementById('new-qty-' + sectionId);
|
||||||
const qty = parseInt(qtyInput?.value) || 1;
|
const qty = parseInt(qtyInput?.value) || 1;
|
||||||
|
const price = componentPricesCache[comp.lot_name] || 0;
|
||||||
|
|
||||||
cart.push({
|
cart.push({
|
||||||
lot_name: comp.lot_name,
|
lot_name: comp.lot_name,
|
||||||
quantity: qty,
|
quantity: qty,
|
||||||
unit_price: comp.current_price,
|
unit_price: price,
|
||||||
estimate_price: comp.current_price,
|
estimate_price: price,
|
||||||
warehouse_price: null,
|
warehouse_price: null,
|
||||||
competitor_price: null,
|
competitor_price: null,
|
||||||
delta_wh_estimate_abs: null,
|
delta_wh_estimate_abs: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user