Files
minicrm/public/users.js
2026-03-26 17:20:04 +05:00

795 lines
31 KiB
JavaScript
Raw Permalink 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.
// users.js - Управление пользователями и пользовательскими списками
let users = [];
let allUsers = [];
let usersLoadingPromise = null;
let filteredUsers = [];
let selectedUsers = [];
let editSelectedUsers = [];
let copySelectedUsers = [];
let acquaintanceSelectedUsers = []; // исполнители для ознакомления
let acquaintanceSelectedAuthor = null; // выбранный автор для ознакомления
// Переменные для пользовательских списков
let userLists = [];
let isUserListsLoading = false;
let currentEditingListId = null;
// Кэш групп пользователей
let userGroupsCache = {};
let isUsersLoading = false;
// ==================== ЗАГРУЗКА ПОЛЬЗОВАТЕЛЕЙ ====================
async function loadUsers() {
// Если загрузка уже идёт, возвращаем существующий промис
if (usersLoadingPromise) return usersLoadingPromise;
usersLoadingPromise = (async () => {
try {
const response = await fetch('/api/users');
const allUsersData = await response.json();
allUsers = allUsersData;
users = filterAssignableUsers(allUsersData);
filteredUsers = [...users];
renderUsersChecklist();
renderEditUsersChecklist();
renderCopyUsersChecklist();
populateFilterDropdowns();
// Загружаем пользовательские списки (не ждём)
loadUserLists();
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
} finally {
usersLoadingPromise = null;
}
})();
return usersLoadingPromise;
}
// ==================== ПОЛУЧЕНИЕ ГРУПП ПОЛЬЗОВАТЕЛЯ ====================
async function getUserGroups(userId) {
if (userGroupsCache[userId]) {
return userGroupsCache[userId];
}
try {
const response = await fetch(`/api2/idusers/user/${userId}/groups`);
if (!response.ok) return [];
const groups = await response.json();
userGroupsCache[userId] = groups || [];
return userGroupsCache[userId];
} catch (error) {
console.error('Ошибка получения групп пользователя:', error);
return [];
}
}
// ==================== ФИЛЬТРАЦИЯ ПОЛЬЗОВАТЕЛЕЙ ПО ПРАВАМ ====================
function filterAssignableUsers(allUsers, taskType = 'regular') {
if (!currentUser) return [];
// Для задач типа "document" только секретари (асинхронно не получится здесь, но мы фильтруем позже)
// В текущей реализации эта функция вызывается синхронно, поэтому для специальных типов фильтрация будет в filterUsers
// Здесь оставляем базовую фильтрацию по ролям
// Администратор видит всех, кроме себя
if (currentUser.role === 'admin') {
return allUsers.filter(user => user.id !== currentUser.id);
}
if (currentUser.role === 'secretary') {
return allUsers.filter(user => user.id !== currentUser.id);
}
if (currentUser.role === 'ithelp') {
return allUsers.filter(user =>
(user.role === 'teacher' || user.role === 'tasks' || user.role === 'help' || user.role === 'request' || user.role === 'ithelp') &&
user.id !== currentUser.id
);
}
if (currentUser.role === 'request') {
return allUsers.filter(user =>
(user.role === 'teacher' || user.role === 'tasks' || user.role === 'help' || user.role === 'request' || user.role === 'ithelp') &&
user.id !== currentUser.id
);
}
if (currentUser.role === 'help') {
return allUsers.filter(user =>
(user.role === 'teacher' || user.role === 'tasks' || user.role === 'help' || user.role === 'request' || user.role === 'ithelp') &&
user.id !== currentUser.id
);
}
if (currentUser.role === 'tasks') {
return allUsers.filter(user =>
(user.role === 'teacher' || user.role === 'tasks' || user.role === 'help' || user.role === 'request' || user.role === 'ithelp') &&
user.id !== currentUser.id
);
}
if (currentUser.role === 'teacher') {
return allUsers.filter(user =>
(user.role === 'help' || user.role === 'request' || user.role === 'ithelp') &&
user.id !== currentUser.id
);
}
return [];
}
// ==================== ЗАПОЛНЕНИЕ ВЫПАДАЮЩИХ СПИСКОВ ФИЛЬТРОВ ====================
function populateFilterDropdowns() {
const creatorFilter = document.getElementById('creator-filter');
const assigneeFilter = document.getElementById('assignee-filter');
if (creatorFilter) {
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
}
if (assigneeFilter) {
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
}
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name} (${user.login})`;
if (creatorFilter) creatorFilter.appendChild(option.cloneNode(true));
if (assigneeFilter) assigneeFilter.appendChild(option.cloneNode(true));
});
}
// ==================== ФИЛЬТРАЦИЯ ПРИ ПОИСКЕ (С УЧЁТОМ ТИПА ЗАДАЧИ) ====================
async function filterUsers() {
const search = document.getElementById('user-search')?.value.toLowerCase() || '';
const taskType = document.getElementById('task-type')?.value || 'regular';
isUsersLoading = true;
renderUsersChecklist(); // Показываем загрузку
try {
// Сначала фильтруем по поиску
let tempFiltered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
// Если тип задачи требует специальной группы, фильтруем по группам
const specialTypes = ['document', 'it', 'ahch', 'psychologist', 'speech_therapist', 'Social_educator', 'hr', 'certificate', 'e_journal'];
if (specialTypes.includes(taskType)) {
const groupNames = {
'document': 'Секретарь',
'it': 'ИТ специалист',
'ahch': 'АХЧ',
'psychologist': 'психолог',
'speech_therapist': 'логопед',
'Social_educator': 'Социальный педагог',
'hr': 'Диспетчер',
'certificate': 'Администрация',
'e_journal': 'Админ ЭЖ'
};
const targetGroup = groupNames[taskType];
filteredUsers = [];
for (const user of tempFiltered) {
const groups = await getUserGroups(user.id);
const hasTargetGroup = groups.some(group =>
group.name === targetGroup ||
(typeof group === 'string' && group.includes(targetGroup))
);
if (hasTargetGroup) {
filteredUsers.push(user);
}
}
} else {
filteredUsers = tempFiltered;
}
} catch (error) {
console.error('Ошибка фильтрации пользователей:', error);
filteredUsers = [];
} finally {
isUsersLoading = false;
renderUsersChecklist();
}
}
async function filterEditUsers() {
const search = document.getElementById('edit-user-search')?.value.toLowerCase() || '';
const taskId = document.getElementById('edit-task-id')?.value;
if (!taskId) return;
const task = window.tasks?.find(t => t.id == taskId);
const taskType = task ? task.task_type : 'regular';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
if (taskType === 'document') {
const filteredByGroup = [];
for (const user of filtered) {
const groups = await getUserGroups(user.id);
const hasSecretaryGroup = groups.some(group =>
group.name === 'Секретарь' ||
(typeof group === 'string' && group.includes('Секретарь'))
);
if (hasSecretaryGroup) filteredByGroup.push(user);
}
filtered = filteredByGroup;
}
renderEditUsersChecklist(filtered);
}
async function filterCopyUsers() {
const search = document.getElementById('copy-user-search')?.value.toLowerCase() || '';
const taskId = document.getElementById('copy-task-id')?.value;
if (!taskId) return;
const task = window.tasks?.find(t => t.id == taskId);
const taskType = task ? task.task_type : 'regular';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
if (taskType === 'document') {
const filteredByGroup = [];
for (const user of filtered) {
const groups = await getUserGroups(user.id);
const hasSecretaryGroup = groups.some(group =>
group.name === 'Секретарь' ||
(typeof group === 'string' && group.includes('Секретарь'))
);
if (hasSecretaryGroup) filteredByGroup.push(user);
}
filtered = filteredByGroup;
}
renderCopyUsersChecklist(filtered);
}
// ==================== РЕНДЕРИНГ ЧЕКБОКСОВ ПОЛЬЗОВАТЕЛЕЙ ====================
function renderUsersChecklist() {
const container = document.getElementById('users-checklist');
if (!container) return;
// Создаём структуру с двумя колонками, если её ещё нет
if (!container.querySelector('.users-two-columns')) {
container.innerHTML = `
<div class="users-two-columns" style="display: flex; gap: 20px;">
<div class="left-column" style="flex: 1; min-width: 0;"></div>
<div class="right-column" style="flex: 1; min-width: 0;"></div>
</div>
`;
}
const leftCol = container.querySelector('.left-column');
const rightCol = container.querySelector('.right-column');
// Левая колонка чекбоксы пользователей
if (isUsersLoading) {
leftCol.innerHTML = '<div class="loading-spinner">⏳ Загрузка пользователей...</div>';
} else if (!filteredUsers || filteredUsers.length === 0) {
leftCol.innerHTML = '<div class="no-users">Нет доступных пользователей</div>';
} else {
leftCol.innerHTML = filteredUsers
.filter(user => user.id !== currentUser?.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}"
onchange="toggleUserSelection(this, ${user.id})"
${selectedUsers.includes(user.id) ? 'checked' : ''}>
${escapeHtml(user.name)}
${getUserTypeLabel(user, document.getElementById('task-type')?.value)}
</label>
</div>
`).join('');
}
// Правая колонка панель списков пользователя
if (!document.getElementById('user-lists-panel')) {
const panel = document.createElement('div');
panel.id = 'user-lists-panel';
rightCol.appendChild(panel);
}
renderUserListsPanel();
}
function renderEditUsersChecklist(filtered = users) {
const container = document.getElementById('edit-users-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}"
onchange="toggleEditUserSelection(this, ${user.id})"
${editSelectedUsers.includes(user.id) ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderCopyUsersChecklist(filtered = users) {
const container = document.getElementById('copy-users-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}"
onchange="toggleCopyUserSelection(this, ${user.id})"
${copySelectedUsers.includes(user.id) ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
// ==================== УПРАВЛЕНИЕ ВЫБРАННЫМИ ПОЛЬЗОВАТЕЛЯМИ ====================
function toggleUserSelection(checkbox, userId) {
if (checkbox.checked) {
if (!selectedUsers.includes(userId)) selectedUsers.push(userId);
} else {
selectedUsers = selectedUsers.filter(id => id !== userId);
}
}
function toggleEditUserSelection(checkbox, userId) {
if (checkbox.checked) {
if (!editSelectedUsers.includes(userId)) editSelectedUsers.push(userId);
} else {
editSelectedUsers = editSelectedUsers.filter(id => id !== userId);
}
}
function toggleCopyUserSelection(checkbox, userId) {
if (checkbox.checked) {
if (!copySelectedUsers.includes(userId)) copySelectedUsers.push(userId);
} else {
copySelectedUsers = copySelectedUsers.filter(id => id !== userId);
}
}
// ==================== ФУНКЦИИ ДЛЯ РАБОТЫ СО СПИСКАМИ ПОЛЬЗОВАТЕЛЕЙ ====================
async function loadUserLists() {
if (!currentUser) return;
isUserListsLoading = true;
renderUserListsPanel();
try {
const response = await fetch('/api/user/lists');
if (response.ok) {
const lists = await response.json();
// Сервер уже отдаёт user_ids как массив, просто копируем в userIds
userLists = lists.map(list => ({
...list,
userIds: list.user_ids || [] // предполагаем, что это массив
}));
} else {
console.error('Ошибка загрузки списков:', response.status);
userLists = [];
}
} catch (error) {
console.error('Сетевая ошибка при загрузке списков:', error);
userLists = [];
} finally {
isUserListsLoading = false;
renderUserListsPanel();
}
}
async function saveUserList(listData) {
try {
const response = await fetch('/api/user/lists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(listData)
});
if (response.ok) {
const newList = await response.json();
const transformed = {
...newList,
userIds: newList.user_ids || []
};
userLists.push(transformed);
renderUserListsPanel();
return transformed;
} else {
const err = await response.json();
alert('Ошибка создания списка: ' + (err.error || 'Неизвестная ошибка'));
return null;
}
} catch (error) {
console.error('Сетевая ошибка:', error);
alert('Не удалось сохранить список. Проверьте соединение.');
return null;
}
}
async function updateUserList(listId, listData) {
try {
const response = await fetch(`/api/user/lists/${listId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(listData)
});
if (response.ok) {
const updatedList = await response.json();
const transformed = {
...updatedList,
userIds: updatedList.user_ids || []
};
const index = userLists.findIndex(l => l.id === listId);
if (index !== -1) userLists[index] = transformed;
renderUserListsPanel();
return transformed;
} else {
const err = await response.json();
alert('Ошибка обновления списка: ' + (err.error || 'Неизвестная ошибка'));
return null;
}
} catch (error) {
console.error('Сетевая ошибка:', error);
alert('Не удалось обновить список.');
return null;
}
}
async function deleteUserList(listId) {
if (!confirm('Вы уверены, что хотите удалить этот список?')) return;
try {
const response = await fetch(`/api/user/lists/${listId}`, {
method: 'DELETE'
});
if (response.ok) {
userLists = userLists.filter(l => l.id !== listId);
renderUserListsPanel();
} else {
const err = await response.json();
alert('Ошибка удаления списка: ' + (err.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Сетевая ошибка:', error);
alert('Не удалось удалить список.');
}
}
function applyUserList(list) {
if (!list || !list.userIds || list.userIds.length === 0) return;
// Очищаем текущий выбор
selectedUsers = [];
// Добавляем всех пользователей из списка
list.userIds.forEach(userId => {
if (!selectedUsers.includes(userId)) {
selectedUsers.push(userId);
}
});
// Обновляем состояние чекбоксов в левой колонке
const checkboxes = document.querySelectorAll('#users-checklist .left-column input[type="checkbox"]');
checkboxes.forEach(cb => {
const userId = parseInt(cb.value);
cb.checked = list.userIds.includes(userId);
});
}
function renderUserListsPanel() {
const panel = document.getElementById('user-lists-panel');
if (!panel) return;
if (isUserListsLoading) {
panel.innerHTML = '<div class="loading-spinner">⏳ Загрузка списков...</div>';
return;
}
let html = `
<div class="user-lists-header">
<h4>Мои списки</h4>
<button class="btn-create-list" onclick="openCreateListModal()"> Создать</button>
</div>
<div class="user-lists-container">
`;
if (userLists.length === 0) {
html += '<p class="no-lists">У вас пока нет списков</p>';
} else {
userLists.forEach(list => {
const memberCount = list.userIds ? list.userIds.length : 0;
// Экранируем название для безопасного использования в onclick
const listJson = JSON.stringify(list).replace(/"/g, '&quot;');
html += `
<div class="user-list-item" data-list-id="${list.id}">
<div class="list-info" onclick="applyUserList(${listJson})">
<span class="list-name">${escapeHtml(list.name)}</span>
<span class="list-count">(${memberCount})</span>
</div>
<div class="list-actions">
<button class="list-edit-btn" onclick="event.stopPropagation(); openEditListModal(${list.id})" title="Редактировать">✏️</button>
<button class="list-delete-btn" onclick="event.stopPropagation(); deleteUserList(${list.id})" title="Удалить">🗑️</button>
</div>
</div>
`;
});
}
html += '</div>';
panel.innerHTML = html;
}
function openCreateListModal() {
currentEditingListId = null;
showListModal(null);
}
function openEditListModal(listId) {
const list = userLists.find(l => l.id === listId);
if (list) {
currentEditingListId = listId;
showListModal(list);
}
}
function showListModal(list) {
// Удаляем предыдущее модальное окно, если есть
const existingModal = document.getElementById('list-modal');
if (existingModal) existingModal.remove();
const modal = document.createElement('div');
modal.id = 'list-modal';
modal.className = 'modal';
modal.style.display = 'block';
const title = list ? 'Редактировать список' : 'Создать список';
const listName = list ? list.name : '';
const selectedUserIds = list ? list.userIds || [] : [];
// Генерируем чекбоксы всех пользователей (allUsers)
const usersCheckboxes = allUsers
.filter(user => user.id !== currentUser?.id)
.map(user => {
const checked = selectedUserIds.includes(user.id) ? 'checked' : '';
return `
<div class="checkbox-item">
<label>
<input type="checkbox" class="list-user-checkbox" value="${user.id}" ${checked}>
${escapeHtml(user.name)} (${escapeHtml(user.login)})
</label>
</div>
`;
}).join('');
modal.innerHTML = `
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3>${title}</h3>
<span class="close" onclick="closeListModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="list-name">Название списка (до 35 символов):</label>
<input type="text" id="list-name" maxlength="35" value="${escapeHtml(listName)}" placeholder="Введите название">
</div>
<div class="form-group">
<label>Выберите пользователей:</label>
<div class="user-search-box" style="margin-bottom: 10px;">
<input type="text" id="list-user-search" placeholder="Поиск пользователей..." oninput="filterListUsers()" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div class="users-checklist-scroll" id="list-users-container" style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
${usersCheckboxes}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeListModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="saveListFromModal()">Сохранить</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
function filterListUsers() {
const searchInput = document.getElementById('list-user-search');
if (!searchInput) return;
const searchTerm = searchInput.value.toLowerCase();
const container = document.getElementById('list-users-container');
if (!container) return;
const items = container.querySelectorAll('.checkbox-item');
items.forEach(item => {
const label = item.querySelector('label')?.innerText.toLowerCase() || '';
if (label.includes(searchTerm) || searchTerm === '') {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
}
function closeListModal() {
const modal = document.getElementById('list-modal');
if (modal) {
modal.style.display = 'none';
setTimeout(() => modal.remove(), 300);
}
currentEditingListId = null;
}
async function saveListFromModal() {
const nameInput = document.getElementById('list-name');
const name = nameInput.value.trim();
if (!name) {
alert('Введите название списка');
return;
}
if (name.length > 35) {
alert('Название не должно превышать 35 символов');
return;
}
// Собираем выбранные ID пользователей
const checkboxes = document.querySelectorAll('#list-modal .list-user-checkbox:checked');
const userIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
const listData = { name, userIds };
if (currentEditingListId) {
await updateUserList(currentEditingListId, listData);
} else {
await saveUserList(listData);
}
closeListModal();
}
// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ (выбор исполнителей) ====================
function renderAcquaintanceUsersChecklist(filtered = users) {
const container = document.getElementById('acquaintance-users-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}"
onchange="toggleAcquaintanceUserSelection(this, ${user.id})"
${acquaintanceSelectedUsers.includes(user.id) ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.login})
</label>
</div>
`).join('');
}
function toggleAcquaintanceUserSelection(checkbox, userId) {
if (checkbox.checked) {
if (!acquaintanceSelectedUsers.includes(userId)) acquaintanceSelectedUsers.push(userId);
} else {
acquaintanceSelectedUsers = acquaintanceSelectedUsers.filter(id => id !== userId);
}
}
async function filterAcquaintanceUsers() {
const search = document.getElementById('acquaintance-user-search')?.value.toLowerCase() || '';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
renderAcquaintanceUsersChecklist(filtered);
}
// ==================== ФУНКЦИИ ДЛЯ ВЫБОРА АВТОРА В ЗАДАЧЕ ОЗНАКОМЛЕНИЯ ====================
function renderAcquaintanceAuthorsChecklist(filtered = users) {
const container = document.getElementById('acquaintance-authors-checklist');
if (!container) return;
container.innerHTML = filtered
.filter(user => user.id !== currentUser?.id) // можно разрешить выбирать себя, если нужно
.map(user => `
<div class="checkbox-item">
<label>
<input type="radio" name="acquaintance-author" value="${user.id}"
onchange="toggleAcquaintanceAuthorSelection(this, ${user.id})"
${acquaintanceSelectedAuthor === user.id ? 'checked' : ''}>
${escapeHtml(user.name)} (${user.login})
</label>
</div>
`).join('');
}
function toggleAcquaintanceAuthorSelection(radio, userId) {
acquaintanceSelectedAuthor = radio.checked ? userId : null;
}
async function filterAcquaintanceAuthors() {
const search = document.getElementById('acquaintance-author-search')?.value.toLowerCase() || '';
let filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
renderAcquaintanceAuthorsChecklist(filtered);
}
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getUserTypeLabel(user, taskType) {
const labels = {
'document': '(Секретарь)',
'it': '(ИТ специалист)',
'ahch': '(АХЧ)',
'psychologist': '(Психолог)',
'speech_therapist': '(Логопед)',
'Social_educator': '(Социальный педагог)',
'hr': '(Диспетчер)',
'certificate': '(Администрация)',
'e_journal': '(Админ ЭЖ)'
};
return labels[taskType] || '';
}
// Экспорт функций в глобальную область (для вызова из HTML)
window.loadUsers = loadUsers;
window.filterUsers = filterUsers;
window.filterEditUsers = filterEditUsers;
window.filterCopyUsers = filterCopyUsers;
window.toggleUserSelection = toggleUserSelection;
window.toggleEditUserSelection = toggleEditUserSelection;
window.toggleCopyUserSelection = toggleCopyUserSelection;
window.openCreateListModal = openCreateListModal;
window.openEditListModal = openEditListModal;
window.deleteUserList = deleteUserList;
window.applyUserList = applyUserList;
window.closeListModal = closeListModal;
window.saveListFromModal = saveListFromModal;
window.filterListUsers = filterListUsers;
// Экспорт функций для ознакомления (исполнители)
window.renderAcquaintanceUsersChecklist = renderAcquaintanceUsersChecklist;
window.toggleAcquaintanceUserSelection = toggleAcquaintanceUserSelection;
window.filterAcquaintanceUsers = filterAcquaintanceUsers;
window.acquaintanceSelectedUsers = acquaintanceSelectedUsers;
// Экспорт функций для выбора автора
window.renderAcquaintanceAuthorsChecklist = renderAcquaintanceAuthorsChecklist;
window.toggleAcquaintanceAuthorSelection = toggleAcquaintanceAuthorSelection;
window.filterAcquaintanceAuthors = filterAcquaintanceAuthors;
window.acquaintanceSelectedAuthor = acquaintanceSelectedAuthor;
// Также экспортируем переменные, которые могут понадобиться в других скриптах
window.selectedUsers = selectedUsers;
window.editSelectedUsers = editSelectedUsers;
window.copySelectedUsers = copySelectedUsers;