список пользователя

This commit is contained in:
2026-03-06 13:54:52 +05:00
parent bbab4434bb
commit 37b03dc1b5
5 changed files with 881 additions and 238 deletions

179
api-user-lists.js Normal file
View File

@@ -0,0 +1,179 @@
// api-user-lists.js - API для управления пользовательскими списками
const express = require('express');
const router = express.Router();
module.exports = function(app, db) {
// Middleware для проверки аутентификации
const requireAuth = (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
// GET /api/user/lists получить все списки текущего пользователя
router.get('/api/user/lists', requireAuth, (req, res) => {
const userId = req.session.user.id;
db.all(
'SELECT id, name, user_ids, created_at, updated_at FROM user_lists WHERE user_id = ? ORDER BY created_at DESC',
[userId],
(err, rows) => {
if (err) {
console.error('❌ Ошибка получения списков:', err);
return res.status(500).json({ error: 'Ошибка получения списков' });
}
// Преобразуем user_ids из JSON в массив
const lists = (rows || []).map(row => ({
...row,
user_ids: JSON.parse(row.user_ids || '[]')
}));
res.json(lists);
}
);
});
// POST /api/user/lists создать новый список
router.post('/api/user/lists', requireAuth, (req, res) => {
const userId = req.session.user.id;
const { name, userIds } = req.body;
if (!name || name.trim() === '') {
return res.status(400).json({ error: 'Название списка обязательно' });
}
if (name.length > 35) {
return res.status(400).json({ error: 'Название не должно превышать 35 символов' });
}
if (!Array.isArray(userIds)) {
return res.status(400).json({ error: 'userIds должен быть массивом' });
}
const user_ids_json = JSON.stringify(userIds);
db.run(
'INSERT INTO user_lists (user_id, name, user_ids) VALUES (?, ?, ?)',
[userId, name.trim(), user_ids_json],
function(err) {
if (err) {
console.error('❌ Ошибка создания списка:', err);
return res.status(500).json({ error: 'Ошибка создания списка' });
}
// Возвращаем созданный список
db.get(
'SELECT id, name, user_ids, created_at, updated_at FROM user_lists WHERE id = ?',
[this.lastID],
(err, row) => {
if (err) {
return res.status(500).json({ error: 'Список создан, но ошибка получения' });
}
const newList = {
...row,
user_ids: JSON.parse(row.user_ids || '[]')
};
res.status(201).json(newList);
}
);
}
);
});
// PUT /api/user/lists/:id обновить список
router.put('/api/user/lists/:id', requireAuth, (req, res) => {
const listId = req.params.id;
const userId = req.session.user.id;
const { name, userIds } = req.body;
// Проверяем, принадлежит ли список пользователю
db.get('SELECT id FROM user_lists WHERE id = ? AND user_id = ?', [listId, userId], (err, list) => {
if (err) {
console.error('❌ Ошибка проверки списка:', err);
return res.status(500).json({ error: 'Ошибка доступа' });
}
if (!list) {
return res.status(404).json({ error: 'Список не найден' });
}
const updates = [];
const params = [];
if (name !== undefined) {
if (name.trim() === '') {
return res.status(400).json({ error: 'Название не может быть пустым' });
}
if (name.length > 35) {
return res.status(400).json({ error: 'Название не должно превышать 35 символов' });
}
updates.push('name = ?');
params.push(name.trim());
}
if (userIds !== undefined) {
if (!Array.isArray(userIds)) {
return res.status(400).json({ error: 'userIds должен быть массивом' });
}
updates.push('user_ids = ?');
params.push(JSON.stringify(userIds));
}
if (updates.length === 0) {
return res.status(400).json({ error: 'Нет данных для обновления' });
}
updates.push('updated_at = CURRENT_TIMESTAMP');
params.push(listId);
const query = `UPDATE user_lists SET ${updates.join(', ')} WHERE id = ?`;
db.run(query, params, function(err) {
if (err) {
console.error('❌ Ошибка обновления списка:', err);
return res.status(500).json({ error: 'Ошибка обновления списка' });
}
// Возвращаем обновлённый список
db.get(
'SELECT id, name, user_ids, created_at, updated_at FROM user_lists WHERE id = ?',
[listId],
(err, row) => {
if (err) {
return res.status(500).json({ error: 'Список обновлён, но ошибка получения' });
}
const updatedList = {
...row,
user_ids: JSON.parse(row.user_ids || '[]')
};
res.json(updatedList);
}
);
});
});
});
// DELETE /api/user/lists/:id удалить список
router.delete('/api/user/lists/:id', requireAuth, (req, res) => {
const listId = req.params.id;
const userId = req.session.user.id;
db.run(
'DELETE FROM user_lists WHERE id = ? AND user_id = ?',
[listId, userId],
function(err) {
if (err) {
console.error('❌ Ошибка удаления списка:', err);
return res.status(500).json({ error: 'Ошибка удаления списка' });
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Список не найден' });
}
res.json({ success: true, message: 'Список удалён' });
}
);
});
// Подключаем роутер к приложению
app.use(router);
console.log('✅ API для пользовательских списков подключено');
};

View File

@@ -540,6 +540,16 @@ db.run(`CREATE TABLE IF NOT EXISTS task_chat_reads (
UNIQUE(message_id, user_id),
FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
// Таблица для пользовательских списков
db.run(`CREATE TABLE IF NOT EXISTS user_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
user_ids TEXT NOT NULL, -- JSON массив ID пользователей
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
console.log('✅ Таблица для сообщений чата задач созданы');
// Создаем индексы для улучшения производительности
@@ -1323,6 +1333,16 @@ async function createPostgresTables() {
refusal_reason TEXT
)
`);
await client.query(`
CREATE TABLE IF NOT EXISTS user_lists (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(35) NOT NULL,
user_ids TEXT NOT NULL, -- JSON массив
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('✅ Все таблицы PostgreSQL созданы/проверены');

View File

@@ -4950,3 +4950,123 @@ button.btn-primary {
cursor: default;
pointer-events: none;
}
/* Стили для панели пользовательских списков */
.user-lists-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.user-lists-header h4 {
margin: 0;
font-size: 16px;
color: #2c3e50;
}
.btn-create-list {
background: #27ae60;
color: white;
border: none;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.btn-create-list:hover {
background: #219a52;
}
.user-lists-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 5px;
background: #f8f9fa;
}
.user-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
margin-bottom: 5px;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
transition: background 0.2s;
}
.user-list-item:hover {
background: #f1f3f5;
}
.list-info {
flex: 1;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
overflow: hidden;
}
.list-name {
font-weight: 500;
color: #495057;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.list-count {
font-size: 12px;
color: #6c757d;
background: #e9ecef;
padding: 2px 6px;
border-radius: 12px;
}
.list-actions {
display: flex;
gap: 5px;
flex-shrink: 0;
}
.list-edit-btn, .list-delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 2px;
opacity: 0.7;
transition: opacity 0.2s;
}
.list-edit-btn:hover {
opacity: 1;
color: #f39c12;
}
.list-delete-btn:hover {
opacity: 1;
color: #e74c3c;
}
.no-lists {
text-align: center;
padding: 20px;
color: #6c757d;
font-style: italic;
}
/* Модальное окно для списка */
#list-modal .users-checklist-scroll {
border: 1px solid #ced4da;
border-radius: 6px;
padding: 10px;
background: #fff;
}

View File

@@ -1,36 +1,54 @@
// users.js - Управление пользователями
// users.js - Управление пользователями и пользовательскими списками
let users = [];
let allUsers = [];
let usersLoadingPromise = null;
let filteredUsers = [];
let selectedUsers = [];
let editSelectedUsers = [];
let copySelectedUsers = [];
// добавить переменную для отслеживания загрузки
let isUsersLoading = false;
// Добавьте переменную для хранения групп пользователей
// Переменные для пользовательских списков
let userLists = [];
let isUserListsLoading = false;
let currentEditingListId = null;
// Кэш групп пользователей
let userGroupsCache = {};
let isUsersLoading = false;
// ==================== ЗАГРУЗКА ПОЛЬЗОВАТЕЛЕЙ ====================
async function loadUsers() {
try {
const response = await fetch('/api/users');
const allUsersData = await response.json();
//users = await response.json();
// Сохраняем всех пользователей
allUsers = allUsersData;
// Фильтруем пользователей в зависимости от прав текущего пользователя
users = filterAssignableUsers(allUsersData);
filteredUsers = [...users];
renderUsersChecklist();
renderEditUsersChecklist();
renderCopyUsersChecklist();
populateFilterDropdowns();
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
}
// Если загрузка уже идёт, возвращаем существующий промис
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];
@@ -49,47 +67,16 @@ async function getUserGroups(userId) {
}
}
// Обновите функцию filterAssignableUsers
// ==================== ФИЛЬТРАЦИЯ ПОЛЬЗОВАТЕЛЕЙ ПО ПРАВАМ ====================
function filterAssignableUsers(allUsers, taskType = 'regular') {
if (!currentUser) return [];
// Для задач типа "document" - только пользователи из группы "Секретарь"
if (taskType === 'document') {
return allUsers.filter(async (user) => {
if (user.id === currentUser.id) return false;
// Для задач типа "document" только секретари (асинхронно не получится здесь, но мы фильтруем позже)
// В текущей реализации эта функция вызывается синхронно, поэтому для специальных типов фильтрация будет в filterUsers
// Здесь оставляем базовую фильтрацию по ролям
// Получаем группы пользователя
const groups = await getUserGroups(user.id);
// Проверяем, есть ли группа "Секретарь"
const hasSecretaryGroup = groups.some(group =>
group.name === 'Секретарь' ||
(typeof group === 'string' && group.includes('Секретарь'))
);
return hasSecretaryGroup;
});
}
// Для задач типа "it" - только пользователи из группы "ИТ специалист"
if (taskType === 'it') {
return allUsers.filter(async (user) => {
if (user.id === currentUser.id) return false;
// Получаем группы пользователя
const groups = await getUserGroups(user.id);
// Проверяем, есть ли группа "Секретарь"
const hasSecretaryGroup = groups.some(group =>
group.name === 'ИТ специалист' ||
(typeof group === 'string' && group.includes('ИТ специалист'))
);
return hasSecretaryGroup;
});
}
// Для других типов задач - обычная фильтрация
// Администратор видит всех пользователей
// Администратор видит всех, кроме себя
if (currentUser.role === 'admin') {
return allUsers.filter(user => user.id !== currentUser.id);
}
@@ -108,21 +95,18 @@ function filterAssignableUsers(allUsers, taskType = 'regular') {
user.id !== currentUser.id
);
}
// tasks видит учителей и других tasks
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
);
}
// tasks видит учителей и других tasks
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') &&
@@ -132,25 +116,32 @@ function filterAssignableUsers(allUsers, taskType = 'regular') {
return [];
}
// ==================== ЗАПОЛНЕНИЕ ВЫПАДАЮЩИХ СПИСКОВ ФИЛЬТРОВ ====================
function populateFilterDropdowns() {
const creatorFilter = document.getElementById('creator-filter');
const assigneeFilter = document.getElementById('assignee-filter');
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
if (creatorFilter) {
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
}
if (assigneeFilter) {
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
}
users.forEach(user => {
const creatorOption = document.createElement('option');
creatorOption.value = user.id;
creatorOption.textContent = `${user.name} (${user.login})`;
creatorFilter.appendChild(creatorOption.cloneNode(true));
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name} (${user.login})`;
const assigneeOption = creatorOption.cloneNode(true);
assigneeFilter.appendChild(assigneeOption);
if (creatorFilter) creatorFilter.appendChild(option.cloneNode(true));
if (assigneeFilter) assigneeFilter.appendChild(option.cloneNode(true));
});
}
// Обновите функцию filterUsers с учетом типа задачи
// ==================== ФИЛЬТРАЦИЯ ПРИ ПОИСКЕ (С УЧЁТОМ ТИПА ЗАДАЧИ) ====================
async function filterUsers() {
const search = document.getElementById('user-search')?.value.toLowerCase() || '';
const taskType = document.getElementById('task-type')?.value || 'regular';
@@ -159,19 +150,16 @@ async function filterUsers() {
renderUsersChecklist(); // Показываем загрузку
try {
if (taskType === 'document' || taskType === 'it' || taskType === 'ahch' ||
taskType === 'psychologist' || taskType === 'speech_therapist' || taskType === 'Social_educator' ||
taskType === 'hr' || taskType === 'certificate' || taskType === 'e_journal') {
// Сначала фильтруем по поиску
let tempFiltered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
(user.email && user.email.toLowerCase().includes(search))
);
// Фильтруем по поиску
let tempFiltered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
// Затем проверяем группы
filteredUsers = [];
// Если тип задачи требует специальной группы, фильтруем по группам
const specialTypes = ['document', 'it', 'ahch', 'psychologist', 'speech_therapist', 'Social_educator', 'hr', 'certificate', 'e_journal'];
if (specialTypes.includes(taskType)) {
const groupNames = {
'document': 'Секретарь',
'it': 'ИТ специалист',
@@ -183,27 +171,21 @@ async function filterUsers() {
'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 = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
filteredUsers = tempFiltered;
}
} catch (error) {
console.error('Ошибка фильтрации пользователей:', error);
@@ -215,110 +197,483 @@ async function filterUsers() {
}
async function filterEditUsers() {
const search = document.getElementById('edit-user-search').value.toLowerCase();
const task = tasks.find(t => t.id === document.getElementById('edit-task-id').value);
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 = [];
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') {
// Для задач типа "document" - только секретари
let tempFiltered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
filtered = [];
for (const user of tempFiltered) {
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) {
filtered.push(user);
}
if (hasSecretaryGroup) filteredByGroup.push(user);
}
} else {
filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
filtered = filteredByGroup;
}
renderEditUsersChecklist(filtered);
}
async function filterCopyUsers() {
const search = document.getElementById('copy-user-search').value.toLowerCase();
const taskId = document.getElementById('copy-task-id').value;
const task = tasks.find(t => t.id === taskId);
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 = [];
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') {
// Для задач типа "document" - только секретари
let tempFiltered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
filtered = [];
for (const user of tempFiltered) {
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) {
filtered.push(user);
}
if (hasSecretaryGroup) filteredByGroup.push(user);
}
} else {
filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
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) {
container.innerHTML = '<div class="loading-spinner">⏳ Загрузка пользователей...</div>';
return;
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 (!filteredUsers || filteredUsers.length === 0) {
container.innerHTML = '<div class="no-users">Нет доступных пользователей</div>';
return;
// Правая колонка панель списков пользователя
if (!document.getElementById('user-lists-panel')) {
const panel = document.createElement('div');
panel.id = 'user-lists-panel';
rightCol.appendChild(panel);
}
renderUserListsPanel();
}
container.innerHTML = filteredUsers
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="toggleUserSelection(this, ${user.id})"
${selectedUsers.includes(user.id) ? 'checked' : ''}>
${user.name}
${getUserTypeLabel(user, document.getElementById('task-type')?.value)}
</label>
</div>
`).join('');
<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 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': '(Секретарь)',
@@ -333,58 +688,24 @@ function getUserTypeLabel(user, taskType) {
};
return labels[taskType] || '';
}
function renderEditUsersChecklist(filtered = users) {
const container = document.getElementById('edit-users-checklist');
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})">
${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');
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})">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
// Экспорт функций в глобальную область (для вызова из 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;
function toggleUserSelection(checkbox, userId) {
if (checkbox.checked) {
selectedUsers.push(userId);
} else {
selectedUsers = selectedUsers.filter(id => id !== userId);
}
}
function toggleEditUserSelection(checkbox, userId) {
if (checkbox.checked) {
editSelectedUsers.push(userId);
} else {
editSelectedUsers = editSelectedUsers.filter(id => id !== userId);
}
}
function toggleCopyUserSelection(checkbox, userId) {
if (checkbox.checked) {
copySelectedUsers.push(userId);
} else {
copySelectedUsers = copySelectedUsers.filter(id => id !== userId);
}
}
// Также экспортируем переменные, которые могут понадобиться в других скриптах
window.selectedUsers = selectedUsers;
window.editSelectedUsers = editSelectedUsers;
window.copySelectedUsers = copySelectedUsers;

View File

@@ -7,6 +7,7 @@ const session = require('express-session');
require('dotenv').config();
const cronJobs = require('./cron-jobs');
const userListsAPI = require('./api-user-lists');
// Импортируем модули
const { initializeDatabase, getDb, isInitialized } = require('./database');
@@ -1580,6 +1581,8 @@ initializeServer().then(() => {
// Подключаем API для чата
chatAPI(app, db, upload);
console.log('✅ API для чата задач подключено');
userListsAPI(app, db);
console.log('✅ API для списков пользователей в задачах');
// Запускаем фоновые задачи
setInterval(checkOverdueTasks, 60000);