Добавлен экспорт неимпортированных строк в CSV при ошибках
Backend: - insertMultipleRows возвращает failedRows с данными и описанием ошибки - getShortErrorMessage формирует понятные сообщения об ошибках Frontend: - Диалог с результатами импорта (успешных/ошибок) - Группировка ошибок по типу - Кнопка "Скачать ошибки CSV" - файл с проблемными строками + колонки _ОШИБКА и _СТРОКА Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
175
public/app.js
175
public/app.js
@@ -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('Сначала выберите таблицу');
|
||||
|
||||
Reference in New Issue
Block a user