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 = `
+
+
+
+
Задача: ${escapeHtml(task.title)}
+
Исполнитель: ${escapeHtml(userName)}
+
+
+
+
+
+
+
+ `;
+
+ 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;