diff --git a/public/app.js b/public/app.js
index eca6457..cb97c87 100644
--- a/public/app.js
+++ b/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 += `
+ ${escapeHtml(errType)}: ${items.length} строк
+
`;
+ });
+
+ dialog.innerHTML = `
+ 📊 Результат импорта
+
+
+
+
${result.inserted}
+
Импортировано
+
+
+
${result.errors}
+
Ошибок
+
+
+
+ Типы ошибок:
+ ${errorSummary}
+
+
+ Скачайте CSV файл с проблемными строками, исправьте данные и импортируйте повторно.
+
+
+
+
+
+
+ `;
+
+ 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('Сначала выберите таблицу');
diff --git a/src/DataService.php b/src/DataService.php
index e987b3f..21650c9 100644
--- a/src/DataService.php
+++ b/src/DataService.php
@@ -300,6 +300,7 @@ public function insertMultipleRows(string $schema, string $table, array $rows, a
$inserted = 0;
$errors = 0;
$errorMessages = [];
+ $failedRows = []; // Строки, которые не удалось импортировать
// Собираем информацию о всех столбцах таблицы
$validColumns = [];
@@ -420,12 +421,19 @@ public function insertMultipleRows(string $schema, string $table, array $rows, a
$inserted++;
} catch (\PDOException $e) {
$errors++;
-
+
// ✅ Формируем детальное сообщение об ошибке с данными строки
$csvLineNumber = $index + 2; // +2 потому что: +1 для человеческой нумерации, +1 для строки заголовка
$errorMsg = $this->formatImportError($e, $csvLineNumber, $row, $columnTypes);
$errorMessages[] = $errorMsg;
-
+
+ // ✅ Сохраняем данные неудачной строки с описанием ошибки
+ $failedRows[] = [
+ 'row' => $row,
+ 'error' => $this->getShortErrorMessage($e),
+ 'line' => $csvLineNumber
+ ];
+
// Логируем только первые 10 ошибок
if ($errors <= 10) {
error_log("Ошибка импорта строки CSV #$csvLineNumber: " . $e->getMessage());
@@ -445,10 +453,60 @@ public function insertMultipleRows(string $schema, string $table, array $rows, a
return [
'inserted' => $inserted,
'errors' => $errors,
- 'errorMessages' => $errorMessages
+ 'errorMessages' => $errorMessages,
+ 'failedRows' => $failedRows
];
}
+/**
+ * Краткое сообщение об ошибке для CSV
+ */
+private function getShortErrorMessage(\PDOException $e): string
+{
+ $msg = $e->getMessage();
+
+ // Duplicate entry
+ if (preg_match("/Duplicate entry '(.+)' for key/", $msg, $m)) {
+ return "Дубликат: {$m[1]}";
+ }
+
+ // Foreign key constraint
+ if (str_contains($msg, 'foreign key constraint')) {
+ if (preg_match("/FOREIGN KEY \(`(.+?)`\)/", $msg, $m)) {
+ return "Неверная ссылка: {$m[1]}";
+ }
+ return "Ошибка внешнего ключа";
+ }
+
+ // Data too long
+ if (preg_match("/Data too long for column '(.+?)'/", $msg, $m)) {
+ return "Слишком длинное значение: {$m[1]}";
+ }
+
+ // Incorrect value
+ if (preg_match("/Incorrect (.+?) value: '(.+?)' for column '(.+?)'/", $msg, $m)) {
+ return "Неверный формат {$m[1]}: {$m[3]}";
+ }
+
+ // Out of range
+ if (preg_match("/Out of range value for column '(.+?)'/", $msg, $m)) {
+ return "Значение вне диапазона: {$m[1]}";
+ }
+
+ // Cannot be null
+ if (preg_match("/Column '(.+?)' cannot be null/", $msg, $m)) {
+ return "Обязательное поле пустое: {$m[1]}";
+ }
+
+ // Missing field
+ if (preg_match("/Missing required field: (.+)/", $msg, $m)) {
+ return "Отсутствует поле: {$m[1]}";
+ }
+
+ // Default: first 100 chars
+ return mb_substr($msg, 0, 100);
+}
+
/**
* Форматирует ошибку импорта с детальной информацией
*/