From 139b53ffbd344ac676a70d8fa196796f23d9861d Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Thu, 12 Feb 2026 19:14:23 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 2 +- public/style.css | 41 +++++++++++ public/ui.js | 173 +++++++++++++++++++++++++++++++++++++++++++--- task-endpoints.js | 77 +++++++++++++++++++++ 4 files changed, 284 insertions(+), 9 deletions(-) diff --git a/public/index.html b/public/index.html index 30e5d0e..6fc03eb 100644 --- a/public/index.html +++ b/public/index.html @@ -28,7 +28,7 @@
-

Управление задачами 0.8

+

Управление задачами 0.9

diff --git a/public/style.css b/public/style.css index c94bc0d..28fa2bf 100644 --- a/public/style.css +++ b/public/style.css @@ -4134,4 +4134,45 @@ button.btn-primary { opacity: 0.3; pointer-events: none; user-select: none; +} +/* Стили для модального окна доработки */ +#rework-assignment-modal .modal-content { + max-width: 550px; + border-radius: 8px; +} + +#rework-assignment-modal textarea { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + resize: vertical; + font-family: inherit; + font-size: 14px; +} + +#rework-assignment-modal textarea:focus { + border-color: #f39c12; + outline: none; + box-shadow: 0 0 5px rgba(243, 156, 18, 0.3); +} + +#rework-assignment-modal .btn-warning { + background-color: #f39c12; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +#rework-assignment-modal .btn-warning:hover { + background-color: #e67e22; +} + +#rework-assignment-modal .btn-warning:disabled { + background-color: #f1c40f; + cursor: not-allowed; + opacity: 0.7; } \ No newline at end of file diff --git a/public/ui.js b/public/ui.js index 8119fa3..ab96bd0 100644 --- a/public/ui.js +++ b/public/ui.js @@ -145,8 +145,7 @@ ${task.assignments && task.assignments.length > 0 ? `; }).join(''); } - -// Улучшенная функция рендеринга списка исполнителей с фильтрацией +// Функция для рендеринга списка исполнителей с фильтрацией function renderAssignmentList(assignments, taskId, canEdit) { if (!assignments || assignments.length === 0) { return '
Не назначены
'; @@ -170,6 +169,157 @@ function renderAssignmentList(assignments, taskId, canEdit) { `; } +// Функция для открытия модального окна доработки для конкретного исполнителя +function openReworkAssignmentModal(taskId, userId, userName) { + const task = tasks.find(t => t.id === taskId); + if (!task) { + alert('Задача не найдена'); + return; + } + + // Проверяем права (только автор задачи или администратор) + if (parseInt(task.created_by) !== currentUser.id && currentUser.role !== 'admin') { + alert('Только автор задачи может отправлять на доработку'); + return; + } + + // Удаляем предыдущее модальное окно, если оно есть + const existingModal = document.getElementById('rework-assignment-modal'); + if (existingModal) { + existingModal.remove(); + } + + // Создаем модальное окно + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.id = 'rework-assignment-modal'; + modal.style.display = 'block'; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + // Фокусируемся на textarea + const textarea = document.getElementById(`rework-comment-${taskId}-${userId}`); + if (textarea) { + setTimeout(() => textarea.focus(), 100); + } +} + +// Вспомогательная функция для экранирования HTML +function escapeHtml(text) { + if (!text) return ''; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// Функция для отправки на доработку конкретного исполнителя +async function submitReworkAssignment(taskId, userId) { + // Получаем textarea по уникальному ID + const textarea = document.getElementById(`rework-comment-${taskId}-${userId}`); + + if (!textarea) { + alert('Ошибка: поле комментария не найдено'); + return; + } + + const comment = textarea.value.trim(); + + // Детальная отладка + console.log('=== ОТПРАВКА НА ДОРАБОТКУ ==='); + console.log('Task ID:', taskId); + console.log('User ID:', userId); + console.log('Comment length:', comment.length); + console.log('Comment text:', comment); + console.log('============================'); + + if (!comment) { + alert('Пожалуйста, укажите комментарий к доработке'); + textarea.style.border = '2px solid red'; + textarea.focus(); + return; + } + + // Блокируем кнопку отправки + const submitBtn = document.querySelector(`#rework-assignment-modal .btn-warning`); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = '⏳ Отправка...'; + } + + try { + const response = await fetch(`/api/tasks/${taskId}/rework-assignment/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + comment: comment // Отправляем явно + }) + }); + + const data = await response.json(); + + if (response.ok) { + alert('✅ Исполнитель отправлен на доработку'); + closeReworkAssignmentModal(); + // Перезагружаем задачи + if (typeof loadTasks === 'function') { + loadTasks(); + } else { + location.reload(); + } + } else { + alert(`❌ Ошибка: ${data.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('❌ Ошибка:', error); + alert('Сетевая ошибка при отправке на доработку: ' + error.message); + } finally { + // Разблокируем кнопку + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = '🔄 Отправить на доработку'; + } + } +} +// Функция для закрытия модального окна доработки +function closeReworkAssignmentModal() { + const modal = document.getElementById('rework-assignment-modal'); + if (modal) { + modal.style.display = 'none'; + setTimeout(() => { + modal.remove(); + }, 300); + } +} + // Функция для фильтрации исполнителей в конкретной задаче function filterAssignments(taskId) { const filterInput = document.querySelector(`.assignment-filter-input[data-task-id="${taskId}"]`); @@ -238,6 +388,7 @@ function getTimeLeftInfo(task) { return null; } +// Функция для рендеринга одного исполнителя function renderAssignment(assignment, taskId, canEdit) { const statusClass = getStatusClass(assignment.status); const isCurrentUser = assignment.user_id === currentUser.id; @@ -246,6 +397,10 @@ function renderAssignment(assignment, taskId, canEdit) { const timeLeftInfo = getAssignmentTimeLeftInfo(assignment); + // Проверяем, является ли текущий пользователь автором задачи + const task = tasks.find(t => t.id === taskId); + const isTaskCreator = task && parseInt(task.created_by) === currentUser.id; + return `
@@ -266,12 +421,14 @@ function renderAssignment(assignment, taskId, canEdit) { ` : ''}
- ${isCurrentUser && assignment.status === 'assigned' ? - `` : ''} - ${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ? - `` : ''} - ${canEdit ? - `` : ''} +${isCurrentUser && assignment.status === 'assigned' ? +`` : ''} +${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ? +`` : ''} +${isTaskCreator && assignment.status !== 'assigned' ? +`` : ''} +${canEdit ? +`` : ''}
`; diff --git a/task-endpoints.js b/task-endpoints.js index 086cd66..b86daed 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -1269,6 +1269,83 @@ app.get('/api/document-approval-tasks', requireAuth, (req, res) => { }); }); }); + // API для отправки конкретного исполнителя на доработку +app.put('/api/tasks/:taskId/rework-assignment/:userId', requireAuth, (req, res) => { + const { taskId, userId } = req.params; + const { comment } = req.body; + const currentUserId = req.session.user.id; + + if (!comment) { + return res.status(400).json({ error: 'Комментарий к доработке обязателен' }); + } + + db.get("SELECT created_by, status FROM tasks WHERE id = ?", [taskId], (err, task) => { + if (err || !task) { + return res.status(404).json({ error: 'Задача не найдена' }); + } + + // Проверяем права: автор задачи или администратор + if (req.session.user.role !== 'admin' && parseInt(task.created_by) !== currentUserId) { + return res.status(403).json({ error: 'Только автор задачи может отправлять на доработку' }); + } + + db.serialize(() => { + // Обновляем статус конкретного исполнителя + db.run( + `UPDATE task_assignments + SET status = 'rework', + rework_comment = ?, + updated_at = CURRENT_TIMESTAMP + WHERE task_id = ? AND user_id = ?`, + [comment, taskId, userId], + function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Исполнитель не найден в задаче' }); + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(taskId, currentUserId, 'ASSIGNMENT_REWORK', + `Исполнитель ${userId} отправлен на доработку: ${comment}`); + } + + // Получаем данные для уведомления + db.get(`SELECT t.title, t.description, u.name as executor_name + FROM tasks t + LEFT JOIN users u ON u.id = ? + WHERE t.id = ?`, + [userId, taskId], (err, taskData) => { + if (!err && taskData) { + const { sendTaskNotifications } = require('./notifications'); + sendTaskNotifications( + 'rework_assignment', + taskId, + taskData.title, + taskData.description, + currentUserId, + comment, + 'rework', + req.session.user.name, + userId // ID исполнителя для персонального уведомления + ); + } + }); + + res.json({ + success: true, + message: 'Исполнитель отправлен на доработку' + }); + } + ); + }); + }); +}); app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params;