Fix pricing tab warehouse totals and guard custom price DOM access

This commit is contained in:
Mikhail Chusavitin
2026-02-27 16:53:34 +03:00
parent a42a80beb8
commit 6e0335af7c

View File

@@ -236,7 +236,7 @@
class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
oninput="onPricingCustomPriceInput()">
<label class="text-sm font-medium text-gray-700">Uplift:</label>
<input type="number" id="pricing-uplift" step="0.0001" min="0" placeholder="1.0000"
<input type="text" id="pricing-uplift" inputmode="decimal" placeholder="1,0000"
class="w-32 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
oninput="onPricingUpliftInput()">
<button onclick="setPricingCustomPriceFromVendor()" class="px-3 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 border border-gray-300 text-sm">
@@ -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 => `<option value="${escapeHtml(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(`<option value="${escapeHtml(l)}">`));
if (priorityLots.length && rest.length) {
// Visual separator inside datalist suggestions.
parts.push(`<option value="${BOM_LOT_DATALIST_DIVIDER}">`);
}
rest.forEach(l => parts.push(`<option value="${escapeHtml(l)}">`));
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() {
<td class="px-3 py-1.5 text-right">${item.quantity}</td>
<td class="px-3 py-1.5 text-right text-xs">${estimateTotal > 0 ? formatCurrency(estimateTotal) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400 pricing-vendor-price">—</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
<td class="px-3 py-1.5 text-right text-xs">${warehouseTotal != null && warehouseTotal > 0 ? formatCurrency(warehouseTotal) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
`;
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() {
<td class="px-3 py-1.5 text-right">${row.quantity}</td>
<td class="px-3 py-1.5 text-right text-xs">${hasEstimateForRow ? formatCurrency(rowEst) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs pricing-vendor-price">${vendorTotal != null ? formatCurrency(vendorTotal) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
<td class="px-3 py-1.5 text-right text-xs">${hasWarehouseForRow ? formatCurrency(rowWarehouse) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
`;
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() {
<td class="px-3 py-1.5 text-right">${item.quantity}</td>
<td class="px-3 py-1.5 text-right text-xs">${estimateTotal > 0 ? formatCurrency(estimateTotal) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400 pricing-vendor-price">—</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
<td class="px-3 py-1.5 text-right text-xs">${warehouseTotal != null && warehouseTotal > 0 ? formatCurrency(warehouseTotal) : '—'}</td>
<td class="px-3 py-1.5 text-right text-xs text-gray-400">—</td>
`;
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);
}