Fix pricing tab warehouse totals and guard custom price DOM access
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user