diff --git a/api-user-lists.js b/api-user-lists.js new file mode 100644 index 0000000..c353995 --- /dev/null +++ b/api-user-lists.js @@ -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 для пользовательских списков подключено'); +}; \ No newline at end of file diff --git a/database.js b/database.js index 3c5cade..da961e7 100644 --- a/database.js +++ b/database.js @@ -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 созданы/проверены'); diff --git a/public/style.css b/public/style.css index 658ac96..ec30ccd 100644 --- a/public/style.css +++ b/public/style.css @@ -4949,4 +4949,124 @@ button.btn-primary { color: #aaa; 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; } \ No newline at end of file diff --git a/public/users.js b/public/users.js index 33e766e..5db3e6d 100644 --- a/public/users.js +++ b/public/users.js @@ -1,45 +1,63 @@ -// 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]; } - + 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]; @@ -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; - - // Получаем группы пользователя - 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; - }); - } - - // Для других типов задач - обычная фильтрация - // Администратор видит всех пользователей + + // Для задач типа "document" – только секретари (асинхронно не получится здесь, но мы фильтруем позже) + // В текущей реализации эта функция вызывается синхронно, поэтому для специальных типов фильтрация будет в filterUsers + // Здесь оставляем базовую фильтрацию по ролям + + // Администратор видит всех, кроме себя if (currentUser.role === 'admin') { return allUsers.filter(user => user.id !== currentUser.id); } @@ -97,81 +84,82 @@ function filterAssignableUsers(allUsers, taskType = 'regular') { return allUsers.filter(user => user.id !== currentUser.id); } if (currentUser.role === 'ithelp') { - return allUsers.filter(user => + 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 => + 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 === 'help') { - return allUsers.filter(user => + 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 => + 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') && + 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'); - - creatorFilter.innerHTML = ''; - assigneeFilter.innerHTML = ''; - + + if (creatorFilter) { + creatorFilter.innerHTML = ''; + } + if (assigneeFilter) { + assigneeFilter.innerHTML = ''; + } + users.forEach(user => { - const creatorOption = document.createElement('option'); - creatorOption.value = user.id; - creatorOption.textContent = `${user.name} (${user.login})`; - creatorFilter.appendChild(creatorOption.cloneNode(true)); - - const assigneeOption = creatorOption.cloneNode(true); - assigneeFilter.appendChild(assigneeOption); + 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)); }); } -// Обновите функцию filterUsers с учетом типа задачи +// ==================== ФИЛЬТРАЦИЯ ПРИ ПОИСКЕ (С УЧЁТОМ ТИПА ЗАДАЧИ) ==================== + async function filterUsers() { const search = document.getElementById('user-search')?.value.toLowerCase() || ''; const taskType = document.getElementById('task-type')?.value || 'regular'; - + isUsersLoading = true; 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.toLowerCase().includes(search) - ); - - // Затем проверяем группы - filteredUsers = []; + // Сначала фильтруем по поиску + 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': 'ИТ специалист', @@ -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 || + 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 === 'Секретарь' || + 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 === 'Секретарь' || + 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 = ` +
+
+
+
+ `; + } + + const leftCol = container.querySelector('.left-column'); + const rightCol = container.querySelector('.right-column'); + + // Левая колонка – чекбоксы пользователей if (isUsersLoading) { - container.innerHTML = '
⏳ Загрузка пользователей...
'; - return; + leftCol.innerHTML = '
⏳ Загрузка пользователей...
'; + } else if (!filteredUsers || filteredUsers.length === 0) { + leftCol.innerHTML = '
Нет доступных пользователей
'; + } else { + leftCol.innerHTML = filteredUsers + .filter(user => user.id !== currentUser?.id) + .map(user => ` +
+ +
+ `).join(''); } - - if (!filteredUsers || filteredUsers.length === 0) { - container.innerHTML = '
Нет доступных пользователей
'; - return; + + // Правая колонка – панель списков пользователя + if (!document.getElementById('user-lists-panel')) { + const panel = document.createElement('div'); + panel.id = 'user-lists-panel'; + rightCol.appendChild(panel); } - - container.innerHTML = filteredUsers + 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 => ` -
- -
- `).join(''); +
+ +
+ `).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 => ` +
+ +
+ `).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 = '
⏳ Загрузка списков...
'; + return; + } + + let html = ` +
+

Мои списки

+ +
+
+ `; + + if (userLists.length === 0) { + html += '

У вас пока нет списков

'; + } else { + userLists.forEach(list => { + const memberCount = list.userIds ? list.userIds.length : 0; + // Экранируем название для безопасного использования в onclick + const listJson = JSON.stringify(list).replace(/"/g, '"'); + html += ` +
+
+ ${escapeHtml(list.name)} + (${memberCount}) +
+
+ + +
+
+ `; + }); + } + + html += '
'; + 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 ` +
+ +
+ `; + }).join(''); + + modal.innerHTML = ` + + `; + + 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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + 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 => ` -
- -
- `).join(''); -} -function renderCopyUsersChecklist(filtered = users) { - const container = document.getElementById('copy-users-checklist'); - container.innerHTML = filtered - .filter(user => user.id !== currentUser.id) - .map(user => ` -
- -
- `).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); - } -} \ No newline at end of file +// Также экспортируем переменные, которые могут понадобиться в других скриптах +window.selectedUsers = selectedUsers; +window.editSelectedUsers = editSelectedUsers; +window.copySelectedUsers = copySelectedUsers; \ No newline at end of file diff --git a/server.js b/server.js index 701adb2..c23c867 100644 --- a/server.js +++ b/server.js @@ -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);