refactor: унифицировать CSV-экспорт, перенести pricing на сервер
- Вынести sortConfigsByLine() — устранить дублирование sort.Slice в ProjectToExportData и ProjectToPricingExportData - Добавить ConfigToPricingExportData() и ExportConfigPricingCSV handler - Зарегистрировать POST /api/configs/:uuid/export/pricing - Заменить клиентский DOM-скрапинг exportPricingCSV() на fetch к новому endpoint; артикул теперь включается через pricingConfigSummaryRow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4316,72 +4316,33 @@ function setPricingCustomPriceFromVendor() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportPricingCSV(table) {
|
||||
const bodyId = table === 'sale' ? 'pricing-body-sale' : 'pricing-body-buy';
|
||||
const rowClass = table === 'sale' ? 'pricing-row-sale' : 'pricing-row-buy';
|
||||
const totalIds = table === 'sale'
|
||||
? { est: 'pricing-total-sale-estimate', wh: 'pricing-total-sale-warehouse', comp: 'pricing-total-sale-competitor', vendor: 'pricing-total-sale-vendor' }
|
||||
: { est: 'pricing-total-buy-estimate', wh: 'pricing-total-buy-warehouse', comp: 'pricing-total-buy-competitor', vendor: 'pricing-total-buy-vendor' };
|
||||
|
||||
const rows = document.querySelectorAll(`#${bodyId} tr.${rowClass}`);
|
||||
if (!rows.length) { showToast('Нет данных для экспорта', 'error'); return; }
|
||||
|
||||
const csvDelimiter = ';';
|
||||
const cleanExportCell = value => {
|
||||
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
||||
if (!text || text === '—') return text || '';
|
||||
return text
|
||||
.replace(/\s*\(.*\)$/, '')
|
||||
.replace(/\s*\*+\s*$/, '')
|
||||
.trim();
|
||||
};
|
||||
const csvEscape = v => {
|
||||
if (v == null) return '';
|
||||
const s = String(v).replace(/"/g, '""');
|
||||
return /[;"\n\r]/.test(s) ? `"${s}"` : s;
|
||||
};
|
||||
|
||||
const headers = ['PN вендора', 'Описание', 'LOT', 'Кол-во', 'Estimate', 'Склад', 'Конкуренты', 'Ручная цена'];
|
||||
const lines = [headers.map(csvEscape).join(csvDelimiter)];
|
||||
|
||||
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 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));
|
||||
});
|
||||
|
||||
// Totals row
|
||||
const tEst = cleanExportCell(document.getElementById(totalIds.est)?.textContent);
|
||||
const tWh = cleanExportCell(document.getElementById(totalIds.wh)?.textContent);
|
||||
const tComp = cleanExportCell(document.getElementById(totalIds.comp)?.textContent);
|
||||
const tVendor = cleanExportCell(document.getElementById(totalIds.vendor)?.textContent);
|
||||
lines.push(['', '', '', 'Итого:', tEst, tWh, tComp, tVendor].map(csvEscape).join(csvDelimiter));
|
||||
|
||||
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;
|
||||
const today = new Date();
|
||||
const datePart = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
||||
const codePart = (projectCode || 'NO-PROJECT').trim();
|
||||
const namePart = (configName || 'config').trim();
|
||||
const suffix = table === 'sale' ? 'SALE' : 'BUY';
|
||||
a.download = `${datePart} (${codePart}) ${namePart} SPEC-${suffix}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
async function exportPricingCSV(table) {
|
||||
if (!configUUID) { showToast('Сохраните конфигурацию перед экспортом', 'error'); return; }
|
||||
const basis = table === 'sale' ? 'ddp' : 'fob';
|
||||
try {
|
||||
const resp = await fetch(`/api/configs/${configUUID}/export/pricing`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
include_lot: true,
|
||||
include_bom: true,
|
||||
include_estimate: true,
|
||||
include_stock: true,
|
||||
include_competitor: true,
|
||||
basis: basis,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) { showToast('Ошибка экспорта', 'error'); return; }
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = getFilenameFromResponse(resp) || `${configName || 'config'} SPEC-${basis.toUpperCase()}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch(e) {
|
||||
showToast('Ошибка экспорта', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
|
||||
Reference in New Issue
Block a user