${assignment.user_name}
${isCurrentUser ? '
(Вы)' : ''}
+ ${timeLeftInfo ? `
${timeLeftInfo.text}` : ''}
${assignment.start_date || assignment.due_date ? `
- ${assignment.start_date ? `Начать: ${formatDateTime(assignment.start_date)}` : ''}
+ ${assignment.start_date ? `Начало: ${formatDateTime(assignment.start_date)}` : ''}
${assignment.due_date ? `Выполнить до: ${formatDateTime(assignment.due_date)}` : ''}
` : ''}
@@ -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;
}
diff --git a/server.js b/server.js
index 5ed5592..270cb7a 100644
--- a/server.js
+++ b/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);
});
\ No newline at end of file