From f5f4f12ff14bae7d94913c21bdd939073ab17449 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Thu, 26 Mar 2026 17:20:04 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BE=D0=B7=D0=BD=D0=B0=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 39 +++++++++++++- public/main.js | 6 +++ public/nav-task-actions.js | 11 +++- public/reports.js | 3 +- public/style.css | 4 ++ public/tasks.js | 105 +++++++++++++++++++++++++++++++++++- public/ui.js | 9 ++-- public/users.js | 84 +++++++++++++++++++++++++++++ task-endpoints.js | 106 +++++++++++++++++++++++++++++++++++++ 9 files changed, 360 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index 8344778..b165c98 100644 --- a/public/index.html +++ b/public/index.html @@ -108,6 +108,7 @@ + @@ -162,6 +163,7 @@
+ @@ -522,7 +524,42 @@
- +
Загрузка Канбан-доски...
diff --git a/public/main.js b/public/main.js index e2468ea..f9f4ae4 100644 --- a/public/main.js +++ b/public/main.js @@ -85,6 +85,12 @@ function setupEventListeners() { notificationForm._hasSubmitListener = true; } + // Ознакомление + const acquaintanceForm = document.getElementById('acquaintance-task-form'); + if (acquaintanceForm && !acquaintanceForm._hasSubmitListener) { + acquaintanceForm.addEventListener('submit', createAcquaintanceTask); + acquaintanceForm._hasSubmitListener = true; + } // Инициализация загрузки файлов initializeFileUploads(); } diff --git a/public/nav-task-actions.js b/public/nav-task-actions.js index 7de4d56..2979ed1 100644 --- a/public/nav-task-actions.js +++ b/public/nav-task-actions.js @@ -131,7 +131,16 @@ if (currentUser && (currentUser.role === 'admin' || (currentUser.role === 'tasks } } } - + + // Кнопка "Создать ознакомление" для админов и tasks + if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks')) { + actions.push({ + label: '📖 Создать ознакомление', + handler: () => openAcquaintanceModal(task.id), + primary: true // можно отнести к админским или отдельной категории + }); + } + // Доработка и изменение срока для необычных задач (исполнитель) if (!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser?.id)) { if (typeof openReworkModal === 'function') { actions.push({ label: '🔄 Доработка', handler: () => openReworkModal(taskId),primary_task: true}); diff --git a/public/reports.js b/public/reports.js index 5dbd66e..eca4ce1 100644 --- a/public/reports.js +++ b/public/reports.js @@ -27,7 +27,8 @@ const TASK_TYPE_OPTIONS = [ { value: 'speech_therapist', label: 'Логопед' }, { value: 'hr', label: 'Диспетчер расписания' }, { value: 'certificate', label: 'Справка' }, - { value: 'e_journal', label: 'Эл. журнал' } + { value: 'e_journal', label: 'Эл. журнал' }, + { value: 'acquaintance', label: 'Ознакомление' } ]; // Функция показа секции отчёта diff --git a/public/style.css b/public/style.css index 98b7480..6e9885b 100644 --- a/public/style.css +++ b/public/style.css @@ -5201,4 +5201,8 @@ button.btn-primary { } .btn-reset:hover { background: #5a6268; +} +.task-type-badge.acquaintance { + background: #3498db; /* или любой подходящий цвет */ + color: white; } \ No newline at end of file diff --git a/public/tasks.js b/public/tasks.js index 8e63fea..81e5011 100644 --- a/public/tasks.js +++ b/public/tasks.js @@ -628,6 +628,106 @@ function canUserAddFilesToTask(task) { return false; } +// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ ==================== + +async function openAcquaintanceModal(taskId) { + try { + const response = await fetch(`/api/tasks/${taskId}`); + if (!response.ok) throw new Error('Ошибка загрузки задачи'); + const task = await response.json(); + + // Заполняем модальное окно + document.getElementById('acquaintance-original-task-id').value = task.id; + document.getElementById('acquaintance-original-title').innerHTML = ` + №${task.id} ${task.title}
+ Автор: ${task.creator_name} + `; + + // Устанавливаем дату выполнения по умолчанию (завтра) + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + document.getElementById('acquaintance-due-date').value = tomorrow.toISOString().split('T')[0]; + document.getElementById('acquaintance-due-time').value = '19:00'; + + // Загружаем пользователей для выбора автора + await loadUsers(); // гарантируем, что users загружены + renderAcquaintanceAuthorsChecklist(users); + + // Информация об исполнителе (текущий пользователь) + const executorInfo = document.getElementById('acquaintance-executor-info'); + if (executorInfo) { + executorInfo.innerHTML = `Исполнитель: ${currentUser.name} (${currentUser.login})`; + } + + // Отображаем модальное окно + document.getElementById('acquaintance-task-modal').style.display = 'block'; + } catch (error) { + console.error('Ошибка открытия модального окна ознакомления:', error); + alert('Не удалось загрузить задачу'); + } +} + +function closeAcquaintanceModal() { + const modal = document.getElementById('acquaintance-task-modal'); + if (modal) modal.style.display = 'none'; + + const authorSearch = document.getElementById('acquaintance-author-search'); + if (authorSearch) authorSearch.value = ''; + + const userSearch = document.getElementById('acquaintance-user-search'); + if (userSearch) userSearch.value = ''; + + acquaintanceSelectedUsers = []; + acquaintanceSelectedAuthor = null; +} + +async function createAcquaintanceTask(event) { + event.preventDefault(); + + const originalTaskId = document.getElementById('acquaintance-original-task-id').value; + const dueDate = document.getElementById('acquaintance-due-date').value; + const dueTime = document.getElementById('acquaintance-due-time').value; + const fullDueDateTime = `${dueDate}T${dueTime}:00`; + const comment = document.getElementById('acquaintance-comment').value.trim(); + const assignedUserIds = [currentUser.id]; // исполнитель – текущий пользователь + const creatorId = document.querySelector('input[name="acquaintance-author"]:checked')?.value; + + if (!creatorId) { + alert('Выберите автора задачи'); + return; + } + + try { + const response = await fetch('/api/tasks/acquaintance', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + originalTaskId, + dueDate: fullDueDateTime, + assignedUserIds, + creatorId, + comment + }) + }); + + const result = await response.json(); + + if (response.ok) { + alert('Задача ознакомления успешно создана!'); + closeAcquaintanceModal(); + loadTasks(); + loadActivityLogs(); + } else { + alert(`Ошибка: ${result.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('Ошибка создания задачи ознакомления:', error); + alert('Сетевая ошибка'); + } +} + // Добавляем отладочную функцию function debugDocumentFields() { console.log('=== ОТЛАДКА ПОЛЕЙ ДОКУМЕНТОВ ==='); @@ -647,4 +747,7 @@ function debugDocumentFields() { window.debugDocumentFields = debugDocumentFields; window.loadTasks = loadTasks; window.updateAssignment = updateAssignment; -window.renderTasksForActiveSection = renderTasksForActiveSection; \ No newline at end of file +window.renderTasksForActiveSection = renderTasksForActiveSection; +window.openAcquaintanceModal = openAcquaintanceModal; +window.closeAcquaintanceModal = closeAcquaintanceModal; +window.createAcquaintanceTask = createAcquaintanceTask; \ No newline at end of file diff --git a/public/ui.js b/public/ui.js index a6ec226..43e8bfa 100644 --- a/public/ui.js +++ b/public/ui.js @@ -1406,7 +1406,8 @@ function getTaskTypeName(type) { 'Social_educator': 'повод для обращения к cоциальному педагогу: ', 'hr': 'вопрос к кадровой службе', 'certificate': 'тип необходимой справки', - 'e_journal': 'информацию для доступа к журналу' + 'e_journal': 'информацию для доступа к журналу', + 'acquaintance': 'Ознакомление с документом' }; return typeNames[type] || 'задачу'; } @@ -1422,7 +1423,8 @@ function getTaskTypeDisplayName(type) { 'Social_educator': 'Социальный педагог: ', 'hr': 'Кадры', 'certificate': 'Справка', - 'e_journal': 'Эл. журнал' + 'e_journal': 'Эл. журнал', + 'acquaintance': 'Ознакомление' }; return typeNames[type] || type; } @@ -1438,7 +1440,8 @@ function getTaskTypeIcon(type) { 'Social_educator': 'fas fa-brain', 'hr': 'fas fa-users', 'certificate': 'fas fa-file-certificate', - 'e_journal': 'fas fa-book' + 'e_journal': 'fas fa-book', + 'acquaintance': 'fas fa-eye' }; return icons[type] || 'fas fa-tasks'; } diff --git a/public/users.js b/public/users.js index 5db3e6d..d165e85 100644 --- a/public/users.js +++ b/public/users.js @@ -7,6 +7,8 @@ let filteredUsers = []; let selectedUsers = []; let editSelectedUsers = []; let copySelectedUsers = []; +let acquaintanceSelectedUsers = []; // исполнители для ознакомления +let acquaintanceSelectedAuthor = null; // выбранный автор для ознакомления // Переменные для пользовательских списков let userLists = []; @@ -662,6 +664,76 @@ async function saveListFromModal() { 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 => ` +
+ +
+ `).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 => ` +
+ +
+ `).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) { @@ -705,6 +777,18 @@ 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; diff --git a/task-endpoints.js b/task-endpoints.js index 5ae6f40..9a4dd96 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -2056,6 +2056,112 @@ app.post('/api/tasks/:taskId/copy', requireAuth, checkTaskCreationTimeout, (req, ); }); }); +// API для создания задачи ознакомления с указанием автора +app.post('/api/tasks/acquaintance', requireAuth, checkTaskCreationTimeout, (req, res) => { + const { originalTaskId, dueDate, assignedUserIds, creatorId, comment } = req.body; + const currentUserId = req.session.user.id; + const currentUser = req.session.user; + + // Валидация + if (!originalTaskId) return res.status(400).json({ error: 'Не указана исходная задача' }); + if (!dueDate) return res.status(400).json({ error: 'Дата выполнения обязательна' }); + if (!assignedUserIds || assignedUserIds.length === 0) return res.status(400).json({ error: 'Не указаны исполнители' }); + if (!creatorId) return res.status(400).json({ error: 'Не указан автор задачи' }); + + // Проверка прав: только admin и tasks могут создавать задачу от имени другого пользователя + if (currentUser.role !== 'admin' && currentUser.role !== 'tasks') { + return res.status(403).json({ error: 'Недостаточно прав для создания задачи от имени другого пользователя' }); + } + + // Исполнителем должен быть только текущий пользователь + if (assignedUserIds.length !== 1 || parseInt(assignedUserIds[0]) !== currentUserId) { + return res.status(400).json({ error: 'Исполнителем может быть только текущий пользователь' }); + } + + if (!req.taskCreationCheckPassed) { + return res.status(429).json({ error: 'Слишком частое создание задач' }); + } + + // Получаем исходную задачу с именем автора + db.get(` + SELECT t.*, u.name as creator_name + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + WHERE t.id = ? + `, [originalTaskId], (err, originalTask) => { + if (err || !originalTask) { + return res.status(404).json({ error: 'Исходная задача не найдена' }); + } + + const startDate = new Date().toISOString(); + const taskType = 'acquaintance'; + + const newTitle = `Ознакомление: ${originalTask.title}`; + const newDescription = `Задача для ознакомления + +Оригинал: задача №${originalTask.id} "${originalTask.title}" +Автор оригинала: ${originalTask.creator_name || 'Неизвестно'} +${comment ? `Комментарий: ${comment}\n` : ''} +--- +${originalTask.description || ''}`; + + // Создаём задачу с указанным автором (creatorId) + db.run( + `INSERT INTO tasks + (title, description, created_by, original_task_id, start_date, due_date, task_type) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate, taskType], + function(err) { + if (err) { + console.error('❌ Ошибка создания задачи ознакомления:', err); + return res.status(500).json({ error: err.message }); + } + + const newTaskId = this.lastID; + + // Обновляем таймаут создания + if (typeof updateLastTaskCreationTime === 'function') { + updateLastTaskCreationTime(currentUserId); + } + + // Сохраняем метаданные + if (typeof saveTaskMetadata === 'function') { + saveTaskMetadata(newTaskId, newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate); + } + + // Назначаем исполнителя (текущий пользователь) + db.run( + `INSERT INTO task_assignments (task_id, user_id, start_date, due_date, status) + VALUES (?, ?, ?, ?, 'assigned')`, + [newTaskId, currentUserId, startDate, dueDate], + function(err) { + if (err) { + console.error('❌ Ошибка назначения исполнителя:', err); + db.run("DELETE FROM tasks WHERE id = ?", [newTaskId]); + return res.status(500).json({ error: err.message }); + } + + if (typeof logActivity === 'function') { + logActivity(newTaskId, currentUserId, 'TASK_CREATED', + `Создана задача ознакомления: ${newTitle} (автор: ${creatorId}, исполнитель: ${currentUserId})`); + } + + if (typeof sendTaskNotifications === 'function') { + sendTaskNotifications('created', newTaskId, newTitle, newDescription, currentUserId); + } + + res.json({ + success: true, + taskId: newTaskId, + message: 'Задача ознакомления успешно создана', + timeoutInfo: { nextAllowedIn: 15 } + }); + } + ); + } + ); + }); +}); } module.exports = { setupTaskEndpoints, getApproverUsers }; \ No newline at end of file