diff --git a/web/templates/index.html b/web/templates/index.html index d7e5bb6..55261af 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -194,7 +194,7 @@ Сохранить BOM @@ -542,7 +542,8 @@ let componentPricesCacheLoading = new Map(); // { category: Promise } - tracks o // Autocomplete state let autocompleteInput = null; let autocompleteCategory = null; -let autocompleteMode = null; // 'single', 'multi', 'section' +let autocompleteMode = null; // 'single', 'multi', 'section', 'edit-item' +let autocompleteEditCategories = null; let autocompleteIndex = -1; let autocompleteFiltered = []; @@ -1389,7 +1390,16 @@ function renderMultiSelectTab(components) { html += ` - ${escapeHtml(item.lot_name)} + +
+ +
+ ${escapeHtml(item.description || comp?.description || '')} ${formatPriceOrNA(item.estimate_price ?? item.unit_price)} @@ -1483,6 +1493,10 @@ function renderMultiSelectTabWithSections(sections) { `; + // Add empty row for new item in this section + const sectionId = section.categories.join('-'); + const categoriesStr = section.categories.join(','); + // Render existing cart items for this section sectionItems.forEach((item) => { const comp = allComponents.find(c => c.lot_name === item.lot_name); @@ -1490,7 +1504,17 @@ function renderMultiSelectTabWithSections(sections) { html += ` - ${escapeHtml(item.lot_name)} + +
+ +
+ ${escapeHtml(item.description || comp?.description || '')} ${formatPriceOrNA(item.estimate_price ?? item.unit_price)} @@ -1511,8 +1535,6 @@ function renderMultiSelectTabWithSections(sections) { }); // Add empty row for new item in this section - const sectionId = section.categories.join('-'); - const categoriesStr = section.categories.join(','); html += ` @@ -1640,6 +1662,10 @@ function renderAutocomplete() { onmousedown = `selectAutocompleteItemSection(${idx}, '${autocompleteCategory}')`; } else if (autocompleteMode === 'multi') { onmousedown = `selectAutocompleteItemMulti(${idx})`; + } else if (autocompleteMode === 'bom') { + onmousedown = `selectAutocompleteItemBOM(${idx}, ${autocompleteCategory})`; + } else if (autocompleteMode === 'edit-item') { + onmousedown = `selectAutocompleteEditItem(${idx})`; } else { // single mode onmousedown = `selectAutocompleteItem(${idx})`; @@ -1921,6 +1947,138 @@ function selectAutocompleteItemSection(index, sectionId) { schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false }); } +// Autocomplete for editing an existing cart item's LOT (multi/section tabs) +async function showAutocompleteEditItem(lotName, input) { + autocompleteInput = input; + autocompleteCategory = lotName; + autocompleteMode = 'edit-item'; + autocompleteIndex = -1; + autocompleteEditCategories = input.dataset.categories + ? input.dataset.categories.split(',').map(c => c.trim().toUpperCase()) + : null; + const components = getComponentsForTab(currentTab); + await ensurePricesLoaded(components); + filterAutocompleteEditItem(input.value); +} + +function filterAutocompleteEditItem(search) { + const searchLower = (search || '').toLowerCase(); + const components = autocompleteEditCategories + ? allComponents.filter(c => autocompleteEditCategories.includes(getComponentCategory(c))) + : getComponentsForTab(currentTab); + autocompleteFiltered = components.filter(c => { + if (!hasComponentPrice(c.lot_name)) return false; + if (!isComponentAllowedByStockFilter(c)) return false; + return (c.lot_name + ' ' + (c.description || '')).toLowerCase().includes(searchLower); + }).sort((a, b) => { + const d = (b.popularity_score || 0) - (a.popularity_score || 0); + return d !== 0 ? d : a.lot_name.localeCompare(b.lot_name); + }); + renderAutocomplete(); +} + +function handleAutocompleteKeyEditItem(event) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1); + renderAutocomplete(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + autocompleteIndex = Math.max(autocompleteIndex - 1, -1); + renderAutocomplete(); + } else if (event.key === 'Enter') { + event.preventDefault(); + if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) { + selectAutocompleteEditItem(autocompleteIndex); + } + } else if (event.key === 'Escape') { + hideAutocomplete(); + } +} + +function selectAutocompleteEditItem(index) { + const comp = autocompleteFiltered[index]; + if (!comp) return; + const lotName = autocompleteCategory; + const oldItem = cart.find(i => i.lot_name === lotName); + const qty = oldItem?.quantity || 1; + cart = cart.filter(i => i.lot_name !== lotName); + const price = componentPricesCache[comp.lot_name] || 0; + cart.push({ + lot_name: comp.lot_name, + quantity: qty, + unit_price: price, + estimate_price: price, + warehouse_price: null, + competitor_price: null, + delta_wh_estimate_abs: null, + delta_wh_estimate_pct: null, + delta_comp_estimate_abs: null, + delta_comp_estimate_pct: null, + delta_comp_wh_abs: null, + delta_comp_wh_pct: null, + price_missing: ['warehouse', 'competitor'], + description: comp.description || '', + category: getComponentCategory(comp) + }); + hideAutocomplete(); + renderTab(); + updateCartUI(); + triggerAutoSave(); + schedulePriceLevelsRefresh({ delay: 80, rerender: true, autosave: false }); +} + +// Autocomplete for BOM LOT mapping +function showAutocompleteBOM(rowIdx, input) { + autocompleteInput = input; + autocompleteCategory = rowIdx; + autocompleteMode = 'bom'; + autocompleteIndex = -1; + filterAutocompleteBOM(rowIdx, input.value); +} + +function filterAutocompleteBOM(rowIdx, search) { + const searchLower = (search || '').toLowerCase(); + autocompleteFiltered = (window._bomAllComponents || allComponents).filter(c => { + const text = (c.lot_name + ' ' + (c.description || '')).toLowerCase(); + return text.includes(searchLower); + }).sort((a, b) => { + const popDiff = (b.popularity_score || 0) - (a.popularity_score || 0); + if (popDiff !== 0) return popDiff; + return a.lot_name.localeCompare(b.lot_name); + }); + renderAutocomplete(); +} + +function handleAutocompleteKeyBOM(event, rowIdx) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + autocompleteIndex = Math.min(autocompleteIndex + 1, autocompleteFiltered.length - 1); + renderAutocomplete(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + autocompleteIndex = Math.max(autocompleteIndex - 1, -1); + renderAutocomplete(); + } else if (event.key === 'Enter') { + event.preventDefault(); + if (autocompleteIndex >= 0 && autocompleteIndex < autocompleteFiltered.length) { + selectAutocompleteItemBOM(autocompleteIndex, rowIdx); + } + } else if (event.key === 'Escape') { + hideAutocomplete(); + } +} + +function selectAutocompleteItemBOM(index, rowIdx) { + const comp = autocompleteFiltered[index]; + if (!comp) return; + const row = bomRows.find(r => r.source_row_index === rowIdx) || bomRows[rowIdx]; + if (!row) return; + row.manual_lot = comp.lot_name; + hideAutocomplete(); + resolveBOM(); +} + function clearSingleSelect(category) { cart = cart.filter(item => (item.category || getCategoryFromLotName(item.lot_name)).toUpperCase() !== category.toUpperCase() @@ -2968,6 +3126,18 @@ function deleteBOMRawRow(rowIdx) { rebuildBOMRowsFromRaw(); } +function clearBOMLotMapping(rowIdx) { + const row = bomRows.find(r => r.source_row_index === rowIdx); + if (!row) return; + row.manual_lot = ''; + row.resolved_lot = ''; + row.resolution_source = 'unresolved'; + row.lot_allocations = []; + row.bundle_enabled = false; + renderBOMTable(); + debouncedResolveBOM(); +} + function _bomRawLotCell(rowIdx) { if (!bomImportRaw || bomImportRaw.mode !== 'raw') return '—'; if (bomImportRaw.ignoredRows?.[rowIdx]) return ''; @@ -2989,13 +3159,12 @@ function _bomRawLotCell(rowIdx) { if (isUnresolved) { const val = map.manual_lot || ''; - const invalid = val && !_bomLotValid(val); - return ` + return `
${renderBOMLotAllocationsEditor(rowIdx)}`; } let suffix = ''; @@ -3379,12 +3548,12 @@ function _renderBOMParsedTable() { let lotCell = ''; if (isUnresolved) { - lotCell = `${renderBOMLotAllocationsEditor(idx)}`; + lotCell = `
${renderBOMLotAllocationsEditor(idx)}`; } else { let suffix = ''; if (qtyMismatch) suffix = ` ≠est(${cartQty})`; @@ -3474,7 +3643,7 @@ function _renderBOMRawTable() { - + `; tbody.appendChild(tr); });