let currentSchema = null; let currentTable = null; let currentMeta = null; let table = null; let enterHandler = null; let selectedRowsData = new Map(); async function api(url, method = 'GET', body) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(url, opts); if (!res.ok) { const txt = await res.text(); throw new Error(`HTTP ${res.status}: ${txt}`); } return res.json(); } function updateSelectionCounter() { const counter = document.getElementById('selectionCounter'); const count = selectedRowsData.size; if (count > 0) { counter.textContent = `Выбрано: ${count}`; counter.style.display = 'block'; } else { counter.style.display = 'none'; } } function createProgressModal(message) { 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: 30px; border-radius: 8px; min-width: 300px; text-align: center; `; dialog.innerHTML = `
${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; progressBar.style.width = progress + '%'; }, 200); modal.stopProgress = () => { clearInterval(interval); progressBar.style.width = '100%'; }; return modal; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getRowKey(rowData) { if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) { const key = JSON.stringify(rowData); return key; } const pkValues = currentMeta.primaryKey.map(pk => { const value = rowData[pk]; if (value === undefined || value === null) { console.warn(`⚠️ getRowKey: PK field '${pk}' is undefined/null in rowData:`, rowData); } return value; }).join('|'); return pkValues; } document.getElementById('loginBtn').addEventListener('click', async () => { const user = document.getElementById('loginUser').value.trim(); const pass = document.getElementById('loginPass').value; const statusEl = document.getElementById('loginStatus'); if (!user || !pass) { statusEl.textContent = 'Введите логин и пароль'; statusEl.style.color = 'red'; return; } statusEl.textContent = 'Проверяем подключение...'; statusEl.style.color = ''; try { const res = await api('/api/login', 'POST', { user, pass }); if (res.ok) { statusEl.textContent = '✓ Авторизация успешна'; statusEl.style.color = 'green'; await loadTree(); } else { statusEl.textContent = 'Ошибка: ' + (res.error || 'Неизвестная ошибка'); statusEl.style.color = 'red'; } } catch (e) { console.error('Login ошибка:', e); statusEl.textContent = 'Ошибка подключения: ' + e.message; statusEl.style.color = 'red'; } }); async function loadTree() { const treeEl = document.getElementById('tree'); treeEl.style.color = ''; treeEl.innerHTML = 'Загрузка...'; try { const tree = await api('/api/tree'); treeEl.innerHTML = ''; tree.forEach(schema => { const schemaEl = document.createElement('div'); schemaEl.className = 'schema'; schemaEl.textContent = schema.name; schemaEl.style.cursor = 'pointer'; schemaEl.style.padding = '4px'; schemaEl.style.fontWeight = 'bold'; treeEl.appendChild(schemaEl); (schema.tables || []).forEach(tbl => { const tableEl = document.createElement('div'); tableEl.className = 'table'; tableEl.textContent = ` ${tbl}`; tableEl.style.cursor = 'pointer'; tableEl.style.paddingLeft = '12px'; tableEl.style.padding = '2px'; tableEl.addEventListener('click', () => selectTable(schema.name, tbl)); treeEl.appendChild(tableEl); }); }); } catch (e) { console.error('loadTree ошибка:', e); treeEl.innerHTML = 'Ошибка загрузки: ' + e.message; treeEl.style.color = 'red'; } } let lastEditedRow = null; 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)}` ); console.log('📋 Метаданные таблицы загружены:', { schema, table: tableName, primaryKey: currentMeta.primaryKey, columnsCount: currentMeta.columns.length }); const columns = [ { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerSort: false, width: 40, cellClick: function(e, cell) { console.log('📌 Клик на чекбокс ячейки'); 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 || [] }; }, rowSelectionChanged: function(data, rows) { console.group('🔔 rowSelectionChanged вызван'); console.log('Параметры:', { 'data.length (все выделенные)': data.length, 'rows.length (изменённые)': rows.length, 'selectedRowsData.size до': selectedRowsData.size }); // Выводим первые 2 элемента из data для проверки if (data.length > 0) { console.log('Первая строка из data:', data[0]); console.log('Проверка getRowKey для первой строки:', getRowKey(data[0])); } // Выводим первые 2 элемента из rows для проверки if (rows.length > 0) { console.log('Первая строка из rows (Row объект):', rows[0]); console.log('getData() первой строки из rows:', rows[0].getData()); } // Получаем все строки текущей страницы const currentPageRows = this.getRows(); console.log('Строк на текущей странице:', currentPageRows.length); // Удаляем все строки текущей страницы из selectedRowsData let deletedCount = 0; currentPageRows.forEach(row => { const rowData = row.getData(); const key = getRowKey(rowData); if (selectedRowsData.has(key)) { selectedRowsData.delete(key); deletedCount++; } }); console.log('Удалено из selectedRowsData:', deletedCount); // Добавляем все выделенные строки из параметра data let addedCount = 0; data.forEach((rowData, index) => { const key = getRowKey(rowData); selectedRowsData.set(key, rowData); addedCount++; if (index < 3) { console.log(` Добавлена строка ${index + 1}: key="${key}"`); } }); console.log('Добавлено в selectedRowsData:', addedCount); console.log('selectedRowsData.size после:', selectedRowsData.size); console.log('Первые 3 ключа в selectedRowsData:', Array.from(selectedRowsData.keys()).slice(0, 3)); console.groupEnd(); 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, 'строк на странице'); } }, cellEdited: function(cell) { const row = cell.getRow(); lastEditedRow = row.getData(); row.getElement().style.backgroundColor = '#fffae6'; }, headerFilterLiveFilterDelay: 800 }); 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('Сначала выберите таблицу'); return; } 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, filters: filters, sort: sort, columns: currentMeta.columns, page: 1, pageSize: 999999 }); const totalRows = result.total; if (totalRows > 1000) { const proceed = confirm( `Вы собираетесь выделить ${totalRows} записей.\n` + `Это может занять некоторое время и использовать много памяти.\n\n` + `Продолжить?` ); if (!proceed) return; } 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(); const key = getRowKey(rowData); if (selectedRowsData.has(key)) { row.select(); } }); updateSelectionCounter(); alert(`Выделено записей: ${totalRows}`); } catch (err) { console.error('Ошибка выделения:', err); alert('Ошибка выделения: ' + err.message); } }); document.getElementById('btnDeselectAll').addEventListener('click', () => { if (!table) return; selectedRowsData.clear(); table.deselectRow(); updateSelectionCounter(); }); document.getElementById('btnInsert').addEventListener('click', async () => { if (!currentSchema || !currentTable || !currentMeta) { alert('Сначала выберите таблицу'); return; } const rowData = {}; const fkFields = []; currentMeta.columns.forEach(col => { const name = col.COLUMN_NAME; if (col.IS_AUTO_INCREMENT) return; if (col.HAS_DEFAULT && col.COLUMN_DEFAULT !== null) { if (String(col.COLUMN_DEFAULT).toUpperCase().includes('CURRENT_TIMESTAMP')) return; rowData[name] = col.COLUMN_DEFAULT; return; } if (col.IS_FOREIGN_KEY) { fkFields.push({ name: name, ref_schema: col.FOREIGN_KEY.ref_schema, ref_table: col.FOREIGN_KEY.ref_table, ref_column: col.FOREIGN_KEY.ref_column, required: col.IS_REQUIRED }); if (!col.IS_REQUIRED) { rowData[name] = null; } return; } if (col.IS_REQUIRED) { if (col.EDITOR_TYPE === 'number') rowData[name] = 0; else if (col.EDITOR_TYPE === 'datetime' || col.EDITOR_TYPE === 'date') rowData[name] = new Date().toISOString().slice(0, 10); else if (col.EDITOR_TYPE === 'time') rowData[name] = '00:00:00'; else rowData[name] = ''; } }); const requiredFKs = fkFields.filter(f => f.required); if (requiredFKs.length > 0) { try { const fkValues = await promptForForeignKeys(requiredFKs); if (!fkValues) { return; } Object.assign(rowData, fkValues); } catch (err) { console.error('Ошибка получения FK значений:', err); alert('Ошибка: ' + err.message); return; } } try { await api('/api/table/insert', 'POST', { schema: currentSchema, table: currentTable, row: rowData }); await table.replaceData(); const lastPage = table.getPageMax(); if (lastPage > 1) { table.setPage(lastPage); } alert('✓ Строка успешно создана'); } catch (e) { console.error('Ошибка вставки:', e); alert('Ошибка вставки: ' + e.message); } }); async function promptForForeignKeys(fkFields) { const modal = document.createElement('div'); modal.className = 'fk-modal'; 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: 500px; width: 90%; max-height: 80vh; overflow-y: auto; `; let html = '

Заполните обязательные поля

'; const fkOptions = {}; for (const fk of fkFields) { try { const result = await api( `/api/fk-values?schema=${encodeURIComponent(fk.ref_schema)}&` + `table=${encodeURIComponent(fk.ref_table)}&` + `column=${encodeURIComponent(fk.ref_column)}` ); fkOptions[fk.name] = result.values || []; } catch (err) { console.error(`Ошибка загрузки FK для ${fk.name}:`, err); fkOptions[fk.name] = []; } } fkFields.forEach(fk => { const options = fkOptions[fk.name]; html += `
`; if (options.length > 0) { html += `'; } else { html += ` ⚠️ Нет доступных значений в ${fk.ref_table} `; } html += '
'; }); html += `
`; dialog.innerHTML = html; modal.appendChild(dialog); document.body.appendChild(modal); return new Promise((resolve) => { document.getElementById('fkCancel').addEventListener('click', () => { document.body.removeChild(modal); resolve(null); }); document.getElementById('fkSubmit').addEventListener('click', () => { const values = {}; let allFilled = true; for (const fk of fkFields) { const input = document.getElementById(`fk_${fk.name}`); const value = input.value.trim(); if (!value) { alert(`Поле "${fk.name}" обязательно для заполнения!`); allFilled = false; break; } values[fk.name] = value; } if (allFilled) { document.body.removeChild(modal); resolve(values); } }); }); } document.getElementById('btnDelete').addEventListener('click', async () => { if (!table || !currentSchema || !currentTable) { alert('Сначала выберите таблицу'); return; } const count = selectedRowsData.size; console.log('🗑️ Удаление:', { selectedRowsDataSize: count, primaryKeys: currentMeta?.primaryKey }); if (count === 0) { const tabulatorSelected = table.getSelectedData(); console.log('Tabulator selected rows:', tabulatorSelected.length); if (tabulatorSelected.length > 0) { console.error('❌ Несоответствие: Tabulator имеет выделенные строки, но selectedRowsData пуст!'); selectedRowsData.clear(); tabulatorSelected.forEach(rowData => { const key = getRowKey(rowData); selectedRowsData.set(key, rowData); }); updateSelectionCounter(); if (selectedRowsData.size > 0) { alert(`Обнаружено ${selectedRowsData.size} выделенных строк. Попробуйте удалить снова.`); return; } } alert('Выберите строки для удаления'); return; } const confirmMsg = count === 1 ? 'Удалить выбранную строку?' : `Удалить ${count} выбранных строк?`; if (!confirm(confirmMsg)) return; const modal = createProgressModal('Удаление записей...'); document.body.appendChild(modal); try { const rowsArray = Array.from(selectedRowsData.values()); const result = await api('/api/table/delete-batch', 'POST', { schema: currentSchema, table: currentTable, rows: rowsArray }); document.body.removeChild(modal); selectedRowsData.clear(); updateSelectionCounter(); await table.replaceData(); if (result.errors > 0) { const errorsToShow = result.errorMessages.slice(0, 10); const moreErrors = result.errorMessages.length > 10 ? `\n... и еще ${result.errorMessages.length - 10} ошибок` : ''; alert(`Удалено строк: ${result.deleted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`); } else { alert(`✓ Удалено строк: ${result.deleted}`); } } catch (e) { document.body.removeChild(modal); console.error('Ошибка удаления:', e); alert('Ошибка удаления: ' + e.message); } }); function detectDelimiter(text) { const firstLine = text.split('\n')[0]; const semicolonCount = (firstLine.match(/;/g) || []).length; const commaCount = (firstLine.match(/,/g) || []).length; if (semicolonCount > 0 && semicolonCount >= commaCount) { return ';'; } return ','; } function parseCSV(text) { const delimiter = detectDelimiter(text); const lines = []; let currentLine = []; let currentField = ''; let inQuotes = false; for (let i = 0; i < text.length; i++) { const char = text[i]; const nextChar = text[i + 1]; if (char === '"') { if (inQuotes && nextChar === '"') { currentField += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === delimiter && !inQuotes) { currentLine.push(currentField.trim()); currentField = ''; } else if ((char === '\n' || char === '\r') && !inQuotes) { if (char === '\r' && nextChar === '\n') { i++; } if (currentField || currentLine.length > 0) { currentLine.push(currentField.trim()); lines.push(currentLine); currentLine = []; currentField = ''; } } else { currentField += char; } } if (currentField || currentLine.length > 0) { currentLine.push(currentField.trim()); lines.push(currentLine); } return lines; } document.getElementById('btnImportCSV').addEventListener('click', () => { if (!currentSchema || !currentTable) { alert('Сначала выберите таблицу'); return; } document.getElementById('csvFileInput').click(); }); document.getElementById('csvFileInput').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const rows = parseCSV(text); if (rows.length === 0) { alert('CSV файл пуст'); return; } const headers = rows[0]; const dataRows = rows.slice(1); if (dataRows.length === 0) { alert('Нет данных для импорта (только заголовки)'); return; } if (dataRows.length > 100) { const proceed = confirm(`Файл содержит ${dataRows.length} строк. Импорт может занять некоторое время. Продолжить?`); if (!proceed) { e.target.value = ''; return; } } const records = dataRows.map(row => { const obj = {}; headers.forEach((header, i) => { obj[header] = row[i] || null; }); return obj; }); const result = await api('/api/table/import-csv', 'POST', { schema: currentSchema, table: currentTable, rows: records }); 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 = ''; } catch (err) { console.error('Ошибка импорта:', err); alert('Ошибка импорта: ' + err.message); e.target.value = ''; } }); document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { alert('Сначала выберите таблицу'); return; } 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/export-csv', 'POST', { schema: currentSchema, table: currentTable, filters: filters, sort: sort, columns: currentMeta.columns }); const csv = result.csv; const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `${currentTable}_${new Date().toISOString().slice(0,10)}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (err) { console.error('Ошибка экспорта:', err); alert('Ошибка экспорта: ' + err.message); } });