Add files via upload
This commit is contained in:
79
database.js
79
database.js
@@ -66,9 +66,13 @@ function initializeDatabase() {
|
||||
original_task_id INTEGER,
|
||||
start_date DATETIME,
|
||||
due_date DATETIME,
|
||||
rework_comment TEXT,
|
||||
closed_at DATETIME,
|
||||
closed_by INTEGER,
|
||||
FOREIGN KEY (created_by) REFERENCES users (id),
|
||||
FOREIGN KEY (deleted_by) REFERENCES users (id),
|
||||
FOREIGN KEY (original_task_id) REFERENCES tasks (id)
|
||||
FOREIGN KEY (original_task_id) REFERENCES tasks (id),
|
||||
FOREIGN KEY (closed_by) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Таблица назначений задач
|
||||
@@ -79,6 +83,7 @@ function initializeDatabase() {
|
||||
status TEXT DEFAULT 'assigned',
|
||||
start_date DATETIME,
|
||||
due_date DATETIME,
|
||||
rework_comment TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks (id),
|
||||
@@ -112,6 +117,43 @@ function initializeDatabase() {
|
||||
)`);
|
||||
|
||||
console.log('База данных инициализирована в папке data');
|
||||
|
||||
// Добавляем недостающие колонки если они не существуют
|
||||
setTimeout(addMissingColumns, 1000);
|
||||
}
|
||||
|
||||
// Функция для добавления недостающих колонок
|
||||
function addMissingColumns() {
|
||||
const columnsToAdd = [
|
||||
{ table: 'tasks', column: 'rework_comment', type: 'TEXT' },
|
||||
{ table: 'tasks', column: 'closed_at', type: 'DATETIME' },
|
||||
{ table: 'tasks', column: 'closed_by', type: 'INTEGER' },
|
||||
{ table: 'task_assignments', column: 'rework_comment', type: 'TEXT' }
|
||||
];
|
||||
|
||||
columnsToAdd.forEach(({ table, column, type }) => {
|
||||
// Используем db.all вместо db.get для получения всех строк
|
||||
db.all(`PRAGMA table_info(${table})`, (err, rows) => {
|
||||
if (err) {
|
||||
console.error(`Ошибка при проверке таблицы ${table}:`, err);
|
||||
return;
|
||||
}
|
||||
|
||||
// rows теперь массив, можно использовать some
|
||||
const columnExists = rows.some(row => row.name === column);
|
||||
if (!columnExists) {
|
||||
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`, (err) => {
|
||||
if (err) {
|
||||
console.error(`Ошибка при добавлении колонки ${column} в таблицу ${table}:`, err);
|
||||
} else {
|
||||
console.log(`✅ Добавлена колонка ${column} в таблицу ${table}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ Колонка ${column} уже существует в таблице ${table}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskFolder(taskId) {
|
||||
@@ -181,17 +223,31 @@ function checkTaskAccess(userId, taskId, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обычные пользователи видят только задачи где они заказчик или исполнитель
|
||||
const query = `
|
||||
SELECT 1 FROM tasks t
|
||||
WHERE t.id = ? AND (
|
||||
t.created_by = ?
|
||||
OR EXISTS (SELECT 1 FROM task_assignments WHERE task_id = t.id AND user_id = ?)
|
||||
)
|
||||
`;
|
||||
// Проверяем, не закрыта ли задача
|
||||
db.get("SELECT status, created_by, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
callback(err, false);
|
||||
return;
|
||||
}
|
||||
|
||||
db.get(query, [taskId, userId, userId], (err, row) => {
|
||||
callback(err, !!row);
|
||||
// Если задача закрыта, доступ есть только у создателя и администраторов
|
||||
if (task.closed_at && task.created_by !== userId && user.role !== 'admin') {
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обычные пользователи видят только задачи где они заказчик или исполнитель
|
||||
const query = `
|
||||
SELECT 1 FROM tasks t
|
||||
WHERE t.id = ? AND (
|
||||
t.created_by = ?
|
||||
OR EXISTS (SELECT 1 FROM task_assignments WHERE task_id = t.id AND user_id = ?)
|
||||
)
|
||||
`;
|
||||
|
||||
db.get(query, [taskId, userId, userId], (err, row) => {
|
||||
callback(err, !!row);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -200,6 +256,7 @@ function checkTaskAccess(userId, taskId, callback) {
|
||||
function checkOverdueTasks() {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Временно убираем проверку на closed_at до добавления колонки
|
||||
const query = `
|
||||
SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date
|
||||
FROM task_assignments ta
|
||||
|
||||
@@ -47,16 +47,36 @@
|
||||
|
||||
<main>
|
||||
<!-- Секция списка задач -->
|
||||
<section id="tasks-section" class="section">
|
||||
<h2>Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<label>
|
||||
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
|
||||
Показать удаленные задачи
|
||||
</label>
|
||||
</div>
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
<section id="tasks-section" class="section">
|
||||
<h2>Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="search-tasks">Поиск:</label>
|
||||
<input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="status-filter">Статус:</label>
|
||||
<select id="status-filter" onchange="loadTasks()">
|
||||
<option value="active,in_progress,assigned,overdue,rework">Все активные</option>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="assigned">Назначена</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="rework">На доработке</option>
|
||||
<option value="overdue">Просрочена</option>
|
||||
<option value="completed">Выполнена</option>
|
||||
<option value="closed">Закрыта</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Чекбокс удаленных задач - только для администраторов -->
|
||||
<label class="show-deleted-label" style="display: none;">
|
||||
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
|
||||
Показать удаленные задачи
|
||||
</label>
|
||||
</div>
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Секция создания задачи -->
|
||||
<section id="create-task-section" class="section">
|
||||
@@ -84,6 +104,9 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="user-search" placeholder="Поиск исполнителей..." oninput="filterUsers()">
|
||||
</div>
|
||||
<div id="users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +133,7 @@
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
<h3>Редактировать задачу</h3>
|
||||
<form id="edit-task-form">
|
||||
<form id="edit-task-form" enctype="multipart/form-data">
|
||||
<input type="hidden" id="edit-task-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Название задачи:</label>
|
||||
@@ -134,9 +157,18 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()">
|
||||
</div>
|
||||
<div id="edit-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-files">Добавить файлы:</label>
|
||||
<input type="file" id="edit-files" name="files" multiple>
|
||||
<div id="edit-file-list"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Сохранить изменения</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -162,6 +194,9 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label>Назначить исполнителей для копии:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()">
|
||||
</div>
|
||||
<div id="copy-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
<button type="submit">Создать копию</button>
|
||||
@@ -190,6 +225,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для возврата на доработку -->
|
||||
<div id="rework-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeReworkModal()">×</span>
|
||||
<h3>Вернуть задачу на доработку</h3>
|
||||
<form id="rework-task-form">
|
||||
<input type="hidden" id="rework-task-id">
|
||||
<div class="form-group">
|
||||
<label for="rework-comment">Комментарий к доработке:</label>
|
||||
<textarea id="rework-comment" name="comment" rows="4" placeholder="Укажите, что нужно исправить..." required></textarea>
|
||||
</div>
|
||||
<button type="submit">Вернуть на доработку</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
246
public/script.js
246
public/script.js
@@ -1,6 +1,7 @@
|
||||
let currentUser = null;
|
||||
let users = [];
|
||||
let tasks = [];
|
||||
let filteredUsers = [];
|
||||
|
||||
// Инициализация при загрузке
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -43,11 +44,17 @@ function showMainInterface() {
|
||||
|
||||
document.getElementById('current-user').textContent = userInfo;
|
||||
|
||||
// Показываем чекбокс удаленных задач только для администраторов
|
||||
if (currentUser.role === 'admin') {
|
||||
document.getElementById('tasks-controls').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('tasks-controls').style.display = 'none';
|
||||
// Показываем фильтры ВСЕМ пользователям, а не только администраторам
|
||||
document.getElementById('tasks-controls').style.display = 'block';
|
||||
|
||||
// Чекбокс удаленных задач показываем только администраторам
|
||||
const showDeletedLabel = document.querySelector('.show-deleted-label');
|
||||
if (showDeletedLabel) {
|
||||
if (currentUser.role === 'admin') {
|
||||
showDeletedLabel.style.display = 'flex';
|
||||
} else {
|
||||
showDeletedLabel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
loadUsers();
|
||||
@@ -62,7 +69,9 @@ function setupEventListeners() {
|
||||
document.getElementById('edit-task-form').addEventListener('submit', updateTask);
|
||||
document.getElementById('copy-task-form').addEventListener('submit', copyTask);
|
||||
document.getElementById('edit-assignment-form').addEventListener('submit', updateAssignment);
|
||||
document.getElementById('rework-task-form').addEventListener('submit', sendForRework);
|
||||
document.getElementById('files').addEventListener('change', updateFileList);
|
||||
document.getElementById('edit-files').addEventListener('change', updateEditFileList);
|
||||
}
|
||||
|
||||
async function login(event) {
|
||||
@@ -122,6 +131,7 @@ async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/users');
|
||||
users = await response.json();
|
||||
filteredUsers = [...users];
|
||||
renderUsersChecklist();
|
||||
renderEditUsersChecklist();
|
||||
renderCopyUsersChecklist();
|
||||
@@ -130,9 +140,49 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтрация пользователей
|
||||
function filterUsers() {
|
||||
const search = document.getElementById('user-search').value.toLowerCase();
|
||||
filteredUsers = users.filter(user =>
|
||||
user.name.toLowerCase().includes(search) ||
|
||||
user.login.toLowerCase().includes(search) ||
|
||||
user.email.toLowerCase().includes(search)
|
||||
);
|
||||
renderUsersChecklist();
|
||||
}
|
||||
|
||||
function filterEditUsers() {
|
||||
const search = document.getElementById('edit-user-search').value.toLowerCase();
|
||||
const filtered = users.filter(user =>
|
||||
user.name.toLowerCase().includes(search) ||
|
||||
user.login.toLowerCase().includes(search) ||
|
||||
user.email.toLowerCase().includes(search)
|
||||
);
|
||||
renderEditUsersChecklist(filtered);
|
||||
}
|
||||
|
||||
function filterCopyUsers() {
|
||||
const search = document.getElementById('copy-user-search').value.toLowerCase();
|
||||
const filtered = users.filter(user =>
|
||||
user.name.toLowerCase().includes(search) ||
|
||||
user.login.toLowerCase().includes(search) ||
|
||||
user.email.toLowerCase().includes(search)
|
||||
);
|
||||
renderCopyUsersChecklist(filtered);
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
const search = document.getElementById('search-tasks')?.value || '';
|
||||
const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework';
|
||||
const showDeleted = document.getElementById('show-deleted')?.checked || false;
|
||||
|
||||
let url = '/api/tasks?';
|
||||
if (search) url += `search=${encodeURIComponent(search)}&`;
|
||||
if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`;
|
||||
if (showDeleted) url += `showDeleted=true&`;
|
||||
|
||||
const response = await fetch(url);
|
||||
tasks = await response.json();
|
||||
renderTasks();
|
||||
|
||||
@@ -157,7 +207,7 @@ async function loadActivityLogs() {
|
||||
|
||||
function renderUsersChecklist() {
|
||||
const container = document.getElementById('users-checklist');
|
||||
container.innerHTML = users
|
||||
container.innerHTML = filteredUsers
|
||||
.filter(user => user.id !== currentUser.id)
|
||||
.map(user => `
|
||||
<div class="checkbox-item">
|
||||
@@ -170,9 +220,9 @@ function renderUsersChecklist() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderEditUsersChecklist() {
|
||||
function renderEditUsersChecklist(filtered = users) {
|
||||
const container = document.getElementById('edit-users-checklist');
|
||||
container.innerHTML = users
|
||||
container.innerHTML = filtered
|
||||
.filter(user => user.id !== currentUser.id)
|
||||
.map(user => `
|
||||
<div class="checkbox-item">
|
||||
@@ -185,9 +235,9 @@ function renderEditUsersChecklist() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderCopyUsersChecklist() {
|
||||
function renderCopyUsersChecklist(filtered = users) {
|
||||
const container = document.getElementById('copy-users-checklist');
|
||||
container.innerHTML = users
|
||||
container.innerHTML = filtered
|
||||
.filter(user => user.id !== currentUser.id)
|
||||
.map(user => `
|
||||
<div class="checkbox-item">
|
||||
@@ -218,17 +268,23 @@ function renderTasks() {
|
||||
const overallStatus = getTaskOverallStatus(task);
|
||||
const statusClass = getStatusClass(overallStatus);
|
||||
const isDeleted = task.status === 'deleted';
|
||||
const isClosed = task.closed_at !== null;
|
||||
const userRole = getUserRoleInTask(task);
|
||||
const canEdit = canUserEditTask(task);
|
||||
const isCopy = task.original_task_id !== null;
|
||||
|
||||
return `
|
||||
<div class="task-card ${isDeleted ? 'deleted' : ''}">
|
||||
<div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}">
|
||||
<div class="task-actions">
|
||||
${!isDeleted ? `
|
||||
<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>
|
||||
${!isDeleted && !isClosed ? `
|
||||
${canEdit ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''}
|
||||
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
|
||||
<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>
|
||||
${canEdit ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
|
||||
${canEdit ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
|
||||
${canEdit ? `<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>` : ''}
|
||||
` : ''}
|
||||
${isClosed && canEdit ? `
|
||||
<button class="reopen-btn" onclick="reopenTask(${task.id})" title="Открыть задачу">🔓</button>
|
||||
` : ''}
|
||||
${isDeleted && currentUser.role === 'admin' ? `
|
||||
<button class="restore-btn" onclick="restoreTask(${task.id})" title="Восстановить">↶</button>
|
||||
@@ -239,6 +295,7 @@ function renderTasks() {
|
||||
<div class="task-title">
|
||||
${task.title}
|
||||
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
|
||||
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
|
||||
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
|
||||
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
|
||||
</div>
|
||||
@@ -253,6 +310,12 @@ function renderTasks() {
|
||||
|
||||
<div class="task-description">${task.description || 'Нет описания'}</div>
|
||||
|
||||
${task.rework_comment ? `
|
||||
<div class="rework-comment">
|
||||
<strong>Комментарий к доработке:</strong> ${task.rework_comment}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${task.start_date || task.due_date ? `
|
||||
<div class="task-dates">
|
||||
${task.start_date ? `<div><strong>Начать:</strong> ${formatDateTime(task.start_date)}</div>` : ''}
|
||||
@@ -276,6 +339,7 @@ function renderTasks() {
|
||||
<div class="task-meta">
|
||||
<small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small>
|
||||
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
|
||||
${task.closed_at ? `<br><small>Закрыта: ${formatDateTime(task.closed_at)}</small>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -286,9 +350,10 @@ function renderAssignment(assignment, taskId, canEdit) {
|
||||
const statusClass = getStatusClass(assignment.status);
|
||||
const isCurrentUser = assignment.user_id === currentUser.id;
|
||||
const isOverdue = assignment.status === 'overdue';
|
||||
const isRework = assignment.status === 'rework';
|
||||
|
||||
return `
|
||||
<div class="assignment ${isOverdue ? 'overdue' : ''}">
|
||||
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
|
||||
<span class="assignment-status ${statusClass}"></span>
|
||||
<div style="flex: 1;">
|
||||
<strong>${assignment.user_name}</strong>
|
||||
@@ -299,11 +364,16 @@ function renderAssignment(assignment, taskId, canEdit) {
|
||||
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
${assignment.rework_comment ? `
|
||||
<div class="assignment-rework-comment">
|
||||
<small><strong>Комментарий:</strong> ${assignment.rework_comment}</small>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
${isCurrentUser && assignment.status === 'assigned' ?
|
||||
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'in_progress')">Приступить</button>` : ''}
|
||||
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue') ?
|
||||
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ?
|
||||
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'completed')">Выполнено</button>` : ''}
|
||||
${canEdit ?
|
||||
`<button class="edit-date-btn" onclick="openEditAssignmentModal(${taskId}, ${assignment.user_id})" title="Редактировать сроки">📅</button>` : ''}
|
||||
@@ -349,6 +419,8 @@ async function createTask(event) {
|
||||
alert('Задача успешно создана!');
|
||||
document.getElementById('create-task-form').reset();
|
||||
document.getElementById('file-list').innerHTML = '';
|
||||
document.getElementById('user-search').value = '';
|
||||
filterUsers();
|
||||
loadTasks();
|
||||
loadActivityLogs();
|
||||
showSection('tasks');
|
||||
@@ -405,6 +477,9 @@ async function openEditModal(taskId) {
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-task-modal').style.display = 'none';
|
||||
document.getElementById('edit-file-list').innerHTML = '';
|
||||
document.getElementById('edit-user-search').value = '';
|
||||
filterEditUsers();
|
||||
}
|
||||
|
||||
async function updateTask(event) {
|
||||
@@ -419,19 +494,22 @@ async function updateTask(event) {
|
||||
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
|
||||
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('title', title);
|
||||
formData.append('description', description);
|
||||
formData.append('assignedUsers', JSON.stringify(assignedUserIds));
|
||||
if (startDate) formData.append('startDate', startDate);
|
||||
if (dueDate) formData.append('dueDate', dueDate);
|
||||
|
||||
const files = document.getElementById('edit-files').files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description,
|
||||
assignedUsers: assignedUserIds,
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null
|
||||
})
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -456,9 +534,10 @@ function openCopyModal(taskId) {
|
||||
|
||||
function closeCopyModal() {
|
||||
document.getElementById('copy-task-modal').style.display = 'none';
|
||||
document.getElementById('copy-user-search').value = '';
|
||||
filterCopyUsers();
|
||||
}
|
||||
|
||||
// В функции copyTask улучшаем обработку ошибок
|
||||
async function copyTask(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -555,6 +634,90 @@ async function updateAssignment(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function openReworkModal(taskId) {
|
||||
document.getElementById('rework-task-id').value = taskId;
|
||||
document.getElementById('rework-task-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeReworkModal() {
|
||||
document.getElementById('rework-task-modal').style.display = 'none';
|
||||
document.getElementById('rework-comment').value = '';
|
||||
}
|
||||
|
||||
async function sendForRework(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const taskId = document.getElementById('rework-task-id').value;
|
||||
const comment = document.getElementById('rework-comment').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/rework`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ comment })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Задача возвращена на доработку!');
|
||||
closeReworkModal();
|
||||
loadTasks();
|
||||
loadActivityLogs();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Ошибка возврата задачи на доработку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка возврата задачи на доработку');
|
||||
}
|
||||
}
|
||||
|
||||
async function closeTask(taskId) {
|
||||
if (!confirm('Вы уверены, что хотите закрыть эту задачу? Исполнители больше не будут видеть её.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/close`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Задача закрыта!');
|
||||
loadTasks();
|
||||
loadActivityLogs();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Ошибка закрытия задачи');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка закрытия задачи');
|
||||
}
|
||||
}
|
||||
|
||||
async function reopenTask(taskId) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/reopen`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Задача открыта!');
|
||||
loadTasks();
|
||||
loadActivityLogs();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Ошибка открытия задачи');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка открытия задачи');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(taskId) {
|
||||
if (!confirm('Вы уверены, что хотите удалить эту задачу?')) {
|
||||
return;
|
||||
@@ -599,7 +762,6 @@ async function restoreTask(taskId) {
|
||||
}
|
||||
}
|
||||
|
||||
// В функции updateStatus улучшаем обработку ошибок
|
||||
async function updateStatus(taskId, userId, status) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/status`, {
|
||||
@@ -625,12 +787,14 @@ async function updateStatus(taskId, userId, status) {
|
||||
|
||||
function getTaskOverallStatus(task) {
|
||||
if (task.status === 'deleted') return 'deleted';
|
||||
if (task.closed_at) return 'closed'; // Закрытые задачи всегда имеют статус 'closed'
|
||||
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
|
||||
|
||||
const assignments = task.assignments;
|
||||
let hasAssigned = false;
|
||||
let hasInProgress = false;
|
||||
let hasOverdue = false;
|
||||
let hasRework = false;
|
||||
let allCompleted = true;
|
||||
|
||||
for (let assignment of assignments) {
|
||||
@@ -643,12 +807,16 @@ function getTaskOverallStatus(task) {
|
||||
} else if (assignment.status === 'overdue') {
|
||||
hasOverdue = true;
|
||||
allCompleted = false;
|
||||
} else if (assignment.status === 'rework') {
|
||||
hasRework = true;
|
||||
allCompleted = false;
|
||||
} else if (assignment.status !== 'completed') {
|
||||
allCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allCompleted) return 'completed';
|
||||
if (hasRework) return 'rework';
|
||||
if (hasOverdue) return 'overdue';
|
||||
if (hasInProgress) return 'in_progress';
|
||||
if (hasAssigned) return 'assigned';
|
||||
@@ -658,9 +826,11 @@ function getTaskOverallStatus(task) {
|
||||
function getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'deleted': return 'status-gray';
|
||||
case 'closed': return 'status-gray';
|
||||
case 'unassigned': return 'status-purple';
|
||||
case 'assigned': return 'status-red';
|
||||
case 'in_progress': return 'status-orange';
|
||||
case 'rework': return 'status-yellow';
|
||||
case 'overdue': return 'status-darkred';
|
||||
case 'completed': return 'status-green';
|
||||
default: return 'status-purple';
|
||||
@@ -670,9 +840,11 @@ function getStatusClass(status) {
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 'deleted': return 'Удалена';
|
||||
case 'closed': return 'Закрыта';
|
||||
case 'unassigned': return 'Не назначена';
|
||||
case 'assigned': return 'Назначена';
|
||||
case 'in_progress': return 'В работе';
|
||||
case 'rework': return 'На доработке';
|
||||
case 'overdue': return 'Просрочена';
|
||||
case 'completed': return 'Выполнена';
|
||||
default: return 'Неизвестно';
|
||||
@@ -729,6 +901,16 @@ function formatDateTimeForInput(dateTimeString) {
|
||||
function updateFileList() {
|
||||
const fileInput = document.getElementById('files');
|
||||
const fileList = document.getElementById('file-list');
|
||||
updateFileListForInput(fileInput, fileList);
|
||||
}
|
||||
|
||||
function updateEditFileList() {
|
||||
const fileInput = document.getElementById('edit-files');
|
||||
const fileList = document.getElementById('edit-file-list');
|
||||
updateFileListForInput(fileInput, fileList);
|
||||
}
|
||||
|
||||
function updateFileListForInput(fileInput, fileList) {
|
||||
const files = fileInput.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
@@ -780,7 +962,11 @@ function getActionText(action) {
|
||||
'TASK_ASSIGNMENTS_UPDATED': 'обновил назначения',
|
||||
'ASSIGNMENT_UPDATED': 'обновил сроки исполнителя',
|
||||
'STATUS_CHANGED': 'изменил статус задачи',
|
||||
'FILE_UPLOADED': 'загрузил файл'
|
||||
'FILE_UPLOADED': 'загрузил файл',
|
||||
'FILE_COPIED': 'скопировал файл',
|
||||
'TASK_SENT_FOR_REWORK': 'вернул задачу на доработку',
|
||||
'TASK_CLOSED': 'закрыл задачу',
|
||||
'TASK_REOPENED': 'открыл задачу'
|
||||
};
|
||||
|
||||
return actions[action] || action;
|
||||
|
||||
132
public/style.css
132
public/style.css
@@ -890,4 +890,136 @@ select:focus {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
/* Добавляем в существующие стили */
|
||||
|
||||
/* Фильтры */
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-search input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Новые статусы */
|
||||
.status-yellow {
|
||||
background: linear-gradient(135deg, #ffc107, #e0a800);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Закрытые задачи */
|
||||
.task-card.closed {
|
||||
background: linear-gradient(135deg, #e9ecef, #dee2e6);
|
||||
border-left-color: #6c757d;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.closed-badge {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Комментарии к доработке */
|
||||
.rework-comment {
|
||||
margin: 10px 0;
|
||||
padding: 12px 15px;
|
||||
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ffc107;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.assignment-rework-comment {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #fff3cd;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #ffc107;
|
||||
}
|
||||
|
||||
.assignment.rework {
|
||||
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
||||
border: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
/* Новые кнопки */
|
||||
button.rework-btn {
|
||||
background: linear-gradient(135deg, #ffc107, #e0a800);
|
||||
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
button.rework-btn:hover {
|
||||
box-shadow: 0 6px 20px rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
button.close-btn {
|
||||
background: linear-gradient(135deg, #6c757d, #5a6268);
|
||||
box-shadow: 0 4px 15px rgba(108, 117, 125, 0.3);
|
||||
}
|
||||
|
||||
button.close-btn:hover {
|
||||
box-shadow: 0 6px 20px rgba(108, 117, 125, 0.4);
|
||||
}
|
||||
|
||||
button.reopen-btn {
|
||||
background: linear-gradient(135deg, #20c997, #1ea085);
|
||||
box-shadow: 0 4px 15px rgba(32, 201, 151, 0.3);
|
||||
}
|
||||
|
||||
button.reopen-btn:hover {
|
||||
box-shadow: 0 6px 20px rgba(32, 201, 151, 0.4);
|
||||
}
|
||||
|
||||
.show-deleted-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: normal;
|
||||
color: #2c3e50;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.show-deleted-label input {
|
||||
margin: 0;
|
||||
}
|
||||
/* В существующие стили добавляем */
|
||||
.show-deleted-label[style*="display: none"] {
|
||||
display: none !important;
|
||||
}
|
||||
275
server.js
275
server.js
@@ -6,7 +6,7 @@ const session = require('express-session');
|
||||
require('dotenv').config();
|
||||
|
||||
const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
|
||||
const authService = require('./auth'); // Добавьте эту строку
|
||||
const authService = require('./auth');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -80,7 +80,37 @@ function checkIfOverdue(dueDate, status) {
|
||||
const due = new Date(dueDate);
|
||||
return due < now;
|
||||
}
|
||||
// Функция для проверки просроченных задач
|
||||
function checkOverdueTasks() {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Проверяем только активные незакрытые задачи
|
||||
const query = `
|
||||
SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date
|
||||
FROM task_assignments ta
|
||||
JOIN tasks t ON ta.task_id = t.id
|
||||
WHERE ta.due_date IS NOT NULL
|
||||
AND ta.due_date < ?
|
||||
AND ta.status NOT IN ('completed', 'overdue')
|
||||
AND t.status = 'active'
|
||||
AND t.closed_at IS NULL
|
||||
`;
|
||||
|
||||
db.all(query, [now], (err, assignments) => {
|
||||
if (err) {
|
||||
console.error('Ошибка при проверке просроченных задач:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
assignments.forEach(assignment => {
|
||||
db.run(
|
||||
"UPDATE task_assignments SET status = 'overdue' WHERE id = ?",
|
||||
[assignment.id]
|
||||
);
|
||||
logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена');
|
||||
});
|
||||
});
|
||||
}
|
||||
// ==================== МАРШРУТЫ АУТЕНТИФИКАЦИИ ====================
|
||||
|
||||
app.post('/api/login', async (req, res) => {
|
||||
@@ -139,7 +169,25 @@ app.get('/api/user', (req, res) => {
|
||||
// ==================== МАРШРУТЫ ПОЛЬЗОВАТЕЛЕЙ ====================
|
||||
|
||||
app.get('/api/users', requireAuth, (req, res) => {
|
||||
db.all("SELECT id, login, name, email, role, auth_type FROM users WHERE role IN ('admin', 'teacher') ORDER BY name", (err, rows) => {
|
||||
const search = req.query.search || '';
|
||||
|
||||
let query = `
|
||||
SELECT id, login, name, email, role, auth_type
|
||||
FROM users
|
||||
WHERE role IN ('admin', 'teacher')
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
query += ` AND (login LIKE ? OR name LIKE ? OR email LIKE ?)`;
|
||||
const searchPattern = `%${search}%`;
|
||||
params.push(searchPattern, searchPattern, searchPattern);
|
||||
}
|
||||
|
||||
query += " ORDER BY name";
|
||||
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
@@ -150,10 +198,12 @@ app.get('/api/users', requireAuth, (req, res) => {
|
||||
|
||||
// ==================== МАРШРУТЫ ЗАДАЧ ====================
|
||||
|
||||
// Получить задачи с учетом прав доступа
|
||||
// Получить задачи с учетом прав доступа и фильтров
|
||||
app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
const showDeleted = req.session.user.role === 'admin';
|
||||
const showDeleted = req.session.user.role === 'admin' && req.query.showDeleted === 'true';
|
||||
const search = req.query.search || '';
|
||||
const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework'; // По умолчанию все кроме выполненных и закрытых
|
||||
|
||||
let query = `
|
||||
SELECT DISTINCT
|
||||
@@ -173,18 +223,64 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// Для обычных пользователей показываем только задачи где они заказчик или исполнитель
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.created_by = ${userId} OR ta.user_id = ${userId})`;
|
||||
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
|
||||
params.push(userId, userId);
|
||||
}
|
||||
|
||||
if (!showDeleted) {
|
||||
query += " AND t.status = 'active'";
|
||||
}
|
||||
|
||||
// Фильтр по статусу
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
const statuses = statusFilter.split(',');
|
||||
|
||||
// Если в фильтре есть 'closed', показываем закрытые задачи
|
||||
if (statuses.includes('closed')) {
|
||||
// Для исполнителей показываем только свои закрытые задачи
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`;
|
||||
params.push(userId);
|
||||
} else {
|
||||
// Для администраторов показываем все закрытые задачи
|
||||
query += ` AND t.closed_at IS NOT NULL`;
|
||||
}
|
||||
} else {
|
||||
// Если 'closed' нет в фильтре, скрываем закрытые задачи для всех
|
||||
query += ` AND t.closed_at IS NULL`;
|
||||
|
||||
// Добавляем фильтрацию по статусам назначений
|
||||
if (statuses.length > 0 && !statuses.includes('all')) {
|
||||
query += ` AND EXISTS (
|
||||
SELECT 1 FROM task_assignments ta2
|
||||
WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')})
|
||||
)`;
|
||||
statuses.forEach(status => params.push(status));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если фильтр 'all', для исполнителей все равно скрываем чужие закрытые задачи
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`;
|
||||
params.push(userId);
|
||||
}
|
||||
// Для администраторов при фильтре 'all' показываем все включая закрытые
|
||||
}
|
||||
|
||||
// Поиск по тексту
|
||||
if (search) {
|
||||
query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
|
||||
const searchPattern = `%${search}%`;
|
||||
params.push(searchPattern, searchPattern);
|
||||
}
|
||||
|
||||
query += " GROUP BY t.id ORDER BY t.created_at DESC";
|
||||
|
||||
db.all(query, (err, tasks) => {
|
||||
db.all(query, params, (err, tasks) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
@@ -301,7 +397,7 @@ app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Копировать задачу
|
||||
// Копировать задачу с файлами
|
||||
app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { assignedUsers, startDate, dueDate } = req.body;
|
||||
@@ -338,6 +434,30 @@ app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
|
||||
saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate);
|
||||
|
||||
logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`);
|
||||
|
||||
// Копируем файлы из оригинальной задачи
|
||||
db.all("SELECT * FROM task_files WHERE task_id = ?", [taskId], (err, originalFiles) => {
|
||||
if (!err && originalFiles && originalFiles.length > 0) {
|
||||
originalFiles.forEach(originalFile => {
|
||||
const originalFilePath = originalFile.file_path;
|
||||
const newFilename = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(originalFile.original_name);
|
||||
const userFolder = createUserTaskFolder(newTaskId, req.session.user.login);
|
||||
const newFilePath = path.join(userFolder, newFilename);
|
||||
|
||||
// Копируем файл
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
fs.copyFileSync(originalFilePath, newFilePath);
|
||||
|
||||
db.run(
|
||||
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[newTaskId, createdBy, newFilename, originalFile.original_name, newFilePath, originalFile.file_size]
|
||||
);
|
||||
|
||||
logActivity(newTaskId, createdBy, 'FILE_COPIED', `Скопирован файл: ${originalFile.original_name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Назначаем пользователей
|
||||
if (assignedUsers && assignedUsers.length > 0) {
|
||||
@@ -387,12 +507,13 @@ app.get('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
LEFT JOIN users ou ON ot.created_by = ou.id
|
||||
WHERE t.id = ?
|
||||
`;
|
||||
const params = [taskId];
|
||||
|
||||
if (!showDeleted) {
|
||||
query += " AND t.status = 'active'";
|
||||
}
|
||||
|
||||
db.get(query, [taskId], (err, task) => {
|
||||
db.get(query, params, (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
@@ -424,8 +545,8 @@ app.get('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Обновить задачу с проверкой прав
|
||||
app.put('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
// Обновить задачу с проверкой прав и возможностью добавления файлов
|
||||
app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { title, description, assignedUsers, startDate, dueDate } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
@@ -460,6 +581,28 @@ app.put('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
|
||||
logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`);
|
||||
|
||||
// Обрабатываем новые файлы
|
||||
if (req.files && req.files.length > 0) {
|
||||
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
|
||||
|
||||
req.files.forEach(file => {
|
||||
const newPath = path.join(userFolder, path.basename(file.filename));
|
||||
fs.renameSync(file.path, newPath);
|
||||
|
||||
db.run(
|
||||
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[taskId, userId, path.basename(file.filename), file.originalname, newPath, file.size]
|
||||
);
|
||||
|
||||
logActivity(taskId, userId, 'FILE_UPLOADED', `Загружен файл: ${file.originalname}`);
|
||||
});
|
||||
|
||||
// Очищаем временную папку
|
||||
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
// Обновляем назначения если переданы
|
||||
if (assignedUsers) {
|
||||
// Удаляем старые назначения
|
||||
@@ -488,6 +631,118 @@ app.put('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Вернуть задачу на доработку
|
||||
app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { comment } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Проверяем права - только создатель или администратор могут возвращать на доработку
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
|
||||
return res.status(403).json({ error: 'У вас нет прав для возврата задачи на доработку' });
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
// Обновляем задачу с комментарием
|
||||
db.run(
|
||||
"UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[comment || 'Требуется доработка', taskId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем статусы всех назначений на 'rework'
|
||||
db.run(
|
||||
"UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?",
|
||||
[comment || 'Требуется доработка', taskId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`);
|
||||
res.json({ success: true, message: 'Задача возвращена на доработку' });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Закрыть задачу
|
||||
app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Проверяем права - только создатель или администратор могут закрывать задачу
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
|
||||
return res.status(403).json({ error: 'У вас нет прав для закрытия этой задачи' });
|
||||
}
|
||||
|
||||
db.run(
|
||||
"UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[userId, taskId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`);
|
||||
res.json({ success: true, message: 'Задача закрыта' });
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Открыть задачу (отменить закрытие)
|
||||
app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Проверяем права - только создатель или администратор могут открывать задачу
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
|
||||
return res.status(403).json({ error: 'У вас нет прав для открытия этой задачи' });
|
||||
}
|
||||
|
||||
db.run(
|
||||
"UPDATE tasks SET closed_at = NULL, closed_by = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[taskId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity(taskId, userId, 'TASK_REOPENED', `Задача открыта`);
|
||||
res.json({ success: true, message: 'Задача открыта' });
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Остальные маршруты остаются без изменений...
|
||||
// (Обновить сроки исполнителя, Удалить задачу, Восстановить задачу, Обновить статус, Файлы, Логи)
|
||||
|
||||
// Обновить сроки для конкретного исполнителя
|
||||
app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
|
||||
const { taskId, userId } = req.params;
|
||||
|
||||
Reference in New Issue
Block a user