r
This commit is contained in:
29
database.js
29
database.js
@@ -3,7 +3,6 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
|
||||
// Создаем папку data если нет
|
||||
const dataDir = path.join(__dirname, 'data');
|
||||
const createDirIfNotExists = (dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
@@ -13,10 +12,7 @@ const createDirIfNotExists = (dirPath) => {
|
||||
|
||||
createDirIfNotExists(dataDir);
|
||||
|
||||
// Путь к базе данных в папке data
|
||||
const dbPath = path.join(dataDir, 'school_crm.db');
|
||||
|
||||
// Папки для загрузок и логов в data
|
||||
const uploadsDir = path.join(dataDir, 'uploads');
|
||||
const tasksDir = path.join(uploadsDir, 'tasks');
|
||||
const logsDir = path.join(dataDir, 'logs');
|
||||
@@ -36,7 +32,6 @@ const db = new sqlite3.Database(dbPath, (err) => {
|
||||
});
|
||||
|
||||
function initializeDatabase() {
|
||||
// Обновленная таблица пользователей с поддержкой LDAP
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT UNIQUE NOT NULL,
|
||||
@@ -52,7 +47,6 @@ function initializeDatabase() {
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
// Таблица задач
|
||||
db.run(`CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
@@ -75,7 +69,6 @@ function initializeDatabase() {
|
||||
FOREIGN KEY (closed_by) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Таблица назначений задач
|
||||
db.run(`CREATE TABLE IF NOT EXISTS task_assignments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
@@ -90,7 +83,6 @@ function initializeDatabase() {
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Таблица файлов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS task_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
@@ -104,7 +96,6 @@ function initializeDatabase() {
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Таблица логов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS activity_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
@@ -116,13 +107,17 @@ function initializeDatabase() {
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS notification_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
notification_key TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
console.log('База данных инициализирована в папке data');
|
||||
|
||||
// Добавляем недостающие колонки если они не существуют
|
||||
setTimeout(addMissingColumns, 1000);
|
||||
}
|
||||
|
||||
// Функция для добавления недостающих колонок
|
||||
function addMissingColumns() {
|
||||
const columnsToAdd = [
|
||||
{ table: 'tasks', column: 'rework_comment', type: 'TEXT' },
|
||||
@@ -132,14 +127,12 @@ function addMissingColumns() {
|
||||
];
|
||||
|
||||
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) => {
|
||||
@@ -208,35 +201,29 @@ function logActivity(taskId, userId, action, details = '') {
|
||||
fs.appendFileSync(path.join(logsDir, 'activity.log'), logEntry);
|
||||
}
|
||||
|
||||
// Функция для проверки прав доступа к задаче
|
||||
function checkTaskAccess(userId, taskId, callback) {
|
||||
// Сначала получаем роль пользователя
|
||||
db.get("SELECT role FROM users WHERE id = ?", [userId], (err, user) => {
|
||||
if (err) {
|
||||
callback(err, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Администраторы имеют доступ ко всем задачам
|
||||
if (user && user.role === 'admin') {
|
||||
callback(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не закрыта ли задача
|
||||
db.get("SELECT status, created_by, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
callback(err, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если задача закрыта, доступ есть только у создателя и администраторов
|
||||
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 (
|
||||
@@ -252,11 +239,9 @@ 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
|
||||
@@ -265,6 +250,7 @@ function checkOverdueTasks() {
|
||||
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) => {
|
||||
@@ -283,7 +269,6 @@ function checkOverdueTasks() {
|
||||
});
|
||||
}
|
||||
|
||||
// Запускаем проверку просроченных задач каждую минуту
|
||||
setInterval(checkOverdueTasks, 60000);
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Форма логина -->
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Вход в School CRM</h2>
|
||||
@@ -46,8 +45,7 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Секция списка задач -->
|
||||
<section id="tasks-section" class="section">
|
||||
<section id="tasks-section" class="section">
|
||||
<h2>Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<div class="filters">
|
||||
@@ -68,8 +66,27 @@
|
||||
<option value="closed">Закрыта</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="creator-filter">Заказчик:</label>
|
||||
<select id="creator-filter" onchange="loadTasks()">
|
||||
<option value="">Все заказчики</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="assignee-filter">Исполнитель:</label>
|
||||
<select id="assignee-filter" onchange="loadTasks()">
|
||||
<option value="">Все исполнители</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="deadline-filter">Срок выполнения:</label>
|
||||
<select id="deadline-filter" onchange="loadTasks()">
|
||||
<option value="">Все сроки</option>
|
||||
<option value="48h">Менее 48 часов</option>
|
||||
<option value="24h">Менее 24 часов</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Чекбокс удаленных задач - только для администраторов -->
|
||||
<label class="show-deleted-label" style="display: none;">
|
||||
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
|
||||
Показать удаленные задачи
|
||||
@@ -78,7 +95,6 @@
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Секция создания задачи -->
|
||||
<section id="create-task-section" class="section">
|
||||
<h2>Создать новую задачу</h2>
|
||||
<form id="create-task-form" enctype="multipart/form-data">
|
||||
@@ -93,13 +109,8 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-date">Дата и время начала (необязательно):</label>
|
||||
<input type="datetime-local" id="start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="due-date">Дата и время выполнения (необязательно):</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate">
|
||||
<label for="due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -120,7 +131,6 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Секция логов -->
|
||||
<section id="logs-section" class="section">
|
||||
<h2>Лог активности</h2>
|
||||
<div id="logs-list"></div>
|
||||
@@ -128,7 +138,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для редактирования задачи -->
|
||||
<div id="edit-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
@@ -145,14 +154,9 @@
|
||||
<textarea id="edit-description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-start-date">Дата и время начала:</label>
|
||||
<input type="datetime-local" id="edit-start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -174,7 +178,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для копирования задачи -->
|
||||
<div id="copy-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCopyModal()">×</span>
|
||||
@@ -182,14 +185,9 @@
|
||||
<form id="copy-task-form">
|
||||
<input type="hidden" id="copy-task-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-start-date">Дата и время начала для копии:</label>
|
||||
<input type="datetime-local" id="copy-start-date" name="startDate">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-due-date">Дата и время выполнения для копии:</label>
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -204,7 +202,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для редактирования сроков исполнителя -->
|
||||
<div id="edit-assignment-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditAssignmentModal()">×</span>
|
||||
@@ -212,20 +209,15 @@
|
||||
<form id="edit-assignment-form">
|
||||
<input type="hidden" id="edit-assignment-task-id">
|
||||
<input type="hidden" id="edit-assignment-user-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-start-date">Дата и время начала:</label>
|
||||
<input type="datetime-local" id="edit-assignment-start-date" name="startDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate">
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate" required>
|
||||
</div>
|
||||
<button type="submit">Сохранить сроки</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для возврата на доработку -->
|
||||
<div id="rework-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeReworkModal()">×</span>
|
||||
|
||||
143
public/script.js
143
public/script.js
@@ -3,7 +3,6 @@ let users = [];
|
||||
let tasks = [];
|
||||
let filteredUsers = [];
|
||||
|
||||
// Инициализация при загрузке
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkAuth();
|
||||
setupEventListeners();
|
||||
@@ -33,7 +32,6 @@ function showMainInterface() {
|
||||
document.getElementById('login-modal').style.display = 'none';
|
||||
document.querySelector('.container').style.display = 'block';
|
||||
|
||||
// Формируем информацию о пользователе
|
||||
let userInfo = `Вы вошли как: ${currentUser.name}`;
|
||||
if (currentUser.auth_type === 'ldap') {
|
||||
userInfo += ` (LDAP)`;
|
||||
@@ -44,10 +42,8 @@ function showMainInterface() {
|
||||
|
||||
document.getElementById('current-user').textContent = userInfo;
|
||||
|
||||
// Показываем фильтры ВСЕМ пользователям, а не только администраторам
|
||||
document.getElementById('tasks-controls').style.display = 'block';
|
||||
|
||||
// Чекбокс удаленных задач показываем только администраторам
|
||||
const showDeletedLabel = document.querySelector('.show-deleted-label');
|
||||
if (showDeletedLabel) {
|
||||
if (currentUser.role === 'admin') {
|
||||
@@ -135,12 +131,30 @@ async function loadUsers() {
|
||||
renderUsersChecklist();
|
||||
renderEditUsersChecklist();
|
||||
renderCopyUsersChecklist();
|
||||
populateFilterDropdowns();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки пользователей:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтрация пользователей
|
||||
function populateFilterDropdowns() {
|
||||
const creatorFilter = document.getElementById('creator-filter');
|
||||
const assigneeFilter = document.getElementById('assignee-filter');
|
||||
|
||||
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
|
||||
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const search = document.getElementById('user-search').value.toLowerCase();
|
||||
filteredUsers = users.filter(user =>
|
||||
@@ -175,18 +189,23 @@ async function loadTasks() {
|
||||
try {
|
||||
const search = document.getElementById('search-tasks')?.value || '';
|
||||
const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework';
|
||||
const creatorFilter = document.getElementById('creator-filter')?.value || '';
|
||||
const assigneeFilter = document.getElementById('assignee-filter')?.value || '';
|
||||
const deadlineFilter = document.getElementById('deadline-filter')?.value || '';
|
||||
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 (creatorFilter) url += `creator=${encodeURIComponent(creatorFilter)}&`;
|
||||
if (assigneeFilter) url += `assignee=${encodeURIComponent(assigneeFilter)}&`;
|
||||
if (deadlineFilter) url += `deadline=${encodeURIComponent(deadlineFilter)}&`;
|
||||
if (showDeleted) url += `showDeleted=true&`;
|
||||
|
||||
const response = await fetch(url);
|
||||
tasks = await response.json();
|
||||
renderTasks();
|
||||
|
||||
// Загружаем файлы для каждой задачи
|
||||
tasks.forEach(task => {
|
||||
loadTaskFiles(task.id);
|
||||
});
|
||||
@@ -273,6 +292,8 @@ function renderTasks() {
|
||||
const canEdit = canUserEditTask(task);
|
||||
const isCopy = task.original_task_id !== null;
|
||||
|
||||
const timeLeftInfo = getTimeLeftInfo(task);
|
||||
|
||||
return `
|
||||
<div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}">
|
||||
<div class="task-actions">
|
||||
@@ -297,6 +318,7 @@ function renderTasks() {
|
||||
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
|
||||
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
|
||||
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
|
||||
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||||
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
|
||||
</div>
|
||||
<div class="task-status ${statusClass}">${getStatusText(overallStatus)}</div>
|
||||
@@ -318,7 +340,7 @@ function renderTasks() {
|
||||
|
||||
${task.start_date || task.due_date ? `
|
||||
<div class="task-dates">
|
||||
${task.start_date ? `<div><strong>Начать:</strong> ${formatDateTime(task.start_date)}</div>` : ''}
|
||||
<div><strong>Создана:</strong> ${formatDateTime(task.start_date || task.created_at)}</div>
|
||||
${task.due_date ? `<div><strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}</div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -346,21 +368,49 @@ function renderTasks() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getTimeLeftInfo(task) {
|
||||
if (!task.due_date || task.closed_at) return null;
|
||||
|
||||
const dueDate = new Date(task.due_date);
|
||||
const now = new Date();
|
||||
const timeLeft = dueDate.getTime() - now.getTime();
|
||||
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
|
||||
|
||||
if (hoursLeft <= 0) return null;
|
||||
|
||||
if (hoursLeft <= 24) {
|
||||
return {
|
||||
text: `Менее 24ч`,
|
||||
class: 'deadline-24h'
|
||||
};
|
||||
} else if (hoursLeft <= 48) {
|
||||
return {
|
||||
text: `Менее 48ч`,
|
||||
class: 'deadline-48h'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
const timeLeftInfo = getAssignmentTimeLeftInfo(assignment);
|
||||
|
||||
return `
|
||||
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
|
||||
<span class="assignment-status ${statusClass}"></span>
|
||||
<div style="flex: 1;">
|
||||
<strong>${assignment.user_name}</strong>
|
||||
${isCurrentUser ? '<small>(Вы)</small>' : ''}
|
||||
${timeLeftInfo ? `<span class="deadline-indicator ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||||
${assignment.start_date || assignment.due_date ? `
|
||||
<div class="assignment-dates">
|
||||
${assignment.start_date ? `<small>Начать: ${formatDateTime(assignment.start_date)}</small>` : ''}
|
||||
${assignment.start_date ? `<small>Начало: ${formatDateTime(assignment.start_date)}</small>` : ''}
|
||||
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -382,6 +432,31 @@ function renderAssignment(assignment, taskId, canEdit) {
|
||||
`;
|
||||
}
|
||||
|
||||
function getAssignmentTimeLeftInfo(assignment) {
|
||||
if (!assignment.due_date || assignment.status === 'completed') return null;
|
||||
|
||||
const dueDate = new Date(assignment.due_date);
|
||||
const now = new Date();
|
||||
const timeLeft = dueDate.getTime() - now.getTime();
|
||||
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
|
||||
|
||||
if (hoursLeft <= 0) return null;
|
||||
|
||||
if (hoursLeft <= 24) {
|
||||
return {
|
||||
text: `Осталось ${hoursLeft}ч`,
|
||||
class: 'deadline-24h'
|
||||
};
|
||||
} else if (hoursLeft <= 48) {
|
||||
return {
|
||||
text: `Осталось ${hoursLeft}ч`,
|
||||
class: 'deadline-48h'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createTask(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -394,12 +469,18 @@ async function createTask(event) {
|
||||
formData.append('title', document.getElementById('title').value);
|
||||
formData.append('description', document.getElementById('description').value);
|
||||
|
||||
const startDate = document.getElementById('start-date').value;
|
||||
const dueDate = document.getElementById('due-date').value;
|
||||
if (startDate) formData.append('startDate', startDate);
|
||||
if (dueDate) formData.append('dueDate', dueDate);
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
formData.append('dueDate', dueDate);
|
||||
|
||||
const assignedUsers = document.querySelectorAll('#users-checklist input[name="assignedUsers"]:checked');
|
||||
if (assignedUsers.length === 0) {
|
||||
alert('Выберите хотя бы одного исполнителя');
|
||||
return;
|
||||
}
|
||||
assignedUsers.forEach(checkbox => {
|
||||
formData.append('assignedUsers', checkbox.value);
|
||||
});
|
||||
@@ -446,7 +527,6 @@ async function openEditModal(taskId) {
|
||||
|
||||
const task = await response.json();
|
||||
|
||||
// Дополнительная проверка прав на клиенте
|
||||
if (!canUserEditTask(task)) {
|
||||
alert('У вас нет прав для редактирования этой задачи');
|
||||
return;
|
||||
@@ -456,11 +536,8 @@ async function openEditModal(taskId) {
|
||||
document.getElementById('edit-title').value = task.title;
|
||||
document.getElementById('edit-description').value = task.description || '';
|
||||
|
||||
// Заполняем даты
|
||||
document.getElementById('edit-start-date').value = task.start_date ? formatDateTimeForInput(task.start_date) : '';
|
||||
document.getElementById('edit-due-date').value = task.due_date ? formatDateTimeForInput(task.due_date) : '';
|
||||
|
||||
// Отмечаем текущих исполнителей
|
||||
const checkboxes = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = task.assignments?.some(assignment =>
|
||||
@@ -488,9 +565,13 @@ async function updateTask(event) {
|
||||
const taskId = document.getElementById('edit-task-id').value;
|
||||
const title = document.getElementById('edit-title').value;
|
||||
const description = document.getElementById('edit-description').value;
|
||||
const startDate = document.getElementById('edit-start-date').value;
|
||||
const dueDate = document.getElementById('edit-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
|
||||
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
|
||||
|
||||
@@ -498,8 +579,7 @@ async function updateTask(event) {
|
||||
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);
|
||||
formData.append('dueDate', dueDate);
|
||||
|
||||
const files = document.getElementById('edit-files').files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
@@ -542,8 +622,13 @@ async function copyTask(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const taskId = document.getElementById('copy-task-id').value;
|
||||
const startDate = document.getElementById('copy-start-date').value;
|
||||
const dueDate = document.getElementById('copy-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны для копии задачи');
|
||||
return;
|
||||
}
|
||||
|
||||
const checkboxes = document.querySelectorAll('#copy-users-checklist input[name="assignedUsers"]:checked');
|
||||
const assignedUserIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
|
||||
@@ -560,8 +645,7 @@ async function copyTask(event) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
assignedUsers: assignedUserIds,
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null
|
||||
dueDate: dueDate
|
||||
})
|
||||
});
|
||||
|
||||
@@ -589,7 +673,6 @@ function openEditAssignmentModal(taskId, userId) {
|
||||
|
||||
document.getElementById('edit-assignment-task-id').value = taskId;
|
||||
document.getElementById('edit-assignment-user-id').value = userId;
|
||||
document.getElementById('edit-assignment-start-date').value = assignment.start_date ? formatDateTimeForInput(assignment.start_date) : '';
|
||||
document.getElementById('edit-assignment-due-date').value = assignment.due_date ? formatDateTimeForInput(assignment.due_date) : '';
|
||||
|
||||
document.getElementById('edit-assignment-modal').style.display = 'block';
|
||||
@@ -604,9 +687,13 @@ async function updateAssignment(event) {
|
||||
|
||||
const taskId = document.getElementById('edit-assignment-task-id').value;
|
||||
const userId = document.getElementById('edit-assignment-user-id').value;
|
||||
const startDate = document.getElementById('edit-assignment-start-date').value;
|
||||
const dueDate = document.getElementById('edit-assignment-due-date').value;
|
||||
|
||||
if (!dueDate) {
|
||||
alert('Дата и время выполнения обязательны');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/assignment/${userId}`, {
|
||||
method: 'PUT',
|
||||
@@ -614,8 +701,7 @@ async function updateAssignment(event) {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null
|
||||
dueDate: dueDate
|
||||
})
|
||||
});
|
||||
|
||||
@@ -787,7 +873,7 @@ async function updateStatus(taskId, userId, status) {
|
||||
|
||||
function getTaskOverallStatus(task) {
|
||||
if (task.status === 'deleted') return 'deleted';
|
||||
if (task.closed_at) return 'closed'; // Закрытые задачи всегда имеют статус 'closed'
|
||||
if (task.closed_at) return 'closed';
|
||||
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
|
||||
|
||||
const assignments = task.assignments;
|
||||
@@ -857,7 +943,6 @@ function getUserRoleInTask(task) {
|
||||
if (currentUser.role === 'admin') return 'Администратор';
|
||||
if (parseInt(task.created_by) === currentUser.id) return 'Заказчик';
|
||||
|
||||
// Проверяем является ли пользователь исполнителем
|
||||
if (task.assignments) {
|
||||
const isExecutor = task.assignments.some(assignment =>
|
||||
parseInt(assignment.user_id) === currentUser.id
|
||||
@@ -880,8 +965,8 @@ function getRoleBadgeClass(role) {
|
||||
function canUserEditTask(task) {
|
||||
if (!currentUser) return false;
|
||||
|
||||
if (currentUser.role === 'admin') return true; // Администратор
|
||||
if (parseInt(task.created_by) === currentUser.id) return true; // Заказчик
|
||||
if (currentUser.role === 'admin') return true;
|
||||
if (parseInt(task.created_by) === currentUser.id) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
329
server.js
329
server.js
@@ -7,20 +7,15 @@ const fetch = require('node-fetch');
|
||||
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;
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Статические файлы из папки data/uploads
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'data', 'uploads')));
|
||||
|
||||
// Сессии
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'fallback_secret_change_in_production',
|
||||
resave: true,
|
||||
@@ -32,7 +27,6 @@ app.use(session({
|
||||
}
|
||||
}));
|
||||
|
||||
// Middleware для проверки аутентификации
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({ error: 'Требуется аутентификация' });
|
||||
@@ -40,7 +34,6 @@ const requireAuth = (req, res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
// Настройка Multer
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const taskId = req.body.taskId || req.params.taskId;
|
||||
@@ -71,14 +64,12 @@ const upload = multer({
|
||||
}
|
||||
});
|
||||
|
||||
// Вспомогательная функция
|
||||
const createDirIfNotExists = (dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Вспомогательная функция для проверки просрочки
|
||||
function checkIfOverdue(dueDate, status) {
|
||||
if (!dueDate || status === 'completed') return false;
|
||||
const now = new Date();
|
||||
@@ -86,11 +77,9 @@ function checkIfOverdue(dueDate, status) {
|
||||
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
|
||||
@@ -118,29 +107,130 @@ function checkOverdueTasks() {
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== СИСТЕМА УВЕДОМЛЕНИЙ ====================
|
||||
function checkUpcomingDeadlines() {
|
||||
const now = new Date();
|
||||
const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
|
||||
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
|
||||
const nowISO = now.toISOString();
|
||||
|
||||
const query = `
|
||||
SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email,
|
||||
creator.name as creator_name, creator.email as creator_email
|
||||
FROM task_assignments ta
|
||||
JOIN tasks t ON ta.task_id = t.id
|
||||
JOIN users u ON ta.user_id = u.id
|
||||
JOIN users creator ON t.created_by = creator.id
|
||||
WHERE ta.due_date IS NOT NULL
|
||||
AND ta.due_date > ?
|
||||
AND ta.due_date <= ?
|
||||
AND ta.status NOT IN ('completed', 'overdue')
|
||||
AND t.status = 'active'
|
||||
AND t.closed_at IS NULL
|
||||
`;
|
||||
|
||||
db.all(query, [nowISO, in48Hours], async (err, assignments) => {
|
||||
if (err) {
|
||||
console.error('Ошибка при проверке сроков задач:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const assignment of assignments) {
|
||||
const dueDate = new Date(assignment.due_date);
|
||||
const timeLeft = dueDate.getTime() - now.getTime();
|
||||
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
|
||||
|
||||
if (hoursLeft <= 48 && hoursLeft > 24) {
|
||||
await sendDeadlineNotification(assignment, 48);
|
||||
} else if (hoursLeft <= 24) {
|
||||
await sendDeadlineNotification(assignment, 24);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendDeadlineNotification(assignment, hoursLeft) {
|
||||
try {
|
||||
if (!process.env.NOTIFICATION_SERVICE_URL ||
|
||||
!process.env.NOTIFICATION_SERVICE_LOGIN ||
|
||||
!process.env.NOTIFICATION_SERVICE_PASSWORD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationKey = `deadline_${hoursLeft}h_task_${assignment.task_id}_user_${assignment.user_id}`;
|
||||
const lastSent = await getLastNotificationSent(notificationKey);
|
||||
const now = new Date();
|
||||
|
||||
if (lastSent) {
|
||||
const timeSinceLast = now.getTime() - new Date(lastSent).getTime();
|
||||
if (timeSinceLast < 12 * 60 * 60 * 1000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const subject = `⚠️ До окончания срока задачи осталось менее ${hoursLeft} часов`;
|
||||
const content = `Задача: ${assignment.title}\n\n` +
|
||||
`Описание: ${assignment.description || 'Без описания'}\n` +
|
||||
`Срок выполнения: ${new Date(assignment.due_date).toLocaleString('ru-RU')}\n` +
|
||||
`Осталось времени: ${hoursLeft} часов\n\n` +
|
||||
`Пожалуйста, завершите задачу в срок.`;
|
||||
|
||||
const recipients = [
|
||||
{ id: assignment.user_id, name: assignment.user_name, email: assignment.user_email },
|
||||
{ id: assignment.created_by, name: assignment.creator_name, email: assignment.creator_email }
|
||||
].filter((value, index, self) =>
|
||||
self.findIndex(r => r.id === value.id) === index
|
||||
);
|
||||
|
||||
const recipientIds = recipients.map(r => r.id);
|
||||
|
||||
const authHeader = Buffer.from(
|
||||
`${process.env.NOTIFICATION_SERVICE_LOGIN}:${process.env.NOTIFICATION_SERVICE_PASSWORD}`
|
||||
).toString('base64');
|
||||
|
||||
const FormData = require('form-data');
|
||||
const formData = new FormData();
|
||||
formData.append('subject', subject);
|
||||
formData.append('content', content);
|
||||
formData.append('recipients', JSON.stringify(recipientIds));
|
||||
formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
|
||||
|
||||
const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Basic ${authHeader}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await saveNotificationSent(notificationKey);
|
||||
console.log(`✅ Уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отправки уведомления о сроке:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getLastNotificationSent(key) {
|
||||
return new Promise((resolve) => {
|
||||
db.get("SELECT created_at FROM notification_logs WHERE notification_key = ? ORDER BY created_at DESC LIMIT 1",
|
||||
[key], (err, row) => {
|
||||
resolve(row ? row.created_at : null);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function saveNotificationSent(key) {
|
||||
db.run("INSERT INTO notification_logs (notification_key) VALUES (?)", [key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Кодирование логина и пароля в Base64 для Basic Auth
|
||||
*/
|
||||
function encodeBasicAuth(login, password) {
|
||||
return Buffer.from(`${login}:${password}`).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка уведомлений всем участникам задачи
|
||||
* @param {string} type - Тип события: 'created', 'updated', 'rework', 'closed', 'status_changed'
|
||||
* @param {number} taskId - ID задачи
|
||||
* @param {string} taskTitle - Название задачи
|
||||
* @param {string} taskDescription - Описание задачи
|
||||
* @param {number} authorId - ID автора изменения
|
||||
* @param {string} comment - Комментарий (для доработки)
|
||||
* @param {string} status - Новый статус (для status_changed)
|
||||
* @param {string} userName - Имя пользователя, изменившего статус
|
||||
*/
|
||||
async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') {
|
||||
try {
|
||||
// Проверяем наличие настроек уведомлений
|
||||
if (!process.env.NOTIFICATION_SERVICE_URL ||
|
||||
!process.env.NOTIFICATION_SERVICE_LOGIN ||
|
||||
!process.env.NOTIFICATION_SERVICE_PASSWORD) {
|
||||
@@ -148,10 +238,8 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем ВСЕХ участников задачи (создателя + исполнителей)
|
||||
const participants = await new Promise((resolve, reject) => {
|
||||
db.all(`
|
||||
-- Получаем создателя задачи
|
||||
SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email, 'creator' as role
|
||||
FROM tasks t
|
||||
LEFT JOIN users u ON t.created_by = u.id
|
||||
@@ -159,7 +247,6 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
|
||||
UNION
|
||||
|
||||
-- Получаем всех исполнителей
|
||||
SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email, 'assignee' as role
|
||||
FROM task_assignments ta
|
||||
LEFT JOIN users u ON ta.user_id = u.id
|
||||
@@ -175,7 +262,6 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем информацию об авторе изменения
|
||||
const author = await new Promise((resolve, reject) => {
|
||||
db.get("SELECT name FROM users WHERE id = ?", [authorId], (err, row) => {
|
||||
if (err) reject(err);
|
||||
@@ -185,7 +271,6 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
|
||||
const authorName = author ? author.name : 'Система';
|
||||
|
||||
// Формируем текст уведомления в зависимости от типа события
|
||||
let subject, content;
|
||||
|
||||
switch (type) {
|
||||
@@ -238,24 +323,20 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
return;
|
||||
}
|
||||
|
||||
// Формируем список ID получателей (исключаем автора изменения, чтобы он не получал уведомление о своем действии)
|
||||
const recipientIds = participants
|
||||
.filter(p => p.user_id !== authorId) // Исключаем автора действия
|
||||
.filter(p => p.user_id !== authorId)
|
||||
.map(p => p.user_id);
|
||||
|
||||
// Если после фильтрации не осталось получателей, выходим
|
||||
if (recipientIds.length === 0) {
|
||||
console.log('Нет получателей для уведомления (все участники - автор изменения)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Кодируем логин и пароль для Basic Auth
|
||||
const authHeader = encodeBasicAuth(
|
||||
process.env.NOTIFICATION_SERVICE_LOGIN,
|
||||
process.env.NOTIFICATION_SERVICE_PASSWORD
|
||||
);
|
||||
|
||||
// Создаем FormData для отправки
|
||||
const FormData = require('form-data');
|
||||
const formData = new FormData();
|
||||
formData.append('subject', subject);
|
||||
@@ -263,7 +344,6 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
formData.append('recipients', JSON.stringify(recipientIds));
|
||||
formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
|
||||
|
||||
// Отправляем уведомление
|
||||
const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -285,13 +365,9 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отправки уведомлений:', error);
|
||||
// Не прерываем выполнение из-за ошибки уведомлений
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить текстовое описание статуса
|
||||
*/
|
||||
function getStatusText(status) {
|
||||
const statusMap = {
|
||||
'assigned': 'Назначена',
|
||||
@@ -303,8 +379,6 @@ function getStatusText(status) {
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// ==================== МАРШРУТЫ АУТЕНТИФИКАЦИИ ====================
|
||||
|
||||
app.post('/api/login', async (req, res) => {
|
||||
const { login, password } = req.body;
|
||||
|
||||
@@ -315,7 +389,6 @@ app.post('/api/login', async (req, res) => {
|
||||
try {
|
||||
const user = await authService.authenticate(login, password);
|
||||
if (user) {
|
||||
// Подготавливаем данные пользователя для сессии
|
||||
const sessionUser = {
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
@@ -326,17 +399,14 @@ app.post('/api/login', async (req, res) => {
|
||||
groups: user.groups ? (typeof user.groups === 'string' ? JSON.parse(user.groups) : user.groups) : []
|
||||
};
|
||||
|
||||
// Сохраняем в сессию
|
||||
req.session.user = sessionUser;
|
||||
|
||||
// Явно сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка сохранения сессии:', err);
|
||||
return res.status(500).json({ error: 'Ошибка сохранения сессии' });
|
||||
}
|
||||
|
||||
// Логируем успешный вход
|
||||
console.log(`Успешная авторизация: ${user.name} (${user.login}) через ${user.auth_type}`);
|
||||
if (user.groups) {
|
||||
console.log(`Группы пользователя: ${user.groups}`);
|
||||
@@ -367,10 +437,8 @@ app.post('/api/logout', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// В server.js обновите маршрут /api/user
|
||||
app.get('/api/user', (req, res) => {
|
||||
if (req.session.user) {
|
||||
// Для LDAP пользователей всегда проверяем актуальную роль
|
||||
if (req.session.user.auth_type === 'ldap') {
|
||||
db.get("SELECT groups FROM users WHERE id = ?", [req.session.user.id], (err, user) => {
|
||||
if (err || !user) {
|
||||
@@ -378,7 +446,6 @@ app.get('/api/user', (req, res) => {
|
||||
return res.status(401).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
// Парсим группы
|
||||
let groups = [];
|
||||
try {
|
||||
groups = JSON.parse(user.groups || '[]');
|
||||
@@ -386,31 +453,26 @@ app.get('/api/user', (req, res) => {
|
||||
groups = [];
|
||||
}
|
||||
|
||||
// Проверяем группы
|
||||
const allowedGroups = process.env.ALLOWED_GROUPS ?
|
||||
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
|
||||
|
||||
const isAdmin = groups.some(group => allowedGroups.includes(group));
|
||||
const actualRole = isAdmin ? 'admin' : 'teacher';
|
||||
|
||||
// Обновляем роль если изменилась
|
||||
if (req.session.user.role !== actualRole) {
|
||||
console.log(`Обновлена роль пользователя ${req.session.user.login} с ${req.session.user.role} на ${actualRole}`);
|
||||
|
||||
// Обновляем в базе
|
||||
db.run(
|
||||
"UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
[actualRole, req.session.user.id]
|
||||
);
|
||||
|
||||
// Обновляем в сессии
|
||||
req.session.user.role = actualRole;
|
||||
}
|
||||
|
||||
res.json({ user: req.session.user });
|
||||
});
|
||||
} else {
|
||||
// Для локальных пользователей просто возвращаем данные
|
||||
res.json({ user: req.session.user });
|
||||
}
|
||||
} else {
|
||||
@@ -418,8 +480,6 @@ app.get('/api/user', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== МАРШРУТЫ ПОЛЬЗОВАТЕЛЕЙ ====================
|
||||
|
||||
app.get('/api/users', requireAuth, (req, res) => {
|
||||
const search = req.query.search || '';
|
||||
|
||||
@@ -448,14 +508,14 @@ 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' && req.query.showDeleted === 'true';
|
||||
const search = req.query.search || '';
|
||||
const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework';
|
||||
const creatorFilter = req.query.creator || '';
|
||||
const assigneeFilter = req.query.assignee || '';
|
||||
const deadlineFilter = req.query.deadline || '';
|
||||
|
||||
let query = `
|
||||
SELECT DISTINCT
|
||||
@@ -477,7 +537,6 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
|
||||
const params = [];
|
||||
|
||||
// Для обычных пользователей показываем только задачи где они заказчик или исполнитель
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
|
||||
params.push(userId, userId);
|
||||
@@ -487,7 +546,6 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
query += " AND t.status = 'active'";
|
||||
}
|
||||
|
||||
// Фильтр по статусу
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
const statuses = statusFilter.split(',');
|
||||
|
||||
@@ -516,7 +574,32 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск по тексту
|
||||
if (creatorFilter) {
|
||||
query += ` AND t.created_by = ?`;
|
||||
params.push(creatorFilter);
|
||||
}
|
||||
|
||||
if (assigneeFilter) {
|
||||
query += ` AND ta.user_id = ?`;
|
||||
params.push(assigneeFilter);
|
||||
}
|
||||
|
||||
if (deadlineFilter) {
|
||||
const now = new Date();
|
||||
let hours = 48;
|
||||
if (deadlineFilter === '24h') hours = 24;
|
||||
|
||||
const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000);
|
||||
const deadlineISO = deadlineTime.toISOString();
|
||||
const nowISO = now.toISOString();
|
||||
|
||||
query += ` AND ta.due_date IS NOT NULL
|
||||
AND ta.due_date > ?
|
||||
AND ta.due_date <= ?
|
||||
AND ta.status NOT IN ('completed', 'overdue')`;
|
||||
params.push(nowISO, deadlineISO);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
|
||||
const searchPattern = `%${search}%`;
|
||||
@@ -545,7 +628,6 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем просрочку для каждого назначения
|
||||
assignments.forEach(assignment => {
|
||||
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
|
||||
assignment.status = 'overdue';
|
||||
@@ -564,19 +646,24 @@ app.get('/api/tasks', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Создать задачу
|
||||
app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
const { title, description, assignedUsers, originalTaskId, startDate, dueDate } = req.body;
|
||||
const { title, description, assignedUsers, originalTaskId, dueDate } = req.body;
|
||||
const createdBy = req.session.user.id;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: 'Название задачи обязательно' });
|
||||
}
|
||||
|
||||
if (!dueDate) {
|
||||
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
db.run(
|
||||
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[title, description, createdBy, originalTaskId || null, startDate || null, dueDate || null],
|
||||
[title, description, createdBy, originalTaskId || null, startDate, dueDate || null],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -585,7 +672,6 @@ app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
|
||||
const taskId = this.lastID;
|
||||
|
||||
// Создаем папку задачи и сохраняем метаданные
|
||||
saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate);
|
||||
|
||||
const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED';
|
||||
@@ -595,7 +681,6 @@ app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
|
||||
logActivity(taskId, createdBy, action, details);
|
||||
|
||||
// Обрабатываем файлы
|
||||
if (req.files && req.files.length > 0) {
|
||||
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
|
||||
|
||||
@@ -611,27 +696,24 @@ app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
logActivity(taskId, createdBy, '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) {
|
||||
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
|
||||
|
||||
userIds.forEach(userId => {
|
||||
db.run(
|
||||
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
|
||||
[taskId, userId, startDate || null, dueDate || null]
|
||||
[taskId, userId, startDate, dueDate || null]
|
||||
);
|
||||
|
||||
logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`);
|
||||
});
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам (создателю и исполнителям)
|
||||
sendTaskNotifications('created', taskId, title, description, createdBy);
|
||||
}
|
||||
|
||||
@@ -645,31 +727,32 @@ 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;
|
||||
const { assignedUsers, dueDate } = req.body;
|
||||
const createdBy = req.session.user.id;
|
||||
|
||||
// Проверяем доступ к оригинальной задаче
|
||||
if (!dueDate) {
|
||||
return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' });
|
||||
}
|
||||
|
||||
checkTaskAccess(createdBy, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
// Получаем данные оригинальной задачи
|
||||
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => {
|
||||
if (err || !originalTask) {
|
||||
return res.status(404).json({ error: 'Оригинальная задача не найдена' });
|
||||
}
|
||||
|
||||
// Создаем копию задачи
|
||||
const newTitle = `Копия: ${originalTask.title}`;
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
db.run(
|
||||
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[newTitle, originalTask.description, createdBy, taskId, startDate || null, dueDate || null],
|
||||
[newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -678,12 +761,10 @@ app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
|
||||
|
||||
const newTaskId = this.lastID;
|
||||
|
||||
// Создаем папку задачи и сохраняем метаданные
|
||||
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 => {
|
||||
@@ -692,7 +773,6 @@ app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
|
||||
const userFolder = createUserTaskFolder(newTaskId, req.session.user.login);
|
||||
const newFilePath = path.join(userFolder, newFilename);
|
||||
|
||||
// Копируем файл
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
fs.copyFileSync(originalFilePath, newFilePath);
|
||||
|
||||
@@ -707,18 +787,16 @@ app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Назначаем пользователей
|
||||
if (assignedUsers && assignedUsers.length > 0) {
|
||||
assignedUsers.forEach(userId => {
|
||||
db.run(
|
||||
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
|
||||
[newTaskId, userId, startDate || null, dueDate || null]
|
||||
[newTaskId, userId, startDate, dueDate || null]
|
||||
);
|
||||
});
|
||||
|
||||
logActivity(newTaskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователям: ${assignedUsers.join(', ')}`);
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам (создателю и исполнителям)
|
||||
sendTaskNotifications('created', newTaskId, newTitle, originalTask.description, createdBy);
|
||||
}
|
||||
|
||||
@@ -734,17 +812,19 @@ app.post('/api/tasks/:taskId/copy', 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 { title, description, assignedUsers, dueDate } = req.body;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: 'Название задачи обязательно' });
|
||||
}
|
||||
|
||||
// Проверяем права - только создатель или администратор могут редактировать
|
||||
if (!dueDate) {
|
||||
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
|
||||
}
|
||||
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
@@ -755,22 +835,19 @@ app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res)
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
// Обновляем задачу
|
||||
db.run(
|
||||
"UPDATE tasks SET title = ?, description = ?, start_date = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[title, description, startDate || null, dueDate || null, taskId],
|
||||
"UPDATE tasks SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[title, description, dueDate || null, taskId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем метаданные
|
||||
updateTaskMetadata(taskId, { title, description, start_date: startDate, due_date: dueDate });
|
||||
updateTaskMetadata(taskId, { title, description, due_date: dueDate });
|
||||
|
||||
logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`);
|
||||
|
||||
// Обрабатываем новые файлы
|
||||
if (req.files && req.files.length > 0) {
|
||||
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
|
||||
|
||||
@@ -786,37 +863,32 @@ app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res)
|
||||
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) {
|
||||
// Удаляем старые назначения
|
||||
db.run("DELETE FROM task_assignments WHERE task_id = ?", [taskId], (err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка удаления старых назначений:', err);
|
||||
}
|
||||
|
||||
// Добавляем новые назначения
|
||||
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
|
||||
userIds.forEach(userId => {
|
||||
const startDate = new Date().toISOString();
|
||||
db.run(
|
||||
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
|
||||
[taskId, userId, startDate || null, dueDate || null]
|
||||
[taskId, userId, startDate, dueDate || null]
|
||||
);
|
||||
});
|
||||
|
||||
logActivity(taskId, userId, 'TASK_ASSIGNMENTS_UPDATED', `Назначения обновлены`);
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам об обновлении
|
||||
sendTaskNotifications('updated', taskId, title, description, userId);
|
||||
});
|
||||
} else {
|
||||
// Если назначения не менялись, все равно отправляем уведомление ВСЕМ участникам об обновлении
|
||||
sendTaskNotifications('updated', taskId, title, description, userId);
|
||||
}
|
||||
|
||||
@@ -827,13 +899,11 @@ app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (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: 'Задача не найдена' });
|
||||
@@ -844,7 +914,6 @@ app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
// Обновляем задачу с комментарием
|
||||
db.run(
|
||||
"UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
[comment || 'Требуется доработка', taskId],
|
||||
@@ -854,7 +923,6 @@ app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем статусы всех назначений на 'rework'
|
||||
db.run(
|
||||
"UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?",
|
||||
[comment || 'Требуется доработка', taskId],
|
||||
@@ -866,7 +934,6 @@ app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
|
||||
|
||||
logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`);
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам о доработке
|
||||
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
|
||||
if (!err && taskData) {
|
||||
sendTaskNotifications('rework', taskId, taskData.title, taskData.description, userId, comment);
|
||||
@@ -882,12 +949,10 @@ app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Закрыть задачу
|
||||
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: 'Задача не найдена' });
|
||||
@@ -908,7 +973,6 @@ app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => {
|
||||
|
||||
logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`);
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам о закрытии
|
||||
db.get("SELECT title FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
|
||||
if (!err && taskData) {
|
||||
sendTaskNotifications('closed', taskId, taskData.title, '', userId);
|
||||
@@ -921,12 +985,10 @@ app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Открыть задачу (отменить закрытие)
|
||||
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: 'Задача не найдена' });
|
||||
@@ -952,13 +1014,15 @@ app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Обновить сроки для конкретного исполнителя
|
||||
app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
|
||||
const { taskId, userId } = req.params;
|
||||
const { startDate, dueDate } = req.body;
|
||||
const { dueDate } = req.body;
|
||||
const currentUserId = req.session.user.id;
|
||||
|
||||
// Проверяем права - только создатель или администратор могут редактировать сроки
|
||||
if (!dueDate) {
|
||||
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
|
||||
}
|
||||
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
@@ -969,8 +1033,8 @@ app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
|
||||
}
|
||||
|
||||
db.run(
|
||||
"UPDATE task_assignments SET start_date = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
|
||||
[startDate || null, dueDate || null, taskId, userId],
|
||||
"UPDATE task_assignments SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
|
||||
[dueDate || null, taskId, userId],
|
||||
function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -983,7 +1047,6 @@ app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
|
||||
|
||||
logActivity(taskId, currentUserId, 'ASSIGNMENT_UPDATED', `Обновлены сроки для пользователя ${userId}`);
|
||||
|
||||
// Отправляем уведомление ВСЕМ участникам об изменении сроков
|
||||
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
|
||||
if (!err && taskData) {
|
||||
sendTaskNotifications('updated', taskId, taskData.title, taskData.description, currentUserId);
|
||||
@@ -996,12 +1059,10 @@ app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Удалить задачу с проверкой прав
|
||||
app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Проверяем права - только создатель или администратор могут удалять
|
||||
db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
@@ -1011,7 +1072,6 @@ app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
return res.status(403).json({ error: 'У вас нет прав для удаления этой задачи' });
|
||||
}
|
||||
|
||||
// Помечаем задачу как удаленную
|
||||
db.run(
|
||||
"UPDATE tasks SET status = 'deleted', deleted_at = CURRENT_TIMESTAMP, deleted_by = ? WHERE id = ?",
|
||||
[userId, taskId],
|
||||
@@ -1021,7 +1081,6 @@ app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем метаданные
|
||||
updateTaskMetadata(taskId, {
|
||||
status: 'deleted',
|
||||
deleted_at: new Date().toISOString(),
|
||||
@@ -1036,12 +1095,10 @@ app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Восстановить задачу
|
||||
app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Только администратор может восстанавливать задачи
|
||||
if (req.session.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||
}
|
||||
@@ -1072,15 +1129,11 @@ app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
// ==================== МАРШРУТЫ СТАТУСОВ ====================
|
||||
|
||||
// Обновить статус задачи
|
||||
app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { userId: targetUserId, status } = req.body;
|
||||
const currentUserId = req.session.user.id;
|
||||
|
||||
// Проверяем, что пользователь обновляет свой статус
|
||||
if (parseInt(targetUserId) !== currentUserId) {
|
||||
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||
}
|
||||
@@ -1089,13 +1142,11 @@ app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
return res.status(400).json({ error: 'userId и status обязательны' });
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь назначен на эту задачу
|
||||
db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => {
|
||||
if (err || !assignment) {
|
||||
return res.status(403).json({ error: 'Вы не назначены на эту задачу' });
|
||||
}
|
||||
|
||||
// Получаем информацию о задаче и пользователе для уведомления
|
||||
db.get(`
|
||||
SELECT t.title, t.description, u.name as user_name
|
||||
FROM tasks t
|
||||
@@ -1106,7 +1157,6 @@ app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
console.error('Ошибка получения данных задачи:', err);
|
||||
}
|
||||
|
||||
// Если задача помечается как выполненная и она просрочена, оставляем статус completed
|
||||
const finalStatus = status === 'completed' ? 'completed' : status;
|
||||
|
||||
db.run(
|
||||
@@ -1125,7 +1175,6 @@ app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
|
||||
logActivity(taskId, targetUserId, 'STATUS_CHANGED', `Статус изменен на: ${finalStatus}`);
|
||||
|
||||
// Отправляем уведомления ВСЕМ участникам об изменении статуса
|
||||
if (taskData) {
|
||||
sendTaskNotifications(
|
||||
'status_changed',
|
||||
@@ -1146,9 +1195,6 @@ app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== МАРШРУТЫ ФАЙЛОВ ====================
|
||||
|
||||
// Добавить файлы к задаче
|
||||
app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
@@ -1157,7 +1203,6 @@ app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (re
|
||||
return res.status(400).json({ error: 'Нет файлов для загрузки' });
|
||||
}
|
||||
|
||||
// Проверяем доступ к задаче
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
|
||||
@@ -1176,12 +1221,10 @@ app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (re
|
||||
});
|
||||
});
|
||||
|
||||
// Получить файлы задачи
|
||||
app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
// Проверяем доступ к задаче
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
|
||||
@@ -1203,7 +1246,6 @@ app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Скачать файл
|
||||
app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
||||
const { fileId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
@@ -1213,7 +1255,6 @@ app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
||||
return res.status(404).json({ error: 'Файл не найдена' });
|
||||
}
|
||||
|
||||
// Проверяем доступ к задаче файла
|
||||
checkTaskAccess(userId, file.task_id, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(404).json({ error: 'Файл не найден или у вас нет прав доступа' });
|
||||
@@ -1228,8 +1269,6 @@ app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== МАРШРУТЫ ЛОГОВ ====================
|
||||
|
||||
app.get('/api/activity-logs', requireAuth, (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
|
||||
@@ -1241,7 +1280,6 @@ app.get('/api/activity-logs', requireAuth, (req, res) => {
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
// Для обычных пользователей показываем только логи их задач
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.created_by = ${userId} OR al.task_id IN (
|
||||
SELECT task_id FROM task_assignments WHERE user_id = ${userId}
|
||||
@@ -1259,7 +1297,6 @@ app.get('/api/activity-logs', requireAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Запуск сервера
|
||||
app.listen(PORT, () => {
|
||||
console.log(`CRM сервер запущен на порту ${PORT}`);
|
||||
console.log(`Откройте http://localhost:${PORT} в браузере`);
|
||||
@@ -1272,6 +1309,6 @@ app.listen(PORT, () => {
|
||||
console.log(`Разрешенные группы: ${process.env.ALLOWED_GROUPS}`);
|
||||
console.log('Система уведомлений активна');
|
||||
|
||||
// Запускаем проверку просроченных задач каждую минуту
|
||||
setInterval(checkOverdueTasks, 60000);
|
||||
setInterval(checkUpcomingDeadlines, 60000);
|
||||
});
|
||||
Reference in New Issue
Block a user