Files
ave-cms/adminx/modules/Requests/assets/requests.js
T
2026-07-27 12:58:44 +03:00

2018 lines
104 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* JS раздела «Запросы». Список: копирование/удаление. Редактор: сохранение
* (ajax), палитра тегов + проверка PHP (lint) + разворот редактора, условия
* (CRUD + drag-and-drop сортировка). Всё через ajax, подтверждения — Adminx.Confirm.
*/
(function (window, document) {
'use strict';
var Adminx = window.Adminx || (window.Adminx = {});
Adminx.Requests = {
activeField: null,
dragRow: null,
explainSearchTimer: null,
init: function () {
this.form = document.getElementById('requestForm');
this.condTable = document.getElementById('condTable');
this.palette = document.querySelector('[data-req-tags-panel]');
this.auditPage = document.querySelector('[data-native-audit-page]');
this.auditRunning = false;
var self = this;
this.initEditorMode();
this.initPresentation();
this.initSortBuilder();
document.addEventListener('click', function (e) { self.onClick(e); });
// активный редактор — тот, в котором фокус
document.addEventListener('focusin', function (e) {
var f = e.target.closest('[data-req-code-field]');
if (f) { self.activeField = f; }
});
// поиск по тегам + фильтр списка запросов
document.addEventListener('input', function (e) {
if (e.target.matches('[data-req-tags-search]')) { self.filterTags(e.target.value); }
if (e.target.matches('[data-requests-search]')) { self.filterList(e.target.value); }
if (e.target.matches('[data-request-explain-search]')) { self.explainSearchInput(e.target); }
if (e.target.matches('[data-request-sort-seed-input]')) { self.syncSortRules(); }
if (e.target.matches('[data-cf="condition_value_key"], [data-cf="condition_value_constant"], [data-cf="condition_value"]')) { self.conditionValueRefresh(e.target.closest('[data-condition-value]')); }
if (e.target.matches('[data-condition-smart-input]')) { self.conditionSmartValue(e.target); }
});
document.addEventListener('change', function (e) {
if (e.target.matches('[data-group-title]')) { self.groupTitle(e.target); }
if (e.target.matches('[data-request-preview-renderer-advanced]')) { self.selectPresentation(e.target.value); }
if (e.target.matches('[data-request-sort-source]')) { self.sortSourceChanged(e.target); }
if (e.target.matches('[data-cf="condition_value_source"], [data-cf="condition_value_mode"], [data-cf="condition_field_id"], [data-cf="condition_compare"]')) {
var conditionRow = e.target.closest('tr');
if (e.target.matches('[data-cf="condition_field_id"]')) { self.conditionFieldRefresh(conditionRow, true); }
else if (e.target.matches('[data-cf="condition_compare"], [data-cf="condition_value_source"]')) { self.conditionFieldRefresh(conditionRow, false); }
self.conditionValueRefresh(conditionRow.querySelector('[data-condition-value]'), e.target);
self.syncEditorAdvancedOptions();
}
if (e.target.matches('[data-condition-smart-select]')) { self.conditionSmartValue(e.target); }
});
// сохранение запроса
if (this.form) {
this.form.addEventListener('submit', function (e) { e.preventDefault(); self.save(); });
}
// drag-and-drop условий
if (this.condTable) {
this.condTable.querySelectorAll('tr[data-cond-id]').forEach(function (row) { self.conditionFieldRefresh(row, false); });
this.condTable.addEventListener('dragstart', function (e) { self.dragStart(e); });
this.condTable.addEventListener('dragover', function (e) { self.dragOver(e); });
this.condTable.addEventListener('drop', function (e) { self.drop(e); });
this.condTable.addEventListener('dragend', function () { self.dragEnd(); });
}
// Esc закрывает fullscreen/палитру
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') { return; }
var fs = document.querySelector('.req-code-field.is-fullscreen');
if (fs) { self.fullscreen(fs, false); }
else if (self.palette && !self.palette.hidden) { self.closePalette(); }
});
},
base: function () { return (this.form && this.form.getAttribute('data-base')) || Adminx.base(); },
initEditorMode: function () {
if (!document.querySelector('[data-request-editor-mode]')) { return; }
var mode = 'basic';
try { mode = window.localStorage.getItem('adminx.requests.editorMode') === 'advanced' ? 'advanced' : 'basic'; } catch (e) {}
this.setEditorMode(mode, false);
},
setEditorMode: function (mode, remember) {
mode = mode === 'advanced' ? 'advanced' : 'basic';
this.editorMode = mode;
document.querySelectorAll('[data-request-advanced]').forEach(function (node) {
node.hidden = mode !== 'advanced';
});
document.querySelectorAll('[data-request-editor-mode-value]').forEach(function (button) {
var active = button.getAttribute('data-request-editor-mode-value') === mode;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
var description = document.querySelector('[data-request-editor-mode-description]');
if (description) {
description.textContent = mode === 'advanced'
? 'Кеш, интеграции, совместимость, контракт результата и Native executor.'
: 'Основные параметры, условия и оформление результата.';
}
this.syncEditorAdvancedOptions();
if (mode === 'advanced') {
window.requestAnimationFrame(function () {
document.querySelectorAll('[data-req-code-field]').forEach(function (field) {
var textarea = field.querySelector('textarea[data-code-editor]');
if (textarea && textarea._adminxCodeMirror) { textarea._adminxCodeMirror.refresh(); }
});
});
}
if (remember !== false) {
try { window.localStorage.setItem('adminx.requests.editorMode', mode); } catch (e) {}
}
},
initPresentation: function () {
var select = document.querySelector('[data-request-preview-renderer]');
if (!select) { return; }
this.selectPresentation(select.value, false);
},
selectPresentation: function (code) {
var select = document.querySelector('[data-request-preview-renderer]');
if (!select) { return; }
var available = Array.prototype.some.call(select.options, function (option) { return option.value === code; });
if (!available) { code = 'data_cards'; }
select.value = code;
var advanced = document.querySelector('[data-request-preview-renderer-advanced]');
if (advanced) { advanced.value = code; }
document.querySelectorAll('[data-request-presentation-value]').forEach(function (button) {
var active = button.getAttribute('data-request-presentation-value') === code;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
},
openCodeEditor: function () {
this.setEditorMode('advanced');
var field = document.getElementById('requestSiteTemplates');
if (!field) { return; }
window.requestAnimationFrame(function () {
field.scrollIntoView({behavior: 'smooth', block: 'start'});
var textarea = field.querySelector('textarea[data-code-editor]');
if (textarea && textarea._adminxCodeMirror) { textarea._adminxCodeMirror.focus(); }
else if (textarea) { textarea.focus(); }
});
},
initSortBuilder: function () {
this.sortList = document.querySelector('[data-request-sort-list]');
if (!this.sortList) { return; }
var self = this;
this.sortList.querySelectorAll('[data-request-sort-source]').forEach(function (select) {
select.setAttribute('data-sort-source-value', select.value);
});
this.sortList.addEventListener('dragstart', function (event) { self.sortDragStart(event); });
this.sortList.addEventListener('dragover', function (event) { self.sortDragOver(event); });
this.sortList.addEventListener('drop', function (event) { self.sortDrop(event); });
this.sortList.addEventListener('dragend', function () { self.sortDragEnd(); });
this.syncSortRules();
},
sortAdd: function (preferred) {
if (!this.sortList) { return; }
var rows = this.sortList.querySelectorAll('[data-request-sort-rule]');
if (rows.length >= 8) { Adminx.Toast.show('Можно добавить не больше восьми уровней', 'warning'); return; }
var randomSelected = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).some(function (select) { return select.value === 'random:RAND()'; });
if (randomSelected) { Adminx.Toast.show('Случайный порядок используется без других уровней', 'warning'); return; }
var template = document.querySelector('[data-request-sort-template]');
if (!template) { return; }
var row = template.content.firstElementChild.cloneNode(true);
var select = row.querySelector('[data-request-sort-source]');
var used = {};
this.sortList.querySelectorAll('[data-request-sort-source]').forEach(function (current) { used[current.value] = true; });
var target = preferred || '';
if (!target || used[target]) {
var option = Array.from(select.options).find(function (candidate) { return candidate.value !== 'random:RAND()' && !used[candidate.value]; });
target = option ? option.value : '';
}
if (!target) { Adminx.Toast.show('Все доступные поля уже добавлены', 'warning'); return; }
select.value = target;
select.setAttribute('data-sort-source-value', target);
if (preferred === 'document:Id') {
row.querySelectorAll('[data-request-sort-direction]').forEach(function (button) {
var active = button.getAttribute('data-request-sort-direction') === 'ASC';
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
this.sortList.appendChild(row);
this.syncSortRules();
},
sortSourceChanged: function (select) {
var previous = select.getAttribute('data-sort-source-value') || '';
var values = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).filter(function (current) { return current !== select; }).map(function (current) { return current.value; });
if (values.indexOf(select.value) !== -1) {
select.value = previous;
Adminx.Toast.show('Это поле уже участвует в сортировке', 'warning');
return;
}
if ((select.value === 'random:RAND()' && values.length) || (select.value !== 'random:RAND()' && values.indexOf('random:RAND()') !== -1)) {
select.value = previous;
Adminx.Toast.show('Случайный порядок нельзя сочетать с другими уровнями', 'warning');
return;
}
select.setAttribute('data-sort-source-value', select.value);
var row = select.closest('[data-request-sort-rule]');
if (row) { row.classList.toggle('is-random', select.value === 'random:RAND()'); }
this.syncSortRules();
if (row && select.value === 'random:RAND()') {
window.requestAnimationFrame(function () {
var seed = row.querySelector('[data-request-sort-seed-input]');
if (seed) { seed.focus(); }
});
}
},
sortDirection: function (button) {
var row = button.closest('[data-request-sort-rule]');
if (!row || row.classList.contains('is-random')) { return; }
row.querySelectorAll('[data-request-sort-direction]').forEach(function (item) {
var active = item === button;
item.classList.toggle('is-active', active);
item.setAttribute('aria-pressed', active ? 'true' : 'false');
});
this.syncSortRules();
},
sortRemove: function (button) {
var row = button.closest('[data-request-sort-rule]');
if (row) { row.remove(); this.syncSortRules(); }
},
sortStable: function () {
if (!this.sortList) { return; }
var hasId = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).some(function (select) { return select.value === 'document:Id'; });
if (hasId) { Adminx.Toast.show('Финальный порядок по ID уже добавлен', 'info'); return; }
this.sortAdd('document:Id');
},
syncSortRules: function () {
if (!this.sortList) { return; }
var rules = [];
this.sortList.querySelectorAll('[data-request-sort-rule]').forEach(function (row, index) {
var position = row.querySelector('[data-request-sort-position]');
if (position) { position.textContent = String(index + 1); }
var select = row.querySelector('[data-request-sort-source]');
if (!select) { return; }
var separator = select.value.indexOf(':');
var source = separator >= 0 ? select.value.slice(0, separator) : '';
var key = separator >= 0 ? select.value.slice(separator + 1) : '';
var activeDirection = row.querySelector('[data-request-sort-direction].is-active');
var rule = {source: source, key: source === 'field' ? parseInt(key, 10) : key, direction: source === 'random' ? 'ASC' : (activeDirection ? activeDirection.getAttribute('data-request-sort-direction') : 'ASC')};
if (source === 'random') {
var seedInput = row.querySelector('[data-request-sort-seed-input]');
var seed = seedInput ? seedInput.value.trim() : '';
if (seed !== '') { rule.seed = seed; }
}
rules.push(rule);
});
var input = document.querySelector('[data-request-sort-rules]');
if (input) { input.value = JSON.stringify(rules); }
var empty = document.querySelector('[data-request-sort-empty]');
if (empty) { empty.hidden = rules.length > 0; }
var stable = document.querySelector('[data-request-sort-stable]');
if (stable) {
stable.disabled = rules.some(function (rule) { return rule.source === 'random' || (rule.source === 'document' && rule.key === 'Id'); });
}
},
sortDragStart: function (event) {
var handle = event.target.closest('[data-request-sort-handle]');
if (!handle) { event.preventDefault(); return; }
this.sortDragRow = handle.closest('[data-request-sort-rule]');
if (!this.sortDragRow) { return; }
this.sortDragRow.classList.add('is-dragging');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', 'request-sort');
},
sortDragOver: function (event) {
if (!this.sortDragRow || !this.sortList) { return; }
event.preventDefault();
var target = event.target.closest('[data-request-sort-rule]');
if (!target || target === this.sortDragRow) { return; }
var rect = target.getBoundingClientRect();
this.sortList.insertBefore(this.sortDragRow, event.clientY < rect.top + rect.height / 2 ? target : target.nextSibling);
},
sortDrop: function (event) {
if (!this.sortDragRow) { return; }
event.preventDefault();
this.sortDragEnd();
this.syncSortRules();
},
sortDragEnd: function () {
if (this.sortDragRow) { this.sortDragRow.classList.remove('is-dragging'); }
this.sortDragRow = null;
},
syncEditorAdvancedOptions: function () {
var basic = this.editorMode !== 'advanced';
document.querySelectorAll('[data-request-advanced-option]').forEach(function (option) {
option.hidden = basic && !option.selected;
});
},
onClick: function (e) {
var self = this;
var editorMode = e.target.closest('[data-request-editor-mode-value]');
if (editorMode) { return this.setEditorMode(editorMode.getAttribute('data-request-editor-mode-value')); }
var presentation = e.target.closest('[data-request-presentation-value]');
if (presentation) { return this.selectPresentation(presentation.getAttribute('data-request-presentation-value')); }
if (e.target.closest('[data-request-open-code]')) { return this.openCodeEditor(); }
if (e.target.closest('[data-request-sort-add]')) { return this.sortAdd(); }
if (e.target.closest('[data-request-sort-stable]')) { return this.sortStable(); }
var sortDirection = e.target.closest('[data-request-sort-direction]');
if (sortDirection) { return this.sortDirection(sortDirection); }
var sortRemove = e.target.closest('[data-request-sort-remove]');
if (sortRemove) { return this.sortRemove(sortRemove); }
// список
var copy = e.target.closest('[data-request-copy]');
if (copy) { return this.copy(copy.closest('tr')); }
var del = e.target.closest('[data-request-delete]');
if (del) { return this.remove(del.closest('tr')); }
// массовая сверка Legacy и Native
var auditOne = e.target.closest('[data-native-audit-one]');
if (auditOne) { return this.auditOne(auditOne.closest('[data-native-audit-row]'), auditOne); }
if (e.target.closest('[data-native-audit-all]')) { return this.auditAll(e.target.closest('[data-native-audit-all]')); }
if (e.target.closest('[data-native-audit-stabilize]')) { return this.auditStabilize(); }
if (e.target.closest('[data-native-audit-activate]')) { return this.auditActivate(); }
if (e.target.closest('[data-native-audit-rollback]')) { return this.auditRollback(); }
if (e.target.closest('[data-requests-reset]')) {
var input = document.querySelector('[data-requests-search]');
if (input) { input.value = ''; }
return this.filterList('');
}
// редактор: тулбар
var tags = e.target.closest('[data-req-tags-toggle]');
if (tags) { return this.openPalette(tags.closest('[data-req-code-field]')); }
if (e.target.closest('[data-req-tags-close]')) { return this.closePalette(); }
var lint = e.target.closest('[data-req-lint]');
if (lint) { return this.lint(lint.closest('[data-req-code-field]')); }
var full = e.target.closest('[data-req-fullscreen]');
if (full) { return this.fullscreen(full.closest('[data-req-code-field]')); }
var preview = e.target.closest('[data-request-preview]');
if (preview) { return this.preview(preview); }
var explainOption = e.target.closest('[data-request-explain-option]');
if (explainOption) { return this.selectExplainDocument(explainOption); }
var explainRun = e.target.closest('[data-request-explain-run]');
if (explainRun) { return this.explainSelected(explainRun); }
var previewExplain = e.target.closest('[data-preview-explain]');
if (previewExplain) { return this.explain(parseInt(previewExplain.getAttribute('data-preview-explain') || '0', 10)); }
// палитра
var tab = e.target.closest('[data-req-tag-tab]');
if (tab) { return this.activateGroup(tab.getAttribute('data-req-tag-tab')); }
var tag = e.target.closest('[data-req-tag]');
if (tag) { return this.insertTag(tag); }
// условия
var condAdd = e.target.closest('[data-cond-add]');
if (condAdd) { return this.condAdd(condAdd.closest('[data-cond-group]')); }
var booleanChoice = e.target.closest('[data-condition-boolean]');
if (booleanChoice) { return this.conditionBooleanValue(booleanChoice); }
var documentPick = e.target.closest('[data-condition-document-pick]');
if (documentPick) { return this.conditionDocumentPicker(documentPick.closest('tr')); }
var save = e.target.closest('[data-cond-save]');
if (save) { return this.condSave(save.closest('tr')); }
var cdel = e.target.closest('[data-cond-delete]');
if (cdel) { return this.condDelete(cdel.closest('tr')); }
var groupOperator = e.target.closest('[data-group-operator]');
if (groupOperator) { return this.groupOperator(groupOperator); }
var groupAdd = e.target.closest('[data-group-add]');
if (groupAdd) { return this.groupAdd(groupAdd.closest('[data-cond-group]')); }
var groupDelete = e.target.closest('[data-group-delete]');
if (groupDelete) { return this.groupDelete(groupDelete.closest('[data-cond-group]')); }
},
// ------------------------------------------------------------------ //
// Список
// ------------------------------------------------------------------ //
auditBase: function () {
return this.auditPage ? this.auditPage.getAttribute('data-base') : Adminx.base();
},
auditRequest: function (url) {
return Adminx.Ajax.post(url, new FormData()).then(function (payload) {
var data = payload.data || {};
if (!payload.ok || !data.success) {
throw new Error(data.message || 'Не удалось выполнить проверку');
}
return data;
});
},
auditSetBusy: function (busy) {
this.auditRunning = busy;
if (!this.auditPage) { return; }
this.auditPage.querySelectorAll('[data-native-audit-one]').forEach(function (button) {
button.disabled = busy;
});
document.querySelectorAll('[data-native-audit-all], [data-native-audit-stabilize], [data-native-audit-activate], [data-native-audit-rollback]').forEach(function (button) {
var unavailable = button.hasAttribute('data-native-audit-activate')
? button.getAttribute('data-audit-enabled') !== '1'
: (button.hasAttribute('data-native-audit-stabilize') && button.getAttribute('data-audit-enabled') !== '1');
button.disabled = busy || unavailable;
});
},
auditProgress: function (done, total, title) {
var progress = this.auditPage && this.auditPage.querySelector('[data-native-audit-progress]');
if (!progress) { return; }
progress.hidden = false;
var heading = progress.querySelector('[data-native-audit-progress-title]');
var copy = progress.querySelector('[data-native-audit-progress-copy]');
var bar = progress.querySelector('[data-native-audit-progress-bar]');
if (heading) { heading.textContent = title || 'Проверка запросов'; }
if (copy) { copy.textContent = done + ' из ' + total; }
if (bar) { bar.style.width = (total ? Math.round(done / total * 100) : 0) + '%'; }
},
auditState: function (row, comparison) {
if (!row) { return; }
var result = row.querySelector('[data-audit-result]');
var status = comparison && comparison.status ? comparison.status : 'error';
var labels = {
matched: ['is-success', 'ti-circle-check', 'Совпадает'],
unstable: ['is-warning', 'ti-arrows-sort', 'Порядок не закреплён'],
unsupported: ['is-warning', 'ti-code', 'Остаётся Legacy'],
order_diff: ['is-warning', 'ti-arrows-sort', 'Отличается порядок'],
mismatch: ['is-danger', 'ti-alert-triangle', 'Результаты различаются'],
incomplete: ['is-warning', 'ti-hourglass', 'Нужна ручная проверка'],
error: ['is-danger', 'ti-alert-triangle', 'Ошибка проверки']
};
var label = labels[status] || labels.error;
var detail = '';
if (comparison) {
if (comparison.reasons && comparison.reasons.length) {
detail = comparison.reasons.join(' ');
} else if (comparison.legacy && comparison.native) {
detail = 'Legacy: ' + comparison.legacy.total + ' · Native: ' + comparison.native.total;
if (comparison.runtime_verification && comparison.runtime_verification.length > 1) {
detail += ' · проверено состояний: ' + comparison.runtime_verification.length;
}
if (comparison.missing_ids && comparison.missing_ids.length) { detail += ' · нет ID: ' + comparison.missing_ids.slice(0, 8).join(', '); }
if (comparison.extra_ids && comparison.extra_ids.length) { detail += ' · лишние ID: ' + comparison.extra_ids.slice(0, 8).join(', '); }
}
var orderDetail = this.auditOrderDetail(comparison.order_diagnostics || {});
if (orderDetail) { detail += (detail ? ' ' : '') + orderDetail; }
}
result.replaceChildren();
var strong = document.createElement('b');
strong.className = label[0];
var icon = document.createElement('i');
icon.className = 'ti ' + label[1];
strong.appendChild(icon);
strong.appendChild(document.createTextNode(label[2]));
result.appendChild(strong);
if (detail) {
var small = document.createElement('small');
small.textContent = detail;
small.setAttribute('data-tooltip', detail);
result.appendChild(small);
}
row.setAttribute('data-audit-verified', status === 'matched' ? '1' : '0');
row.setAttribute('data-audit-status', status);
},
auditOrderDetail: function (diagnostics) {
if (!diagnostics || !diagnostics.position) { return ''; }
var detail = 'Позиция ' + diagnostics.position + ': Legacy #' + diagnostics.legacy_id + ', Native #' + diagnostics.native_id + '.';
var criterion = diagnostics.criteria && diagnostics.criteria.length ? diagnostics.criteria[0] : null;
if (criterion) {
detail += ' ' + criterion.label + ': ' + criterion.legacy_value + ' / ' + criterion.native_value + '.';
}
return detail;
},
auditOne: function (row, button, quiet) {
if (!row || this.auditRunning && !quiet) { return Promise.resolve(false); }
var self = this;
var id = row.getAttribute('data-native-audit-row');
var icon = button && button.querySelector('i');
if (button) { button.disabled = true; }
if (icon) { icon.classList.add('is-spinning'); }
return this.auditRequest(this.auditBase() + '/requests/' + encodeURIComponent(id) + '/native-audit').then(function (data) {
self.auditState(row, data.data && data.data.comparison ? data.data.comparison : {});
self.auditRefreshStats();
if (!quiet) { Adminx.Toast.show(data.message || 'Запрос проверен', 'success'); }
return true;
}).catch(function (error) {
self.auditState(row, { status: 'error', reasons: [error.message] });
if (!quiet) { Adminx.Toast.show(error.message, 'error'); }
return false;
}).then(function (result) {
if (button) { button.disabled = false; }
if (icon) { icon.classList.remove('is-spinning'); }
return result;
});
},
auditAll: function () {
if (!this.auditPage || this.auditRunning) { return; }
var self = this;
var rows = Array.prototype.slice.call(this.auditPage.querySelectorAll('[data-native-audit-row]'));
this.auditSetBusy(true);
this.auditProgress(0, rows.length, 'Подготовка Shadow');
this.auditRequest(this.auditBase() + '/requests/native-audit/prepare').then(function () {
rows.forEach(function (row) {
var mode = row.querySelector('[data-audit-mode]');
if (mode) { mode.className = 'badge badge-blue'; mode.textContent = 'Shadow'; }
});
var done = 0;
var matched = 0;
var run = function () {
if (done >= rows.length) {
self.auditSetBusy(false);
self.auditRefreshStats();
self.auditProgress(done, rows.length, 'Проверка завершена');
Adminx.Toast.show('Проверка завершена: совпало ' + matched + ' из ' + rows.length, matched ? 'success' : 'warning');
return;
}
self.auditProgress(done, rows.length, 'Проверяется запрос #' + rows[done].getAttribute('data-native-audit-row'));
var button = rows[done].querySelector('[data-native-audit-one]');
self.auditOne(rows[done], button, true).then(function (ok) {
if (ok && rows[done].getAttribute('data-audit-verified') === '1') { matched++; }
done++;
self.auditProgress(done, rows.length, 'Проверка запросов');
window.setTimeout(run, 30);
});
};
run();
}).catch(function (error) {
self.auditSetBusy(false);
self.auditProgress(0, rows.length, 'Проверка остановлена');
Adminx.Toast.show(error.message, 'error');
});
},
auditRefreshStats: function () {
if (!this.auditPage) { return; }
var rows = this.auditPage.querySelectorAll('[data-native-audit-row]');
var verified = this.auditPage.querySelectorAll('[data-audit-verified="1"]').length;
var nativeCount = 0;
rows.forEach(function (row) {
var mode = row.querySelector('[data-audit-mode]');
if (mode && mode.textContent.trim().toLowerCase() === 'native') { nativeCount++; }
});
var verifiedStat = document.querySelector('[data-audit-stat="verified"]');
var nativeStat = document.querySelector('[data-audit-stat="native"]');
if (verifiedStat) { verifiedStat.textContent = String(verified); }
if (nativeStat) { nativeStat.textContent = String(nativeCount); }
var activate = document.querySelector('[data-native-audit-activate]');
if (activate) {
activate.setAttribute('data-audit-enabled', verified ? '1' : '0');
activate.disabled = this.auditRunning || !verified;
}
var unstable = this.auditPage.querySelectorAll('[data-audit-status="unstable"], [data-audit-status="order_diff"]').length;
var stabilize = document.querySelector('[data-native-audit-stabilize]');
if (stabilize) {
stabilize.setAttribute('data-audit-enabled', unstable ? '1' : '0');
stabilize.disabled = this.auditRunning || !unstable;
}
},
auditStabilize: function () {
if (!this.auditPage || this.auditRunning) { return; }
var self = this;
var rows = Array.prototype.slice.call(this.auditPage.querySelectorAll('[data-audit-status="unstable"], [data-audit-status="order_diff"]'));
if (!rows.length) {
Adminx.Toast.show('Запросов с незакреплённым порядком нет', 'info');
return;
}
Adminx.Confirm.open({
kind: 'warning',
title: 'Закрепить порядок без изменения выдачи?',
message: 'Для каждого запроса полная Legacy-последовательность будет сверена с ID по возрастанию и убыванию. Native включится только при точном совпадении; любое расхождение автоматически отменит изменение.',
confirmLabel: 'Проверить и закрепить',
onConfirm: function () {
self.auditSetBusy(true);
var done = 0;
var applied = 0;
var skipped = 0;
var run = function () {
if (done >= rows.length) {
self.auditSetBusy(false);
self.auditRefreshStats();
self.auditProgress(done, rows.length, 'Порядок проверен');
var message = 'Порядок закреплён: ' + applied;
if (skipped) { message += ' · без изменений: ' + skipped; }
Adminx.Toast.show(message, applied ? 'success' : 'warning');
window.setTimeout(function () { window.location.reload(); }, 650);
return;
}
var row = rows[done];
var id = row.getAttribute('data-native-audit-row');
var originalStatus = row.getAttribute('data-audit-status') || 'unstable';
self.auditProgress(done, rows.length, 'Проверяется порядок запроса #' + id);
self.auditRequest(self.auditBase() + '/requests/' + encodeURIComponent(id) + '/native-stabilize').then(function (data) {
var result = data.data || {};
if (result.status === 'applied' && result.comparison) {
applied++;
self.auditState(row, result.comparison);
var mode = row.querySelector('[data-audit-mode]');
if (mode) { mode.className = 'badge badge-green'; mode.textContent = 'Native'; }
var plan = row.querySelector('[data-audit-plan]');
if (plan) { plan.className = 'badge badge-blue'; plan.textContent = 'Готов'; }
} else {
skipped++;
var reason = result.message || 'Безопасный порядок не найден';
self.auditState(row, { status: originalStatus, reasons: [reason] });
}
}).catch(function (error) {
skipped++;
self.auditState(row, { status: 'error', reasons: [error.message] });
}).then(function () {
done++;
self.auditRefreshStats();
self.auditProgress(done, rows.length, 'Закрепление порядка');
window.setTimeout(run, 30);
});
};
self.auditProgress(0, rows.length, 'Подготовка безопасной проверки');
run();
}
});
},
auditActivate: function () {
if (this.auditRunning) { return; }
var self = this;
Adminx.Confirm.open({
kind: 'warning',
title: 'Включить Native executor?',
message: 'Новый путь включится только у запросов с точным совпадением текущего плана. Остальные продолжат работать через Legacy.',
confirmLabel: 'Включить подтверждённые',
onConfirm: function () {
self.auditSetBusy(true);
self.auditRequest(self.auditBase() + '/requests/native-audit/activate').then(function (data) {
Adminx.Toast.show((data.message || 'Native включён') + ': ' + ((data.data && data.data.count) || 0), 'success');
window.location.reload();
}).catch(function (error) {
self.auditSetBusy(false);
Adminx.Toast.show(error.message, 'error');
});
}
});
},
auditRollback: function () {
if (this.auditRunning) { return; }
var self = this;
Adminx.Confirm.open({
kind: 'warning',
title: 'Вернуть все запросы в Legacy?',
message: 'Публичный сайт сразу вернётся к прежнему executor. Результаты проверки сохранятся.',
confirmLabel: 'Вернуть Legacy',
onConfirm: function () {
self.auditSetBusy(true);
self.auditRequest(self.auditBase() + '/requests/native-audit/rollback').then(function (data) {
Adminx.Toast.show(data.message || 'Legacy восстановлен', 'success');
window.location.reload();
}).catch(function (error) {
self.auditSetBusy(false);
Adminx.Toast.show(error.message, 'error');
});
}
});
},
filterList: function (query) {
var root = document.querySelector('[data-requests-page]');
if (!root) { return; }
var q = String(query || '').trim().toLowerCase();
var rows = root.querySelectorAll('[data-requests-list] tr[data-request-row]');
var visible = 0;
rows.forEach(function (row) {
var show = !q || (row.getAttribute('data-search') || '').indexOf(q) !== -1;
row.hidden = !show;
if (show) { visible++; }
});
var count = root.querySelector('[data-requests-count]');
if (count) { count.textContent = String(visible); }
var summary = root.querySelector('[data-requests-summary]');
if (summary) { summary.textContent = q ? (visible + ' из ' + rows.length + ' по фильтру.') : (rows.length + ' запросов в списке.'); }
var empty = root.querySelector('[data-requests-empty]');
if (empty) { empty.hidden = !(q && visible === 0); }
var reset = root.querySelector('[data-requests-reset]');
if (reset) { reset.hidden = !q; }
},
copy: function (row) {
var base = Adminx.base();
Adminx.Loader.show();
Adminx.Ajax.post(base + '/requests/' + row.dataset.id + '/copy').then(function (p) {
Adminx.Loader.hide();
var d = p.data || {};
if (d.success && d.redirect) { window.location.href = d.redirect; }
else { Adminx.Toast.show(d.message || 'Не удалось', 'error'); }
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
},
remove: function (row) {
var base = Adminx.base();
Adminx.Confirm.open({
kind: 'error', title: 'Удалить запрос?',
message: 'Запрос «' + row.dataset.alias + '» и его условия будут удалены.',
confirmLabel: 'Удалить', confirmClass: 'btn-danger',
onConfirm: function () {
Adminx.Loader.show();
Adminx.Ajax.post(base + '/requests/' + row.dataset.id + '/delete').then(function (p) {
Adminx.Loader.hide();
var d = p.data || {};
if (d.success) { row.remove(); Adminx.Toast.show(d.message, 'success'); }
else { Adminx.Toast.show(d.message || 'Ошибка', 'error'); }
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
}
});
},
// ------------------------------------------------------------------ //
// Сохранение запроса
// ------------------------------------------------------------------ //
save: function () {
var self = this;
this.form.querySelectorAll('[data-error]').forEach(function (s) { s.textContent = ''; });
var id = this.form.getAttribute('data-id') || '0';
var url = this.base() + '/requests' + (id !== '0' ? '/' + id : '');
Adminx.Loader.show();
Adminx.Ajax.post(url, new FormData(this.form)).then(function (p) {
Adminx.Loader.hide();
var d = p.data || {};
if (d.success) {
if (d.redirect) { window.location.href = d.redirect; return; }
Adminx.Toast.show(d.message || 'Сохранено', 'success');
return;
}
var errors = d.errors || {};
Object.keys(errors).forEach(function (f) {
var span = self.form.querySelector('[data-error="' + f + '"]');
if (span) { span.textContent = errors[f]; }
var inp = self.form.querySelector('[name="' + f + '"]');
if (inp) { inp.classList.add('is-invalid'); }
});
Adminx.Toast.show(d.message || 'Проверьте поля', 'error');
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
},
preview: function (button) {
if (!this.form || !button || button.disabled) { return; }
var id = parseInt(this.form.getAttribute('data-id') || '0', 10);
if (id <= 0) { return; }
var fd = new FormData();
this.form.querySelectorAll('[name="result_system[]"]:checked').forEach(function (input) {
fd.append('result_system[]', input.value);
});
this.form.querySelectorAll('[name="result_fields[]"]:checked').forEach(function (input) {
fd.append('result_fields[]', input.value);
});
var limit = document.querySelector('[data-request-preview-limit]');
fd.append('limit', limit ? limit.value : '10');
var renderer = document.querySelector('[data-request-preview-renderer]');
fd.append('renderer', renderer ? renderer.value : 'data_cards');
var self = this;
button.disabled = true;
button.classList.add('is-loading');
Adminx.Ajax.post(this.base() + '/requests/' + id + '/preview', fd).then(function (payload) {
var response = payload.data || {};
if (!response.success) {
Adminx.Toast.show(response.message || 'Предпросмотр не выполнен', 'error');
return;
}
self.renderPreview(response.data || {});
Adminx.Toast.show(response.message || 'Предпросмотр обновлён', 'success');
}).catch(function () {
Adminx.Toast.show('Ошибка сети при построении предпросмотра', 'error');
}).then(function () {
button.disabled = false;
button.classList.remove('is-loading');
});
},
renderPreview: function (data) {
var section = document.querySelector('[data-request-preview-section]');
if (!section) { return; }
var empty = section.querySelector('[data-request-preview-empty]');
var result = section.querySelector('[data-request-preview-result]');
if (empty) { empty.hidden = true; }
if (result) { result.hidden = false; }
this.setText(section, '[data-preview-total]', String(data.total || 0));
this.setText(section, '[data-preview-returned]', String((data.items || []).length));
this.setText(section, '[data-preview-time]', this.previewMilliseconds(data.query_time));
this.renderPreviewPlan(section.querySelector('[data-request-preview-plan]'), data.plan || {});
this.renderExecutorComparison(section.querySelector('[data-request-executor-comparison]'), data.executor_comparison || {});
this.renderPreviewItems(section.querySelector('[data-request-preview-items]'), data);
var sql = section.querySelector('[data-request-preview-sql]');
if (sql) { sql.textContent = data.sql || 'SQL доступен только пользователю с правом управления запросами.'; }
},
renderExecutorComparison: function (root, comparison) {
if (!root) { return; }
root.textContent = '';
var status = comparison.status || '';
if (!status) { root.hidden = true; return; }
root.hidden = false;
var states = {
matched: ['is-success', 'ti-circle-check', 'Legacy и Native совпали', 'Количество, состав и порядок документов одинаковы.'],
unstable: ['is-warning', 'ti-arrows-sort', 'Состав совпал, порядок не закреплён', 'Публичный вывод остаётся на Legacy, пока одинаковые значения сортировки не получат безопасный финальный порядок.'],
order_diff: ['is-warning', 'ti-arrows-sort', 'Состав совпал, порядок отличается', 'До переключения проверьте сортировку запроса.'],
mismatch: ['is-danger', 'ti-alert-triangle', 'Результаты различаются', 'Native включать рано: сравните отсутствующие и лишние ID.'],
incomplete: ['is-warning', 'ti-database-exclamation', 'Выборка слишком велика для автоподтверждения', 'Проверены первые 5000 документов. Оставьте Legacy или разбейте представление на более узкие запросы.'],
unsupported: ['is-warning', 'ti-code', 'Запрос пока требует Legacy', 'В плане есть PHP, свободный SQL или неподдерживаемая операция.'],
error: ['is-danger', 'ti-alert-circle', 'Сравнение не выполнено', 'Legacy продолжает работать; ошибка записана в системный журнал.']
};
var state = states[status] || states.error;
var nativeOption = document.querySelector('[data-native-executor-option]');
var nativeState = document.querySelector('[data-native-plan-state]');
if (nativeOption && !nativeOption.selected) { nativeOption.disabled = status !== 'matched'; }
if (nativeState) {
nativeState.className = 'badge ' + (status === 'matched' ? 'badge-green' : (status === 'unsupported' ? 'badge-amber' : 'badge-blue'));
nativeState.textContent = status === 'matched'
? 'Native подтверждён'
: (status === 'unsupported' ? 'Требуется Legacy' : (status === 'unstable' ? 'Нужен стабильный порядок' : 'Нужно проверить'));
}
root.className = 'request-executor-comparison ' + state[0];
var icon = document.createElement('span');
icon.className = 'icon-tile';
icon.innerHTML = '<i class="ti ' + state[1] + '"></i>';
var copy = document.createElement('div');
var title = document.createElement('b');
title.textContent = state[2];
var description = document.createElement('span');
description.textContent = state[3];
copy.appendChild(title);
copy.appendChild(description);
root.appendChild(icon);
root.appendChild(copy);
var details = [];
if (comparison.legacy && comparison.native) {
details.push('Legacy: ' + comparison.legacy.total);
details.push('Native: ' + comparison.native.total);
}
if (comparison.runtime_verification && comparison.runtime_verification.length > 1) {
details.push('Runtime-состояний: ' + comparison.runtime_verification.length);
}
if ((comparison.missing_ids || []).length) { details.push('Нет в Native: #' + comparison.missing_ids.join(', #')); }
if ((comparison.extra_ids || []).length) { details.push('Лишние в Native: #' + comparison.extra_ids.join(', #')); }
(comparison.reasons || []).forEach(function (reason) { details.push(reason); });
if (details.length) {
var meta = document.createElement('small');
meta.textContent = details.join(' · ');
root.appendChild(meta);
}
},
setText: function (root, selector, value) {
var node = root && root.querySelector(selector);
if (node) { node.textContent = value; }
},
previewMilliseconds: function (seconds) {
var value = Number(seconds || 0) * 1000;
if (!isFinite(value)) { value = 0; }
return (value < 10 ? value.toFixed(2) : value.toFixed(0)) + ' мс';
},
renderPreviewPlan: function (root, plan) {
if (!root) { return; }
root.textContent = '';
var items = [
['ti-category', 'Рубрика #' + (plan.rubric_id || 0)],
['ti-brackets', (plan.groups || 0) + ' групп'],
['ti-filter-check', (plan.conditions || 0) + ' активных условий'],
['ti-sort-descending', plan.order || 'Без явной сортировки'],
['ti-calendar-check', plan.publication || 'Проверка публикации'],
];
items.forEach(function (item) {
var chip = document.createElement('span');
var icon = document.createElement('i');
icon.className = 'ti ' + item[0];
chip.appendChild(icon);
chip.appendChild(document.createTextNode(item[1]));
root.appendChild(chip);
});
},
renderPreviewItems: function (root, data) {
if (!root) { return; }
root.textContent = '';
var items = Array.isArray(data.items) ? data.items : [];
var renderer = data.renderer || {};
if (renderer.presentation === 'json') {
var json = document.createElement('pre');
json.className = 'request-preview-json';
json.textContent = JSON.stringify({
total: Number(data.total || 0),
limit: Number(data.limit || 0),
items: items
}, null, 2);
root.appendChild(json);
return;
}
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'request-preview-no-items';
empty.innerHTML = '<i class="ti ti-filter-off"></i><b>Документы не найдены</b><span>Проверьте условия, статус публикации и выбранную рубрику.</span>';
root.appendChild(empty);
return;
}
var systemOptions = data.system_options || {};
var fieldOptions = data.field_options || {};
if (renderer.presentation === 'list') {
this.renderPreviewList(root, items, systemOptions, fieldOptions);
return;
}
if (renderer.presentation === 'table') {
this.renderPreviewTable(root, items, systemOptions, fieldOptions);
return;
}
items.forEach(function (item, index) {
var card = document.createElement('article');
card.className = 'request-preview-item';
card.style.setProperty('--preview-order', String(Math.min(index, 8)));
var head = document.createElement('header');
var title = document.createElement('div');
var titleText = item.system && item.system.title ? String(item.system.title) : 'Документ #' + item.document_id;
title.innerHTML = '<span class="icon-tile"><i class="ti ti-file-text"></i></span>';
var copy = document.createElement('span');
var strong = document.createElement('b');
strong.textContent = titleText;
var small = document.createElement('small');
small.textContent = '#' + item.document_id;
copy.appendChild(strong);
copy.appendChild(small);
title.appendChild(copy);
head.appendChild(title);
var explain = document.createElement('button');
explain.className = 'btn btn-ghost btn-icon btn-sm ax-act ax-act-view';
explain.type = 'button';
explain.setAttribute('data-preview-explain', String(item.document_id));
explain.setAttribute('data-tooltip', 'Объяснить результат');
explain.setAttribute('aria-label', 'Объяснить результат');
explain.innerHTML = '<i class="ti ti-route"></i>';
head.appendChild(explain);
card.appendChild(head);
var values = document.createElement('dl');
values.className = 'request-preview-values';
Object.keys(item.system || {}).forEach(function (key) {
if (key === 'title') { return; }
Adminx.Requests.appendPreviewValue(values, systemOptions[key] || key, item.system[key], key);
});
Object.keys(item.fields || {}).forEach(function (key) {
var field = item.fields[key] || {};
var option = fieldOptions[key] || {};
var label = option.title || field.alias || ('Поле #' + key);
Adminx.Requests.appendPreviewValue(values, label, field.value, field.alias || key);
});
card.appendChild(values);
root.appendChild(card);
});
},
renderPreviewList: function (root, items, systemOptions, fieldOptions) {
items.forEach(function (item, index) {
var row = document.createElement('article');
row.className = 'request-preview-list-item';
row.style.setProperty('--preview-order', String(Math.min(index, 8)));
var main = document.createElement('div');
main.className = 'request-preview-list-main';
main.innerHTML = '<span class="icon-tile"><i class="ti ti-file-text"></i></span>';
var copy = document.createElement('span');
var title = document.createElement('b');
title.textContent = Adminx.Requests.previewTitle(item);
var id = document.createElement('small');
id.textContent = '#' + item.document_id;
copy.appendChild(title);
copy.appendChild(id);
main.appendChild(copy);
row.appendChild(main);
var values = document.createElement('div');
values.className = 'request-preview-list-values';
Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).slice(0, 3).forEach(function (entry) {
var value = document.createElement('span');
var label = document.createElement('small');
label.textContent = entry.label;
var text = document.createElement('b');
text.textContent = Adminx.Requests.previewValue(entry.value);
value.appendChild(label);
value.appendChild(text);
values.appendChild(value);
});
row.appendChild(values);
row.appendChild(Adminx.Requests.previewExplainButton(item.document_id));
root.appendChild(row);
});
},
renderPreviewTable: function (root, items, systemOptions, fieldOptions) {
var columns = [];
var known = {};
items.forEach(function (item) {
Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).forEach(function (entry) {
if (!known[entry.key]) { known[entry.key] = true; columns.push({key: entry.key, label: entry.label}); }
});
});
var wrap = document.createElement('div');
wrap.className = 'request-preview-table-wrap';
var table = document.createElement('table');
table.className = 'table request-preview-table';
var head = document.createElement('thead');
var headRow = document.createElement('tr');
var titleHead = document.createElement('th');
titleHead.textContent = 'Материал';
headRow.appendChild(titleHead);
columns.forEach(function (column) {
var th = document.createElement('th');
th.textContent = column.label;
headRow.appendChild(th);
});
var actionHead = document.createElement('th');
actionHead.className = 'request-preview-table-action';
headRow.appendChild(actionHead);
head.appendChild(headRow);
table.appendChild(head);
var body = document.createElement('tbody');
items.forEach(function (item) {
var values = {};
Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).forEach(function (entry) { values[entry.key] = entry.value; });
var tr = document.createElement('tr');
var titleCell = document.createElement('td');
titleCell.className = 'request-preview-table-title';
var title = document.createElement('b');
title.textContent = Adminx.Requests.previewTitle(item);
var id = document.createElement('small');
id.textContent = '#' + item.document_id;
titleCell.appendChild(title);
titleCell.appendChild(id);
tr.appendChild(titleCell);
columns.forEach(function (column) {
var td = document.createElement('td');
td.textContent = Adminx.Requests.previewValue(Object.prototype.hasOwnProperty.call(values, column.key) ? values[column.key] : null);
tr.appendChild(td);
});
var action = document.createElement('td');
action.className = 'request-preview-table-action';
action.appendChild(Adminx.Requests.previewExplainButton(item.document_id));
tr.appendChild(action);
body.appendChild(tr);
});
table.appendChild(body);
wrap.appendChild(table);
root.appendChild(wrap);
},
previewTitle: function (item) {
return item.system && item.system.title ? String(item.system.title) : 'Документ #' + item.document_id;
},
previewEntries: function (item, systemOptions, fieldOptions) {
var entries = [];
Object.keys(item.system || {}).forEach(function (key) {
if (key !== 'title') { entries.push({key: 'system.' + key, label: systemOptions[key] || key, value: item.system[key]}); }
});
Object.keys(item.fields || {}).forEach(function (key) {
var field = item.fields[key] || {};
var option = fieldOptions[key] || {};
entries.push({key: 'field.' + key, label: option.title || field.alias || ('Поле #' + key), value: field.value});
});
return entries;
},
previewExplainButton: function (documentId) {
var button = document.createElement('button');
button.className = 'btn btn-ghost btn-icon btn-sm ax-act ax-act-view';
button.type = 'button';
button.setAttribute('data-preview-explain', String(documentId));
button.setAttribute('data-tooltip', 'Объяснить результат');
button.setAttribute('aria-label', 'Объяснить результат');
button.innerHTML = '<i class="ti ti-route"></i>';
return button;
},
appendPreviewValue: function (root, label, value, code) {
var group = document.createElement('div');
var term = document.createElement('dt');
term.textContent = label;
var system = document.createElement('small');
system.textContent = code;
term.appendChild(system);
var description = document.createElement('dd');
description.textContent = this.previewValue(value);
group.appendChild(term);
group.appendChild(description);
root.appendChild(group);
},
previewValue: function (value) {
if (value === null || typeof value === 'undefined' || value === '') { return '—'; }
if (typeof value === 'boolean') { return value ? 'Да' : 'Нет'; }
if (typeof value === 'object') {
try { return JSON.stringify(value, null, 2); } catch (e) { return '[структурированные данные]'; }
}
return String(value);
},
explainSearchInput: function (input) {
input.removeAttribute('data-document-id');
var button = document.querySelector('[data-request-explain-run]');
var directId = /^\s*#?\d+\s*$/.test(input.value) ? parseInt(input.value.replace(/\D/g, ''), 10) : 0;
if (directId > 0) {
input.setAttribute('data-document-id', String(directId));
if (button) { button.disabled = false; }
} else if (button) {
button.disabled = true;
}
window.clearTimeout(this.explainSearchTimer);
var self = this;
this.explainSearchTimer = window.setTimeout(function () { self.searchExplainDocuments(input); }, 220);
},
searchExplainDocuments: function (input) {
if (!this.form || !input) { return; }
var id = parseInt(this.form.getAttribute('data-id') || '0', 10);
if (id <= 0) { return; }
var options = document.querySelector('[data-request-explain-options]');
if (!options) { return; }
var query = String(input.value || '').trim();
var url = this.base() + '/requests/' + id + '/explain-documents?q=' + encodeURIComponent(query) + '&limit=12';
options.hidden = false;
options.innerHTML = '<div class="request-explain-options-state"><i class="ti ti-loader-2"></i>Поиск…</div>';
fetch(url, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) {
if (!response.ok) { throw new Error('HTTP ' + response.status); }
return response.json();
}).then(function (payload) {
var response = payload || {};
var items = response.data && response.data.items ? response.data.items : [];
Adminx.Requests.renderExplainOptions(options, items);
}).catch(function () {
options.innerHTML = '<div class="request-explain-options-state is-error"><i class="ti ti-alert-circle"></i>Поиск недоступен</div>';
});
},
renderExplainOptions: function (root, items) {
root.textContent = '';
if (!items.length) {
root.innerHTML = '<div class="request-explain-options-state"><i class="ti ti-file-off"></i>Документы не найдены</div>';
return;
}
items.forEach(function (item) {
var button = document.createElement('button');
button.type = 'button';
button.setAttribute('data-request-explain-option', String(item.id));
button.setAttribute('data-title', item.title || ('Документ #' + item.id));
var icon = document.createElement('span');
icon.className = 'icon-tile';
icon.innerHTML = '<i class="ti ti-file-text"></i>';
var copy = document.createElement('span');
var title = document.createElement('b');
title.textContent = item.title || ('Документ #' + item.id);
var meta = document.createElement('small');
meta.textContent = '#' + item.id + (item.alias ? ' · ' + item.alias : '') + (item.deleted ? ' · удалён' : (!item.status ? ' · снят с публикации' : ''));
copy.appendChild(title);
copy.appendChild(meta);
button.appendChild(icon);
button.appendChild(copy);
root.appendChild(button);
});
},
selectExplainDocument: function (option) {
var input = document.querySelector('[data-request-explain-search]');
var root = document.querySelector('[data-request-explain-options]');
var id = parseInt(option.getAttribute('data-request-explain-option') || '0', 10);
if (!input || id <= 0) { return; }
input.value = '#' + id + ' · ' + (option.getAttribute('data-title') || 'Документ');
input.setAttribute('data-document-id', String(id));
if (root) { root.hidden = true; }
var run = document.querySelector('[data-request-explain-run]');
if (run) { run.disabled = false; run.focus(); }
},
explainSelected: function (button) {
var input = document.querySelector('[data-request-explain-search]');
var id = input ? parseInt(input.getAttribute('data-document-id') || '0', 10) : 0;
if (id > 0) { this.explain(id, button); }
},
explain: function (documentId, button) {
if (!this.form || documentId <= 0) { return; }
var requestId = parseInt(this.form.getAttribute('data-id') || '0', 10);
if (requestId <= 0) { return; }
var drawer = document.getElementById('requestExplainDrawer');
if (!drawer) { return; }
this.resetExplainDrawer(drawer);
if (Adminx.Drawer) { Adminx.Drawer.open('requestExplainDrawer'); }
if (button) { button.disabled = true; }
var fd = new FormData();
fd.append('document_id', String(documentId));
Adminx.Ajax.post(this.base() + '/requests/' + requestId + '/explain', fd).then(function (payload) {
var response = payload.data || {};
if (!response.success) {
Adminx.Requests.renderExplainError(drawer, response.message || 'Проверка не выполнена');
return;
}
Adminx.Requests.renderExplain(drawer, response.data || {});
}).catch(function () {
Adminx.Requests.renderExplainError(drawer, 'Ошибка сети при проверке документа');
}).then(function () {
if (button) { button.disabled = false; }
});
},
resetExplainDrawer: function (drawer) {
var loading = drawer.querySelector('[data-request-explain-loading]');
var content = drawer.querySelector('[data-request-explain-content]');
if (loading) { loading.hidden = false; loading.innerHTML = '<i class="ti ti-loader-2"></i><span>Проверяем запрос…</span>'; }
if (content) { content.hidden = true; }
this.setText(drawer, '[data-request-explain-title]', 'Проверка документа');
this.setText(drawer, '[data-request-explain-subtitle]', 'Условия запроса и фактический итог executor.');
this.setText(drawer, '[data-request-explain-footer]', 'Выполняется');
var footer = drawer.querySelector('[data-request-explain-footer]');
if (footer) { footer.className = 'badge badge-gray'; }
var icon = drawer.querySelector('[data-request-explain-icon]');
if (icon) { icon.className = 'dialog-icon info'; icon.innerHTML = '<i class="ti ti-route"></i>'; }
},
renderExplainError: function (drawer, message) {
var loading = drawer.querySelector('[data-request-explain-loading]');
if (loading) { loading.hidden = false; loading.innerHTML = '<i class="ti ti-alert-triangle"></i><span></span>'; loading.querySelector('span').textContent = message; }
this.setText(drawer, '[data-request-explain-footer]', 'Ошибка');
var footer = drawer.querySelector('[data-request-explain-footer]');
if (footer) { footer.className = 'badge badge-red'; }
var icon = drawer.querySelector('[data-request-explain-icon]');
if (icon) { icon.className = 'dialog-icon danger'; icon.innerHTML = '<i class="ti ti-alert-triangle"></i>'; }
},
renderExplain: function (drawer, data) {
var loading = drawer.querySelector('[data-request-explain-loading]');
var content = drawer.querySelector('[data-request-explain-content]');
if (loading) { loading.hidden = true; }
if (content) { content.hidden = false; }
var documentData = data.document || {};
var matched = !!data.matched;
this.setText(drawer, '[data-request-explain-title]', documentData.title || ('Документ #' + documentData.id));
this.setText(drawer, '[data-request-explain-subtitle]', '#' + documentData.id + (documentData.alias ? ' · ' + documentData.alias : ''));
this.setText(drawer, '[data-request-explain-footer]', matched ? 'Входит в запрос' : 'Исключён');
var footer = drawer.querySelector('[data-request-explain-footer]');
if (footer) { footer.className = 'badge ' + (matched ? 'badge-green' : 'badge-red'); }
var icon = drawer.querySelector('[data-request-explain-icon]');
if (icon) { icon.className = 'dialog-icon ' + (matched ? 'success' : 'danger'); icon.innerHTML = '<i class="ti ti-' + (matched ? 'circle-check' : 'circle-x') + '"></i>'; }
this.renderExplainChecks(drawer.querySelector('[data-request-explain-base]'), data.base || []);
this.renderExplainTree(drawer.querySelector('[data-request-explain-tree]'), data.groups || []);
this.renderExplainNotes(drawer.querySelector('[data-request-explain-notes]'), data.notes || []);
},
renderExplainChecks: function (root, checks) {
if (!root) { return; }
root.textContent = '';
checks.forEach(function (check) {
var item = document.createElement('article');
item.className = 'request-explain-check is-' + check.state;
item.innerHTML = '<i class="ti ti-' + (check.state === 'passed' ? 'check' : 'x') + '"></i><div><b></b><span></span></div>';
item.querySelector('b').textContent = check.label || check.code;
item.querySelector('span').textContent = check.detail || '';
root.appendChild(item);
});
},
renderExplainTree: function (root, groups) {
if (!root) { return; }
root.textContent = '';
if (!groups.length) {
root.innerHTML = '<div class="request-explain-tree-empty"><i class="ti ti-filter-off"></i><span>Активных условий нет. Результат определяется системными ограничениями.</span></div>';
return;
}
groups.forEach(function (group) { root.appendChild(Adminx.Requests.explainGroupNode(group)); });
},
explainGroupNode: function (group) {
var section = document.createElement('section');
section.className = 'request-explain-group is-' + (group.state || 'empty');
var head = document.createElement('header');
head.innerHTML = '<span class="request-explain-group-state"><i class="ti"></i></span><div><b></b><small></small></div><span class="badge"></span>';
head.querySelector('b').textContent = group.title || ('Группа #' + group.id);
head.querySelector('small').textContent = group.operator === 'OR' ? 'Достаточно одного совпадения' : 'Должны совпасть все элементы';
head.querySelector('.badge').textContent = group.operator === 'OR' ? 'ИЛИ' : 'И';
head.querySelector('.badge').className = 'badge ' + (group.operator === 'OR' ? 'badge-amber' : 'badge-blue');
head.querySelector('.ti').className = 'ti ti-' + this.explainStateIcon(group.state);
section.appendChild(head);
var body = document.createElement('div');
body.className = 'request-explain-group-body';
(group.conditions || []).forEach(function (condition) { body.appendChild(Adminx.Requests.explainConditionNode(condition)); });
(group.children || []).forEach(function (child) { body.appendChild(Adminx.Requests.explainGroupNode(child)); });
if (!body.children.length) { body.innerHTML = '<div class="request-explain-tree-empty">В группе нет активных условий</div>'; }
section.appendChild(body);
return section;
},
explainConditionNode: function (condition) {
var item = document.createElement('article');
item.className = 'request-explain-condition is-' + (condition.state || 'unknown');
item.innerHTML = '<span class="request-explain-condition-state"><i class="ti"></i></span><div class="request-explain-condition-copy"><div><b></b><code></code></div><dl><div><dt>Фактически</dt><dd data-actual></dd></div><div><dt>Условие</dt><dd data-expected></dd></div></dl><p hidden></p></div>';
item.querySelector('.ti').className = 'ti ti-' + this.explainStateIcon(condition.state);
item.querySelector('b').textContent = condition.field || ('Поле #' + condition.field_id);
item.querySelector('code').textContent = (condition.alias || ('#' + condition.field_id)) + ' ' + condition.operator;
item.querySelector('[data-actual]').textContent = this.previewValue(condition.actual);
item.querySelector('[data-expected]').textContent = this.previewValue(condition.expected);
var note = item.querySelector('p');
if (condition.note) { note.hidden = false; note.textContent = condition.note; }
return item;
},
explainStateIcon: function (state) {
if (state === 'passed') { return 'check'; }
if (state === 'failed') { return 'x'; }
if (state === 'unknown') { return 'help'; }
return 'minus';
},
renderExplainNotes: function (root, notes) {
if (!root) { return; }
root.textContent = '';
notes.forEach(function (note) {
var item = document.createElement('span');
item.innerHTML = '<i class="ti ti-info-circle"></i>';
item.appendChild(document.createTextNode(note));
root.appendChild(item);
});
},
// ------------------------------------------------------------------ //
// Редактор кода: fullscreen / lint / палитра тегов
// ------------------------------------------------------------------ //
cmOf: function (field) {
var ta = field && field.querySelector('textarea[data-code-editor]');
return ta && ta._adminxCodeMirror ? ta._adminxCodeMirror : null;
},
codeOf: function (field) {
var cm = this.cmOf(field);
if (cm) { cm.save(); return cm.getValue(); }
var ta = field && field.querySelector('textarea');
return ta ? ta.value : '';
},
fullscreen: function (field, force) {
if (!field) { return; }
var on = typeof force === 'boolean' ? force : !field.classList.contains('is-fullscreen');
field.classList.toggle('is-fullscreen', on);
document.body.classList.toggle('req-code-fullscreen-open', on);
var cm = this.cmOf(field);
if (cm) { setTimeout(function () { cm.refresh(); cm.focus(); }, 30); }
},
lint: function (field) {
if (!field) { return; }
var out = field.querySelector('[data-req-lint-result]');
if (out) { out.textContent = 'Проверка…'; out.classList.remove('is-ok', 'is-error'); }
var fd = new FormData();
fd.append('code', this.codeOf(field));
Adminx.Ajax.post(this.base() + '/requests/lint', fd).then(function (p) {
var d = p.data || {};
if (d.success) {
if (out) { out.textContent = d.message; out.classList.add('is-ok'); }
Adminx.Toast.show(d.message || 'Синтаксис без ошибок', 'success');
} else {
var detail = (d.errors && d.errors.code) ? d.errors.code : (d.message || 'Ошибка синтаксиса');
if (out) { out.textContent = detail; out.classList.add('is-error'); }
Adminx.Toast.show(d.message || 'Ошибка синтаксиса', 'error');
}
}).catch(function () {
if (out) { out.textContent = 'Не удалось выполнить проверку.'; out.classList.add('is-error'); }
Adminx.Toast.show('Ошибка сети', 'error');
});
},
openPalette: function (field) {
if (!this.palette || !field) { return; }
this.activeField = field;
var textarea = field.querySelector('textarea[data-req-editor]');
this.paletteScope = textarea && textarea.name === 'request_template_item' ? 'item' : 'main';
// переносим палитру под тулбар активного редактора
var ta = field.querySelector('textarea[data-code-editor]');
var host = ta ? ta.parentNode : field.querySelector('.card-body');
if (ta) { host.insertBefore(this.palette, ta); } else { host.appendChild(this.palette); }
this.palette.hidden = false;
this.applyPaletteScope();
var search = this.palette.querySelector('[data-req-tags-search]');
if (search) { search.value = ''; this.filterTags(''); search.focus(); }
},
closePalette: function () { if (this.palette) { this.palette.hidden = true; } },
applyPaletteScope: function () {
if (!this.palette) { return; }
var scope = this.paletteScope || 'main';
this.palette.querySelectorAll('[data-req-tag-scope]').forEach(function (node) {
var target = node.getAttribute('data-req-tag-scope') || 'all';
node.hidden = target !== 'all' && target !== scope;
if (node.hidden) { node.classList.remove('is-active'); }
});
var first = this.palette.querySelector('[data-req-tag-tab]:not([hidden])');
if (first) { this.activateGroup(first.getAttribute('data-req-tag-tab')); }
},
activateGroup: function (index) {
if (!this.palette) { return; }
this.palette.querySelectorAll('[data-req-tag-tab]').forEach(function (t) {
t.classList.toggle('is-active', !t.hidden && t.getAttribute('data-req-tag-tab') === String(index));
});
this.palette.querySelectorAll('[data-req-tag-panel]').forEach(function (g) {
g.classList.toggle('is-active', !g.hidden && g.getAttribute('data-req-tag-panel') === String(index));
});
},
filterTags: function (query) {
if (!this.palette) { return; }
query = (query || '').trim().toLowerCase();
var shown = 0;
this.palette.querySelectorAll('[data-req-tag]').forEach(function (b) {
var group = b.closest('[data-req-tag-group]');
var scope = group ? (group.getAttribute('data-req-tag-scope') || 'all') : '';
var allowed = group && (scope === 'all' || scope === (this.paletteScope || 'main'));
var match = allowed && (query === '' || b.textContent.toLowerCase().indexOf(query) !== -1);
b.hidden = !match;
if (match) { shown++; }
}, this);
// При поиске показываем все подходящие группы активного редактора.
this.palette.querySelectorAll('[data-req-tag-group]').forEach(function (g) {
var scope = g.getAttribute('data-req-tag-scope') || 'all';
var allowed = scope === 'all' || scope === (this.paletteScope || 'main');
g.hidden = !allowed || (query !== '' && !g.querySelector('[data-req-tag]:not([hidden])'));
if (query !== '') { g.classList.toggle('is-active', !g.hidden); }
}, this);
this.palette.querySelectorAll('[data-req-tag-tab]').forEach(function (tab) {
var scope = tab.getAttribute('data-req-tag-scope') || 'all';
var allowed = scope === 'all' || scope === (this.paletteScope || 'main');
var panel = this.palette.querySelector('[data-req-tag-panel="' + tab.getAttribute('data-req-tag-tab') + '"]');
tab.hidden = !allowed || (query !== '' && (!panel || panel.hidden));
}, this);
if (query === '') {
var active = this.palette.querySelector('[data-req-tag-tab].is-active:not([hidden])');
var first = active || this.palette.querySelector('[data-req-tag-tab]:not([hidden])');
if (first) { this.activateGroup(first.getAttribute('data-req-tag-tab')); }
}
var empty = this.palette.querySelector('[data-req-tags-empty]');
if (empty) { empty.hidden = shown !== 0; }
},
insertTag: function (button) {
var value = button.getAttribute('data-req-tag') || '';
var select = button.getAttribute('data-req-tag-select') || '';
var cm = this.cmOf(this.activeField);
if (!value || !cm) { this.closePalette(); return; }
var start = cm.indexFromPos(cm.getCursor());
cm.replaceSelection(value, 'around');
cm.focus();
if (select && value.indexOf(select) !== -1) {
cm.setSelection(cm.posFromIndex(start + value.indexOf(select)), cm.posFromIndex(start + value.indexOf(select) + select.length));
} else {
cm.setCursor(cm.posFromIndex(start + value.length));
}
cm.save();
this.closePalette();
},
// ------------------------------------------------------------------ //
// Условия
// ------------------------------------------------------------------ //
condAdd: function (group) {
if (!this.condTable || !group) { return; }
var tpl = document.querySelector('[data-cond-tpl]');
var body = group.querySelector(':scope > .req-condition-table-wrap [data-cond-body]');
if (!tpl || !body) { return; }
var empty = body.querySelector('[data-cond-empty]');
if (empty) { empty.remove(); }
var row = tpl.content.firstElementChild.cloneNode(true);
body.appendChild(row);
this.conditionFieldRefresh(row, true);
var f = row.querySelector('[data-cf="condition_field_id"]');
if (f) { f.focus(); }
},
conditionFieldRefresh: function (row, changed) {
if (!row) { return; }
var field = row.querySelector('[data-cf="condition_field_id"]');
var option = field && field.options[field.selectedIndex];
if (!option) { return; }
var meta = row.querySelector('[data-condition-field-meta]');
var typeLabel = option.getAttribute('data-cond-type-label') || option.getAttribute('data-cond-type') || 'Поле';
var alias = option.getAttribute('data-cond-alias') || ('field_' + option.value);
if (meta) { meta.textContent = ''; meta.hidden = true; }
if (field) { field.title = 'Тип: ' + typeLabel + '. Системное имя: ' + alias; }
var mode = row.querySelector('[data-cf="condition_value_mode"]');
if (changed && mode && option.getAttribute('data-cond-mode')) { mode.value = option.getAttribute('data-cond-mode'); }
this.conditionOperatorRefresh(row, option, changed);
this.conditionSmartControl(row, option);
this.conditionValueRefresh(row.querySelector('[data-condition-value]'));
},
conditionOperatorRefresh: function (row, fieldOption, changed) {
var compare = row.querySelector('[data-cf="condition_compare"]');
var source = row.querySelector('[data-cf="condition_value_source"]');
if (!compare) { return; }
var allowed = this.conditionJson(fieldOption.getAttribute('data-cond-operators'), []);
var legacy = source && source.value === 'legacy';
Array.prototype.forEach.call(compare.options, function (option) {
var visible = legacy || allowed.indexOf(option.value) !== -1 || (!changed && option.selected);
option.hidden = !visible;
option.disabled = !visible;
});
if (changed && compare.options[compare.selectedIndex] && compare.options[compare.selectedIndex].disabled) {
for (var i = 0; i < compare.options.length; i++) {
if (!compare.options[i].disabled) { compare.value = compare.options[i].value; break; }
}
}
},
conditionSmartControl: function (row, fieldOption) {
var editor = row.querySelector('[data-condition-value]');
if (!editor) { return; }
var source = editor.querySelector('[data-cf="condition_value_source"]');
var storage = editor.querySelector('[data-condition-literal-storage]');
var root = editor.querySelector('[data-condition-smart-control]');
if (!storage || !root) { return; }
root.innerHTML = '';
storage.hidden = false;
storage.classList.remove('mono');
if (!source || source.value !== 'literal') { return; }
var kind = fieldOption.getAttribute('data-cond-kind') || 'text';
var compare = row.querySelector('[data-cf="condition_compare"]');
var operator = compare ? compare.value : '==';
var options = this.conditionJson(fieldOption.getAttribute('data-cond-options'), []);
var disabled = storage.disabled;
if (kind === 'text' && options.length === 0) {
storage.placeholder = operator === 'IN=' || operator === 'NOTIN=' ? 'Значения через запятую' : 'Введите значение';
return;
}
if (kind === 'boolean') {
storage.hidden = true;
root.innerHTML = '<span class="req-condition-boolean" role="group" aria-label="Логическое значение">'
+ '<button type="button" data-condition-boolean="1"><i class="ti ti-check"></i>Да</button>'
+ '<button type="button" data-condition-boolean="0"><i class="ti ti-x"></i>Нет</button></span>';
root.querySelectorAll('[data-condition-boolean]').forEach(function (button) {
button.disabled = disabled;
button.setAttribute('aria-pressed', button.getAttribute('data-condition-boolean') === storage.value ? 'true' : 'false');
});
return;
}
if ((kind === 'choice' || kind === 'choice_list') && options.length) {
storage.hidden = true;
var select = document.createElement('select');
select.className = 'select req-condition-choice';
select.setAttribute('data-condition-smart-select', '');
select.disabled = disabled;
var multiple = kind === 'choice_list' || operator === 'IN=' || operator === 'NOTIN=';
select.multiple = multiple;
if (multiple) { select.size = Math.min(4, Math.max(2, options.length)); }
else {
var empty = document.createElement('option'); empty.value = ''; empty.textContent = '— выберите —'; select.appendChild(empty);
}
var selected = multiple ? this.conditionCsv(storage.value) : [String(storage.value || '')];
options.forEach(function (item) {
var option = document.createElement('option');
option.value = String(item.value == null ? '' : item.value);
option.textContent = String(item.label == null ? option.value : item.label);
option.selected = selected.indexOf(option.value) !== -1;
select.appendChild(option);
});
root.appendChild(select);
return;
}
if (kind === 'relation') {
storage.hidden = true;
var relation = document.createElement('div');
relation.className = 'req-condition-document';
relation.innerHTML = '<div class="input-wrap"><i class="ti ti-file"></i><input class="input" type="text" inputmode="numeric" data-condition-smart-input placeholder="ID документа"></div>'
+ '<button class="btn btn-secondary btn-icon" type="button" data-condition-document-pick data-tooltip="Найти документ" aria-label="Найти документ"><i class="ti ti-file-search"></i></button>';
relation.querySelector('input').value = storage.value || '';
relation.querySelector('input').disabled = disabled;
relation.querySelector('button').disabled = disabled;
root.appendChild(relation);
return;
}
if (kind === 'date' || kind === 'datetime') {
storage.hidden = true;
var date = document.createElement('input');
date.className = 'input'; date.type = kind === 'date' ? 'date' : 'datetime-local';
date.setAttribute('data-condition-smart-input', ''); date.setAttribute('data-condition-date-kind', kind);
var dateValue = this.conditionDateInput(storage.value, kind);
date.value = dateValue.value; date.setAttribute('data-condition-date-format', dateValue.format); date.disabled = disabled;
root.appendChild(date);
return;
}
storage.hidden = true;
var input = document.createElement('input');
input.className = 'input'; input.type = 'text'; input.setAttribute('data-condition-smart-input', ''); input.value = storage.value || ''; input.disabled = disabled;
if (kind === 'number') { input.inputMode = 'decimal'; input.placeholder = operator === 'SEGMENT' || operator === 'INTERVAL' ? 'Например, 10, 50' : 'Введите число'; }
else if (kind === 'relation_list') { input.placeholder = 'ID документов через запятую'; }
else { input.placeholder = 'Введите значение'; }
root.appendChild(input);
},
conditionSmartValue: function (control) {
var editor = control.closest('[data-condition-value]');
var storage = editor && editor.querySelector('[data-condition-literal-storage]');
if (!storage) { return; }
if (control.matches('select[multiple]')) {
var values = Array.prototype.filter.call(control.options, function (item) { return item.selected; }).map(function (item) { return item.value; });
storage.value = this.conditionCsvEncode(values);
} else if (control.hasAttribute('data-condition-date-kind')) {
storage.value = this.conditionDateStorage(control.value, control.getAttribute('data-condition-date-kind'), control.getAttribute('data-condition-date-format'));
} else { storage.value = control.value; }
storage.dispatchEvent(new Event('input', { bubbles: true }));
},
conditionBooleanValue: function (button) {
var editor = button.closest('[data-condition-value]');
var storage = editor && editor.querySelector('[data-condition-literal-storage]');
if (!storage) { return; }
storage.value = button.getAttribute('data-condition-boolean');
button.parentNode.querySelectorAll('[data-condition-boolean]').forEach(function (item) {
item.setAttribute('aria-pressed', item === button ? 'true' : 'false');
});
storage.dispatchEvent(new Event('input', { bubbles: true }));
},
conditionJson: function (value, fallback) {
try { var parsed = JSON.parse(value || ''); return parsed || fallback; } catch (e) { return fallback; }
},
conditionCsv: function (value) {
var matches = String(value || '').match(/(?:^|,)\s*(?:"([^"]*(?:""[^"]*)*)"|([^,]*))/g) || [];
return matches.map(function (item) { return item.replace(/^\s*,?\s*/, '').replace(/^"|"$/g, '').replace(/""/g, '"').trim(); }).filter(Boolean);
},
conditionCsvEncode: function (values) {
return values.map(function (value) {
value = String(value); return /[,"]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value;
}).join(',');
},
conditionDateInput: function (value, kind) {
value = String(value || '').trim();
var format = kind === 'datetime' ? 'timestamp' : 'dmy';
var date = null;
if (/^\d{9,10}$/.test(value)) { date = new Date(parseInt(value, 10) * 1000); format = 'timestamp'; }
else if (/^\d{2}\.\d{2}\.\d{4}$/.test(value)) { var p = value.split('.'); date = new Date(p[2] + '-' + p[1] + '-' + p[0] + 'T00:00:00'); format = 'dmy'; }
else if (/^\d{4}-\d{2}-\d{2}/.test(value)) { date = new Date(value.replace(' ', 'T')); format = 'iso'; }
if (!date || isNaN(date.getTime())) { return { value: '', format: format }; }
var pad = function (number) { return String(number).padStart(2, '0'); };
var result = date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
if (kind === 'datetime') { result += 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); }
return { value: result, format: format };
},
conditionDateStorage: function (value, kind, format) {
if (!value) { return ''; }
if (format === 'timestamp' || kind === 'datetime') { return String(Math.floor(new Date(value).getTime() / 1000)); }
if (format === 'dmy') { var parts = value.split('-'); return parts[2] + '.' + parts[1] + '.' + parts[0]; }
return value;
},
conditionDocumentPicker: function (row) {
var self = this;
var field = row.querySelector('[data-cf="condition_field_id"]');
var option = field && field.options[field.selectedIndex];
var overlay = document.createElement('div');
overlay.className = 'overlay request-condition-picker-overlay';
overlay.innerHTML = '<div class="modal picker-modal request-condition-picker" role="dialog" aria-modal="true" aria-labelledby="requestConditionPickerTitle">'
+ '<div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div><h3 id="requestConditionPickerTitle">Выбрать документ</h3><p class="text-secondary">Поиск по ID, названию или адресу.</p></div><button class="modal-close" type="button" data-condition-picker-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div>'
+ '<div class="modal-body"><div class="input-wrap request-condition-picker-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="Начните вводить название" data-condition-picker-search></div><div class="request-condition-picker-status" data-condition-picker-status>Загрузка...</div><div class="request-condition-picker-list" data-condition-picker-list></div></div>'
+ '<div class="modal-footer"><div class="mf-left" data-condition-picker-count></div><button class="btn btn-ghost" type="button" data-condition-picker-close>Закрыть</button></div></div>';
document.body.appendChild(overlay);
requestAnimationFrame(function () { overlay.classList.add('show'); });
var search = overlay.querySelector('[data-condition-picker-search]');
var list = overlay.querySelector('[data-condition-picker-list]');
var status = overlay.querySelector('[data-condition-picker-status]');
var count = overlay.querySelector('[data-condition-picker-count]');
var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); };
var load = function () {
var params = new URLSearchParams(); params.set('q', search.value.trim()); params.set('limit', '30');
if (option && option.getAttribute('data-cond-rubrics')) { params.set('rubric_id', option.getAttribute('data-cond-rubrics')); }
status.hidden = false; status.textContent = 'Загрузка...'; list.innerHTML = '';
fetch(self.base() + '/requests/documents/picker?' + params.toString(), { credentials: 'same-origin', headers: { 'Accept': 'application/json' } })
.then(function (response) { return response.json(); })
.then(function (payload) {
var items = payload && payload.data && payload.data.items ? payload.data.items : [];
items.forEach(function (item) {
var button = document.createElement('button'); button.type = 'button'; button.className = 'request-condition-picker-item'; button.setAttribute('data-condition-picker-item', String(item.id));
var main = document.createElement('span'); main.innerHTML = '<b></b><small></small>'; main.querySelector('b').textContent = item.title || 'Без названия'; main.querySelector('small').textContent = '/' + String(item.alias || '').replace(/^\/+/, '');
var id = document.createElement('span'); id.className = 'request-condition-picker-id'; id.textContent = '#' + item.id;
var badge = document.createElement('span'); badge.className = 'badge badge-blue'; badge.textContent = item.rubric_title || ('Рубрика #' + item.rubric_id);
button.appendChild(id); button.appendChild(main); button.appendChild(badge); list.appendChild(button);
});
status.hidden = items.length > 0; status.textContent = search.value.trim() ? 'Ничего не найдено' : 'Документы не найдены'; count.textContent = items.length ? 'Найдено: ' + items.length : '';
}).catch(function () { status.hidden = false; status.textContent = 'Не удалось загрузить документы'; });
};
var timer = null;
search.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(load, 220); });
overlay.addEventListener('click', function (event) {
if (event.target === overlay || event.target.closest('[data-condition-picker-close]')) { close(); return; }
var item = event.target.closest('[data-condition-picker-item]');
if (!item) { return; }
var storage = row.querySelector('[data-condition-literal-storage]'); if (storage) { storage.value = item.getAttribute('data-condition-picker-item'); }
self.conditionSmartControl(row, option); self.conditionValueRefresh(row.querySelector('[data-condition-value]')); close();
});
load(); search.focus();
},
conditionValueRefresh: function (editor, trigger) {
if (!editor) { return; }
var row = editor.closest('tr');
var source = editor.querySelector('[data-cf="condition_value_source"]');
var literal = editor.querySelector('[data-condition-literal]');
var input = editor.querySelector('[data-condition-input]');
var key = editor.querySelector('[data-cf="condition_value_key"]');
var mode = editor.querySelector('[data-cf="condition_value_mode"]');
var constant = editor.querySelector('[data-condition-constant]');
var tag = editor.querySelector('[data-condition-tag]');
var technical = editor.querySelector('[data-condition-technical]');
var literalLabel = editor.querySelector('[data-condition-literal-label]');
var preview = editor.querySelector('[data-condition-preview-text]');
var isInput = source && source.value === 'input';
if (literal) { literal.hidden = isInput; }
if (input) { input.hidden = !isInput; }
if (constant) { constant.hidden = !mode || mode.value !== 'presence_constant'; }
if (technical) { technical.hidden = !isInput; }
if (literalLabel) { literalLabel.textContent = source && source.value === 'legacy' ? 'Сохранённый PHP-код' : 'Значение для сравнения'; }
if (isInput && key && !key.value && trigger && trigger.matches('[data-cf="condition_value_source"], [data-cf="condition_field_id"]')) {
var field = row && row.querySelector('[data-cf="condition_field_id"]');
var option = field && field.options[field.selectedIndex];
key.value = option ? (option.getAttribute('data-cond-alias') || ('field_' + option.value)) : '';
}
if (tag) { tag.textContent = key && key.value ? '[tag:cond:' + key.value.trim() + ']' : '[tag:cond:alias]'; }
if (preview) { preview.textContent = this.conditionValuePreview(row, source, key, mode); }
},
conditionValuePreview: function (row, source, key, mode) {
var field = row && row.querySelector('[data-cf="condition_field_id"]');
var compare = row && row.querySelector('[data-cf="condition_compare"]');
var literal = row && row.querySelector('[data-cf="condition_value"]');
var constant = row && row.querySelector('[data-cf="condition_value_constant"]');
var fieldText = field && field.options[field.selectedIndex] ? field.options[field.selectedIndex].textContent.trim() : 'выбранное поле';
fieldText = fieldText.replace(/^#\d+\s*·\s*/, '').replace(/\s*·\s*#\d+$/, '');
var compareLabels = {'==':'равно','!=':'не равно','%%':'содержит','%':'начинается с','--':'не содержит','!-':'не начинается с','>':'больше','<':'меньше','>=':'больше или равно','<=':'меньше или равно','N>':'больше','N<':'меньше','N>=':'больше или равно','N<=':'меньше или равно','IN=':'входит в список','NOTIN=':'не входит в список','ANY':'совпадает с одним из значений','FRE':'проверяется свободным условием'};
var relation = compare && compareLabels[compare.value] ? compareLabels[compare.value] : 'сравнивается со значением';
var sourceValue = source ? source.value : 'literal';
if (sourceValue === 'legacy') {
return 'Поле «' + fieldText + '» ' + relation + ' результатом сохранённого PHP-кода.';
}
if (sourceValue !== 'input') {
var fixed = literal ? literal.value.trim() : '';
return 'Поле «' + fieldText + '» ' + relation + ' фиксированное значение' + (fixed ? ' «' + fixed + '».' : '.');
}
var parameter = key && key.value.trim() ? key.value.trim() : 'параметр';
var modeValue = mode ? mode.value : 'string_scalar';
var valueDescription = 'значением параметра «' + parameter + '»';
if (modeValue === 'integer_scalar') { valueDescription = 'целым числом из параметра «' + parameter + '»'; }
else if (modeValue === 'decimal_scalar') { valueDescription = 'десятичным числом из параметра «' + parameter + '»'; }
else if (modeValue === 'string_list' || modeValue === 'integer_list') { valueDescription = 'одним из значений параметра «' + parameter + '»'; }
else if (modeValue === 'integer_range' || modeValue === 'decimal_range') { valueDescription = 'диапазоном min/max из параметра «' + parameter + '»'; }
else if (modeValue === 'integer_member') { valueDescription = 'значение параметра «' + parameter + '» в формате |значение|'; }
else if (modeValue === 'integer_member_list') { valueDescription = 'одним из значений параметра «' + parameter + '» в формате |значение|'; }
else if (modeValue === 'presence_constant') {
var constantValue = constant ? constant.value.trim() : '';
valueDescription = 'значением «' + (constantValue || 'укажите константу') + '», когда параметр «' + parameter + '» заполнен';
}
return 'Поле «' + fieldText + '» ' + relation + ' ' + valueDescription + '. Пустой параметр не участвует в фильтрации.';
},
condSave: function (row) {
var reqId = this.condTable.getAttribute('data-req-id');
var fd = new FormData();
fd.append('cond_id', row.getAttribute('data-cond-id') || '0');
fd.append('condition_group_id', row.closest('[data-cond-body]').getAttribute('data-group-id') || '0');
row.querySelectorAll('[data-cf]').forEach(function (el) {
fd.append(el.getAttribute('data-cf'), el.type === 'checkbox' ? (el.checked ? '1' : '0') : el.value);
});
Adminx.Loader.show();
Adminx.Ajax.post(this.base() + '/requests/' + reqId + '/conditions', fd).then(function (p) {
Adminx.Loader.hide();
var d = p.data || {};
if (d.success) {
if (d.data && d.data.id) { row.setAttribute('data-cond-id', d.data.id); row.removeAttribute('data-cond-new'); }
Adminx.Toast.show(d.message || 'Сохранено', 'success');
} else { Adminx.Toast.show(d.message || 'Ошибка', 'error'); }
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
},
condDelete: function (row) {
var self = this;
var condId = row.getAttribute('data-cond-id') || '0';
if (condId === '0') { row.remove(); this.normalizeEmptyGroups(); return; }
Adminx.Confirm.open({
kind: 'error', title: 'Удалить условие?', confirmLabel: 'Удалить', confirmClass: 'btn-danger',
onConfirm: function () {
Adminx.Loader.show();
Adminx.Ajax.post(self.base() + '/requests/conditions/' + condId + '/delete').then(function (p) {
Adminx.Loader.hide();
var d = p.data || {};
if (d.success) { row.remove(); self.normalizeEmptyGroups(); Adminx.Toast.show(d.message, 'success'); }
else { Adminx.Toast.show(d.message || 'Ошибка', 'error'); }
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
}
});
},
groupOperator: function (button) {
var group = button.closest('[data-cond-group]');
var fd = new FormData();
fd.append('group_id', group.getAttribute('data-group-id'));
fd.append('parent_id', group.getAttribute('data-parent-id') || '0');
fd.append('group_operator', button.getAttribute('data-group-operator'));
var title = group.querySelector(':scope > .req-condition-group-head [data-group-title]');
fd.append('group_title', title ? title.value : '');
this.saveGroup(fd, function () {
group.querySelectorAll(':scope > .req-condition-group-head [data-group-operator]').forEach(function (item) {
item.classList.toggle('is-active', item === button);
});
var hint = group.querySelector(':scope > .req-condition-group-head .req-condition-group-title span:last-child');
if (hint) { hint.textContent = button.getAttribute('data-group-operator') === 'AND' ? 'Должны выполняться все элементы внутри' : 'Достаточно любого элемента внутри'; }
});
},
groupAdd: function (parent) {
var self = this;
var fd = new FormData();
fd.append('parent_id', parent.getAttribute('data-group-id'));
fd.append('group_operator', 'AND');
fd.append('group_title', '');
this.saveGroup(fd, function (data) {
var tpl = document.querySelector('[data-group-tpl]');
if (!tpl || !data.id) { return; }
var group = tpl.content.firstElementChild.cloneNode(true);
var depth = parseInt(parent.style.getPropertyValue('--condition-depth') || '0', 10) + 1;
group.setAttribute('data-group-id', data.id);
group.setAttribute('data-parent-id', parent.getAttribute('data-group-id'));
self.setGroupDepth(group, depth);
group.querySelector('[data-cond-body]').setAttribute('data-group-id', data.id);
parent.querySelector(':scope > [data-group-children]').appendChild(group);
Adminx.Tooltip && Adminx.Tooltip.init && Adminx.Tooltip.init(group);
});
},
groupTitle: function (input) {
var group = input.closest('[data-cond-group]');
var active = group.querySelector(':scope > .req-condition-group-head [data-group-operator].is-active');
var fd = new FormData();
fd.append('group_id', group.getAttribute('data-group-id'));
fd.append('parent_id', group.getAttribute('data-parent-id') || '0');
fd.append('group_operator', active ? active.getAttribute('data-group-operator') : 'AND');
fd.append('group_title', input.value.trim());
this.saveGroup(fd);
},
setGroupDepth: function (group, depth) {
group.style.setProperty('--condition-depth', depth);
var add = group.querySelector(':scope > .req-condition-group-head [data-group-add]');
if (add && depth >= 3) { add.remove(); }
group.querySelectorAll(':scope > [data-group-children] > [data-cond-group]').forEach(function (child) {
Adminx.Requests.setGroupDepth(child, depth + 1);
});
},
normalizeEmptyGroups: function () {
if (!this.condTable) { return; }
this.condTable.querySelectorAll('[data-cond-body]').forEach(function (body) {
var rows = body.querySelectorAll(':scope > tr[data-cond-id]');
var empty = body.querySelector(':scope > [data-cond-empty]');
if (rows.length && empty) { empty.remove(); }
if (!rows.length && !empty) {
var row = document.createElement('tr');
row.setAttribute('data-cond-empty', '');
row.innerHTML = '<td colspan="6" class="ax-empty">В этой группе пока нет условий</td>';
body.appendChild(row);
}
});
},
saveGroup: function (fd, done) {
var reqId = this.condTable.getAttribute('data-req-id');
Adminx.Ajax.post(this.base() + '/requests/' + reqId + '/condition-groups', fd).then(function (p) {
var d = p.data || {};
if (!d.success) { Adminx.Toast.show(d.message || 'Не удалось сохранить группу', 'error'); return; }
if (done) { done(d.data || {}); }
Adminx.Toast.show(d.message || 'Группа сохранена', 'success');
}).catch(function () { Adminx.Toast.show('Ошибка сети', 'error'); });
},
groupDelete: function (group) {
var self = this;
Adminx.Confirm.open({
kind: 'error', title: 'Удалить группу?',
message: 'Условия и вложенные группы будут перенесены на уровень выше.',
confirmLabel: 'Удалить', confirmClass: 'btn-danger',
onConfirm: function () {
var reqId = self.condTable.getAttribute('data-req-id');
var parent = group.parentNode.closest('[data-cond-group]');
Adminx.Ajax.post(self.base() + '/requests/' + reqId + '/condition-groups/' + group.getAttribute('data-group-id') + '/delete').then(function (p) {
var d = p.data || {};
if (!d.success) { Adminx.Toast.show(d.message || 'Не удалось удалить группу', 'error'); return; }
var parentBody = parent.querySelector(':scope > .req-condition-table-wrap [data-cond-body]');
var parentEmpty = parentBody.querySelector(':scope > [data-cond-empty]');
if (parentEmpty) { parentEmpty.remove(); }
group.querySelectorAll(':scope > .req-condition-table-wrap tbody > tr[data-cond-id]').forEach(function (row) { parentBody.appendChild(row); });
var parentChildren = parent.querySelector(':scope > [data-group-children]');
group.querySelectorAll(':scope > [data-group-children] > [data-cond-group]').forEach(function (child) {
child.setAttribute('data-parent-id', parent.getAttribute('data-group-id'));
self.setGroupDepth(child, parseInt(parent.style.getPropertyValue('--condition-depth') || '0', 10) + 1);
parentChildren.appendChild(child);
});
group.remove();
self.normalizeEmptyGroups();
Adminx.Toast.show(d.message, 'success');
}).catch(function () { Adminx.Toast.show('Ошибка сети', 'error'); });
}
});
},
// ---- drag-and-drop сортировка условий ----
dragStart: function (e) {
var handle = e.target.closest('[data-cond-drag]');
if (!handle) { return; }
this.dragRow = handle.closest('tr');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', ''); } catch (err) {}
var row = this.dragRow;
setTimeout(function () { row.classList.add('cond-dragging'); }, 0);
},
dragOver: function (e) {
if (!this.dragRow) { return; }
e.preventDefault();
var row = e.target.closest('tr');
var targetBody = e.target.closest('[data-cond-body]');
if (!targetBody) { return; }
var empty = targetBody.querySelector(':scope > [data-cond-empty]');
if (empty) { empty.remove(); }
if (!row || row === this.dragRow || !row.hasAttribute('data-cond-id')) {
targetBody.appendChild(this.dragRow);
return;
}
var rect = row.getBoundingClientRect();
var body = row.parentNode;
if (e.clientY > rect.top + rect.height / 2) { body.insertBefore(this.dragRow, row.nextSibling); }
else { body.insertBefore(this.dragRow, row); }
},
drop: function (e) { if (this.dragRow) { e.preventDefault(); } },
dragEnd: function () {
if (!this.dragRow) { return; }
this.dragRow.classList.remove('cond-dragging');
this.dragRow = null;
this.normalizeEmptyGroups();
this.persistOrder();
},
persistOrder: function () {
var reqId = this.condTable.getAttribute('data-req-id');
var items = [];
this.condTable.querySelectorAll('[data-cond-body] tr[data-cond-id]').forEach(function (r) {
var id = r.getAttribute('data-cond-id');
if (id && id !== '0') { items.push({ id: id, group_id: r.closest('[data-cond-body]').getAttribute('data-group-id') }); }
});
if (!items.length) { return; }
var fd = new FormData();
items.forEach(function (item, index) {
fd.append('items[' + index + '][id]', item.id);
fd.append('items[' + index + '][group_id]', item.group_id);
});
Adminx.Ajax.post(this.base() + '/requests/' + reqId + '/conditions/reorder', fd).then(function (p) {
var d = p.data || {};
Adminx.Toast.show(d.success ? 'Порядок сохранён' : (d.message || 'Ошибка'), d.success ? 'success' : 'error');
}).catch(function () { Adminx.Toast.show('Ошибка сети', 'error'); });
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { Adminx.Requests.init(); });
} else {
Adminx.Requests.init();
}
})(window, document);