// ===== 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 загружен');