diff --git a/web/templates/index.html b/web/templates/index.html index d070764..0aa4c10 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -236,7 +236,7 @@ class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500" oninput="onPricingCustomPriceInput()"> Uplift: - @@ -833,8 +833,11 @@ async function loadAllComponents() { function _bomLots() { return [...new Set((window._bomAllComponents || allComponents).map(c => c.lot_name).filter(Boolean))].sort(); } +const BOM_LOT_DATALIST_DIVIDER = '────────'; function _bomLotValid(v) { - return (window._bomAllComponents || allComponents).some(c => c.lot_name === v); + const lot = (v || '').trim(); + if (!lot || lot === BOM_LOT_DATALIST_DIVIDER) return false; + return (window._bomAllComponents || allComponents).some(c => c.lot_name === lot); } function updateServerCount() { @@ -2308,13 +2311,25 @@ function renderSalePriceTable() { // Custom price functionality function calculateCustomPrice() { const customPriceInput = document.getElementById('custom-price-input'); + const adjustedPricesEl = document.getElementById('adjusted-prices'); + const discountInfoEl = document.getElementById('discount-info'); + const discountPercentEl = document.getElementById('discount-percent'); + const adjustedPricesBodyEl = document.getElementById('adjusted-prices-body'); + const adjustedTotalOriginalEl = document.getElementById('adjusted-total-original'); + const adjustedTotalNewEl = document.getElementById('adjusted-total-new'); + const adjustedTotalFinalEl = document.getElementById('adjusted-total-final'); + + if (!customPriceInput || !adjustedPricesEl || !discountInfoEl || !discountPercentEl || + !adjustedPricesBodyEl || !adjustedTotalOriginalEl || !adjustedTotalNewEl || !adjustedTotalFinalEl) { + return; + } const customPrice = parseFloat(customPriceInput.value) || 0; const originalTotal = cart.reduce((sum, item) => sum + (getDisplayPrice(item) * item.quantity), 0); if (customPrice <= 0 || cart.length === 0 || originalTotal <= 0) { - document.getElementById('adjusted-prices').classList.add('hidden'); - document.getElementById('discount-info').classList.add('hidden'); + adjustedPricesEl.classList.add('hidden'); + discountInfoEl.classList.add('hidden'); return; } @@ -2323,11 +2338,11 @@ function calculateCustomPrice() { const coefficient = customPrice / originalTotal; // Show discount info - document.getElementById('discount-info').classList.remove('hidden'); - document.getElementById('discount-percent').textContent = discountPercent.toFixed(1) + '%'; + discountInfoEl.classList.remove('hidden'); + discountPercentEl.textContent = discountPercent.toFixed(1) + '%'; // Update discount color based on value - const discountEl = document.getElementById('discount-percent'); + const discountEl = discountPercentEl; if (discountPercent > 0) { discountEl.className = 'text-2xl font-bold text-green-600'; } else if (discountPercent < 0) { @@ -2370,17 +2385,21 @@ function calculateCustomPrice() { `; }); - document.getElementById('adjusted-prices-body').innerHTML = html; - document.getElementById('adjusted-total-original').textContent = formatMoney(totalOriginal); - document.getElementById('adjusted-total-new').textContent = formatMoney(totalNew); - document.getElementById('adjusted-total-final').textContent = formatMoney(totalNew); - document.getElementById('adjusted-prices').classList.remove('hidden'); + adjustedPricesBodyEl.innerHTML = html; + adjustedTotalOriginalEl.textContent = formatMoney(totalOriginal); + adjustedTotalNewEl.textContent = formatMoney(totalNew); + adjustedTotalFinalEl.textContent = formatMoney(totalNew); + adjustedPricesEl.classList.remove('hidden'); } function clearCustomPrice() { - document.getElementById('custom-price-input').value = ''; - document.getElementById('adjusted-prices').classList.add('hidden'); - document.getElementById('discount-info').classList.add('hidden'); + const customPriceInput = document.getElementById('custom-price-input'); + const adjustedPricesEl = document.getElementById('adjusted-prices'); + const discountInfoEl = document.getElementById('discount-info'); + + if (customPriceInput) customPriceInput.value = ''; + if (adjustedPricesEl) adjustedPricesEl.classList.add('hidden'); + if (discountInfoEl) discountInfoEl.classList.add('hidden'); triggerAutoSave(); } @@ -2596,7 +2615,34 @@ function _ensureBomDatalist() { dl.id = 'lot-autocomplete-list'; document.body.appendChild(dl); } - dl.innerHTML = _bomLots().map(l => ``).join(''); + const all = _bomLots(); + const cartLots = []; + const seenCart = new Set(); + (window._currentCart || []).forEach(item => { + const lot = (item?.lot_name || '').trim(); + if (!lot || seenCart.has(lot)) return; + seenCart.add(lot); + cartLots.push(lot); + }); + + const mappedSet = new Set(); + bomRows.forEach(r => { + _getRowCanonicalLotMappings(r).forEach(m => { + if (m?.lot_name) mappedSet.add(m.lot_name); + }); + }); + + const priorityLots = cartLots.filter(l => !mappedSet.has(l)); + const prioritySet = new Set(priorityLots); + const rest = all.filter(l => !prioritySet.has(l)); + const parts = []; + priorityLots.forEach(l => parts.push(``)); + if (priorityLots.length && rest.length) { + // Visual separator inside datalist suggestions. + parts.push(``); + } + rest.forEach(l => parts.push(``)); + dl.innerHTML = parts.join(''); return dl; } @@ -3494,8 +3540,8 @@ async function renderPricingTab() { } catch(e) { /* silent — pricing tab renders with available data */ } } - let totalVendor = 0, totalEstimate = 0; - let hasVendor = false, hasEstimate = false; + let totalVendor = 0, totalEstimate = 0, totalWarehouse = 0; + let hasVendor = false, hasEstimate = false, hasWarehouse = false; tbody.innerHTML = ''; @@ -3510,8 +3556,11 @@ async function renderPricingTab() { tr.classList.add('pricing-row'); const pl = priceMap[item.lot_name]; const estUnit = (pl && pl.estimate_price > 0) ? pl.estimate_price : (item.unit_price || 0); + const warehouseUnit = (pl && pl.warehouse_price > 0) ? pl.warehouse_price : null; const estimateTotal = estUnit * item.quantity; + const warehouseTotal = warehouseUnit != null ? warehouseUnit * item.quantity : null; if (estimateTotal > 0) { totalEstimate += estimateTotal; hasEstimate = true; } + if (warehouseTotal != null && warehouseTotal > 0) { totalWarehouse += warehouseTotal; hasWarehouse = true; } tr.dataset.est = estimateTotal; const desc = (compMap[item.lot_name] || {}).description || ''; tr.dataset.vendorOrig = ''; @@ -3522,7 +3571,7 @@ async function renderPricingTab() { ${item.quantity} ${estimateTotal > 0 ? formatCurrency(estimateTotal) : '—'} — - — + ${warehouseTotal != null && warehouseTotal > 0 ? formatCurrency(warehouseTotal) : '—'} — `; tbody.appendChild(tr); @@ -3541,26 +3590,39 @@ async function renderPricingTab() { let rowEst = 0; let hasEstimateForRow = false; + let rowWarehouse = 0; + let hasWarehouseForRow = false; if (baseLot) { const pl = priceMap[baseLot]; const estimateUnit = (pl && pl.estimate_price > 0) ? pl.estimate_price : null; + const warehouseUnit = (pl && pl.warehouse_price > 0) ? pl.warehouse_price : null; if (estimateUnit != null) { rowEst += estimateUnit * row.quantity * _getRowLotQtyPerPN(row); hasEstimateForRow = true; } + if (warehouseUnit != null) { + rowWarehouse += warehouseUnit * row.quantity * _getRowLotQtyPerPN(row); + hasWarehouseForRow = true; + } } allocs.forEach(a => { const pl = priceMap[a.lot_name]; const estimateUnit = (pl && pl.estimate_price > 0) ? pl.estimate_price : null; + const warehouseUnit = (pl && pl.warehouse_price > 0) ? pl.warehouse_price : null; if (estimateUnit != null) { rowEst += estimateUnit * row.quantity * a.quantity; hasEstimateForRow = true; } + if (warehouseUnit != null) { + rowWarehouse += warehouseUnit * row.quantity * a.quantity; + hasWarehouseForRow = true; + } }); const vendorTotal = row.total_price != null ? row.total_price : (row.unit_price != null ? row.unit_price * row.quantity : null); if (vendorTotal != null) { totalVendor += vendorTotal; hasVendor = true; } if (hasEstimateForRow) { totalEstimate += rowEst; hasEstimate = true; } + if (hasWarehouseForRow) { totalWarehouse += rowWarehouse; hasWarehouse = true; } tr.dataset.est = rowEst; tr.dataset.vendorOrig = vendorTotal != null ? vendorTotal : ''; @@ -3580,7 +3642,7 @@ async function renderPricingTab() { ${row.quantity} ${hasEstimateForRow ? formatCurrency(rowEst) : '—'} ${vendorTotal != null ? formatCurrency(vendorTotal) : '—'} - — + ${hasWarehouseForRow ? formatCurrency(rowWarehouse) : '—'} — `; tbody.appendChild(tr); @@ -3594,8 +3656,11 @@ async function renderPricingTab() { tr.classList.add('bg-blue-50'); const pl = priceMap[item.lot_name]; const estUnit = (pl && pl.estimate_price > 0) ? pl.estimate_price : (item.unit_price || 0); + const warehouseUnit = (pl && pl.warehouse_price > 0) ? pl.warehouse_price : null; const estimateTotal = estUnit * item.quantity; + const warehouseTotal = warehouseUnit != null ? warehouseUnit * item.quantity : null; if (estimateTotal > 0) { totalEstimate += estimateTotal; hasEstimate = true; } + if (warehouseTotal != null && warehouseTotal > 0) { totalWarehouse += warehouseTotal; hasWarehouse = true; } tr.dataset.est = estimateTotal; tr.dataset.vendorOrig = ''; const desc = (compMap[item.lot_name] || {}).description || ''; @@ -3606,7 +3671,7 @@ async function renderPricingTab() { ${item.quantity} ${estimateTotal > 0 ? formatCurrency(estimateTotal) : '—'} — - — + ${warehouseTotal != null && warehouseTotal > 0 ? formatCurrency(warehouseTotal) : '—'} — `; tbody.appendChild(tr); @@ -3616,7 +3681,7 @@ async function renderPricingTab() { // Totals row document.getElementById('pricing-total-vendor').textContent = hasVendor ? formatCurrency(totalVendor) : '—'; document.getElementById('pricing-total-estimate').textContent = hasEstimate ? formatCurrency(totalEstimate) : '—'; - document.getElementById('pricing-total-warehouse').textContent = '—'; + document.getElementById('pricing-total-warehouse').textContent = hasWarehouse ? formatCurrency(totalWarehouse) : '—'; tfoot.classList.remove('hidden'); // Update custom price proportional breakdown @@ -3672,6 +3737,18 @@ function getPricingEstimateTotal() { return estimateTotal; } +function parseDecimalInput(raw) { + if (raw == null) return 0; + const cleaned = String(raw).trim().replace(/\s/g, '').replace(',', '.'); + const n = parseFloat(cleaned); + return Number.isFinite(n) ? n : 0; +} + +function formatUpliftInput(value) { + if (!Number.isFinite(value) || value <= 0) return ''; + return value.toFixed(4).replace('.', ','); +} + function syncPricingLinkedInputs(source) { const customPriceInput = document.getElementById('pricing-custom-price'); const upliftInput = document.getElementById('pricing-uplift'); @@ -3683,11 +3760,11 @@ function syncPricingLinkedInputs(source) { } if (source === 'price') { const customPrice = parseFloat(customPriceInput.value) || 0; - upliftInput.value = customPrice > 0 ? (customPrice / estimateTotal).toFixed(4) : ''; + upliftInput.value = customPrice > 0 ? formatUpliftInput(customPrice / estimateTotal) : ''; return; } if (source === 'uplift') { - const uplift = parseFloat(upliftInput.value) || 0; + const uplift = parseDecimalInput(upliftInput.value); customPriceInput.value = uplift > 0 ? (estimateTotal * uplift).toFixed(2) : ''; } } @@ -3773,25 +3850,31 @@ function exportPricingCSV() { return /[,"\n]/.test(s) ? `"${s}"` : s; }; - const headers = ['LOT', 'PN вендора', 'Описание', 'Кол-во', 'Estimate', 'Цена вендора', 'Склад', 'Конкуренты']; + const headers = ['Цена вендора']; const lines = [headers.map(csvEscape).join(',')]; rows.forEach(tr => { const cells = tr.querySelectorAll('td'); - const rowData = Array.from(cells).map(td => td.textContent.trim()); - lines.push(rowData.map(csvEscape).join(',')); + const vendorPrice = cells[5] ? cells[5].textContent.trim() : ''; + lines.push([vendorPrice].map(csvEscape).join(',')); }); // Totals row - const estTotal = document.getElementById('pricing-total-estimate').textContent.trim(); const vendorTotal = document.getElementById('pricing-total-vendor').textContent.trim(); - lines.push(['', '', '', 'Итого', estTotal, vendorTotal, '', ''].map(csvEscape).join(',')); + lines.push(['Итого: ' + vendorTotal].map(csvEscape).join(',')); const blob = new Blob(['\uFEFF' + lines.join('\r\n')], {type: 'text/csv;charset=utf-8;'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `pricing_${configUUID || 'export'}.csv`; + const today = new Date(); + const yyyy = today.getFullYear(); + const mm = String(today.getMonth() + 1).padStart(2, '0'); + const dd = String(today.getDate()).padStart(2, '0'); + const datePart = `${yyyy}-${mm}-${dd}`; + const codePart = (projectCode || 'NO-PROJECT').trim(); + const namePart = (configName || 'config').trim(); + a.download = `${datePart} (${codePart}) ${namePart} SPEC.csv`; a.click(); URL.revokeObjectURL(url); }