Files
minicrm/api-chat.js

506 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// api-chat.js - API для чата задач
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
module.exports = function(app, db, upload) {
// Получаем функции из database.js
let logActivity, checkTaskAccess;
// Пытаемся импортировать функции
try {
const dbModule = require('./database');
logActivity = dbModule.logActivity;
checkTaskAccess = dbModule.checkTaskAccess;
console.log('✅ Функции database.js загружены в api-chat');
} catch (error) {
console.error('❌ Ошибка загрузки функций из database.js:', error);
// Создаем заглушки
logActivity = (taskId, userId, action, details) => {
console.log(`[LOG] Task ${taskId}, User ${userId}: ${action} - ${details}`);
};
checkTaskAccess = (userId, taskId, callback) => {
// Заглушка - даем доступ всем
callback(null, true);
};
}
// Middleware для аутентификации
const requireAuth = (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
// GET /api/chat/tasks/:taskId/messages - Получить сообщения чата задачи
router.get('/api/chat/tasks/:taskId/messages', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
const { before, limit = 50 } = req.query;
console.log(`📨 Запрос сообщений для задачи ${taskId} от пользователя ${userId}`);
// Проверяем доступ к задаче
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err) {
console.error('❌ Ошибка проверки доступа:', err);
return res.status(500).json({ error: 'Ошибка проверки доступа' });
}
if (!hasAccess) {
return res.status(403).json({ error: 'Нет доступа к задаче' });
}
// Сначала проверим существование таблицы
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='task_chat_messages'", [], (tableErr, tableExists) => {
if (tableErr || !tableExists) {
console.error('❌ Таблица task_chat_messages не существует');
return res.status(500).json({ error: 'Таблица чата не создана', messages: [], hasMore: false });
}
let query = `
SELECT
m.*,
u.name as user_name,
u.login as user_login,
rm.message as reply_to_message,
ru.name as reply_to_user_name
FROM task_chat_messages m
LEFT JOIN users u ON m.user_id = u.id
LEFT JOIN task_chat_messages rm ON m.reply_to_id = rm.id
LEFT JOIN users ru ON rm.user_id = ru.id
WHERE m.task_id = ? AND m.is_deleted = 0
`;
const params = [taskId];
if (before) {
query += ` AND m.created_at < ?`;
params.push(before);
}
query += ` ORDER BY m.created_at DESC LIMIT ?`;
params.push(parseInt(limit));
db.all(query, params, (err, messages) => {
if (err) {
console.error('❌ Ошибка получения сообщений:', err);
return res.status(500).json({ error: 'Ошибка получения сообщений', details: err.message });
}
// Получаем файлы для каждого сообщения
const messageIds = messages.map(m => m.id);
if (messageIds.length === 0) {
return res.json({ messages: [], hasMore: false });
}
const placeholders = messageIds.map(() => '?').join(',');
db.all(`
SELECT * FROM task_chat_files
WHERE message_id IN (${placeholders})
`, messageIds, (fileErr, files) => {
if (fileErr) {
console.error('❌ Ошибка получения файлов:', fileErr);
}
// Группируем файлы по сообщениям
const filesByMessage = {};
if (files) {
files.forEach(file => {
if (!filesByMessage[file.message_id]) {
filesByMessage[file.message_id] = [];
}
filesByMessage[file.message_id].push(file);
});
}
// Добавляем файлы к сообщениям
const messagesWithFiles = messages.map(msg => ({
...msg,
files: filesByMessage[msg.id] || []
}));
// Помечаем сообщения как прочитанные
if (messagesWithFiles.length > 0) {
const unreadMessageIds = messagesWithFiles
.filter(m => m.user_id != userId)
.map(m => m.id);
if (unreadMessageIds.length > 0) {
const unreadPlaceholders = unreadMessageIds.map(() => '?').join(',');
db.run(`
INSERT OR IGNORE INTO task_chat_reads (message_id, user_id)
SELECT id, ? FROM task_chat_messages
WHERE id IN (${unreadPlaceholders})
`, [userId, ...unreadMessageIds], (readErr) => {
if (readErr) console.error('❌ Ошибка отметки прочитанных:', readErr);
});
}
}
res.json({
messages: messagesWithFiles,
hasMore: messages.length === parseInt(limit)
});
});
});
});
});
});
// POST /api/chat/tasks/:taskId/messages - Отправить сообщение
router.post('/api/chat/tasks/:taskId/messages', requireAuth, upload.array('files', 5), (req, res) => {
const { taskId } = req.params;
const { message, reply_to_id } = req.body;
const userId = req.session.user.id;
console.log(`📝 Отправка сообщения в задачу ${taskId} от пользователя ${userId}`);
if (!message || message.trim() === '') {
return res.status(400).json({ error: 'Сообщение не может быть пустым' });
}
// Проверяем доступ к задаче
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к задаче' });
}
// Проверяем существование таблицы
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='task_chat_messages'", [], (tableErr, tableExists) => {
if (tableErr || !tableExists) {
console.error('❌ Таблица task_chat_messages не существует');
return res.status(500).json({ error: 'Система чата не инициализирована' });
}
// Вставляем сообщение
db.run(
`INSERT INTO task_chat_messages (task_id, user_id, message, reply_to_id)
VALUES (?, ?, ?, ?)`,
[taskId, userId, message.trim(), reply_to_id || null],
function(err) {
if (err) {
console.error('❌ Ошибка создания сообщения:', err);
return res.status(500).json({ error: 'Ошибка создания сообщения' });
}
const messageId = this.lastID;
const uploadedFiles = [];
// Если есть файлы, сохраняем их
if (req.files && req.files.length > 0) {
const chatDir = path.join(__dirname, 'data', 'uploads', 'chat', taskId.toString());
if (!fs.existsSync(chatDir)) {
fs.mkdirSync(chatDir, { recursive: true });
}
let filesProcessed = 0;
req.files.forEach((file, index) => {
const fileExt = path.extname(file.originalname);
const fileName = `${messageId}_${Date.now()}_${index}${fileExt}`;
const filePath = path.join(chatDir, fileName);
try {
fs.renameSync(file.path, filePath);
} catch (renameErr) {
console.error('❌ Ошибка перемещения файла:', renameErr);
}
db.run(
`INSERT INTO task_chat_files (message_id, file_path, original_name, file_size, file_type)
VALUES (?, ?, ?, ?, ?)`,
[messageId, filePath, file.originalname, file.size, file.mimetype],
function(fileErr) {
if (!fileErr) {
uploadedFiles.push({
id: this.lastID,
original_name: file.originalname,
file_size: file.size,
file_type: file.mimetype
});
}
filesProcessed++;
if (filesProcessed === req.files.length) {
finishTransaction();
}
}
);
});
} else {
finishTransaction();
}
function finishTransaction() {
// Логируем действие
if (logActivity) {
logActivity(parseInt(taskId), userId, 'CHAT_MESSAGE', 'Отправлено сообщение в чат');
}
// Получаем полную информацию о сообщении
db.get(`
SELECT
m.*,
u.name as user_name,
u.login as user_login,
rm.message as reply_to_message,
ru.name as reply_to_user_name
FROM task_chat_messages m
LEFT JOIN users u ON m.user_id = u.id
LEFT JOIN task_chat_messages rm ON m.reply_to_id = rm.id
LEFT JOIN users ru ON rm.user_id = ru.id
WHERE m.id = ?
`, [messageId], (err, newMessage) => {
if (err) {
console.error('❌ Ошибка получения сообщения:', err);
return res.json({
success: true,
messageId,
files: uploadedFiles
});
}
newMessage.files = uploadedFiles;
// Отправляем уведомления участникам задачи
// notifyTaskParticipants(taskId, userId, newMessage);
res.json({
success: true,
message: newMessage
});
});
}
}
);
});
});
});
// PUT /api/chat/messages/:messageId - Редактировать сообщение
router.put('/api/chat/messages/:messageId', requireAuth, (req, res) => {
const { messageId } = req.params;
const { message } = req.body;
const userId = req.session.user.id;
if (!message || message.trim() === '') {
return res.status(400).json({ error: 'Сообщение не может быть пустым' });
}
db.get(
'SELECT task_id, user_id FROM task_chat_messages WHERE id = ? AND is_deleted = 0',
[messageId],
(err, msg) => {
if (err || !msg) {
return res.status(404).json({ error: 'Сообщение не найдено' });
}
// Проверяем, что пользователь - автор сообщения или админ
if (parseInt(msg.user_id) !== parseInt(userId) && req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для редактирования этого сообщения' });
}
db.run(
`UPDATE task_chat_messages
SET message = ?, is_edited = 1, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[message.trim(), messageId],
function(err) {
if (err) {
console.error('❌ Ошибка редактирования сообщения:', err);
return res.status(500).json({ error: 'Ошибка редактирования сообщения' });
}
if (logActivity) {
logActivity(msg.task_id, userId, 'CHAT_MESSAGE_EDITED', 'Сообщение отредактировано');
}
res.json({
success: true,
message: 'Сообщение отредактировано'
});
}
);
}
);
});
// DELETE /api/chat/messages/:messageId - Удалить сообщение (soft delete)
router.delete('/api/chat/messages/:messageId', requireAuth, (req, res) => {
const { messageId } = req.params;
const userId = req.session.user.id;
db.get(
'SELECT task_id, user_id FROM task_chat_messages WHERE id = ? AND is_deleted = 0',
[messageId],
(err, msg) => {
if (err || !msg) {
return res.status(404).json({ error: 'Сообщение не найдено' });
}
// Проверяем, что пользователь - автор сообщения или админ
if (parseInt(msg.user_id) !== parseInt(userId) && req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для удаления этого сообщения' });
}
db.run(
'UPDATE task_chat_messages SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[messageId],
function(err) {
if (err) {
console.error('❌ Ошибка удаления сообщения:', err);
return res.status(500).json({ error: 'Ошибка удаления сообщения' });
}
if (logActivity) {
logActivity(msg.task_id, userId, 'CHAT_MESSAGE_DELETED', 'Сообщение удалено');
}
res.json({
success: true,
message: 'Сообщение удалено'
});
}
);
}
);
});
// GET /api/chat/tasks/:taskId/unread-count - Получить количество непрочитанных сообщений
router.get('/api/chat/tasks/:taskId/unread-count', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к задаче' });
}
db.get(`
SELECT COUNT(*) as unread_count
FROM task_chat_messages m
LEFT JOIN task_chat_reads r ON m.id = r.message_id AND r.user_id = ?
WHERE m.task_id = ?
AND m.user_id != ?
AND m.is_deleted = 0
AND r.id IS NULL
`, [userId, taskId, userId], (err, result) => {
if (err) {
console.error('❌ Ошибка подсчета непрочитанных:', err);
return res.status(500).json({ error: 'Ошибка подсчета' });
}
res.json({ unread_count: result?.unread_count || 0 });
});
});
});
// POST /api/chat/tasks/:taskId/mark-read - Отметить все сообщения как прочитанные
router.post('/api/chat/tasks/:taskId/mark-read', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к задаче' });
}
db.run(`
INSERT OR IGNORE INTO task_chat_reads (message_id, user_id)
SELECT id, ? FROM task_chat_messages
WHERE task_id = ? AND user_id != ?
`, [userId, taskId, userId], function(err) {
if (err) {
console.error('❌ Ошибка отметки прочитанных:', err);
return res.status(500).json({ error: 'Ошибка отметки' });
}
res.json({
success: true,
marked_count: this.changes
});
});
});
});
// GET /api/chat/files/:fileId/download - Скачать файл из чата
router.get('/api/chat/files/:fileId/download', requireAuth, (req, res) => {
const { fileId } = req.params;
const userId = req.session.user.id;
db.get(`
SELECT f.*, m.task_id
FROM task_chat_files f
JOIN task_chat_messages m ON f.message_id = m.id
WHERE f.id = ?
`, [fileId], (err, file) => {
if (err || !file) {
return res.status(404).json({ error: 'Файл не найден' });
}
checkTaskAccess(userId, file.task_id, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к файлу' });
}
if (!fs.existsSync(file.file_path)) {
return res.status(404).json({ error: 'Файл не найден на сервере' });
}
const encodedFileName = encodeURIComponent(file.original_name);
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Type', file.file_type || 'application/octet-stream');
res.sendFile(file.file_path);
});
});
});
// Вспомогательная функция для уведомлений участников
function notifyTaskParticipants(taskId, senderId, message) {
db.all(`
SELECT DISTINCT user_id
FROM task_assignments
WHERE task_id = ? AND user_id != ?
UNION
SELECT created_by as user_id
FROM tasks
WHERE id = ? AND created_by != ?
`, [taskId, senderId, taskId, senderId], (err, participants) => {
if (err || !participants || participants.length === 0) return;
// Пытаемся импортировать функцию уведомлений
try {
const { sendTaskNotifications } = require('./notifications');
participants.forEach(p => {
try {
sendTaskNotifications(
'chat_message',
taskId,
'Новое сообщение в чате',
message.message.substring(0, 100),
senderId,
'',
'chat',
message.user_name,
p.user_id
);
} catch (notifyErr) {
console.error('Ошибка отправки уведомления:', notifyErr);
}
});
} catch (importErr) {
console.log('Модуль уведомлений не загружен');
}
});
}
// Подключаем роутер
app.use(router);
console.log('✅ API для чата задач подключено');
};