diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c593fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/.DS_Store +vendor/ +composer.lock +composer.phar diff --git a/public/js/operations.js b/public/js/operations.js index 1ae4364..ec3c80f 100644 --- a/public/js/operations.js +++ b/public/js/operations.js @@ -322,7 +322,88 @@ function initOperationsHandlers() { } /** - * Диалог ввода обязательных полей (Tabulator форма) + * Генерация HTML для поля ввода с поддержкой FK datalist + */ +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} + `; + + // FK поля используют datalist для autocomplete + if (field.is_fk && fkOptions[field.name]?.length > 0) { + const datalistId = `fk_datalist_${field.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const placeholder = isRequired + ? 'Начните вводить или выберите из списка...' + : '-- Не выбрано (NULL) --'; + + html += ` + + + `; + + fkOptions[field.name].forEach(val => { + 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; +} + +/** + * Диалог ввода обязательных полей (простая HTML форма с datalist) */ async function promptForRequiredFields(requiredFields, optionalFields) { const fkOptions = {}; @@ -345,31 +426,6 @@ async function promptForRequiredFields(requiredFields, optionalFields) { } } - // Подготовить данные для Tabulator - const tableData = []; - - requiredFields.forEach(field => { - tableData.push({ - field_name: `${field.name} *`, - field_value: null, - _field_meta: field, - _field_comment: field.comment || '', - _fk_info: field.is_fk ? field.fk_info : null, - _required: true - }); - }); - - optionalFields.forEach(field => { - tableData.push({ - field_name: field.name, - field_value: null, - _field_meta: field, - _field_comment: field.comment || '', - _fk_info: field.is_fk ? field.fk_info : null, - _required: false - }); - }); - // Создать модальное окно const modal = document.createElement('div'); modal.className = 'insert-modal-overlay'; @@ -382,14 +438,32 @@ async function promptForRequiredFields(requiredFields, optionalFields) { const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 20px; border-radius: 8px; - max-width: 700px; width: 90%; max-height: 80vh; overflow: hidden; + max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; display: flex; flex-direction: column; `; let html = `

➕ Вставить запись в ${currentSchema}.${currentTable}

-
-
+ `; + + // Обязательные поля + 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 += ` +
@@ -403,83 +477,6 @@ async function promptForRequiredFields(requiredFields, optionalFields) { modal.appendChild(dialog); document.body.appendChild(modal); - // Создать Tabulator таблицу - const tabulatorInstance = new Tabulator('#insertFormTable', { - data: tableData, - layout: 'fitColumns', - height: '100%', - columns: [ - { - title: 'Поле', - field: 'field_name', - width: 250, - headerSort: false, - formatter: function(cell) { - const data = cell.getRow().getData(); - let html = `
${escapeHtml(data.field_name)}
`; - - // FK информация - if (data._fk_info) { - html += `
- → ${data._fk_info.ref_table}.${data._fk_info.ref_column} -
`; - } - - // Комментарий из базы - if (data._field_comment) { - html += `
- 💡 ${escapeHtml(data._field_comment)} -
`; - } - - return html; - } - }, - { - title: 'Значение', - field: 'field_value', - headerSort: false, - editor: function(cell) { - const meta = cell.getRow().getData()._field_meta; - - if (meta.is_fk && fkOptions[meta.name]?.length > 0) { - return 'list'; - } - - return 'input'; - }, - editorParams: function(cell) { - const meta = cell.getRow().getData()._field_meta; - const isRequired = cell.getRow().getData()._required; - - if (meta.is_fk && fkOptions[meta.name]?.length > 0) { - return { - values: fkOptions[meta.name], - autocomplete: true, - clearable: !isRequired, - listOnEmpty: true, - freetext: false, - emptyValue: isRequired ? undefined : null, - filterFunc: function(term, label, value, item) { - if (!term) return true; - return String(label).toLowerCase().includes(term.toLowerCase()); - }, - elementAttributes: { - placeholder: isRequired ? 'Выберите значение' : 'Не выбрано (NULL)' - } - }; - } - - return { - elementAttributes: { - placeholder: meta.type === 'number' ? 'Число' : 'Текст' - } - }; - } - } - ] - }); - return new Promise((resolve) => { document.getElementById('insertCancel').addEventListener('click', () => { if (document.body.contains(modal)) { @@ -501,12 +498,21 @@ async function promptForRequiredFields(requiredFields, optionalFields) { const values = {}; let allRequiredFilled = true; - const rows = tabulatorInstance.getData(); - for (const row of rows) { - const field = row._field_meta; - const value = row.field_value; + const allFields = [...requiredFields, ...optionalFields]; - if (row._required && (value === null || value === '')) { + for (const field of allFields) { + const inputElement = document.getElementById(`field_${field.name}`); + if (!inputElement) continue; + + const value = inputElement.value; + + if (field.type === 'datetime' || field.data_type.includes('date')) { + if (allFields.find(f => f.name === field.name).type === 'datetime' || field.data_type.includes('date')) { + // Дата остается строкой для отправки на сервер + } + } + + if (requiredFields.find(f => f.name === field.name) && (value === null || value === '')) { alert(`Обязательное поле "${field.name}" не заполнено!`); allRequiredFilled = false; break; @@ -514,7 +520,7 @@ async function promptForRequiredFields(requiredFields, optionalFields) { if (value !== null && value !== '') { values[field.name] = convertFieldValue(value, field.type, field.data_type); - } else if (!row._required) { + } else { values[field.name] = null; } } @@ -547,7 +553,7 @@ function convertFieldValue(value, editorType, dataType) { } /** - * Модальное окно редактирования (Tabulator форма) + * Модальное окно редактирования (простая HTML форма с datalist) */ async function showEditModal(selectedRows) { const isSingleRow = selectedRows.length === 1; @@ -603,17 +609,6 @@ async function showEditModal(selectedRows) { } } - // Подготовить данные для Tabulator - const tableData = editableFields.map(field => ({ - update: isSingleRow, - field_name: field.name, - field_value: field.valuesMatch ? field.commonValue : null, - _has_mixed: !field.valuesMatch, - _field_meta: field, - _field_comment: field.comment || '', - _fk_info: field.is_fk ? field.fk_info : null - })); - // Создать модальное окно const modal = document.createElement('div'); modal.className = 'edit-modal-overlay'; @@ -626,7 +621,7 @@ async function showEditModal(selectedRows) { const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 20px; border-radius: 8px; - max-width: 700px; width: 90%; max-height: 80vh; overflow: hidden; + max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; display: flex; flex-direction: column; `; @@ -637,14 +632,99 @@ async function showEditModal(selectedRows) { if (!isSingleRow) { html += `
- ⚠️ Отметьте галочками поля для изменения во всех выбранных записях. + ⚠️ Значения применятся к${count === 1 ? '' : ' всем'} выбранной записи.
`; } + // Отобразить поля редактирования + editableFields.forEach(field => { + const currentValue = field.valuesMatch ? field.commonValue : null; + const hasMixedValues = !field.valuesMatch; + + let fieldHtml = ` +
+ '; + + if (hasMixedValues) { + fieldHtml += ` +
+ ⚠️ разные значения +
+ `; + } + + if (field.comment) { + fieldHtml += ` +
+ 💡 ${escapeHtml(field.comment)} +
+ `; + } + + // FK поля используют datalist + if (field.is_fk && fkOptions[field.name]?.length > 0) { + const datalistId = `fk_datalist_edit_${field.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const placeholder = field.is_nullable + ? 'Начните вводить или выберите из списка...' + : 'Выберите значение...'; + + fieldHtml += ` + + + `; + + fkOptions[field.name].forEach(val => { + fieldHtml += `'; + + } else if (field.type === 'number') { + fieldHtml += ` + + `; + } else if (field.type === 'datetime' || field.data_type.includes('date')) { + const inputType = field.data_type === 'date' ? 'date' : 'datetime-local'; + fieldHtml += ` + + `; + } else { + fieldHtml += ` + + `; + } + + fieldHtml += '
'; + html += fieldHtml; + }); + html += ` -
-
+
@@ -658,122 +738,6 @@ async function showEditModal(selectedRows) { modal.appendChild(dialog); document.body.appendChild(modal); - // Подготовить колонки для Tabulator - const columns = []; - - // Колонка checkbox (только для batch edit) - if (!isSingleRow) { - columns.push({ - title: '', - field: 'update', - width: 40, - hozAlign: 'center', - headerSort: false, - formatter: 'tickCross', - cellClick: function(e, cell) { - cell.setValue(!cell.getValue()); - } - }); - } - - // Колонка "Поле" - columns.push({ - title: 'Поле', - field: 'field_name', - width: 250, - headerSort: false, - formatter: function(cell) { - const data = cell.getRow().getData(); - let html = '
'; - - html += `
🔑 ${escapeHtml(data.field_name)}
`; - - // FK информация - if (data._fk_info) { - html += `
- → ${data._fk_info.ref_table}.${data._fk_info.ref_column} -
`; - } - - // Комментарий из базы - if (data._field_comment) { - html += `
- 💡 ${escapeHtml(data._field_comment)} -
`; - } - - // "(разные значения)" индикатор - if (!isSingleRow && data._has_mixed) { - html += `
- (разные значения) -
`; - } - - html += '
'; - return html; - } - }); - - // Колонка "Значение" - columns.push({ - title: 'Значение', - field: 'field_value', - headerSort: false, - editable: function(cell) { - const data = cell.getRow().getData(); - if (!isSingleRow && !data.update) return false; - return true; - }, - editor: function(cell) { - const meta = cell.getRow().getData()._field_meta; - if (meta.is_fk && fkOptions[meta.name]?.length > 0) { - return 'list'; - } - return 'input'; - }, - editorParams: function(cell) { - const meta = cell.getRow().getData()._field_meta; - - if (meta.is_fk && fkOptions[meta.name]?.length > 0) { - return { - values: fkOptions[meta.name], - autocomplete: true, - clearable: meta.is_nullable, - listOnEmpty: true, - freetext: false, - emptyValue: meta.is_nullable ? null : undefined, - filterFunc: function(term, label, value, item) { - if (!term) return true; - return String(label).toLowerCase().includes(term.toLowerCase()); - }, - elementAttributes: { - placeholder: 'Выберите значение' - } - }; - } - - return { - elementAttributes: { - placeholder: meta.type === 'number' ? 'Число' : 'Текст' - } - }; - }, - cellEdited: function(cell) { - if (!isSingleRow) { - const row = cell.getRow(); - row.update('update', true); - } - } - }); - - // Создать Tabulator - const tabulatorInstance = new Tabulator('#editFormTable', { - data: tableData, - layout: 'fitColumns', - height: '100%', - columns: columns - }); - document.getElementById('editCancel').addEventListener('click', () => { if (document.body.contains(modal)) { document.body.removeChild(modal); @@ -792,22 +756,26 @@ async function showEditModal(selectedRows) { const changes = {}; let hasChanges = false; - const rows = tabulatorInstance.getData(); - for (const row of rows) { - const shouldUpdate = isSingleRow || row.update; - if (!shouldUpdate) continue; + for (const field of editableFields) { + const inputElement = document.getElementById(`field_${field.name}`); + if (!inputElement) continue; - const field = row._field_meta; - let value = row.field_value; + const value = inputElement.value; + const oldValue = field.valuesMatch ? field.commonValue : null; - if (value === null || value === '') { - value = null; - } else { - value = convertFieldValue(value, field.type, field.data_type); + // Проверить изменения + const oldValueStr = oldValue !== null && oldValue !== undefined ? String(oldValue) : ''; + const newValueStr = value !== null && value !== undefined ? String(value) : ''; + + if (oldValueStr !== newValueStr) { + hasChanges = true; } - changes[field.name] = value; - hasChanges = true; + if (value !== null && value !== '') { + changes[field.name] = convertFieldValue(value, field.type, field.data_type); + } else { + changes[field.name] = null; + } } if (!hasChanges) {