Добавлен экспорт неимпортированных строк в CSV при ошибках

Backend:
- insertMultipleRows возвращает failedRows с данными и описанием ошибки
- getShortErrorMessage формирует понятные сообщения об ошибках

Frontend:
- Диалог с результатами импорта (успешных/ошибок)
- Группировка ошибок по типу
- Кнопка "Скачать ошибки CSV" - файл с проблемными строками + колонки _ОШИБКА и _СТРОКА

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 22:39:06 +03:00
parent 15214b2f69
commit cc75ba85e3
2 changed files with 179 additions and 60 deletions

View File

@@ -1916,64 +1916,14 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
rows: records
});
if (result.errorMessages && result.errorMessages.length > 0) {
// ✅ Группируем ошибки по типам
const fkErrors = result.errorMessages.filter(msg => msg.includes('СВЯЗАННОЕ ЗНАЧЕНИЕ') || msg.includes('ОШИБКА СВЯЗИ'));
const otherErrors = result.errorMessages.filter(msg => !fkErrors.includes(msg));
let errorDetails = `📊 РЕЗУЛЬТАТ ИМПОРТА:\n`;
errorDetails += `✅ Импортировано: ${result.inserted}\n`;
errorDetails += `❌ Ошибок: ${result.errors}\n\n`;
if (fkErrors.length > 0) {
errorDetails += `⚠️ ОШИБКИ СВЯЗАННЫХ КЛЮЧЕЙ (${fkErrors.length}):\n`;
errorDetails += `═══════════════════════════════════════\n`;
errorDetails += `Значения не найдены в справочниках.\n`;
errorDetails += `Создайте их вручную или исправьте CSV.\n\n`;
// Показываем первые 5 FK ошибок
const fkToShow = fkErrors.slice(0, 5);
errorDetails += fkToShow.join('\n\n');
if (fkErrors.length > 5) {
errorDetails += `\n\n... и еще ${fkErrors.length - 5} FK ошибок`;
}
}
if (otherErrors.length > 0) {
errorDetails += `\n\n📋 ДРУГИЕ ОШИБКИ (${otherErrors.length}):\n`;
errorDetails += `═══════════════════════════════════════\n`;
const othersToShow = otherErrors.slice(0, 5);
errorDetails += othersToShow.join('\n\n');
if (otherErrors.length > 5) {
errorDetails += `\n\n... и еще ${otherErrors.length - 5} ошибок`;
}
}
alert(errorDetails);
// Детальный лог в консоль
console.group('📋 ДЕТАЛИ ИМПОРТА CSV');
console.log(`✅ Успешно: ${result.inserted}`);
console.log(`❌ Ошибок: ${result.errors}`);
if (fkErrors.length > 0) {
console.group(`⚠️ FK Ошибки (${fkErrors.length})`);
fkErrors.forEach((msg, idx) => console.log(`${idx + 1}. ${msg}`));
console.groupEnd();
}
if (otherErrors.length > 0) {
console.group(`📋 Другие ошибки (${otherErrors.length})`);
otherErrors.forEach((msg, idx) => console.log(`${idx + 1}. ${msg}`));
console.groupEnd();
}
console.groupEnd();
if (result.errors > 0 && result.failedRows && result.failedRows.length > 0) {
// ✅ Есть ошибки - предлагаем скачать CSV с проблемными строками
showImportErrorDialog(result, headers);
} else if (result.errors > 0) {
// Ошибки без детальных данных (старый формат)
alert(`📊 Импорт завершён:\n✅ Импортировано: ${result.inserted}\n❌ Ошибок: ${result.errors}`);
} else {
alert(` Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
alert(` Импортировано строк: ${result.inserted}`);
}
await table.replaceData();
@@ -1984,6 +1934,117 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
e.target.value = '';
}
});
// ✅ Диалог результатов импорта с ошибками
function showImportErrorDialog(result, headers) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); display: flex;
align-items: center; justify-content: center; z-index: 10000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white; padding: 20px; border-radius: 8px;
max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto;
`;
// Группируем ошибки по типу
const errorGroups = {};
result.failedRows.forEach(item => {
const errType = item.error || 'Неизвестная ошибка';
if (!errorGroups[errType]) {
errorGroups[errType] = [];
}
errorGroups[errType].push(item);
});
let errorSummary = '';
Object.entries(errorGroups).forEach(([errType, items]) => {
errorSummary += `<div style="margin-bottom: 10px; padding: 8px; background: #fff3cd; border-radius: 4px;">
<strong>${escapeHtml(errType)}</strong>: ${items.length} строк
</div>`;
});
dialog.innerHTML = `
<h3 style="margin-top: 0; color: #d32f2f;">📊 Результат импорта</h3>
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
<div style="flex: 1; padding: 15px; background: #e8f5e9; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #2e7d32;">${result.inserted}</div>
<div style="color: #666;">Импортировано</div>
</div>
<div style="flex: 1; padding: 15px; background: #ffebee; border-radius: 8px; text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #c62828;">${result.errors}</div>
<div style="color: #666;">Ошибок</div>
</div>
</div>
<h4 style="margin-bottom: 10px;">Типы ошибок:</h4>
${errorSummary}
<p style="color: #666; margin-top: 15px;">
Скачайте CSV файл с проблемными строками, исправьте данные и импортируйте повторно.
</p>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; border-top: 1px solid #ddd; padding-top: 15px;">
<button id="importErrorClose" style="padding: 10px 20px; cursor: pointer; background: #ddd; border: none; border-radius: 4px;">
Закрыть
</button>
<button id="importErrorDownload" style="padding: 10px 20px; cursor: pointer; background: #f44336; color: white; border: none; border-radius: 4px;">
📥 Скачать ошибки CSV
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
dialog.querySelector('#importErrorClose').onclick = () => modal.remove();
dialog.querySelector('#importErrorDownload').onclick = () => {
// Формируем CSV с ошибками
const delimiter = ';';
const errorHeaders = [...headers, '_ОШИБКА', '_СТРОКА'];
let csvContent = errorHeaders.join(delimiter) + '\n';
result.failedRows.forEach(item => {
const rowValues = headers.map(h => {
let val = item.row[h];
if (val === null || val === undefined) val = '';
val = String(val);
if (val.includes(delimiter) || val.includes('"') || val.includes('\n')) {
val = '"' + val.replace(/"/g, '""') + '"';
}
return val;
});
// Добавляем колонки с ошибкой и номером строки
let errorVal = item.error || '';
if (errorVal.includes(delimiter) || errorVal.includes('"')) {
errorVal = '"' + errorVal.replace(/"/g, '""') + '"';
}
rowValues.push(errorVal);
rowValues.push(item.line || '');
csvContent += rowValues.join(delimiter) + '\n';
});
// Скачиваем
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
const filename = new Date().toISOString().slice(0, 10) + '-' + currentTable + '_errors.csv';
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
modal.remove();
};
}
document.getElementById('btnAnalyzeCSV').addEventListener('click', () => {
if (!currentSchema || !currentTable) {
alert('Сначала выберите таблицу');