// ===== 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 += `
${showCheckbox ? ` ` : ''} ${!isSingleRow && !field.valuesMatch ? 'разные значения' : ''}
${commentInfo} `; const displayValue = field.valuesMatch && field.commonValue !== null ? field.commonValue : ''; const isDisabled = showCheckbox && !isChecked; if (field.is_fk && fkOptions[field.name]?.length > 0) { html += `'; } else if (field.type === 'number') { html += ` `; } else if (field.type === 'datetime' || field.data_type.includes('date')) { const inputType = field.data_type === 'date' ? 'date' : 'datetime-local'; let dateValue = displayValue; if (dateValue && inputType === 'datetime-local') { dateValue = dateValue.replace(' ', 'T').slice(0, 16); } html += ` `; } else { html += ` `; } 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 загружен');