// ===== TABLE.JS - Дерево и Tabulator ===== /** * Загрузить дерево баз данных и таблиц */ 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); }); }); // Восстанавливаем последнюю открытую таблицу const lastTable = loadLastTable(); if (lastTable) { console.log('📂 Восстанавливаем последнюю таблицу:', lastTable.schema, '.', lastTable.table); const schemaExists = tree.find(s => s.name === lastTable.schema); if (schemaExists && schemaExists.tables.includes(lastTable.table)) { selectTable(lastTable.schema, lastTable.table, true); } } } catch (e) { console.error('loadTree ошибка:', e); treeEl.innerHTML = 'Ошибка загрузки: ' + e.message; treeEl.style.color = 'red'; } } /** * Показать меню управления столбцами */ function showColumnManager() { if (!table || !currentMeta) { alert('Сначала выберите таблицу'); return; } const overlay = document.createElement('div'); overlay.className = 'columns-menu-overlay'; const menu = document.createElement('div'); menu.className = 'columns-menu'; let html = '

Выбор столбцов

'; html += '
'; html += '💡 Выберите столбцы для отображения. Настройки сохраняются в браузере.'; html += '
'; const columns = table.getColumns(); const userColumns = columns.filter(col => { const field = col.getField(); const definition = col.getDefinition(); if (!field || field === 'undefined' || field.startsWith('__tabulator')) { return false; } if (definition.formatter === 'rowSelection') { return false; } return true; }); console.log('📋 Столбцов для настройки:', userColumns.length); html += '
'; html += ''; html += ''; html += '
'; html += '
'; userColumns.forEach(col => { const field = col.getField(); const definition = col.getDefinition(); const visible = col.isVisible(); const title = definition.title || field; const metaCol = currentMeta.columns.find(c => c.COLUMN_NAME === field); const comment = metaCol?.COLUMN_COMMENT || ''; html += `
`; }); html += '
'; html += `
`; menu.innerHTML = html; document.body.appendChild(overlay); document.body.appendChild(menu); document.getElementById('selectAllColumns').addEventListener('click', () => { document.querySelectorAll('.column-checkbox input[type="checkbox"]').forEach(cb => { cb.checked = true; }); }); document.getElementById('deselectAllColumns').addEventListener('click', () => { document.querySelectorAll('.column-checkbox input[type="checkbox"]').forEach(cb => { cb.checked = false; }); }); document.getElementById('columnMenuCancel').addEventListener('click', () => { document.body.removeChild(overlay); document.body.removeChild(menu); }); document.getElementById('columnMenuApply').addEventListener('click', () => { const visibilityMap = {}; const checkboxes = document.querySelectorAll('.column-checkbox input[type="checkbox"]'); checkboxes.forEach(cb => { const field = cb.dataset.field; const visible = cb.checked; visibilityMap[field] = visible; const column = table.getColumn(field); if (column) { if (visible) { column.show(); } else { column.hide(); } } }); saveColumnVisibility(visibilityMap); document.body.removeChild(overlay); document.body.removeChild(menu); console.log('✅ Видимость столбцов обновлена'); }); overlay.addEventListener('click', () => { document.body.removeChild(overlay); document.body.removeChild(menu); }); } /** * Выбрать и отобразить таблицу */ async function selectTable(schema, tableName, restoreState = false) { console.log('🔄 SELECTTABLE ВЫЗВАН:', schema, '.', tableName, restoreState ? '(с восстановлением)' : ''); currentSchema = schema; currentTable = tableName; selectedRowsDataGlobal.clear(); updateSelectionCounter(); if (!restoreState) { saveLastTable(); } const savedState = restoreState ? loadTableState() : null; if (savedState) { console.log('📂 Сохранённое состояние:', savedState); } if (enterHandler) { document.removeEventListener('keydown', enterHandler); enterHandler = null; } currentMeta = await api( `/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}` ); console.log('📋 Метаданные получены:', currentMeta); // Загружаем значения для всех FK полей const fkValuesCache = new Map(); const fkTotals = new Map(); for (const col of currentMeta.columns) { if (col.IS_FOREIGN_KEY && col.FOREIGN_KEY) { console.log('🔗 Загрузка FK значений для:', col.COLUMN_NAME); try { const result = await api( `/api/fk-values?schema=${encodeURIComponent(col.FOREIGN_KEY.ref_schema)}&` + `table=${encodeURIComponent(col.FOREIGN_KEY.ref_table)}&` + `column=${encodeURIComponent(col.FOREIGN_KEY.ref_column)}` ); fkValuesCache.set(col.COLUMN_NAME, result.values || []); fkTotals.set(col.COLUMN_NAME, result.total || 0); console.log(` ✅ ${col.COLUMN_NAME}: загружено ${result.loaded}/${result.total}`); } catch (err) { console.error(' ❌ Ошибка загрузки FK:', err); fkValuesCache.set(col.COLUMN_NAME, []); fkTotals.set(col.COLUMN_NAME, 0); } } } // Формируем колонки const columns = [ { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerHozAlign: "center", headerSort: false, width: 40, minWidth: 40, maxWidth: 40, resizable: false, frozen: true, cellClick: function(e, cell) { e.stopPropagation(); cell.getRow().toggleSelect(); } }, ...currentMeta.columns.map(col => { let sorterType = "string"; if (col.DATA_TYPE && (col.DATA_TYPE.includes('int') || col.DATA_TYPE.includes('decimal') || col.DATA_TYPE.includes('float'))) { sorterType = "number"; } else if (col.DATA_TYPE && (col.DATA_TYPE.includes('date') || col.DATA_TYPE.includes('time'))) { sorterType = "datetime"; } const colDef = { title: col.COLUMN_NAME, field: col.COLUMN_NAME, headerSort: true, sorter: sorterType, ...(sorterType === "number" && { hozAlign: "right", formatter: function(cell) { const value = cell.getValue(); if (value === null || value === undefined || value === '') return ''; const num = parseFloat(value); if (isNaN(num)) return value; const decimalPlaces = (String(value).split('.')[1] || '').length; return new Intl.NumberFormat('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: Math.min(decimalPlaces, 6) }).format(num); }, mutatorEdit: function(value) { if (value === null || value === undefined || value === '') return value; let cleaned = String(value).replace(/[\s\u00A0\u2007\u202F]+/g, ''); cleaned = cleaned.replace(',', '.'); const num = parseFloat(cleaned); return isNaN(num) ? value : num; } }), headerTooltip: function(e, column) { const comment = col.COLUMN_COMMENT; if (comment) { let tooltip = `${col.COLUMN_NAME}
`; tooltip += `Тип: ${col.COLUMN_TYPE}
`; tooltip += `${comment}`; if (col.IS_FOREIGN_KEY) { tooltip += `
→ ${col.FOREIGN_KEY.ref_table}.${col.FOREIGN_KEY.ref_column}`; } return tooltip; } else { let tooltip = `${col.COLUMN_NAME}
`; tooltip += `Тип: ${col.COLUMN_TYPE}`; if (col.IS_FOREIGN_KEY) { tooltip += `
→ ${col.FOREIGN_KEY.ref_table}.${col.FOREIGN_KEY.ref_column}`; } return tooltip; } } }; // Фильтр и редактор для FK полей if (col.IS_FOREIGN_KEY && fkValuesCache.has(col.COLUMN_NAME)) { const values = fkValuesCache.get(col.COLUMN_NAME); const total = fkTotals.get(col.COLUMN_NAME); if (values.length > 0) { const allLoaded = values.length >= total; if (allLoaded || total <= 1000) { colDef.headerFilter = "list"; colDef.headerFilterParams = { values: values, autocomplete: true, clearable: true, listOnEmpty: true, freetext: true, placeholderEmpty: "Введите для поиска...", filterFunc: function(term, label, value, item) { if (!term) return true; return String(label).toLowerCase().includes(term.toLowerCase()); } }; colDef.headerFilterFunc = function() { return true; }; } else { colDef.headerFilter = "input"; colDef.headerFilterPlaceholder = `Поиск (${total})...`; colDef.headerFilterFunc = function() { return true; }; } if (allLoaded || total <= 1000) { colDef.editor = "list"; colDef.editorParams = { values: values, clearable: col.IS_NULLABLE, autocomplete: true, freetext: false, listOnEmpty: true, emptyValue: col.IS_NULLABLE ? null : undefined, filterFunc: function(term, label, value, item) { return label.toLowerCase().includes(term.toLowerCase()); }, elementAttributes: { placeholder: `Выберите значение (${values.length})` } }; console.log(` 📋 ${col.COLUMN_NAME}: список с ${values.length} значениями`); } else { colDef.editor = "list"; colDef.editorParams = { values: values, clearable: col.IS_NULLABLE, freetext: false, autocomplete: true, listOnEmpty: false, emptyValue: col.IS_NULLABLE ? null : undefined, valuesLookup: async function(cell, filterTerm) { if (!filterTerm || filterTerm.length < 2) { return values.slice(0, 100); } try { const result = await api( `/api/fk-values?schema=${encodeURIComponent(col.FOREIGN_KEY.ref_schema)}&` + `table=${encodeURIComponent(col.FOREIGN_KEY.ref_table)}&` + `column=${encodeURIComponent(col.FOREIGN_KEY.ref_column)}&` + `search=${encodeURIComponent(filterTerm)}` ); console.log(` 🔍 Поиск "${filterTerm}": найдено ${result.values.length}`); return result.values; } catch (err) { console.error('Ошибка поиска:', err); return values.slice(0, 100); } }, elementAttributes: { placeholder: `Введите для поиска (всего: ${total})` } }; console.log(` 🔍 ${col.COLUMN_NAME}: динамический поиск (всего: ${total})`); } } else { colDef.headerFilter = "input"; colDef.headerFilterFunc = function() { return true; }; colDef.editor = "input"; colDef.editorParams = { elementAttributes: { placeholder: `⚠️ Нет значений в ${col.FOREIGN_KEY.ref_table}` } }; } } else { colDef.headerFilter = "input"; colDef.headerFilterFunc = function() { return true; }; colDef.editor = "input"; } return colDef; }) ]; if (table) { table.destroy(); table = null; } const dirtyRows = new Map(); console.log('🏗️ Создание Tabulator...'); table = new Tabulator("#table", { selectableRows: true, selectableRowsPersistence: true, columns: columns, layout: "fitDataStretch", resizableColumns: true, resizableColumnFit: false, columnMinWidth: 100, columnDefaults: { resizable: true, headerSort: 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() { let filters = []; let sort = null; if (typeof this.getHeaderFilters === 'function') { const headerFilters = this.getHeaderFilters(); filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value })).filter(f => f.value !== null && f.value !== ''); } if (typeof this.getSorters === 'function') { const sorters = this.getSorters(); if (sorters && sorters.length > 0) { sort = { field: sorters[0].field, dir: sorters[0].dir }; } } console.log('📊 AJAX запрос:', { filters, sort, page: this.getPage ? this.getPage() : 1 }); return { schema: currentSchema, table: currentTable, filters: filters, sort: sort, columns: currentMeta.columns }; }, ajaxResponse: function(url, params, response) { console.log('📊 Ответ сервера:', { total: response.total, lastPage: response.last_page }); return { last_page: response.last_page || 1, data: response.data || [] }; }, headerFilterLiveFilterDelay: 800 }); let stateRestored = false; let filtersApplied = false; table.on("tableBuilt", function() { console.log('🏗️ Таблица построена'); const checkboxColumn = table.getColumns().find(col => { const def = col.getDefinition(); return def.formatter === 'rowSelection'; }); if (checkboxColumn) { const element = checkboxColumn.getElement(); if (element) { element.style.width = '40px'; element.style.minWidth = '40px'; element.style.maxWidth = '40px'; element.style.flex = '0 0 40px'; element.style.padding = '0'; element.style.boxSizing = 'border-box'; } setTimeout(() => { const headerCell = element?.querySelector('.tabulator-cell'); if (headerCell) { headerCell.style.width = '40px'; headerCell.style.minWidth = '40px'; headerCell.style.maxWidth = '40px'; headerCell.style.padding = '4px'; headerCell.style.boxSizing = 'border-box'; } const dataCells = document.querySelectorAll('.tabulator-row .tabulator-cell:first-child'); dataCells.forEach(cell => { cell.style.width = '40px'; cell.style.minWidth = '40px'; cell.style.maxWidth = '40px'; cell.style.padding = '4px'; cell.style.boxSizing = 'border-box'; }); }, 50); } // Применяем сохранённые фильтры if (savedState && savedState.filters && savedState.filters.length > 0 && !filtersApplied) { console.log('📂 Применяем сохранённые фильтры:', savedState.filters); filtersApplied = true; savedState.filters.forEach(filter => { if (filter.field && filter.value) { table.setHeaderFilterValue(filter.field, filter.value); } }); } // Применяем сохранённую видимость столбцов const savedVisibility = loadColumnVisibility(); if (savedVisibility) { console.log('📂 Применяем сохранённую видимость столбцов:', savedVisibility); Object.keys(savedVisibility).forEach(field => { const column = table.getColumn(field); if (column) { if (savedVisibility[field]) { column.show(); } else { column.hide(); } } }); } }); console.log('✅ Tabulator создан, подключаем события...'); table.on("dataSorting", function(sorters) { console.log('🔄 Сортировка начата:', sorters); }); table.on("dataSorted", function(sorters, rows) { console.log('✅ Сортировка завершена:', sorters); }); // Функция сохранения строки async function saveRow(rowPos, rowData, rowElement, originalRowData = null) { console.log('💾 === СОХРАНЕНИЕ ==='); console.log(' Data:', rowData); if (!currentSchema || !currentTable) { console.error('❌ Нет schema/table'); return; } dirtyRows.delete(rowPos); try { const result = await api('/api/table/update', 'POST', { schema: currentSchema, table: currentTable, row: rowData, originalRow: originalRowData || rowData }); console.log('✅ СОХРАНЕНО:', result); if (rowElement && rowElement.getElement) { rowElement.getElement().style.backgroundColor = '#e8f5e9'; setTimeout(() => { if (rowElement && rowElement.getElement) { rowElement.getElement().style.backgroundColor = ''; } }, 1500); } } catch (err) { console.error('❌ ОШИБКА:', err); if (rowElement && rowElement.getElement) { rowElement.getElement().style.backgroundColor = '#ffebee'; setTimeout(() => { if (confirm('Ошибка сохранения:\n' + err.message + '\n\nОбновить таблицу?')) { table.replaceData(); } }, 100); } else { alert('Ошибка: ' + err.message); } } } // События выделения table.on("rowSelectionChanged", function(data, rows) { table.getRows().forEach(row => { const cells = row.getCells(); if (cells.length > 0) { const checkbox = cells[0].getElement().querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.checked = row.isSelected(); } } const rowData = row.getData(); const key = getRowKey(rowData); if (row.isSelected()) { selectedRowsDataGlobal.set(key, rowData); } else { selectedRowsDataGlobal.delete(key); } }); updateSelectionCounter(); }); table.on("dataLoaded", function(data) { console.log('📊 Данные загружены:', data.length, 'строк'); if (selectedRowsDataGlobal.size > 0) { const rows = table.getRows(); rows.forEach(row => { const rowData = row.getData(); const key = getRowKey(rowData); if (selectedRowsDataGlobal.has(key)) { row.select(); const cell = row.getCells()[0]; if (cell) { const checkbox = cell.getElement().querySelector('input[type="checkbox"]'); if (checkbox) checkbox.checked = true; } } }); } // Восстанавливаем страницу if (savedState && savedState.page && savedState.page > 1 && !stateRestored) { stateRestored = true; const maxPage = table.getPageMax ? table.getPageMax() : 1; const targetPage = Math.min(savedState.page, maxPage); if (targetPage > 1) { console.log('📂 Переход на сохранённую страницу:', targetPage); setTimeout(() => { table.setPage(targetPage); }, 100); } } }); table.on("cellEdited", function(cell) { const oldValue = cell.getOldValue(); const newValue = cell.getValue(); const normalizeValue = (v) => { if (v === null || v === undefined || v === '') return null; return String(v); }; const valuesEqual = normalizeValue(oldValue) === normalizeValue(newValue); if (valuesEqual) { console.log('⏭️ Значение не изменилось:', cell.getField(), oldValue, '→', newValue); cell.getRow().getElement().style.backgroundColor = ''; return; } console.log('✏️ ИЗМЕНЕНО:', cell.getField(), oldValue, '→', newValue); const row = cell.getRow(); const rowData = row.getData(); const rowPos = row.getPosition(); row.getElement().style.backgroundColor = '#fffae6'; if (dirtyRows.has(rowPos)) { clearTimeout(dirtyRows.get(rowPos).timeout); } const originalRowData = { ...rowData, [cell.getField()]: oldValue }; const timeout = setTimeout(async () => { console.log('⏰ Автосохранение...'); await saveRow(rowPos, rowData, row, originalRowData); }, 2000); dirtyRows.set(rowPos, { data: rowData, element: row, timeout: timeout }); console.log('📝 Несохраненных:', dirtyRows.size); }); table.on("pageLoaded", async function() { if (dirtyRows.size > 0) { console.log('📄 Смена страницы: сохранение', dirtyRows.size); const savePromises = []; dirtyRows.forEach((info, rowPos) => { clearTimeout(info.timeout); savePromises.push(saveRow(rowPos, info.data, info.element)); }); await Promise.all(savePromises); } saveTableState(); }); table.on("dataFiltered", function(filters, rows) { console.log('🔍 Фильтры изменены:', filters); saveTableState(); }); enterHandler = async function(e) { if (e.key === 'Enter' && dirtyRows.size > 0) { e.preventDefault(); console.log(`⏎ ENTER: сохранение ${dirtyRows.size}`); const savePromises = []; dirtyRows.forEach((info, rowPos) => { clearTimeout(info.timeout); savePromises.push(saveRow(rowPos, info.data, info.element)); }); await Promise.all(savePromises); if (document.activeElement) { document.activeElement.blur(); } } }; document.addEventListener('keydown', enterHandler); console.log('✅ ВСЕ СОБЫТИЯ ПОДКЛЮЧЕНЫ'); } console.log('✅ table.js загружен');