394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
/**
|
|
* @fileoverview Role Manager - Управление ролями и правами отделов
|
|
* Включает:
|
|
* - Автокомплит для выбора роли
|
|
* - Управление правами (permissions) через чипы
|
|
* - Создание/редактирование/удаление ролей
|
|
* @module components/roleManager
|
|
*/
|
|
|
|
import { esc, norm } from '../utils/stringUtils.js';
|
|
|
|
/**
|
|
* Инициализирует менеджер ролей отдела
|
|
* @returns {Object|null} API компонента или null если элементы не найдены
|
|
*/
|
|
export function initRoleManager() {
|
|
// Основные элементы
|
|
const editForm = document.getElementById('roleEditForm');
|
|
const roleNameInp = document.getElementById('role_name_text');
|
|
const roleIdHidden = document.getElementById('role_id_hidden');
|
|
const roleIdInput = document.getElementById('edit_role_id');
|
|
const roleSaveBtn = document.getElementById('roleSaveBtn');
|
|
const roleDeleteBtn = document.getElementById('roleDeleteBtn');
|
|
const editChips = document.getElementById('edit_role_perms_chips');
|
|
const editHidden = document.getElementById('edit_role_perms_hidden');
|
|
|
|
const createForm = document.getElementById('roleCreateForm');
|
|
const newRoleName = document.getElementById('new_role_name');
|
|
const newChips = document.getElementById('new_role_perms_chips');
|
|
const newHidden = document.getElementById('new_role_perms_hidden');
|
|
|
|
// Если основных элементов нет, выходим
|
|
if (!editForm || !roleNameInp) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Создаёт элемент чипа для отображения прав
|
|
*/
|
|
function makeChip(text, value) {
|
|
const span = document.createElement('span');
|
|
span.className = 'chip';
|
|
span.dataset.value = value;
|
|
span.textContent = text;
|
|
return span;
|
|
}
|
|
|
|
/**
|
|
* Строит словарь прав из списка checkbox'ов
|
|
*/
|
|
function buildDict(listSelector) {
|
|
const res = new Map();
|
|
document.querySelectorAll(listSelector + ' .list-group-item').forEach(li => {
|
|
const cb = li.querySelector('input[type=checkbox]');
|
|
const name = li.querySelector('.flex-grow-1')?.textContent?.trim();
|
|
const code = li.querySelector('.text-muted')?.textContent?.trim();
|
|
res.set(cb.value, { name, code });
|
|
});
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Отображает чипы выбранных прав
|
|
*/
|
|
function setChips(container, ids, dict) {
|
|
container.innerHTML = '';
|
|
ids.forEach(id => container.appendChild(makeChip((dict.get(id)?.name || id), id)));
|
|
}
|
|
|
|
/**
|
|
* Устанавливает состояние checkbox'ов согласно списку ID
|
|
*/
|
|
function setChecks(listSelector, ids) {
|
|
document.querySelectorAll(listSelector + ' input[type=checkbox]').forEach(cb => {
|
|
cb.checked = ids.includes(cb.value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Синхронизирует скрытые input'ы для отправки формы
|
|
*/
|
|
function syncHidden(containerEl, ids) {
|
|
containerEl.innerHTML = '';
|
|
ids.forEach(id => {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'hidden';
|
|
inp.name = 'permission_ids';
|
|
inp.value = id;
|
|
containerEl.appendChild(inp);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует автокомплит для выбора роли
|
|
*/
|
|
function initRolePicker() {
|
|
const root = document.querySelector('[data-role-picker]');
|
|
if (!root || !roleNameInp) return;
|
|
|
|
const choices = JSON.parse(root.dataset.choices || '[]');
|
|
|
|
// Создаём меню в body для правильного z-index
|
|
const menuLayer = document.createElement('div');
|
|
menuLayer.className = 'dropdown-menu shadow';
|
|
menuLayer.style.position = 'fixed';
|
|
menuLayer.style.zIndex = '2000';
|
|
menuLayer.style.maxHeight = '240px';
|
|
menuLayer.style.overflowY = 'auto';
|
|
document.body.appendChild(menuLayer);
|
|
|
|
/**
|
|
* Позиционирует меню относительно input
|
|
*/
|
|
function placeMenu() {
|
|
const r = roleNameInp.getBoundingClientRect();
|
|
menuLayer.style.left = r.left + 'px';
|
|
menuLayer.style.top = (r.bottom) + 'px';
|
|
menuLayer.style.width = r.width + 'px';
|
|
}
|
|
|
|
/**
|
|
* Отображает список ролей
|
|
*/
|
|
function listItems(arr) {
|
|
if (!arr.length) {
|
|
menuLayer.innerHTML = '<div class="dropdown-item disabled">Ничего не найдено</div>';
|
|
} else {
|
|
menuLayer.innerHTML = arr.map(c => `
|
|
<button type="button" class="dropdown-item"
|
|
data-id="${esc(c.id)}"
|
|
data-name="${esc(c.name)}"
|
|
data-perms="${esc(c.perms || '')}">
|
|
${esc(c.name)}
|
|
</button>
|
|
`).join('');
|
|
}
|
|
placeMenu();
|
|
menuLayer.classList.add('show');
|
|
window.addEventListener('scroll', placeMenu, true);
|
|
window.addEventListener('resize', placeMenu);
|
|
}
|
|
|
|
/**
|
|
* Скрывает меню
|
|
*/
|
|
function hideMenu() {
|
|
menuLayer.classList.remove('show');
|
|
window.removeEventListener('scroll', placeMenu, true);
|
|
window.removeEventListener('resize', placeMenu);
|
|
}
|
|
|
|
/**
|
|
* Фильтрует роли по запросу
|
|
*/
|
|
function filter(q) {
|
|
const s = norm(q);
|
|
const res = s
|
|
? choices.filter(c => norm(c.name).includes(s)).slice(0, 10)
|
|
: choices.slice(0, 10);
|
|
listItems(res);
|
|
}
|
|
|
|
// События input для ролей
|
|
roleNameInp.addEventListener('focus', () => filter(roleNameInp.value));
|
|
|
|
roleNameInp.addEventListener('input', () => {
|
|
roleIdHidden.value = '';
|
|
roleIdInput.value = '';
|
|
roleSaveBtn.disabled = true;
|
|
roleDeleteBtn.disabled = true;
|
|
setChips(editChips, [], buildDict('#permListEdit'));
|
|
syncHidden(editHidden, []);
|
|
document.querySelectorAll('#permListEdit input[type=checkbox]').forEach(cb => cb.checked = false);
|
|
filter(roleNameInp.value);
|
|
});
|
|
|
|
roleNameInp.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
const first = menuLayer.querySelector('.dropdown-item:not(.disabled)');
|
|
if (menuLayer.classList.contains('show') && first) {
|
|
e.preventDefault();
|
|
first.click();
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
hideMenu();
|
|
}
|
|
});
|
|
|
|
// Выбор роли из списка
|
|
menuLayer.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.dropdown-item');
|
|
if (!btn || btn.classList.contains('disabled')) return;
|
|
|
|
const id = btn.dataset.id || '';
|
|
const name = btn.dataset.name || '';
|
|
const permsCsv = btn.dataset.perms || '';
|
|
|
|
roleNameInp.value = name;
|
|
roleIdHidden.value = id;
|
|
roleIdInput.value = id;
|
|
roleSaveBtn.disabled = !id;
|
|
roleDeleteBtn.disabled = !id;
|
|
|
|
const ids = (permsCsv ? permsCsv.split(',').map(s => s.trim()).filter(Boolean) : []);
|
|
setChecks('#permListEdit', ids);
|
|
setChips(editChips, ids, buildDict('#permListEdit'));
|
|
syncHidden(editHidden, ids);
|
|
|
|
hideMenu();
|
|
});
|
|
|
|
// Закрытие при клике вне
|
|
document.addEventListener('click', (e) => {
|
|
if (!roleNameInp.contains(e.target) && !menuLayer.contains(e.target)) {
|
|
hideMenu();
|
|
}
|
|
});
|
|
|
|
// Закрытие при открытии модалки
|
|
document.addEventListener('show.bs.modal', hideMenu);
|
|
}
|
|
|
|
/**
|
|
* Инициализирует переключатель формы создания роли
|
|
*/
|
|
function initToggleNewRole() {
|
|
document.addEventListener('click', function(e) {
|
|
const btn = e.target.closest('#toggleNewRole');
|
|
if (!btn) return;
|
|
|
|
const targetSel = btn.getAttribute('data-toggle-target') || '#roleCreateForm';
|
|
const frm = document.querySelector(targetSel);
|
|
if (!frm) return;
|
|
|
|
const willShow = frm.classList.contains('d-none');
|
|
|
|
if (willShow) {
|
|
frm.classList.remove('d-none');
|
|
// Копируем имя роли если оно было введено в поиске
|
|
if (roleNameInp?.value && !roleIdHidden?.value && newRoleName) {
|
|
newRoleName.value = roleNameInp.value.trim();
|
|
}
|
|
newRoleName?.focus();
|
|
} else {
|
|
frm.classList.add('d-none');
|
|
frm.reset?.();
|
|
if (newChips) newChips.innerHTML = '';
|
|
if (newHidden) newHidden.innerHTML = '';
|
|
document.querySelectorAll('#permListCreate input[type=checkbox]')
|
|
.forEach(cb => cb.checked = false);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует кнопку "Применить" для прав редактирования
|
|
*/
|
|
function initApplyPermsEdit() {
|
|
document.getElementById('applyPermsEdit')?.addEventListener('click', function() {
|
|
const ids = Array.from(
|
|
document.querySelectorAll('#permListEdit input[type=checkbox]:checked')
|
|
).map(cb => cb.value);
|
|
|
|
setChips(editChips, ids, buildDict('#permListEdit'));
|
|
syncHidden(editHidden, ids);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует кнопку удаления роли
|
|
*/
|
|
function initRoleDelete() {
|
|
roleDeleteBtn?.addEventListener('click', function() {
|
|
if (!roleIdInput.value) return;
|
|
|
|
if (confirm('Удалить выбранную роль?')) {
|
|
document.getElementById('delete_role_id').value = roleIdInput.value;
|
|
document.getElementById('roleDeleteForm').submit();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует кнопку "Применить" для прав создания
|
|
*/
|
|
function initApplyPermsCreate() {
|
|
document.getElementById('applyPermsCreate')?.addEventListener('click', function() {
|
|
const ids = Array.from(
|
|
document.querySelectorAll('#permListCreate input[type=checkbox]:checked')
|
|
).map(cb => cb.value);
|
|
|
|
setChips(newChips, ids, buildDict('#permListCreate'));
|
|
syncHidden(newHidden, ids);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует синхронизацию при отправке формы создания
|
|
*/
|
|
function initCreateFormSubmit() {
|
|
createForm?.addEventListener('submit', function() {
|
|
const ids = Array.from(
|
|
document.querySelectorAll('#permListCreate input[type=checkbox]:checked')
|
|
).map(cb => cb.value);
|
|
|
|
syncHidden(newHidden, ids);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Инициализирует кнопку "Отмена" для формы создания
|
|
*/
|
|
function initCancelNewRole() {
|
|
document.addEventListener('click', function(e) {
|
|
const btn = e.target.closest('#cancelNewRole');
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
|
|
const frm = document.getElementById('roleCreateForm');
|
|
if (!frm) return;
|
|
|
|
frm.classList.add('d-none');
|
|
frm.reset?.();
|
|
|
|
if (newChips) newChips.innerHTML = '';
|
|
if (newHidden) newHidden.innerHTML = '';
|
|
|
|
document.querySelectorAll('#permListCreate input[type=checkbox]')
|
|
.forEach(cb => cb.checked = false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Перемещает модалки в body для правильного z-index
|
|
*/
|
|
function fixModals() {
|
|
document.querySelectorAll('.modal').forEach(m => {
|
|
if (m.parentElement !== document.body) {
|
|
document.body.appendChild(m);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Инициализируем все компоненты
|
|
initRolePicker();
|
|
initToggleNewRole();
|
|
initApplyPermsEdit();
|
|
initRoleDelete();
|
|
initApplyPermsCreate();
|
|
initCreateFormSubmit();
|
|
initCancelNewRole();
|
|
fixModals();
|
|
|
|
return {
|
|
/**
|
|
* Переинициализирует компонент
|
|
*/
|
|
refresh: () => {
|
|
initRoleManager();
|
|
},
|
|
|
|
/**
|
|
* Получить выбранную роль
|
|
*/
|
|
getSelectedRole: () => ({
|
|
id: roleIdInput?.value || '',
|
|
name: roleNameInp?.value || ''
|
|
}),
|
|
|
|
/**
|
|
* Получить выбранные права редактирования
|
|
*/
|
|
getEditPermissions: () => {
|
|
return Array.from(
|
|
document.querySelectorAll('#permListEdit input[type=checkbox]:checked')
|
|
).map(cb => cb.value);
|
|
},
|
|
|
|
/**
|
|
* Получить выбранные права создания
|
|
*/
|
|
getCreatePermissions: () => {
|
|
return Array.from(
|
|
document.querySelectorAll('#permListCreate input[type=checkbox]:checked')
|
|
).map(cb => cb.value);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Публикуем в window для совместимости
|
|
if (typeof window !== 'undefined') {
|
|
window.initRoleManager = initRoleManager;
|
|
}
|