// ===== OPERATIONS.JS - CRUD операции =====
/**
* Инициализация обработчиков операций
*/
function initOperationsHandlers() {
// Выделить все
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;
}
selectedRowsDataGlobal.clear();
result.data.forEach(rowData => {
const key = getRowKey(rowData);
selectedRowsDataGlobal.set(key, rowData);
});
const rows = table.getRows();
rows.forEach(row => {
const rowData = row.getData();
const key = getRowKey(rowData);
if (selectedRowsDataGlobal.has(key)) {
row.select();
}
});
updateSelectionCounter();
alert(`Выделено записей: ${totalRows}`);
} catch (err) {
console.error('Ошибка выделения:', err);
alert('Ошибка выделения: ' + err.message);
}
});
// Снять выделение
document.getElementById('btnDeselectAll').addEventListener('click', () => {
if (!table) return;
selectedRowsDataGlobal.clear();
table.deselectRow();
updateSelectionCounter();
});
// Вставить
document.getElementById('btnInsert').addEventListener('click', async () => {
if (!currentSchema || !currentTable || !currentMeta) {
alert('Сначала выберите таблицу');
return;
}
const requiredFields = [];
const optionalFields = [];
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;
}
}
if (col.IS_REQUIRED) {
requiredFields.push({
name: name,
type: col.EDITOR_TYPE,
data_type: col.DATA_TYPE,
is_fk: col.IS_FOREIGN_KEY,
fk_info: col.FOREIGN_KEY,
comment: col.COLUMN_COMMENT || ''
});
} else {
optionalFields.push({
name: name,
type: col.EDITOR_TYPE,
data_type: col.DATA_TYPE,
is_fk: col.IS_FOREIGN_KEY,
fk_info: col.FOREIGN_KEY,
comment: col.COLUMN_COMMENT || ''
});
}
});
if (requiredFields.length > 0) {
try {
const values = await promptForRequiredFields(requiredFields, optionalFields);
if (!values) return;
await api('/api/table/insert', 'POST', {
schema: currentSchema,
table: currentTable,
row: values
});
await table.replaceData();
const lastPage = table.getPageMax();
if (lastPage > 1) {
table.setPage(lastPage);
}
alert('✓ Строка успешно создана');
} catch (e) {
console.error('Ошибка вставки:', e);
alert('Ошибка вставки: ' + e.message);
}
} else {
try {
await api('/api/table/insert', 'POST', {
schema: currentSchema,
table: currentTable,
row: {}
});
await table.replaceData();
alert('✓ Строка успешно создана');
} catch (e) {
console.error('Ошибка вставки:', e);
alert('Ошибка вставки: ' + e.message);
}
}
});
// Копировать строку
document.getElementById('btnCopy').addEventListener('click', async () => {
if (!table || !currentSchema || !currentTable || !currentMeta) {
alert('Сначала выберите таблицу');
return;
}
const tabulatorSelected = table.getSelectedData();
const allSelectedData = new Map();
tabulatorSelected.forEach(rowData => {
const key = getRowKey(rowData);
allSelectedData.set(key, rowData);
});
selectedRowsDataGlobal.forEach((rowData, key) => {
allSelectedData.set(key, rowData);
});
const selectedRows = Array.from(allSelectedData.values());
if (selectedRows.length === 0) {
alert('Выберите строку для копирования');
return;
}
if (selectedRows.length > 1) {
const proceed = confirm(`Выбрано ${selectedRows.length} строк. Скопировать только первую?`);
if (!proceed) return;
}
const sourceRow = selectedRows[0];
const rowToInsert = {};
const primaryKeySet = new Set(currentMeta.primaryKey || []);
currentMeta.columns.forEach(col => {
const name = col.COLUMN_NAME;
if (col.IS_AUTO_INCREMENT) return;
if (primaryKeySet.has(name)) return;
if (Object.prototype.hasOwnProperty.call(sourceRow, name)) {
rowToInsert[name] = sourceRow[name];
}
});
try {
await api('/api/table/insert', 'POST', {
schema: currentSchema,
table: currentTable,
row: rowToInsert
});
await table.replaceData();
const lastPage = table.getPageMax ? table.getPageMax() : 1;
if (lastPage > 1) {
await table.setPage(lastPage);
}
alert('✓ Строка скопирована');
} catch (e) {
console.error('Ошибка копирования строки:', e);
alert('Ошибка копирования: ' + e.message);
}
});
// Удалить
document.getElementById('btnDelete').addEventListener('click', async () => {
if (!table || !currentSchema || !currentTable) {
alert('Сначала выберите таблицу');
return;
}
const tabulatorSelected = table.getSelectedData();
const allSelectedData = new Map();
tabulatorSelected.forEach(rowData => {
const key = getRowKey(rowData);
allSelectedData.set(key, rowData);
});
selectedRowsDataGlobal.forEach((rowData, key) => {
allSelectedData.set(key, rowData);
});
const count = allSelectedData.size;
console.log('🗑️ Удаление:', {
tabulatorSelected: tabulatorSelected.length,
globalSelected: selectedRowsDataGlobal.size,
total: count
});
if (count === 0) {
alert('Выберите строки для удаления');
return;
}
const confirmMsg = count === 1
? 'Удалить выбранную строку?'
: `Удалить ${count} выбранных строк?`;
if (!confirm(confirmMsg)) return;
const modal = createProgressModal('Удаление записей...');
document.body.appendChild(modal);
try {
const rowsArray = Array.from(allSelectedData.values());
const result = await api('/api/table/delete-batch', 'POST', {
schema: currentSchema,
table: currentTable,
rows: rowsArray
});
document.body.removeChild(modal);
selectedRowsDataGlobal.clear();
table.deselectRow();
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);
}
});
// Редактировать
document.getElementById('btnEdit').addEventListener('click', async () => {
if (!table || !currentSchema || !currentTable || !currentMeta) {
alert('Сначала выберите таблицу');
return;
}
const tabulatorSelected = table.getSelectedData();
const allSelectedData = new Map();
tabulatorSelected.forEach(rowData => {
const key = getRowKey(rowData);
allSelectedData.set(key, rowData);
});
selectedRowsDataGlobal.forEach((rowData, key) => {
allSelectedData.set(key, rowData);
});
const selectedRows = Array.from(allSelectedData.values());
const count = selectedRows.length;
if (count === 0) {
alert('Выберите строки для редактирования');
return;
}
showEditModal(selectedRows);
});
// Управление столбцами
document.getElementById('btnManageColumns').addEventListener('click', () => {
showColumnManager();
});
}
/**
* Диалог ввода обязательных полей
*/
async function promptForRequiredFields(requiredFields, optionalFields) {
const modal = document.createElement('div');
modal.className = 'insert-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: 600px; width: 90%; max-height: 80vh; overflow-y: auto;
`;
let html = '
Создание новой записи
';
const fkOptions = {};
const allFields = [...requiredFields, ...optionalFields];
for (const field of allFields) {
if (field.is_fk && field.fk_info) {
try {
const result = await api(
`/api/fk-values?schema=${encodeURIComponent(field.fk_info.ref_schema)}&` +
`table=${encodeURIComponent(field.fk_info.ref_table)}&` +
`column=${encodeURIComponent(field.fk_info.ref_column)}`
);
fkOptions[field.name] = result.values || [];
} catch (err) {
console.error(`Ошибка загрузки FK для ${field.name}:`, err);
fkOptions[field.name] = [];
}
}
}
if (requiredFields.length > 0) {
html += 'Обязательные поля *
';
requiredFields.forEach(field => {
html += renderFieldInput(field, fkOptions, true);
});
}
if (optionalFields.length > 0) {
html += 'Дополнительные поля (необязательные)
';
optionalFields.forEach(field => {
html += renderFieldInput(field, fkOptions, false);
});
}
html += `
`;
dialog.innerHTML = html;
modal.appendChild(dialog);
document.body.appendChild(modal);
return new Promise((resolve) => {
document.getElementById('insertCancel').addEventListener('click', () => {
document.body.removeChild(modal);
resolve(null);
});
document.getElementById('insertSubmit').addEventListener('click', () => {
const values = {};
let allRequiredFilled = true;
for (const field of requiredFields) {
const input = document.getElementById(`field_${field.name}`);
const value = input.value.trim();
if (!value) {
alert(`Обязательное поле "${field.name}" не заполнено!`);
input.focus();
allRequiredFilled = false;
break;
}
values[field.name] = convertFieldValue(value, field.type, field.data_type);
}
if (!allRequiredFilled) return;
for (const field of optionalFields) {
const input = document.getElementById(`field_${field.name}`);
const value = input.value.trim();
if (value === '' || value === 'NULL') {
values[field.name] = null;
} else {
values[field.name] = convertFieldValue(value, field.type, field.data_type);
}
}
document.body.removeChild(modal);
resolve(values);
});
});
}
/**
* Рендер поля ввода
*/
function renderFieldInput(field, fkOptions, isRequired) {
const requiredMark = isRequired ? '*' : '';
let fkInfo = '';
let commentInfo = '';
if (field.is_fk && field.fk_info) {
fkInfo = `
(→ ${field.fk_info.ref_table}.${field.fk_info.ref_column})
`;
}
if (field.comment) {
commentInfo = `
💡 ${escapeHtml(field.comment)}
`;
}
let html = `
${commentInfo}
`;
if (field.is_fk && fkOptions[field.name]?.length > 0) {
html += `';
} else if (field.type === 'number') {
const placeholder = isRequired ? 'Введите число' : 'Оставьте пустым для NULL';
html += `
`;
} else if (field.type === 'datetime' || field.data_type.includes('date')) {
const inputType = field.data_type === 'date' ? 'date' : 'datetime-local';
const placeholder = isRequired ? '' : 'Оставьте пустым для NULL';
html += `
`;
} else {
const placeholder = isRequired ? 'Введите значение' : 'Оставьте пустым для NULL';
html += `
`;
}
html += '
';
return html;
}
/**
* Конвертация значения поля
*/
function convertFieldValue(value, editorType, dataType) {
if (value === null || value === '') {
return null;
}
if (editorType === 'number' || dataType.includes('int') || dataType.includes('decimal') || dataType.includes('float')) {
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
return value;
}
/**
* Модальное окно редактирования
*/
async function showEditModal(selectedRows) {
const isSingleRow = selectedRows.length === 1;
const count = selectedRows.length;
const editableFields = [];
const fkOptions = {};
for (const col of currentMeta.columns) {
const name = col.COLUMN_NAME;
if (col.IS_AUTO_INCREMENT) continue;
if (currentMeta.primaryKey && currentMeta.primaryKey.includes(name)) continue;
let commonValue = null;
let valuesMatch = true;
if (selectedRows.length > 0) {
commonValue = selectedRows[0][name];
for (let i = 1; i < selectedRows.length; i++) {
if (selectedRows[i][name] !== commonValue) {
valuesMatch = false;
commonValue = null;
break;
}
}
}
editableFields.push({
name: name,
type: col.EDITOR_TYPE,
data_type: col.DATA_TYPE,
is_fk: col.IS_FOREIGN_KEY,
fk_info: col.FOREIGN_KEY,
comment: col.COLUMN_COMMENT || '',
is_nullable: col.IS_NULLABLE,
commonValue: commonValue,
valuesMatch: valuesMatch
});
if (col.IS_FOREIGN_KEY && col.FOREIGN_KEY) {
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)}`
);
fkOptions[name] = result.values || [];
} catch (err) {
console.error(`Ошибка загрузки FK для ${name}:`, err);
fkOptions[name] = [];
}
}
}
const modal = document.createElement('div');
modal.className = 'edit-modal-overlay';
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;
`;
let html = `✏️ Редактирование ${isSingleRow ? 'записи' : `${count} записей`}
`;
if (!isSingleRow) {
html += `
⚠️ Отметьте галочками поля, которые хотите изменить во всех выбранных записях.
`;
}
html += '';
editableFields.forEach(field => {
const checkboxId = `edit_check_${field.name}`;
const inputId = `edit_field_${field.name}`;
const showCheckbox = !isSingleRow;
const isChecked = isSingleRow ? true : false;
let fkInfo = '';
let commentInfo = '';
if (field.is_fk && field.fk_info) {
fkInfo = `
(→ ${field.fk_info.ref_table}.${field.fk_info.ref_column})
`;
}
if (field.comment) {
commentInfo = `
💡 ${escapeHtml(field.comment)}
`;
}
html += `
';
});
html += '
';
html += `
`;
dialog.innerHTML = html;
modal.appendChild(dialog);
document.body.appendChild(modal);
document.getElementById('editCancel').addEventListener('click', () => {
document.body.removeChild(modal);
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
document.getElementById('editSubmit').addEventListener('click', async () => {
const changes = {};
let hasChanges = false;
editableFields.forEach(field => {
const checkboxEl = document.getElementById(`edit_check_${field.name}`);
const inputEl = document.getElementById(`edit_field_${field.name}`);
const shouldUpdate = isSingleRow || (checkboxEl && checkboxEl.checked);
if (shouldUpdate && inputEl && !inputEl.disabled) {
let value = inputEl.value;
if (value === '__NULL__') {
value = null;
} else if (value === '' && !isSingleRow) {
return;
} else if (value === '') {
value = null;
}
if (value !== null) {
if (field.type === 'number' || field.data_type.includes('int') || field.data_type.includes('decimal')) {
const num = parseFloat(value);
value = isNaN(num) ? null : num;
}
}
changes[field.name] = value;
hasChanges = true;
}
});
if (!hasChanges) {
alert('Нет изменений для сохранения');
return;
}
document.body.removeChild(modal);
const progressModal = createProgressModal(`Обновление ${count} записей...`);
document.body.appendChild(progressModal);
try {
let updated = 0;
let errors = 0;
const errorMessages = [];
for (const row of selectedRows) {
const updatedRow = { ...row };
Object.keys(changes).forEach(key => {
updatedRow[key] = changes[key];
});
try {
await api('/api/table/update', 'POST', {
schema: currentSchema,
table: currentTable,
row: updatedRow
});
updated++;
} catch (err) {
errors++;
if (errorMessages.length < 5) {
errorMessages.push(err.message);
}
}
}
document.body.removeChild(progressModal);
selectedRowsDataGlobal.clear();
table.deselectRow();
updateSelectionCounter();
await table.replaceData();
if (errors > 0) {
alert(`Обновлено: ${updated}\nОшибок: ${errors}\n\n${errorMessages.join('\n')}`);
} else {
alert(`✓ Успешно обновлено записей: ${updated}`);
}
} catch (e) {
document.body.removeChild(progressModal);
console.error('Ошибка обновления:', e);
alert('Ошибка обновления: ' + e.message);
}
});
}
console.log('✅ operations.js загружен');