feat: поддержка импорта BOM Inspur в формате PN*qty

Добавлен парсер для текстового формата Inspur (опциональный '|' в начале
строки, разделитель '*' перед количеством). На BOM-вкладке вставка такого
текста автоматически определяется и разбивается на колонки P/N + Qty без
ручного выбора типов. На бэкенде тот же формат поддерживается через
POST /api/projects/:uuid/vendor-import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:04:10 +03:00
parent 55acbe138b
commit 67a761345f
5 changed files with 308 additions and 10 deletions

View File

@@ -2834,14 +2834,24 @@ async function refreshPrices() {
refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
}
const componentSyncResp = await fetch('/api/sync/components', { method: 'POST' });
if (!componentSyncResp.ok) {
throw new Error('component sync failed');
}
let serverSyncSkipped = false;
try {
const statusResp = await fetch('/api/sync/status');
const statusData = statusResp.ok ? await statusResp.json() : null;
if (statusData && statusData.is_online) {
const componentSyncResp = await fetch('/api/sync/components', { method: 'POST' });
if (!componentSyncResp.ok) throw new Error('component sync failed');
const pricelistSyncResp = await fetch('/api/sync/pricelists', { method: 'POST' });
if (!pricelistSyncResp.ok) {
throw new Error('pricelist sync failed');
const pricelistSyncResp = await fetch('/api/sync/pricelists', { method: 'POST' });
if (!pricelistSyncResp.ok) throw new Error('pricelist sync failed');
} else {
serverSyncSkipped = true;
}
} catch(syncErr) {
if (syncErr.message === 'component sync failed' || syncErr.message === 'pricelist sync failed') {
throw syncErr;
}
serverSyncSkipped = true;
}
await Promise.all([
@@ -2876,7 +2886,7 @@ async function refreshPrices() {
}
}
showToast('Цены обновлены', 'success');
showToast(serverSyncSkipped ? 'Цены обновлены (без связи с сервером)' : 'Цены обновлены', 'success');
} catch(e) {
showToast('Ошибка обновления цен', 'error');
} finally {
@@ -3046,11 +3056,62 @@ function _normalizeBomRawRows(rows) {
});
}
function _parseInspurBOMText(text) {
const lines = text.split(/\r?\n/);
const result = [];
for (const raw of lines) {
const line = raw.trim();
if (!line) continue;
const clean = line.startsWith('|') ? line.slice(1).trim() : line;
if (!clean) continue;
const starIdx = clean.lastIndexOf('*');
if (starIdx > 0) {
const suffix = clean.slice(starIdx + 1).trim();
if (/^\d+$/.test(suffix)) {
result.push([clean.slice(0, starIdx).trim(), suffix]);
continue;
}
}
result.push([clean, '1']);
}
return result;
}
function _isInspurBOMText(text) {
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
if (!lines.length) return false;
let matches = 0;
for (const line of lines) {
const t = line.trim();
const idx = t.lastIndexOf('*');
if (idx > 0 && /^\d+$/.test(t.slice(idx + 1).trim())) matches++;
}
return matches > 0 && matches >= Math.ceil(lines.length * 0.5);
}
function handleBOMPaste(event) {
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
if (!text || !text.trim()) return;
if (_isInspurBOMText(text)) {
const parsed = _parseInspurBOMText(text);
if (!parsed.length) return;
bomImportRaw = {
mode: 'raw',
rows: parsed,
columnTypes: ['pn', 'qty'],
ignoredRows: {},
rowErrors: {},
uiError: ''
};
bomRows = [];
_setBomUIError('');
rebuildBOMRowsFromRaw();
renderBOMTable();
return;
}
const lines = text.split(/\r?\n/).filter(l => l.length > 0);
if (!lines.length) return;
const rows = lines.map(l => l.split('\t').map(c => c.trim()));