diff --git a/public/app.js b/public/app.js index 2e6d45b..dd4fca9 100644 --- a/public/app.js +++ b/public/app.js @@ -3,9 +3,8 @@ let currentTable = null; let currentMeta = null; let table = null; let enterHandler = null; -let selectedRowsData = new Map(); // Хранилище выделенных строк для всех страниц +let selectedRowsData = new Map(); -// Простая обёртка для fetch JSON async function api(url, method = 'GET', body) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); @@ -18,7 +17,6 @@ async function api(url, method = 'GET', body) { return res.json(); } -// Обновление счетчика выделенных строк function updateSelectionCounter() { const counter = document.getElementById('selectionCounter'); const count = selectedRowsData.size; @@ -31,7 +29,6 @@ function updateSelectionCounter() { } } -// Функция создания модального окна с прогресс-баром function createProgressModal(message) { const modal = document.createElement('div'); modal.style.cssText = ` @@ -75,12 +72,11 @@ function createProgressModal(message) { modal.appendChild(dialog); - // Анимация прогресс-бара (имитация) const progressBar = dialog.querySelector('.progress-bar'); let progress = 0; const interval = setInterval(() => { progress += Math.random() * 15; - if (progress > 90) progress = 90; // Останавливаемся на 90% + if (progress > 90) progress = 90; progressBar.style.width = progress + '%'; }, 200); @@ -98,7 +94,15 @@ function escapeHtml(text) { return div.innerHTML; } -// Логин +function getRowKey(rowData) { + if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) { + return JSON.stringify(rowData); + } + + const pkValues = currentMeta.primaryKey.map(pk => rowData[pk]).join('|'); + return pkValues; +} + document.getElementById('loginBtn').addEventListener('click', async () => { const user = document.getElementById('loginUser').value.trim(); const pass = document.getElementById('loginPass').value; @@ -131,7 +135,6 @@ document.getElementById('loginBtn').addEventListener('click', async () => { } }); -// loadTree async function loadTree() { const treeEl = document.getElementById('tree'); treeEl.style.color = ''; @@ -170,118 +173,6 @@ async function loadTree() { let lastEditedRow = null; -// Получение уникального ключа строки на основе PK -function getRowKey(rowData) { - if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) { - // Если нет PK, используем все поля - const key = JSON.stringify(rowData); - console.log(' 🔑 getRowKey (no PK):', key.substring(0, 100)); - return key; - } - - const pkValues = currentMeta.primaryKey.map(pk => { - const value = rowData[pk]; - if (value === undefined || value === null) { - console.warn(` ⚠️ PK field '${pk}' is undefined/null in rowData:`, rowData); - } - return value; - }).join('|'); - - console.log(' 🔑 getRowKey (PK):', pkValues, 'from fields:', currentMeta.primaryKey); - return pkValues; -} - - -async function selectTable(schema, tableName) { - currentSchema = schema; - currentTable = tableName; - lastEditedRow = null; - selectedRowsData.clear(); // Очищаем выделение при смене таблицы - updateSelectionCounter(); - - if (enterHandler) { - document.removeEventListener('keydown', enterHandler); - enterHandler = null; - } - - currentMeta = await api( - `/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}` - ); - - // Добавляем столбец с чекбоксами в начало - const columns = [ - { - formatter: "rowSelection", - titleFormatter: "rowSelection", - hozAlign: "center", - headerSort: false, - width: 40, - cellClick: function(e, cell) { - cell.getRow().toggleSelect(); - } - }, - ...currentMeta.columns.map(col => ({ - title: col.COLUMN_NAME, - field: col.COLUMN_NAME, - editor: "input", - headerFilter: "input" - })) - ]; - - if (table) { - table.destroy(); - table = null; - } - - table = new Tabulator("#table", { - selectableRows: true, - columns: columns, - layout: "fitColumns", - resizableColumnFit: true, - - pagination: true, - paginationMode: "remote", - paginationSize: 50, - paginationSizeSelector: [25, 50, 100, 200], - - filterMode: "remote", - sortMode: "remote", - - ajaxURL: "/api/table/data", - ajaxConfig: "POST", - ajaxContentType: "json", - - ajaxParams: function () { - const headerFilters = this.getHeaderFilters ? this.getHeaderFilters() : []; - const filters = (headerFilters || []).map(f => ({ - field: f.field, - value: f.value - })).filter(f => f.value !== null && f.value !== ''); - - const sorters = this.getSorters ? this.getSorters() : []; - const sort = (sorters && sorters.length > 0) ? { - field: sorters[0].field, - dir: sorters[0].dir - } : null; - - return { - schema: currentSchema, - table: currentTable, - filters: filters, - sort: sort, - columns: currentMeta.columns, - page: this.getPage ? this.getPage() : 1, - pageSize: this.getPageSize ? this.getPageSize() : 50 - }; - }, - - ajaxResponse: function (url, params, response) { - return { - last_page: response.last_page || 1, - data: response.data || [] - }; - }, - async function selectTable(schema, tableName) { currentSchema = schema; currentTable = tableName; @@ -371,58 +262,39 @@ async function selectTable(schema, tableName) { }; }, - // ✅ ИСПРАВЛЕНО: Используем параметр data, который содержит ВСЕ выделенные строки rowSelectionChanged: function(data, rows) { - console.log('🔔 rowSelectionChanged called:', { + console.log('🔔 rowSelectionChanged:', { 'data (all selected)': data.length, - 'rows (changed)': rows.length, - 'selectedRowsData before': selectedRowsData.size + 'rows (changed)': rows.length }); - // ВАЖНО: параметр data содержит массив данных ВСЕХ выделенных строк - // параметр rows содержит только Row объекты измененных строк - - // Получаем все выделенные строки напрямую из Tabulator - const allSelectedData = data; // это уже массив данных всех выделенных строк - - // Удаляем из selectedRowsData все строки текущей страницы const currentPageRows = this.getRows(); currentPageRows.forEach(row => { const rowData = row.getData(); const key = getRowKey(rowData); selectedRowsData.delete(key); - console.log(` 🧹 Removed from selectedRowsData: ${key}`); }); - // Добавляем все выделенные строки обратно - allSelectedData.forEach(rowData => { + data.forEach(rowData => { const key = getRowKey(rowData); selectedRowsData.set(key, rowData); - console.log(` ✅ Added to selectedRowsData: ${key}`); }); - console.log(` 📊 selectedRowsData after: ${selectedRowsData.size}`); + console.log(` 📊 selectedRowsData: ${selectedRowsData.size}`); updateSelectionCounter(); }, dataLoaded: function(data) { - console.log('📄 dataLoaded, восстанавливаем выделение для', selectedRowsData.size, 'строк'); - if (selectedRowsData.size > 0) { const rows = this.getRows(); - let restoredCount = 0; - rows.forEach(row => { const rowData = row.getData(); const key = getRowKey(rowData); if (selectedRowsData.has(key)) { row.select(); - restoredCount++; } }); - - console.log(' ✅ Восстановлено выделение для', restoredCount, 'строк на странице'); } }, @@ -463,67 +335,6 @@ async function selectTable(schema, tableName) { document.addEventListener('keydown', enterHandler); } - // После загрузки данных восстанавливаем выделение - dataLoaded: function(data) { - console.log('📄 dataLoaded, восстанавливаем выделение для', selectedRowsData.size, 'строк'); - - if (selectedRowsData.size > 0) { - const rows = this.getRows(); - let restoredCount = 0; - - rows.forEach(row => { - const rowData = row.getData(); - const key = getRowKey(rowData); - - if (selectedRowsData.has(key)) { - row.select(); - restoredCount++; - } - }); - - console.log(' ✅ Восстановлено выделение для', restoredCount, 'строк на странице'); - } - }, - - cellEdited: function(cell) { - const row = cell.getRow(); - lastEditedRow = row.getData(); - row.getElement().style.backgroundColor = '#fffae6'; - }, - - headerFilterLiveFilterDelay: 800 - }); - - // Сохранение по Enter - enterHandler = async function(e) { - if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) { - e.preventDefault(); - - try { - const res = await api('/api/table/update', 'POST', { - schema: currentSchema, - table: currentTable, - row: lastEditedRow - }); - - lastEditedRow = null; - - table.getRows().forEach(r => { - r.getElement().style.backgroundColor = ''; - }); - - await table.replaceData(); - } catch (err) { - console.error('Ошибка сохранения:', err); - alert('Ошибка: ' + err.message); - } - } - }; - - document.addEventListener('keydown', enterHandler); -} - -// ✅ ВЫДЕЛИТЬ ВСЕ (НА ВСЕХ СТРАНИЦАХ) document.getElementById('btnSelectAll').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { alert('Сначала выберите таблицу'); @@ -531,13 +342,11 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => { } try { - // Получаем текущие фильтры const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : []; const filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value })); const sorters = table.getSorters ? table.getSorters() : []; const sort = (sorters && sorters.length > 0) ? { field: sorters[0].field, dir: sorters[0].dir } : null; - // Получаем все данные с сервера (с учетом фильтров, но без пагинации) const result = await api('/api/table/data', 'POST', { schema: currentSchema, table: currentTable, @@ -545,12 +354,11 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => { sort: sort, columns: currentMeta.columns, page: 1, - pageSize: 999999 // Получаем все записи + pageSize: 999999 }); const totalRows = result.total; - // Предупреждение для больших таблиц if (totalRows > 1000) { const proceed = confirm( `Вы собираетесь выделить ${totalRows} записей.\n` + @@ -561,14 +369,12 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => { if (!proceed) return; } - // Добавляем все строки в selectedRowsData selectedRowsData.clear(); result.data.forEach(rowData => { const key = getRowKey(rowData); selectedRowsData.set(key, rowData); }); - // Выделяем видимые строки на текущей странице const rows = table.getRows(); rows.forEach(row => { const rowData = row.getData(); @@ -587,7 +393,6 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => { } }); -// ✅ СНЯТЬ ВЫДЕЛЕНИЕ document.getElementById('btnDeselectAll').addEventListener('click', () => { if (!table) return; @@ -596,7 +401,6 @@ document.getElementById('btnDeselectAll').addEventListener('click', () => { updateSelectionCounter(); }); -// ✅ ВСТАВИТЬ document.getElementById('btnInsert').addEventListener('click', async () => { if (!currentSchema || !currentTable || !currentMeta) { alert('Сначала выберите таблицу'); @@ -663,15 +467,11 @@ document.getElementById('btnInsert').addEventListener('click', async () => { row: rowData }); - const selected = table.getSelectedRows(); - await table.replaceData(); - if (selected.length === 0) { - const lastPage = table.getPageMax(); - if (lastPage > 1) { - table.setPage(lastPage); - } + const lastPage = table.getPageMax(); + if (lastPage > 1) { + table.setPage(lastPage); } alert('✓ Строка успешно создана'); @@ -681,7 +481,6 @@ document.getElementById('btnInsert').addEventListener('click', async () => { } }); -// Функция для выбора FK значений async function promptForForeignKeys(fkFields) { const modal = document.createElement('div'); modal.className = 'fk-modal'; @@ -804,12 +603,7 @@ async function promptForForeignKeys(fkFields) { }); } -// ✅ УДАЛИТЬ (оптимизированное с batch delete) document.getElementById('btnDelete').addEventListener('click', async () => { -// ВРЕМЕННАЯ ОТЛАДКА -console.log('Current Primary Keys:', currentMeta?.primaryKey); -console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.values())[0]); - if (!table || !currentSchema || !currentTable) { alert('Сначала выберите таблицу'); return; @@ -819,19 +613,15 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val console.log('🗑️ Удаление:', { selectedRowsDataSize: count, - selectedRowsDataKeys: Array.from(selectedRowsData.keys()) + primaryKeys: currentMeta?.primaryKey }); if (count === 0) { - // Дополнительная проверка - может быть строки выделены в Tabulator, но не в selectedRowsData? const tabulatorSelected = table.getSelectedData(); console.log('Tabulator selected rows:', tabulatorSelected.length); if (tabulatorSelected.length > 0) { console.error('❌ Несоответствие: Tabulator имеет выделенные строки, но selectedRowsData пуст!'); - console.log('Попытка синхронизации...'); - - // Синхронизируем selectedRowsData.clear(); tabulatorSelected.forEach(rowData => { const key = getRowKey(rowData); @@ -856,25 +646,18 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val if (!confirm(confirmMsg)) return; - // Создаем модальное окно с прогресс-баром const modal = createProgressModal('Удаление записей...'); document.body.appendChild(modal); try { - // Преобразуем Map в массив const rowsArray = Array.from(selectedRowsData.values()); - console.log('Отправка на удаление:', rowsArray.length, 'строк'); - - // Используем batch delete для оптимизации const result = await api('/api/table/delete-batch', 'POST', { schema: currentSchema, table: currentTable, rows: rowsArray }); - console.log('Результат удаления:', result); - document.body.removeChild(modal); selectedRowsData.clear(); @@ -897,9 +680,6 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val } }); - -// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ========== - function detectDelimiter(text) { const firstLine = text.split('\n')[0]; const semicolonCount = (firstLine.match(/;/g) || []).length; @@ -956,7 +736,6 @@ function parseCSV(text) { return lines; } -// ========== ИМПОРТ CSV ========== document.getElementById('btnImportCSV').addEventListener('click', () => { if (!currentSchema || !currentTable) { alert('Сначала выберите таблицу'); @@ -1008,36 +787,31 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => rows: records }); -if (result.errorMessages && result.errorMessages.length > 0) { - // Показываем только первые 10 ошибок - const errorsToShow = result.errorMessages.slice(0, 10); - const moreErrors = result.errorMessages.length > 10 - ? `\n... и еще ${result.errorMessages.length - 10} ошибок` - : ''; - - // Создаем детальное сообщение - let errorDetails = `Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\n`; - errorDetails += 'ПЕРВЫЕ ОШИБКИ:\n'; - errorDetails += '═══════════════════════════════════════\n'; - errorDetails += errorsToShow.join('\n\n'); - errorDetails += moreErrors; - - // Выводим в alert (для больших сообщений можно использовать модальное окно) - alert(errorDetails); - - // Также выводим в консоль для детального анализа - console.group('📋 Детали импорта CSV'); - console.log(`✅ Импортировано: ${result.inserted}`); - console.log(`❌ Ошибок: ${result.errors}`); - console.log('\nВсе ошибки:'); - result.errorMessages.forEach((msg, idx) => { - console.log(`${idx + 1}. ${msg}`); - }); - console.groupEnd(); -} else { - alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`); -} - + if (result.errorMessages && result.errorMessages.length > 0) { + const errorsToShow = result.errorMessages.slice(0, 10); + const moreErrors = result.errorMessages.length > 10 + ? `\n... и еще ${result.errorMessages.length - 10} ошибок` + : ''; + + let errorDetails = `Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\n`; + errorDetails += 'ПЕРВЫЕ ОШИБКИ:\n'; + errorDetails += '═══════════════════════════════════════\n'; + errorDetails += errorsToShow.join('\n\n'); + errorDetails += moreErrors; + + alert(errorDetails); + + console.group('📋 Детали импорта CSV'); + console.log(`✅ Импортировано: ${result.inserted}`); + console.log(`❌ Ошибок: ${result.errors}`); + console.log('\nВсе ошибки:'); + result.errorMessages.forEach((msg, idx) => { + console.log(`${idx + 1}. ${msg}`); + }); + console.groupEnd(); + } else { + alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`); + } await table.replaceData(); e.target.value = ''; @@ -1048,7 +822,6 @@ if (result.errorMessages && result.errorMessages.length > 0) { } }); -// ========== ЭКСПОРТ CSV ========== document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { alert('Сначала выберите таблицу');