341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
/**
|
||
* employeeGroupsManager.js
|
||
* Управление членством сотрудника в группах (добавление, удаление, создание, удаление групп)
|
||
*
|
||
* @module employeeGroupsManager
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
/**
|
||
* Инициализация менеджера групп сотрудника
|
||
* @param {Object} options - Опции конфигурации
|
||
* @param {string} options.employeeId - ID сотрудника
|
||
* @param {string} options.bulkAddUrl - URL для массового добавления
|
||
* @param {string} options.bulkRemoveUrl - URL для массового удаления
|
||
* @param {string} options.createGroupUrl - URL для создания группы
|
||
* @param {string} options.deleteGroupUrl - URL для удаления группы
|
||
*/
|
||
export function initEmployeeGroupsManager(options = {}) {
|
||
// Элементы
|
||
const elems = {
|
||
assignBtn: document.getElementById('empGroupsAssignBtn'),
|
||
removeBtn: document.getElementById('empGroupsRemoveBtn'),
|
||
createBtn: document.getElementById('empGroupsCreateBtn'),
|
||
deleteBtn: document.getElementById('empGroupsDeleteBtn'),
|
||
msg: document.getElementById('empGroupsMsg'),
|
||
createMsg: document.getElementById('empGroupsCreateMsg'),
|
||
deleteMsg: document.getElementById('empGroupsDeleteMsg'),
|
||
groupsList: document.getElementById('empGroupsList'),
|
||
assignedList: document.getElementById('empGroupsAssigned'),
|
||
searchInput: document.getElementById('empGroupsSearch'),
|
||
clearBtn: document.getElementById('empGroupsClear'),
|
||
doneBtn: document.getElementById('empGroupsDone'),
|
||
createNameInput: document.getElementById('empGroupsCreateName'),
|
||
deleteSelect: document.getElementById('empGroupsDeleteSelect')
|
||
};
|
||
|
||
// Если основных элементов нет - функционал не используется на этой странице
|
||
if (!elems.assignBtn && !elems.groupsList) {
|
||
console.log('employeeGroupsManager: skip (no elements)');
|
||
return null;
|
||
}
|
||
|
||
console.log('employeeGroupsManager: initializing with elements:', {
|
||
assignBtn: !!elems.assignBtn,
|
||
groupsList: !!elems.groupsList,
|
||
assignedList: !!elems.assignedList
|
||
});
|
||
|
||
// Конфигурация из data-атрибутов блока
|
||
const block = document.getElementById('empGroupsBlock');
|
||
const config = {
|
||
bulkUrl: options.bulkUrl || block?.dataset.bulkUrl || '',
|
||
createGroupUrl: options.createGroupUrl || block?.dataset.createUrl || '',
|
||
deleteGroupUrl: options.deleteGroupUrl || block?.dataset.deleteUrl || ''
|
||
};
|
||
|
||
if (!config.bulkUrl) {
|
||
console.warn('employeeGroupsManager: bulkUrl not configured');
|
||
}
|
||
|
||
// ===== Helpers =====
|
||
|
||
function busy(btn, on) {
|
||
if (!btn) return;
|
||
btn.disabled = on;
|
||
const spinner = btn.querySelector('.spinner-border');
|
||
if (spinner) {
|
||
spinner.classList.toggle('d-none', !on);
|
||
}
|
||
}
|
||
|
||
function showMsg(text, success = true, targetElem = elems.msg) {
|
||
if (!targetElem) return;
|
||
targetElem.textContent = text;
|
||
targetElem.className = success ? 'alert alert-success mt-2' : 'alert alert-danger mt-2';
|
||
targetElem.classList.remove('d-none');
|
||
setTimeout(() => targetElem.classList.add('d-none'), 4000);
|
||
}
|
||
|
||
// Получить выбранные ID из модального окна
|
||
function selectedIds() {
|
||
const checks = elems.groupsList?.querySelectorAll('.emp-group-check:checked') || [];
|
||
return Array.from(checks).map((ch) => ch.value);
|
||
}
|
||
|
||
// Получить карту имен групп
|
||
function getGroupNames() {
|
||
const nameById = {};
|
||
const checks = elems.groupsList?.querySelectorAll('.emp-group-check') || [];
|
||
checks.forEach((ch) => {
|
||
nameById[ch.value] = ch.dataset.cn || `#${ch.value}`;
|
||
});
|
||
return nameById;
|
||
}
|
||
|
||
// Сбросить выбор в модале
|
||
function clearSelection() {
|
||
const checks = elems.groupsList?.querySelectorAll('.emp-group-check') || [];
|
||
checks.forEach((ch) => (ch.checked = false));
|
||
}
|
||
|
||
async function postJSON(url, body) {
|
||
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
|
||
const resp = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfInput?.value || ''
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (!resp.ok) {
|
||
const txt = await resp.text();
|
||
throw new Error(txt || resp.statusText);
|
||
}
|
||
return await resp.json();
|
||
}
|
||
|
||
// ===== Pills rendering =====
|
||
|
||
function addAssignedBadges(groupIds, groupNames = {}) {
|
||
if (!elems.assignedList || !groupIds.length) return;
|
||
|
||
// Удалим "Нет персональных групп" если есть
|
||
const emptyMsg = elems.assignedList.querySelector('.text-secondary.small');
|
||
if (emptyMsg) emptyMsg.remove();
|
||
|
||
groupIds.forEach((gid) => {
|
||
const existing = elems.assignedList.querySelector(`[data-gid="${gid}"]`);
|
||
if (existing) return; // уже есть
|
||
|
||
const badge = document.createElement('span');
|
||
badge.className = 'badge rounded-pill bg-primary-subtle text-primary-emphasis border';
|
||
badge.dataset.gid = gid;
|
||
badge.textContent = groupNames[gid] || `#${gid}`;
|
||
elems.assignedList.appendChild(badge);
|
||
});
|
||
}
|
||
|
||
function removeAssignedBadges(groupIds) {
|
||
if (!elems.assignedList) return;
|
||
groupIds.forEach((gid) => {
|
||
const badge = elems.assignedList.querySelector(`[data-gid="${gid}"]`);
|
||
if (badge) badge.remove();
|
||
});
|
||
|
||
// Если больше нет badges, покажем "Нет персональных групп"
|
||
if (!elems.assignedList.querySelector('[data-gid]')) {
|
||
const emptyMsg = document.createElement('span');
|
||
emptyMsg.className = 'text-secondary small';
|
||
emptyMsg.textContent = 'Нет персональных групп';
|
||
elems.assignedList.appendChild(emptyMsg);
|
||
}
|
||
}
|
||
|
||
// ===== Assign/Remove groups =====
|
||
|
||
async function assignGroups() {
|
||
const ids = selectedIds();
|
||
if (!ids.length) {
|
||
showMsg('Не выбрано ни одной группы', false);
|
||
return;
|
||
}
|
||
|
||
if (!config.bulkUrl) {
|
||
showMsg('URL не настроен', false);
|
||
return;
|
||
}
|
||
|
||
busy(elems.assignBtn, true);
|
||
try {
|
||
const data = await postJSON(config.bulkUrl, {
|
||
action: 'add',
|
||
group_ids: ids
|
||
});
|
||
showMsg(data.message || 'Группы назначены', true);
|
||
|
||
// Добавляем badges
|
||
const groupNames = getGroupNames();
|
||
addAssignedBadges(ids, groupNames);
|
||
|
||
clearSelection();
|
||
} catch (err) {
|
||
showMsg('Ошибка: ' + err.message, false);
|
||
} finally {
|
||
busy(elems.assignBtn, false);
|
||
}
|
||
}
|
||
|
||
async function removeGroups() {
|
||
const ids = selectedIds();
|
||
if (!ids.length) {
|
||
showMsg('Не выбрано ни одной группы', false);
|
||
return;
|
||
}
|
||
|
||
if (!config.bulkUrl) {
|
||
showMsg('URL не настроен', false);
|
||
return;
|
||
}
|
||
|
||
busy(elems.removeBtn, true);
|
||
try {
|
||
const data = await postJSON(config.bulkUrl, {
|
||
action: 'remove',
|
||
group_ids: ids
|
||
});
|
||
showMsg(data.message || 'Группы сняты', true);
|
||
|
||
removeAssignedBadges(ids);
|
||
clearSelection();
|
||
} catch (err) {
|
||
showMsg('Ошибка: ' + err.message, false);
|
||
} finally {
|
||
busy(elems.removeBtn, false);
|
||
}
|
||
}
|
||
|
||
// ===== Create group =====
|
||
|
||
async function createGroup() {
|
||
const name = elems.createNameInput?.value.trim();
|
||
if (!name) {
|
||
showMsg('Введите имя группы', false, elems.createMsg);
|
||
return;
|
||
}
|
||
if (!config.createGroupUrl) {
|
||
showMsg('URL создания не настроен', false, elems.createMsg);
|
||
return;
|
||
}
|
||
|
||
busy(elems.createBtn, true);
|
||
try {
|
||
const data = await postJSON(config.createGroupUrl, { name });
|
||
showMsg(data.message || 'Группа создана', true, elems.createMsg);
|
||
if (elems.createNameInput) elems.createNameInput.value = '';
|
||
|
||
// Перезагрузим страницу через секунду
|
||
setTimeout(() => location.reload(), 1500);
|
||
} catch (err) {
|
||
showMsg('Ошибка: ' + err.message, false, elems.createMsg);
|
||
} finally {
|
||
busy(elems.createBtn, false);
|
||
}
|
||
}
|
||
|
||
// ===== Delete group =====
|
||
|
||
async function deleteGroup() {
|
||
const gid = elems.deleteSelect?.value;
|
||
if (!gid) {
|
||
showMsg('Выберите группу для удаления', false, elems.deleteMsg);
|
||
return;
|
||
}
|
||
|
||
const groupName = elems.deleteSelect?.selectedOptions[0]?.textContent || '';
|
||
if (!confirm(`Удалить группу "${groupName}"?`)) return;
|
||
|
||
if (!config.deleteGroupUrl) {
|
||
showMsg('URL удаления не настроен', false, elems.deleteMsg);
|
||
return;
|
||
}
|
||
|
||
busy(elems.deleteBtn, true);
|
||
try {
|
||
const data = await postJSON(config.deleteGroupUrl, { group_id: gid });
|
||
showMsg(data.message || 'Группа удалена', true, elems.deleteMsg);
|
||
|
||
// Удалим из селекта
|
||
const option = elems.deleteSelect?.querySelector(`option[value="${gid}"]`);
|
||
if (option) option.remove();
|
||
|
||
// Перезагрузим страницу через секунду
|
||
setTimeout(() => location.reload(), 1500);
|
||
} catch (err) {
|
||
showMsg('Ошибка: ' + err.message, false, elems.deleteMsg);
|
||
} finally {
|
||
busy(elems.deleteBtn, false);
|
||
}
|
||
}
|
||
|
||
// ===== Search in modal =====
|
||
|
||
function filterGroups() {
|
||
const query = elems.searchInput?.value.toLowerCase().trim() || '';
|
||
const items = elems.groupsList?.querySelectorAll('.list-group-item') || [];
|
||
|
||
items.forEach((item) => {
|
||
const text = item.textContent.toLowerCase();
|
||
const match = !query || text.includes(query);
|
||
item.style.display = match ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
// ===== Event listeners =====
|
||
|
||
elems.assignBtn?.addEventListener('click', assignGroups);
|
||
elems.removeBtn?.addEventListener('click', removeGroups);
|
||
elems.createBtn?.addEventListener('click', createGroup);
|
||
elems.deleteBtn?.addEventListener('click', deleteGroup);
|
||
|
||
// Поиск в модале
|
||
elems.searchInput?.addEventListener('input', filterGroups);
|
||
|
||
// Кнопки модала
|
||
elems.clearBtn?.addEventListener('click', clearSelection);
|
||
elems.doneBtn?.addEventListener('click', () => {
|
||
// Просто закрываем модал (Bootstrap сделает это сам через data-bs-dismiss)
|
||
});
|
||
|
||
// Переместим модал в body (важно для правильного z-index)
|
||
const modal = document.getElementById('empGroupsPickerModal');
|
||
if (modal) {
|
||
console.log('employeeGroupsManager: moving modal to body, current parent:', modal.parentNode.nodeName);
|
||
if (modal.parentNode !== document.body) {
|
||
document.body.appendChild(modal);
|
||
console.log('employeeGroupsManager: modal moved to body');
|
||
} else {
|
||
console.log('employeeGroupsManager: modal already in body');
|
||
}
|
||
// Убедимся, что у модала правильные атрибуты
|
||
if (!modal.hasAttribute('data-bs-backdrop')) {
|
||
modal.setAttribute('data-bs-backdrop', 'true');
|
||
}
|
||
if (!modal.hasAttribute('data-bs-keyboard')) {
|
||
modal.setAttribute('data-bs-keyboard', 'true');
|
||
}
|
||
} else {
|
||
console.warn('employeeGroupsManager: modal #empGroupsPickerModal not found');
|
||
}
|
||
|
||
// Публичный API
|
||
return {
|
||
assign: assignGroups,
|
||
remove: removeGroups,
|
||
createGroup,
|
||
deleteGroup,
|
||
refresh: () => location.reload()
|
||
};
|
||
}
|