Pricing tab: per-LOT row expansion with rowspan grouping
- Reorder columns: PN вендора / Описание / LOT / Кол-во / Estimate / Склад / Конкуренты / Ручная цена - Explode multi-LOT BOM rows into individual LOT sub-rows; PN вендора + Описание use rowspan to span the group - Rename "Своя цена" → "Ручная цена", "Проставить цены BOM" → "BOM Цена" - CSV export reads PN/Desc/LOT from data attributes to handle rowspan offset correctly - Document pricing tab layout contract in bible-local/02-architecture.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,32 @@ Rules:
|
|||||||
- latest pricelist selection ignores snapshots without items;
|
- latest pricelist selection ignores snapshots without items;
|
||||||
- auto pricelist mode stays auto and must not be persisted as an explicit resolved ID.
|
- auto pricelist mode stays auto and must not be persisted as an explicit resolved ID.
|
||||||
|
|
||||||
|
## Pricing tab layout
|
||||||
|
|
||||||
|
The Pricing tab (Ценообразование) has two tables: Buy (Цена покупки) and Sale (Цена продажи).
|
||||||
|
|
||||||
|
Column order (both tables):
|
||||||
|
|
||||||
|
```
|
||||||
|
PN вендора | Описание | LOT | Кол-во | Estimate | Склад | Конкуренты | Ручная цена
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-LOT row expansion rules:
|
||||||
|
- each `lot_mappings` entry in a BOM row becomes its own table row with its own quantity and prices;
|
||||||
|
- `baseLot` (resolved LOT without an explicit mapping) is treated as the first sub-row with `quantity_per_pn` from `_getRowLotQtyPerPN`;
|
||||||
|
- when one vendor PN expands into N LOT sub-rows, PN вендора and Описание cells use `rowspan="N"` and appear only on the first sub-row;
|
||||||
|
- a visual top border (`border-t border-gray-200`) separates each vendor PN group.
|
||||||
|
|
||||||
|
Vendor price attachment:
|
||||||
|
- `vendorOrig` and `vendorOrigUnit` (BOM unit/total price) are attached to the first LOT sub-row only;
|
||||||
|
- subsequent sub-rows carry empty `data-vendor-orig` so `setPricingCustomPriceFromVendor` counts each vendor PN exactly once.
|
||||||
|
|
||||||
|
Controls terminology:
|
||||||
|
- custom price input is labeled **Ручная цена** (not "Своя цена");
|
||||||
|
- the button that fills custom price from BOM totals is labeled **BOM Цена** (not "Проставить цены BOM").
|
||||||
|
|
||||||
|
CSV export reads PN вендора, Описание, and LOT from `data-vendor-pn`, `data-desc`, `data-lot` row attributes to bypass the rowspan cell offset problem.
|
||||||
|
|
||||||
## Configuration versioning
|
## Configuration versioning
|
||||||
|
|
||||||
Configuration revisions are append-only snapshots stored in `local_configuration_versions`.
|
Configuration revisions are append-only snapshots stored in `local_configuration_versions`.
|
||||||
|
|||||||
@@ -211,9 +211,9 @@
|
|||||||
<table class="w-full text-sm border-collapse">
|
<table class="w-full text-sm border-collapse">
|
||||||
<thead class="bg-gray-50 text-gray-700">
|
<thead class="bg-gray-50 text-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2 text-left border-b">LOT</th>
|
|
||||||
<th class="px-3 py-2 text-left border-b">PN вендора</th>
|
<th class="px-3 py-2 text-left border-b">PN вендора</th>
|
||||||
<th class="px-3 py-2 text-left border-b">Описание</th>
|
<th class="px-3 py-2 text-left border-b">Описание</th>
|
||||||
|
<th class="px-3 py-2 text-left border-b">LOT</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Кол-во</th>
|
<th class="px-3 py-2 text-right border-b">Кол-во</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Estimate</th>
|
<th class="px-3 py-2 text-right border-b">Estimate</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Склад</th>
|
<th class="px-3 py-2 text-right border-b">Склад</th>
|
||||||
@@ -236,12 +236,12 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-4">
|
<div class="mt-4 flex flex-wrap items-center gap-4">
|
||||||
<label class="text-sm font-medium text-gray-700">Своя цена:</label>
|
<label class="text-sm font-medium text-gray-700">Ручная цена:</label>
|
||||||
<input type="number" id="pricing-custom-price-buy" step="0.01" min="0" placeholder="0.00"
|
<input type="number" id="pricing-custom-price-buy" step="0.01" min="0" placeholder="0.00"
|
||||||
class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
||||||
oninput="onBuyCustomPriceInput()">
|
oninput="onBuyCustomPriceInput()">
|
||||||
<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">
|
<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">
|
||||||
Проставить цены BOM
|
BOM Цена
|
||||||
</button>
|
</button>
|
||||||
<button onclick="exportPricingCSV('buy')" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm">
|
<button onclick="exportPricingCSV('buy')" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm">
|
||||||
Экспорт CSV
|
Экспорт CSV
|
||||||
@@ -260,9 +260,9 @@
|
|||||||
<table class="w-full text-sm border-collapse">
|
<table class="w-full text-sm border-collapse">
|
||||||
<thead class="bg-gray-50 text-gray-700">
|
<thead class="bg-gray-50 text-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2 text-left border-b">LOT</th>
|
|
||||||
<th class="px-3 py-2 text-left border-b">PN вендора</th>
|
<th class="px-3 py-2 text-left border-b">PN вендора</th>
|
||||||
<th class="px-3 py-2 text-left border-b">Описание</th>
|
<th class="px-3 py-2 text-left border-b">Описание</th>
|
||||||
|
<th class="px-3 py-2 text-left border-b">LOT</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Кол-во</th>
|
<th class="px-3 py-2 text-right border-b">Кол-во</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Estimate</th>
|
<th class="px-3 py-2 text-right border-b">Estimate</th>
|
||||||
<th class="px-3 py-2 text-right border-b">Склад</th>
|
<th class="px-3 py-2 text-right border-b">Склад</th>
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
<input type="text" id="pricing-uplift-sale" inputmode="decimal" placeholder="1,3000"
|
<input type="text" id="pricing-uplift-sale" inputmode="decimal" placeholder="1,3000"
|
||||||
class="w-28 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
class="w-28 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
||||||
oninput="onSaleMarkupInput()">
|
oninput="onSaleMarkupInput()">
|
||||||
<label class="text-sm font-medium text-gray-700">Своя цена:</label>
|
<label class="text-sm font-medium text-gray-700">Ручная цена:</label>
|
||||||
<input type="number" id="pricing-custom-price-sale" step="0.01" min="0" placeholder="0.00"
|
<input type="number" id="pricing-custom-price-sale" step="0.01" min="0" placeholder="0.00"
|
||||||
class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
class="w-40 px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500"
|
||||||
oninput="onSaleCustomPriceInput()">
|
oninput="onSaleCustomPriceInput()">
|
||||||
@@ -3609,6 +3609,7 @@ async function renderPricingTab() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ─── Build shared row data (unit prices for display, totals for math) ────
|
// ─── Build shared row data (unit prices for display, totals for math) ────
|
||||||
|
// Each BOM row is exploded into per-LOT sub-rows; grouped by vendor PN via groupStart/groupSize.
|
||||||
const _buildRows = () => {
|
const _buildRows = () => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const coveredLots = new Set();
|
const coveredLots = new Set();
|
||||||
@@ -3618,7 +3619,8 @@ async function renderPricingTab() {
|
|||||||
const u = _getUnitPrices(pl);
|
const u = _getUnitPrices(pl);
|
||||||
const estUnit = u.estUnit > 0 ? u.estUnit : (item.unit_price || 0);
|
const estUnit = u.estUnit > 0 ? u.estUnit : (item.unit_price || 0);
|
||||||
result.push({
|
result.push({
|
||||||
lotCell: escapeHtml(item.lot_name), vendorPN: null,
|
lotCell: escapeHtml(item.lot_name), lotText: item.lot_name,
|
||||||
|
vendorPN: null,
|
||||||
desc: (compMap[item.lot_name] || {}).description || '',
|
desc: (compMap[item.lot_name] || {}).description || '',
|
||||||
qty: item.quantity,
|
qty: item.quantity,
|
||||||
estUnit, warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
|
estUnit, warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
|
||||||
@@ -3626,6 +3628,7 @@ async function renderPricingTab() {
|
|||||||
warehouse: u.warehouseUnit != null ? u.warehouseUnit * item.quantity : null,
|
warehouse: u.warehouseUnit != null ? u.warehouseUnit * item.quantity : null,
|
||||||
competitor: u.competitorUnit != null ? u.competitorUnit * item.quantity : null,
|
competitor: u.competitorUnit != null ? u.competitorUnit * item.quantity : null,
|
||||||
vendorOrig: null, vendorOrigUnit: null, isEstOnly,
|
vendorOrig: null, vendorOrigUnit: null, isEstOnly,
|
||||||
|
groupStart: true, groupSize: 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3640,42 +3643,66 @@ async function renderPricingTab() {
|
|||||||
if (baseLot) coveredLots.add(baseLot);
|
if (baseLot) coveredLots.add(baseLot);
|
||||||
allocs.forEach(a => coveredLots.add(a.lot_name));
|
allocs.forEach(a => coveredLots.add(a.lot_name));
|
||||||
|
|
||||||
// Accumulate unit prices per 1 vendor PN (base + allocs)
|
|
||||||
let rowEstUnit = 0, rowWhUnit = 0, rowCompUnit = 0;
|
|
||||||
let hasEst = false, hasWh = false, hasComp = false;
|
|
||||||
if (baseLot) {
|
|
||||||
const u = _getUnitPrices(priceMap[baseLot]);
|
|
||||||
const lotQty = _getRowLotQtyPerPN(row);
|
|
||||||
if (u.estUnit > 0) { rowEstUnit += u.estUnit * lotQty; hasEst = true; }
|
|
||||||
if (u.warehouseUnit != null) { rowWhUnit += u.warehouseUnit * lotQty; hasWh = true; }
|
|
||||||
if (u.competitorUnit != null) { rowCompUnit += u.competitorUnit * lotQty; hasComp = true; }
|
|
||||||
}
|
|
||||||
allocs.forEach(a => {
|
|
||||||
const u = _getUnitPrices(priceMap[a.lot_name]);
|
|
||||||
if (u.estUnit > 0) { rowEstUnit += u.estUnit * a.quantity; hasEst = true; }
|
|
||||||
if (u.warehouseUnit != null) { rowWhUnit += u.warehouseUnit * a.quantity; hasWh = true; }
|
|
||||||
if (u.competitorUnit != null) { rowCompUnit += u.competitorUnit * a.quantity; hasComp = true; }
|
|
||||||
});
|
|
||||||
|
|
||||||
let lotCell = '<span class="text-red-500">н/д</span>';
|
|
||||||
if (baseLot && allocs.length) lotCell = `${escapeHtml(baseLot)} <span class="text-gray-400">+${allocs.length}</span>`;
|
|
||||||
else if (baseLot) lotCell = escapeHtml(baseLot);
|
|
||||||
else if (allocs.length) lotCell = `${escapeHtml(allocs[0].lot_name)}${allocs.length > 1 ? ` <span class="text-gray-400">+${allocs.length - 1}</span>` : ''}`;
|
|
||||||
|
|
||||||
const vendorOrigUnit = row.unit_price != null ? row.unit_price
|
const vendorOrigUnit = row.unit_price != null ? row.unit_price
|
||||||
: (row.total_price != null && row.quantity > 0 ? row.total_price / row.quantity : null);
|
: (row.total_price != null && row.quantity > 0 ? row.total_price / row.quantity : null);
|
||||||
const vendorOrig = row.total_price != null ? row.total_price
|
const vendorOrig = row.total_price != null ? row.total_price
|
||||||
: (row.unit_price != null ? row.unit_price * row.quantity : null);
|
: (row.unit_price != null ? row.unit_price * row.quantity : null);
|
||||||
const desc = row.description || (baseLot ? ((compMap[baseLot] || {}).description || '') : '');
|
const desc = row.description || (baseLot ? ((compMap[baseLot] || {}).description || '') : '');
|
||||||
result.push({
|
|
||||||
lotCell, vendorPN: row.vendor_pn, desc, qty: row.quantity,
|
// Build per-LOT sub-rows
|
||||||
estUnit: hasEst ? rowEstUnit : 0,
|
const subRows = [];
|
||||||
warehouseUnit: hasWh ? rowWhUnit : null,
|
if (baseLot) {
|
||||||
competitorUnit: hasComp ? rowCompUnit : null,
|
const u = _getUnitPrices(priceMap[baseLot]);
|
||||||
est: hasEst ? rowEstUnit * row.quantity : 0,
|
const lotQty = _getRowLotQtyPerPN(row);
|
||||||
warehouse: hasWh ? rowWhUnit * row.quantity : null,
|
const qty = row.quantity * lotQty;
|
||||||
competitor: hasComp ? rowCompUnit * row.quantity : null,
|
subRows.push({
|
||||||
vendorOrig, vendorOrigUnit, isEstOnly: false,
|
lotCell: escapeHtml(baseLot), lotText: baseLot, qty,
|
||||||
|
estUnit: u.estUnit > 0 ? u.estUnit : 0,
|
||||||
|
warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
|
||||||
|
est: u.estUnit > 0 ? u.estUnit * qty : 0,
|
||||||
|
warehouse: u.warehouseUnit != null ? u.warehouseUnit * qty : null,
|
||||||
|
competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
allocs.forEach(a => {
|
||||||
|
const u = _getUnitPrices(priceMap[a.lot_name]);
|
||||||
|
const qty = row.quantity * a.quantity;
|
||||||
|
subRows.push({
|
||||||
|
lotCell: escapeHtml(a.lot_name), lotText: a.lot_name, qty,
|
||||||
|
estUnit: u.estUnit > 0 ? u.estUnit : 0,
|
||||||
|
warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
|
||||||
|
est: u.estUnit > 0 ? u.estUnit * qty : 0,
|
||||||
|
warehouse: u.warehouseUnit != null ? u.warehouseUnit * qty : null,
|
||||||
|
competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subRows.length) {
|
||||||
|
result.push({
|
||||||
|
lotCell: '<span class="text-red-500">н/д</span>', lotText: '',
|
||||||
|
vendorPN: row.vendor_pn, desc, qty: row.quantity,
|
||||||
|
estUnit: 0, warehouseUnit: null, competitorUnit: null,
|
||||||
|
est: 0, warehouse: null, competitor: null,
|
||||||
|
vendorOrig, vendorOrigUnit, isEstOnly: false,
|
||||||
|
groupStart: true, groupSize: 1,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupSize = subRows.length;
|
||||||
|
subRows.forEach((sub, idx) => {
|
||||||
|
result.push({
|
||||||
|
lotCell: sub.lotCell, lotText: sub.lotText,
|
||||||
|
vendorPN: row.vendor_pn, desc,
|
||||||
|
qty: sub.qty,
|
||||||
|
estUnit: sub.estUnit, warehouseUnit: sub.warehouseUnit, competitorUnit: sub.competitorUnit,
|
||||||
|
est: sub.est, warehouse: sub.warehouse, competitor: sub.competitor,
|
||||||
|
vendorOrig: idx === 0 ? vendorOrig : null,
|
||||||
|
vendorOrigUnit: idx === 0 ? vendorOrigUnit : null,
|
||||||
|
isEstOnly: false,
|
||||||
|
groupStart: idx === 0,
|
||||||
|
groupSize: idx === 0 ? groupSize : 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3708,19 +3735,28 @@ async function renderPricingTab() {
|
|||||||
tr.dataset.qty = r.qty;
|
tr.dataset.qty = r.qty;
|
||||||
tr.dataset.vendorOrig = r.vendorOrig != null ? r.vendorOrig : '';
|
tr.dataset.vendorOrig = r.vendorOrig != null ? r.vendorOrig : '';
|
||||||
tr.dataset.vendorOrigUnit = r.vendorOrigUnit != null ? r.vendorOrigUnit : '';
|
tr.dataset.vendorOrigUnit = r.vendorOrigUnit != null ? r.vendorOrigUnit : '';
|
||||||
|
tr.dataset.groupStart = r.groupStart ? 'true' : 'false';
|
||||||
|
tr.dataset.vendorPn = r.vendorPN || '';
|
||||||
|
tr.dataset.desc = r.desc;
|
||||||
|
tr.dataset.lot = r.lotText;
|
||||||
if (r.est > 0) { totEst += r.est; hasEst = true; }
|
if (r.est > 0) { totEst += r.est; hasEst = true; }
|
||||||
if (r.warehouse != null) { totWh += r.warehouse; hasWh = true; cntWh++; }
|
if (r.warehouse != null) { totWh += r.warehouse; hasWh = true; cntWh++; }
|
||||||
if (r.competitor != null) { totComp += r.competitor; hasComp = true; cntComp++; }
|
if (r.competitor != null) { totComp += r.competitor; hasComp = true; cntComp++; }
|
||||||
if (r.vendorOrig != null) { totVendor += r.vendorOrig; hasVendor = true; }
|
if (r.vendorOrig != null) { totVendor += r.vendorOrig; hasVendor = true; }
|
||||||
|
const borderTop = r.groupStart ? 'border-t border-gray-200' : '';
|
||||||
|
const pnDescHtml = r.groupStart ? (() => {
|
||||||
|
const rs = r.groupSize > 1 ? ` rowspan="${r.groupSize}"` : '';
|
||||||
|
return `<td${rs} class="px-3 py-1.5 font-mono text-xs border-t border-gray-200 align-top ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
|
||||||
|
<td${rs} class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs border-t border-gray-200 align-top">${escapeHtml(r.desc)}</td>`;
|
||||||
|
})() : '';
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="px-3 py-1.5 text-xs">${r.lotCell}</td>
|
${pnDescHtml}
|
||||||
<td class="px-3 py-1.5 font-mono text-xs ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
|
<td class="px-3 py-1.5 text-xs ${borderTop}">${r.lotCell}</td>
|
||||||
<td class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs">${escapeHtml(r.desc)}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.qty}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${r.qty}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.estUnit > 0 ? formatCurrency(r.estUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${r.estUnit > 0 ? formatCurrency(r.estUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.warehouseUnit != null ? formatCurrency(r.warehouseUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${r.warehouseUnit != null ? formatCurrency(r.warehouseUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.competitorUnit != null ? formatCurrency(r.competitorUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${r.competitorUnit != null ? formatCurrency(r.competitorUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-buy ${borderTop} ${r.vendorOrigUnit == null ? 'text-gray-400' : ''}">${r.vendorOrigUnit != null ? formatCurrency(r.vendorOrigUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-buy ${r.vendorOrigUnit == null ? 'text-gray-400' : ''}">${r.vendorOrigUnit != null ? formatCurrency(r.vendorOrigUnit) : '—'}</td>
|
|
||||||
`;
|
`;
|
||||||
tbodyBuy.appendChild(tr);
|
tbodyBuy.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -3752,18 +3788,27 @@ async function renderPricingTab() {
|
|||||||
const saleCompTotal = saleCompUnit != null ? saleCompUnit * r.qty : null;
|
const saleCompTotal = saleCompUnit != null ? saleCompUnit * r.qty : null;
|
||||||
tr.dataset.estSale = saleEstTotal;
|
tr.dataset.estSale = saleEstTotal;
|
||||||
tr.dataset.qty = r.qty;
|
tr.dataset.qty = r.qty;
|
||||||
|
tr.dataset.groupStart = r.groupStart ? 'true' : 'false';
|
||||||
|
tr.dataset.vendorPn = r.vendorPN || '';
|
||||||
|
tr.dataset.desc = r.desc;
|
||||||
|
tr.dataset.lot = r.lotText;
|
||||||
if (saleEstTotal > 0) { totEst += saleEstTotal; hasEst = true; }
|
if (saleEstTotal > 0) { totEst += saleEstTotal; hasEst = true; }
|
||||||
if (saleWhTotal != null) { totWh += saleWhTotal; hasWh = true; cntWh++; }
|
if (saleWhTotal != null) { totWh += saleWhTotal; hasWh = true; cntWh++; }
|
||||||
if (saleCompTotal != null) { totComp += saleCompTotal; hasComp = true; cntComp++; }
|
if (saleCompTotal != null) { totComp += saleCompTotal; hasComp = true; cntComp++; }
|
||||||
|
const borderTop = r.groupStart ? 'border-t border-gray-200' : '';
|
||||||
|
const pnDescHtml = r.groupStart ? (() => {
|
||||||
|
const rs = r.groupSize > 1 ? ` rowspan="${r.groupSize}"` : '';
|
||||||
|
return `<td${rs} class="px-3 py-1.5 font-mono text-xs border-t border-gray-200 align-top ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
|
||||||
|
<td${rs} class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs border-t border-gray-200 align-top">${escapeHtml(r.desc)}</td>`;
|
||||||
|
})() : '';
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="px-3 py-1.5 text-xs">${r.lotCell}</td>
|
${pnDescHtml}
|
||||||
<td class="px-3 py-1.5 font-mono text-xs ${r.vendorPN == null ? 'text-gray-400' : ''}">${r.vendorPN != null ? escapeHtml(r.vendorPN) : '—'}</td>
|
<td class="px-3 py-1.5 text-xs ${borderTop}">${r.lotCell}</td>
|
||||||
<td class="px-3 py-1.5 text-xs text-gray-500 truncate max-w-xs">${escapeHtml(r.desc)}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${r.qty}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${r.qty}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleEstUnit > 0 ? formatCurrency(saleEstUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${saleEstUnit > 0 ? formatCurrency(saleEstUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleWhUnit != null ? formatCurrency(saleWhUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${saleWhUnit != null ? formatCurrency(saleWhUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs ${borderTop}">${saleCompUnit != null ? formatCurrency(saleCompUnit) : '—'}</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs">${saleCompUnit != null ? formatCurrency(saleCompUnit) : '—'}</td>
|
<td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-sale ${borderTop} text-gray-400">—</td>
|
||||||
<td class="px-3 py-1.5 text-right text-xs pricing-vendor-price-sale text-gray-400">—</td>
|
|
||||||
`;
|
`;
|
||||||
tbodySale.appendChild(tr);
|
tbodySale.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -3961,12 +4006,25 @@ function exportPricingCSV(table) {
|
|||||||
return /[;"\n\r]/.test(s) ? `"${s}"` : s;
|
return /[;"\n\r]/.test(s) ? `"${s}"` : s;
|
||||||
};
|
};
|
||||||
|
|
||||||
const headers = ['Lot', 'PN вендора', 'Описание', 'Кол-во', 'Estimate', 'Склад', 'Конкуренты', 'Ручная цена'];
|
const headers = ['PN вендора', 'Описание', 'LOT', 'Кол-во', 'Estimate', 'Склад', 'Конкуренты', 'Ручная цена'];
|
||||||
const lines = [headers.map(csvEscape).join(csvDelimiter)];
|
const lines = [headers.map(csvEscape).join(csvDelimiter)];
|
||||||
|
|
||||||
rows.forEach(tr => {
|
rows.forEach(tr => {
|
||||||
|
// PN вендора, Описание, LOT are stored in dataset to handle rowspan correctly
|
||||||
|
const pn = cleanExportCell(tr.dataset.vendorPn || '');
|
||||||
|
const desc = cleanExportCell(tr.dataset.desc || '');
|
||||||
|
const lot = cleanExportCell(tr.dataset.lot || '');
|
||||||
|
// Qty..Ручная цена: cells at offset 2 for group-start rows, offset 0 for sub-rows
|
||||||
|
const isGroupStart = tr.dataset.groupStart === 'true';
|
||||||
const cells = tr.querySelectorAll('td');
|
const cells = tr.querySelectorAll('td');
|
||||||
const cols = [0,1,2,3,4,5,6,7].map(i => cells[i] ? cleanExportCell(cells[i].textContent) : '');
|
const o = isGroupStart ? 2 : 0;
|
||||||
|
const cols = [pn, desc, lot,
|
||||||
|
cleanExportCell(cells[o]?.textContent),
|
||||||
|
cleanExportCell(cells[o+1]?.textContent),
|
||||||
|
cleanExportCell(cells[o+2]?.textContent),
|
||||||
|
cleanExportCell(cells[o+3]?.textContent),
|
||||||
|
cleanExportCell(cells[o+4]?.textContent),
|
||||||
|
];
|
||||||
lines.push(cols.map(csvEscape).join(csvDelimiter));
|
lines.push(cols.map(csvEscape).join(csvDelimiter));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user