чат
This commit is contained in:
506
api-chat.js
Normal file
506
api-chat.js
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
// 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 для чата задач подключено');
|
||||||
|
};
|
||||||
46
database.js
46
database.js
@@ -500,7 +500,45 @@ function createSQLiteTables() {
|
|||||||
)`);
|
)`);
|
||||||
|
|
||||||
console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
|
console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
|
||||||
|
// Таблица для сообщений чата задач
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS task_chat_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_edited BOOLEAN DEFAULT false,
|
||||||
|
is_deleted BOOLEAN DEFAULT false,
|
||||||
|
reply_to_id INTEGER,
|
||||||
|
FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (reply_to_id) REFERENCES task_chat_messages (id) ON DELETE SET NULL
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для файлов в сообщениях
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS task_chat_files (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id INTEGER NOT NULL,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
original_name TEXT NOT NULL,
|
||||||
|
file_size INTEGER NOT NULL,
|
||||||
|
file_type TEXT,
|
||||||
|
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Таблица для прочитанных сообщений
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS task_chat_reads (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
read_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(message_id, user_id),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
)`);
|
||||||
|
console.log('✅ Таблица для сообщений чата задач созданы');
|
||||||
// Создаем индексы для улучшения производительности
|
// Создаем индексы для улучшения производительности
|
||||||
createSQLiteIndexes();
|
createSQLiteIndexes();
|
||||||
|
|
||||||
@@ -623,7 +661,13 @@ function createSQLiteIndexes() {
|
|||||||
|
|
||||||
// Индексы для простых документов
|
// Индексы для простых документов
|
||||||
"CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)",
|
"CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)"
|
"CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)",
|
||||||
|
// Индексы для оптимизации
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_task_chat_messages_task_id ON task_chat_messages(task_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_task_chat_messages_created_at ON task_chat_messages(created_at)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_task_chat_files_message_id ON task_chat_files(message_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_task_chat_reads_message_id ON task_chat_reads(message_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_task_chat_reads_user_id ON task_chat_reads(user_id)"
|
||||||
];
|
];
|
||||||
|
|
||||||
indexes.forEach(indexQuery => {
|
indexes.forEach(indexQuery => {
|
||||||
|
|||||||
817
public/chat-ui.js
Normal file
817
public/chat-ui.js
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
// chat-ui.js - Клиентская часть для чата задач
|
||||||
|
|
||||||
|
class TaskChat {
|
||||||
|
constructor(taskId, taskTitle) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
this.taskTitle = taskTitle;
|
||||||
|
this.messages = [];
|
||||||
|
this.currentUserId = null;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.hasMore = true;
|
||||||
|
this.lastMessageDate = null;
|
||||||
|
this.replyToMessage = null;
|
||||||
|
this.autoRefreshInterval = null;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadCurrentUser();
|
||||||
|
this.createChatModal();
|
||||||
|
this.loadMessages();
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCurrentUser() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user');
|
||||||
|
const data = await response.json();
|
||||||
|
this.currentUserId = data.user.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки пользователя:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createChatModal() {
|
||||||
|
const modalHtml = `
|
||||||
|
<div class="modal" id="task-chat-modal-${this.taskId}">
|
||||||
|
<div class="modal-content chat-modal-content">
|
||||||
|
<div class="modal-header chat-header">
|
||||||
|
<div class="chat-header-actions">
|
||||||
|
<span class="chat-unread-badge" id="chat-unread-${this.taskId}" style="display: none;">0</span>
|
||||||
|
<button class="chat-refresh-btn" onclick="window.taskChats[${this.taskId}].refreshMessages()" title="Обновить">🔄</button>
|
||||||
|
<span class="close" onclick="window.taskChats[${this.taskId}].close()">×</span>
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
<span class="chat-icon">💬</span>
|
||||||
|
Чат задачи №${this.taskId}: "${this.taskTitle}"
|
||||||
|
</h3>
|
||||||
|
<h3>
|
||||||
|
<span class="chat-icon"> </span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body chat-body" style="padding: 0; display: flex; flex-direction: column; height: 500px;">
|
||||||
|
<div class="chat-messages-container" id="chat-messages-${this.taskId}">
|
||||||
|
<div class="chat-messages" id="chat-messages-list-${this.taskId}">
|
||||||
|
<div class="chat-loading" style="display: none;">Загрузка сообщений...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-reply-info" id="chat-reply-info-${this.taskId}" style="display: none;">
|
||||||
|
<span>Ответ на сообщение <span id="reply-to-text-${this.taskId}"></span></span>
|
||||||
|
<button class="chat-cancel-reply" onclick="window.taskChats[${this.taskId}].cancelReply()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-input-area">
|
||||||
|
<textarea
|
||||||
|
class="chat-input"
|
||||||
|
id="chat-input-${this.taskId}"
|
||||||
|
placeholder="Напишите сообщение..."
|
||||||
|
rows="1"
|
||||||
|
></textarea>
|
||||||
|
<div class="chat-attachments" id="chat-attachments-${this.taskId}"></div>
|
||||||
|
<div class="chat-actions">
|
||||||
|
<button class="chat-send-btn" onclick="window.taskChats[${this.taskId}].sendMessage()">➤</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Добавляем стили для чата
|
||||||
|
this.addChatStyles();
|
||||||
|
|
||||||
|
// Добавляем модальное окно в DOM
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.innerHTML = modalHtml;
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
// Показываем модальное окно
|
||||||
|
setTimeout(() => {
|
||||||
|
const modal = document.getElementById(`task-chat-modal-${this.taskId}`);
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
// Фокус на поле ввода
|
||||||
|
document.getElementById(`chat-input-${this.taskId}`).focus();
|
||||||
|
|
||||||
|
// Настройка авто-изменения высоты textarea
|
||||||
|
this.setupTextareaAutoResize();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addChatStyles() {
|
||||||
|
if (document.getElementById('chat-styles')) return;
|
||||||
|
|
||||||
|
const styles = `
|
||||||
|
<style id="chat-styles">
|
||||||
|
.chat-modal-content {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 90%;
|
||||||
|
height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-unread-badge {
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-refresh-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 8px; /* Увеличили область клика */
|
||||||
|
border-radius: 4px; /* Скругленные углы при наведении */
|
||||||
|
transition: background-color 0.2s; /* Плавный переход цвета */
|
||||||
|
line-height: 1; /* Фиксируем высоту строки */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-refresh-btn:hover {
|
||||||
|
background-color: rgba(0,0,0,0.05); /* Легкий серый фон при наведении */
|
||||||
|
opacity: 1; /* Убираем прозрачность или оставляем как есть */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
position: relative;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-own {
|
||||||
|
align-self: flex-end;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-other {
|
||||||
|
align-self: flex-start;
|
||||||
|
background-color: white;
|
||||||
|
color: #333;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-own .chat-message-header {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-other .chat-message-header {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-author {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-edited {
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-files {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-file {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-own .chat-file {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-file:hover {
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-reply-info {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-cancel-reply {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-attachments {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-attachment {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-remove-attachment {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-attach-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-no-messages {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.head.insertAdjacentHTML('beforeend', styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTextareaAutoResize() {
|
||||||
|
const textarea = document.getElementById(`chat-input-${this.taskId}`);
|
||||||
|
textarea.addEventListener('input', function() {
|
||||||
|
this.style.height = 'auto';
|
||||||
|
this.style.height = (this.scrollHeight) + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Отправка по Enter (но не с Shift)
|
||||||
|
const textarea = document.getElementById(`chat-input-${this.taskId}`);
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загрузка файлов
|
||||||
|
const fileInput = document.getElementById(`chat-file-input-${this.taskId}`);
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
this.handleFileSelect(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Бесконечная прокрутка для загрузки старых сообщений
|
||||||
|
const messagesContainer = document.getElementById(`chat-messages-${this.taskId}`);
|
||||||
|
messagesContainer.addEventListener('scroll', () => {
|
||||||
|
if (messagesContainer.scrollTop === 0 && !this.isLoading && this.hasMore) {
|
||||||
|
this.loadMoreMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect(files) {
|
||||||
|
const attachmentsContainer = document.getElementById(`chat-attachments-${this.taskId}`);
|
||||||
|
attachmentsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
this.selectedFiles = Array.from(files);
|
||||||
|
|
||||||
|
this.selectedFiles.forEach((file, index) => {
|
||||||
|
const attachment = document.createElement('div');
|
||||||
|
attachment.className = 'chat-attachment';
|
||||||
|
attachment.innerHTML = `
|
||||||
|
📎 ${file.name} (${this.formatFileSize(file.size)})
|
||||||
|
<button class="chat-remove-attachment" onclick="window.taskChats[${this.taskId}].removeAttachment(${index})">✕</button>
|
||||||
|
`;
|
||||||
|
attachmentsContainer.appendChild(attachment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAttachment(index) {
|
||||||
|
if (this.selectedFiles) {
|
||||||
|
this.selectedFiles.splice(index, 1);
|
||||||
|
this.handleFileSelect(this.selectedFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMessages(before = null) {
|
||||||
|
if (this.isLoading) return;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
const loadingEl = document.querySelector(`#chat-messages-list-${this.taskId} .chat-loading`);
|
||||||
|
if (loadingEl) loadingEl.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = `/api/chat/tasks/${this.taskId}/messages?limit=30`;
|
||||||
|
if (before) {
|
||||||
|
url += `&before=${before}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.messages && data.messages.length > 0) {
|
||||||
|
if (before) {
|
||||||
|
// Добавляем старые сообщения в начало
|
||||||
|
this.messages = [...data.messages.reverse(), ...this.messages];
|
||||||
|
} else {
|
||||||
|
// Первая загрузка
|
||||||
|
this.messages = data.messages.reverse();
|
||||||
|
}
|
||||||
|
this.hasMore = data.hasMore;
|
||||||
|
this.renderMessages();
|
||||||
|
|
||||||
|
if (!before) {
|
||||||
|
// Прокручиваем вниз при первой загрузке
|
||||||
|
this.scrollToBottom();
|
||||||
|
} else {
|
||||||
|
// Сохраняем позицию прокрутки при загрузке старых сообщений
|
||||||
|
const container = document.getElementById(`chat-messages-${this.taskId}`);
|
||||||
|
const oldHeight = container.scrollHeight;
|
||||||
|
setTimeout(() => {
|
||||||
|
container.scrollTop = container.scrollHeight - oldHeight;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!before) {
|
||||||
|
this.renderEmpty();
|
||||||
|
}
|
||||||
|
this.hasMore = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки сообщений:', error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
if (loadingEl) loadingEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMoreMessages() {
|
||||||
|
if (this.messages.length > 0) {
|
||||||
|
const oldestMessage = this.messages[0];
|
||||||
|
await this.loadMessages(oldestMessage.created_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMessages() {
|
||||||
|
const messagesList = document.getElementById(`chat-messages-list-${this.taskId}`);
|
||||||
|
messagesList.innerHTML = '<div class="chat-loading" style="display: none;">Загрузка сообщений...</div>';
|
||||||
|
|
||||||
|
if (this.messages.length === 0) {
|
||||||
|
this.renderEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.forEach(message => {
|
||||||
|
const messageEl = this.createMessageElement(message);
|
||||||
|
messagesList.appendChild(messageEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createMessageElement(message) {
|
||||||
|
const isOwn = message.user_id === this.currentUserId;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `chat-message ${isOwn ? 'chat-message-own' : 'chat-message-other'}`;
|
||||||
|
div.dataset.messageId = message.id;
|
||||||
|
|
||||||
|
const time = new Date(message.created_at).toLocaleString('ru-RU', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
let replyHtml = '';
|
||||||
|
if (message.reply_to_message) {
|
||||||
|
replyHtml = `
|
||||||
|
<div class="chat-message-reply">
|
||||||
|
↪ Ответ ${message.reply_to_user_name}: "${message.reply_to_message.substring(0, 30)}${message.reply_to_message.length > 30 ? '...' : ''}"
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filesHtml = '';
|
||||||
|
if (message.files && message.files.length > 0) {
|
||||||
|
filesHtml = '<div class="chat-message-files">';
|
||||||
|
message.files.forEach(file => {
|
||||||
|
filesHtml += `
|
||||||
|
<div class="chat-file" onclick="window.taskChats[${this.taskId}].downloadFile(${file.id})">
|
||||||
|
📎 ${file.original_name} (${this.formatFileSize(file.file_size)})
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
filesHtml += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionsHtml = '';
|
||||||
|
if (isOwn || window.currentUserRole === 'admin') {
|
||||||
|
actionsHtml = `
|
||||||
|
<div class="chat-message-actions">
|
||||||
|
${isOwn ? `<button onclick="window.taskChats[${this.taskId}].editMessage(${message.id})" title="Редактировать">✎</button>` : ''}
|
||||||
|
<button onclick="window.taskChats[${this.taskId}].deleteMessage(${message.id})" title="Удалить">🗑️</button>
|
||||||
|
<!-- <button onclick="window.taskChats[${this.taskId}].replyToMessage(${message.id}, '${message.user_name.replace(/'/g, "\\'")}', '${message.message.replace(/'/g, "\\'").substring(0, 30)}')" title="Ответить">↩</button> -->
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
${replyHtml}
|
||||||
|
<div class="chat-message-header">
|
||||||
|
<span class="chat-message-author">${message.user_name}</span>
|
||||||
|
<span class="chat-message-time">${time}</span>
|
||||||
|
${message.is_edited ? '<span class="chat-message-edited">(ред.)</span>' : ''}
|
||||||
|
${actionsHtml}
|
||||||
|
</div>
|
||||||
|
<div class="chat-message-text">${this.escapeHtml(message.message)}</div>
|
||||||
|
${filesHtml}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEmpty() {
|
||||||
|
const messagesList = document.getElementById(`chat-messages-list-${this.taskId}`);
|
||||||
|
messagesList.innerHTML = `
|
||||||
|
<div class="chat-no-messages">
|
||||||
|
💬 Нет сообщений. Напишите что-нибудь...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const input = document.getElementById(`chat-input-${this.taskId}`);
|
||||||
|
const message = input.value.trim();
|
||||||
|
|
||||||
|
if (!message && (!this.selectedFiles || this.selectedFiles.length === 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('message', message);
|
||||||
|
|
||||||
|
if (this.replyToMessage) {
|
||||||
|
formData.append('reply_to_id', this.replyToMessage.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedFiles && this.selectedFiles.length > 0) {
|
||||||
|
this.selectedFiles.forEach(file => {
|
||||||
|
formData.append('files', file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/tasks/${this.taskId}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
input.value = '';
|
||||||
|
input.style.height = 'auto';
|
||||||
|
this.selectedFiles = [];
|
||||||
|
document.getElementById(`chat-attachments-${this.taskId}`).innerHTML = '';
|
||||||
|
this.cancelReply();
|
||||||
|
|
||||||
|
// Добавляем новое сообщение
|
||||||
|
this.messages.push(data.message);
|
||||||
|
this.renderMessages();
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка отправки сообщения:', error);
|
||||||
|
alert('Ошибка отправки сообщения');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editMessage(messageId) {
|
||||||
|
const message = this.messages.find(m => m.id === messageId);
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const newText = prompt('Редактировать сообщение:', message.message);
|
||||||
|
if (newText && newText.trim() !== message.message) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/messages/${messageId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message: newText.trim() })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
message.message = newText.trim();
|
||||||
|
message.is_edited = true;
|
||||||
|
this.renderMessages();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка редактирования:', error);
|
||||||
|
alert('Ошибка редактирования сообщения');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMessage(messageId) {
|
||||||
|
if (!confirm('Удалить это сообщение?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/messages/${messageId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
this.messages = this.messages.filter(m => m.id !== messageId);
|
||||||
|
this.renderMessages();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления:', error);
|
||||||
|
alert('Ошибка удаления сообщения');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replyToMessage(messageId, userName, messagePreview) {
|
||||||
|
this.replyToMessage = {
|
||||||
|
id: messageId,
|
||||||
|
userName: userName,
|
||||||
|
preview: messagePreview
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyInfo = document.getElementById(`chat-reply-info-${this.taskId}`);
|
||||||
|
document.getElementById(`reply-to-text-${this.taskId}`).textContent =
|
||||||
|
`${userName}: "${messagePreview}${messagePreview.length > 30 ? '...' : ''}"`;
|
||||||
|
replyInfo.style.display = 'flex';
|
||||||
|
|
||||||
|
document.getElementById(`chat-input-${this.taskId}`).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelReply() {
|
||||||
|
this.replyToMessage = null;
|
||||||
|
document.getElementById(`chat-reply-info-${this.taskId}`).style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadFile(fileId) {
|
||||||
|
window.open(`/api/chat/files/${fileId}/download`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const container = document.getElementById(`chat-messages-${this.taskId}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAutoRefresh() {
|
||||||
|
// Обновляем непрочитанные каждые 10 секунд
|
||||||
|
this.autoRefreshInterval = setInterval(() => {
|
||||||
|
this.updateUnreadCount();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUnreadCount() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/tasks/${this.taskId}/unread-count`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const badge = document.getElementById(`chat-unread-${this.taskId}`);
|
||||||
|
if (data.unread_count > 0) {
|
||||||
|
badge.textContent = data.unread_count;
|
||||||
|
badge.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
badge.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка обновления непрочитанных:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead() {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/chat/tasks/${this.taskId}/mark-read`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
document.getElementById(`chat-unread-${this.taskId}`).style.display = 'none';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка отметки прочитанных:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMessages() {
|
||||||
|
this.messages = [];
|
||||||
|
this.loadMessages();
|
||||||
|
this.markAllAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.autoRefreshInterval) {
|
||||||
|
clearInterval(this.autoRefreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById(`task-chat-modal-${this.taskId}`);
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.parentElement.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальный объект для хранения экземпляров чатов
|
||||||
|
window.taskChats = window.taskChats || {};
|
||||||
|
|
||||||
|
// Функция для открытия чата (заменяет существующую openTaskChat)
|
||||||
|
function openTaskChat(taskId) {
|
||||||
|
// Находим задачу
|
||||||
|
const task = window.tasks?.find(t => t.id === taskId);
|
||||||
|
|
||||||
|
// Если уже есть открытый чат для этой задачи, просто показываем его
|
||||||
|
if (window.taskChats[taskId]) {
|
||||||
|
const existingModal = document.getElementById(`task-chat-modal-${taskId}`);
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.style.display = 'block';
|
||||||
|
window.taskChats[taskId].refreshMessages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новый экземпляр чата
|
||||||
|
window.taskChats[taskId] = new TaskChat(taskId, task ? task.title : `Задача #${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для закрытия чата
|
||||||
|
function closeTaskChat(taskId) {
|
||||||
|
if (window.taskChats[taskId]) {
|
||||||
|
window.taskChats[taskId].close();
|
||||||
|
delete window.taskChats[taskId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке страницы
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Получаем роль текущего пользователя для прав доступа
|
||||||
|
fetch('/api/user')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
window.currentUserRole = data.user.role;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Ошибка загрузки пользователя:', error));
|
||||||
|
});
|
||||||
@@ -449,6 +449,7 @@
|
|||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
<script src="tasks_files.js"></script>
|
<script src="tasks_files.js"></script>
|
||||||
<script src="navbar.js"></script>
|
<script src="navbar.js"></script>
|
||||||
|
<script src="chat-ui.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -20,6 +20,8 @@ const userManagementAPI = require('./api-users');
|
|||||||
//
|
//
|
||||||
const api2Groups = require('./api2-groups');
|
const api2Groups = require('./api2-groups');
|
||||||
//
|
//
|
||||||
|
const chatAPI = require('./api-chat');
|
||||||
|
//
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
@@ -1461,7 +1463,9 @@ initializeServer().then(() => {
|
|||||||
console.log('🔐 LDAP авторизация доступна для пользователей школы');
|
console.log('🔐 LDAP авторизация доступна для пользователей школы');
|
||||||
console.log(`👥 Разрешенные группы: ${process.env.ALLOWED_GROUPS}`);
|
console.log(`👥 Разрешенные группы: ${process.env.ALLOWED_GROUPS}`);
|
||||||
console.log('📢 Система уведомлений активна');
|
console.log('📢 Система уведомлений активна');
|
||||||
|
// Подключаем API для чата
|
||||||
|
chatAPI(app, db, upload);
|
||||||
|
console.log('✅ API для чата задач подключено');
|
||||||
// Запускаем фоновые задачи
|
// Запускаем фоновые задачи
|
||||||
setInterval(checkOverdueTasks, 60000);
|
setInterval(checkOverdueTasks, 60000);
|
||||||
setInterval(checkUpcomingDeadlines, 60000);
|
setInterval(checkUpcomingDeadlines, 60000);
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
// task-endpoints.js
|
// task-endpoints.js
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
// Функция для добавления кнопки чата в HTML задачи
|
||||||
|
function addChatButtonToTask(taskElement, taskId) {
|
||||||
|
const chatButton = document.createElement('button');
|
||||||
|
chatButton.className = 'task-chat-btn';
|
||||||
|
chatButton.innerHTML = '💬 Чат';
|
||||||
|
chatButton.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openTaskChat(taskId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавляем кнопку в шапку задачи или в actions
|
||||||
|
const header = taskElement.querySelector('.task-header');
|
||||||
|
if (header) {
|
||||||
|
header.appendChild(chatButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
function getApproverUsers(groupId) {
|
function getApproverUsers(groupId) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
db.all(`
|
db.all(`
|
||||||
|
|||||||
603
test-bd.js
Normal file
603
test-bd.js
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
// test-bd.js - Универсальная проверка и исправление структуры базы данных
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Цвета для вывода в консоль
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
cyan: '\x1b[36m'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`${colors.cyan}🔍 ЗАПУСК ПРОВЕРКИ БАЗЫ ДАННЫХ${colors.reset}`);
|
||||||
|
console.log('=' .repeat(60));
|
||||||
|
|
||||||
|
// Путь к базе данных
|
||||||
|
const dbPath = path.join(__dirname, 'data', 'school_crm.db');
|
||||||
|
console.log(`${colors.blue}📁 База данных:${colors.reset} ${dbPath}`);
|
||||||
|
|
||||||
|
// Проверяем существование файла базы данных
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
console.log(`${colors.yellow}⚠️ Файл базы данных не найден. Он будет создан при первом запуске.${colors.reset}`);
|
||||||
|
} else {
|
||||||
|
const stats = fs.statSync(dbPath);
|
||||||
|
console.log(`${colors.green}✅ Файл базы данных существует (${(stats.size / 1024).toFixed(2)} KB)${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подключаемся к базе данных
|
||||||
|
const db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
// Определяем ожидаемую структуру всех таблиц
|
||||||
|
const expectedTables = {
|
||||||
|
// Основные таблицы пользователей
|
||||||
|
users: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'login', type: 'TEXT UNIQUE NOT NULL' },
|
||||||
|
{ name: 'password', type: 'TEXT' },
|
||||||
|
{ name: 'name', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'email', type: 'TEXT UNIQUE NOT NULL' },
|
||||||
|
{ name: 'role', type: 'TEXT DEFAULT "teacher"' },
|
||||||
|
{ name: 'auth_type', type: 'TEXT DEFAULT "local"' },
|
||||||
|
{ name: 'groups', type: 'TEXT' },
|
||||||
|
{ name: 'description', type: 'TEXT' },
|
||||||
|
{ name: 'avatar', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'last_login', type: 'DATETIME' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_users_login ON users(login)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Таблица задач
|
||||||
|
tasks: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'title', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'description', type: 'TEXT' },
|
||||||
|
{ name: 'status', type: 'TEXT DEFAULT "active"' },
|
||||||
|
{ name: 'created_by', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'deleted_at', type: 'DATETIME' },
|
||||||
|
{ name: 'deleted_by', type: 'INTEGER' },
|
||||||
|
{ name: 'original_task_id', type: 'INTEGER' },
|
||||||
|
{ name: 'start_date', type: 'DATETIME' },
|
||||||
|
{ name: 'due_date', type: 'DATETIME' },
|
||||||
|
{ name: 'rework_comment', type: 'TEXT' },
|
||||||
|
{ name: 'closed_at', type: 'DATETIME' },
|
||||||
|
{ name: 'closed_by', type: 'INTEGER' },
|
||||||
|
{ name: 'task_type', type: 'TEXT DEFAULT "regular"' },
|
||||||
|
{ name: 'type', type: 'TEXT' },
|
||||||
|
{ name: 'approver_group_id', type: 'INTEGER' },
|
||||||
|
{ name: 'document_id', type: 'INTEGER' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_tasks_original_task_id ON tasks(original_task_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Назначения задач
|
||||||
|
task_assignments: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'status', type: 'TEXT DEFAULT "assigned"' },
|
||||||
|
{ name: 'start_date', type: 'DATETIME' },
|
||||||
|
{ name: 'due_date', type: 'DATETIME' },
|
||||||
|
{ name: 'rework_comment', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_task_assignments_unique ON task_assignments(task_id, user_id) WHERE status != "deleted"'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Файлы задач
|
||||||
|
task_files: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'filename', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'original_name', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'file_path', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'file_size', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_files_user_id ON task_files(user_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Логи активности
|
||||||
|
activity_logs: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'action', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'details', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Группы пользователей
|
||||||
|
user_groups: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' },
|
||||||
|
{ name: 'description', type: 'TEXT' },
|
||||||
|
{ name: 'color', type: 'TEXT DEFAULT "#3498db"' },
|
||||||
|
{ name: 'can_approve_documents', type: 'BOOLEAN DEFAULT 0' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_user_groups_name ON user_groups(name)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Членство в группах
|
||||||
|
user_group_memberships: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'group_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_memberships_unique ON user_group_memberships(user_id, group_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Типы документов (простые)
|
||||||
|
simple_document_types: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'name', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'description', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_simple_document_types_name ON simple_document_types(name)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Документы (простые)
|
||||||
|
simple_documents: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'document_type_id', type: 'INTEGER' },
|
||||||
|
{ name: 'document_number', type: 'TEXT' },
|
||||||
|
{ name: 'document_date', type: 'DATE' },
|
||||||
|
{ name: 'pages_count', type: 'INTEGER' },
|
||||||
|
{ name: 'urgency_level', type: 'TEXT CHECK(urgency_level IN ("normal", "urgent", "very_urgent"))' },
|
||||||
|
{ name: 'comment', type: 'TEXT' },
|
||||||
|
{ name: 'refusal_reason', type: 'TEXT' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Настройки пользователей
|
||||||
|
user_settings: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' },
|
||||||
|
{ name: 'email_notifications', type: 'BOOLEAN DEFAULT 1' },
|
||||||
|
{ name: 'notification_email', type: 'TEXT' },
|
||||||
|
{ name: 'telegram_notifications', type: 'BOOLEAN DEFAULT 0' },
|
||||||
|
{ name: 'telegram_chat_id', type: 'TEXT' },
|
||||||
|
{ name: 'vk_notifications', type: 'BOOLEAN DEFAULT 0' },
|
||||||
|
{ name: 'vk_user_id', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// История уведомлений
|
||||||
|
notification_history: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'notification_type', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_history_unique ON notification_history(user_id, task_id, notification_type)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Очередь email
|
||||||
|
email_queue: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'to_email', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'subject', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'html_content', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER' },
|
||||||
|
{ name: 'notification_type', type: 'TEXT' },
|
||||||
|
{ name: 'retry_count', type: 'INTEGER DEFAULT 0' },
|
||||||
|
{ name: 'status', type: 'TEXT DEFAULT "pending"' },
|
||||||
|
{ name: 'error_message', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ЧАТА =====
|
||||||
|
|
||||||
|
// Сообщения чата задач
|
||||||
|
task_chat_messages: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'task_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'message', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
|
{ name: 'is_edited', type: 'BOOLEAN DEFAULT 0' },
|
||||||
|
{ name: 'is_deleted', type: 'BOOLEAN DEFAULT 0' },
|
||||||
|
{ name: 'reply_to_id', type: 'INTEGER' }
|
||||||
|
],
|
||||||
|
foreign_keys: [
|
||||||
|
'FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE',
|
||||||
|
'FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE',
|
||||||
|
'FOREIGN KEY (reply_to_id) REFERENCES task_chat_messages (id) ON DELETE SET NULL'
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_messages_task_id ON task_chat_messages(task_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_messages_user_id ON task_chat_messages(user_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_messages_created_at ON task_chat_messages(created_at)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_messages_reply_to ON task_chat_messages(reply_to_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Файлы в сообщениях чата
|
||||||
|
task_chat_files: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'message_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'file_path', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'original_name', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'file_size', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'file_type', type: 'TEXT' },
|
||||||
|
{ name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
foreign_keys: [
|
||||||
|
'FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE'
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_files_message_id ON task_chat_files(message_id)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Прочитанные сообщения
|
||||||
|
task_chat_reads: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
|
{ name: 'message_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||||
|
{ name: 'read_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||||
|
],
|
||||||
|
foreign_keys: [
|
||||||
|
'FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE',
|
||||||
|
'FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE'
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_reads_message_id ON task_chat_reads(message_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_task_chat_reads_user_id ON task_chat_reads(user_id)',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_task_chat_reads_unique ON task_chat_reads(message_id, user_id)'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для получения списка существующих таблиц
|
||||||
|
function getExistingTables() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name", [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(rows.map(row => row.name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения структуры таблицы
|
||||||
|
function getTableInfo(tableName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(`PRAGMA table_info(${tableName})`, [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения индексов таблицы
|
||||||
|
function getTableIndexes(tableName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(`PRAGMA index_list(${tableName})`, [], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для добавления колонки
|
||||||
|
function addColumn(tableName, columnName, columnType) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(`${colors.yellow} ➕ Добавление колонки: ${columnName} (${columnType})${colors.reset}`);
|
||||||
|
db.run(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log(`${colors.red} ❌ Ошибка: ${err.message}${colors.reset}`);
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.green} ✅ Колонка добавлена${colors.reset}`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для создания таблицы
|
||||||
|
function createTable(tableName, tableDefinition) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(`${colors.yellow} 🏗️ Создание таблицы: ${tableName}${colors.reset}`);
|
||||||
|
|
||||||
|
let createSQL = `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
|
||||||
|
|
||||||
|
// Добавляем колонки
|
||||||
|
const columnDefs = tableDefinition.columns.map(col => ` ${col.name} ${col.type}`).join(',\n');
|
||||||
|
createSQL += columnDefs;
|
||||||
|
|
||||||
|
// Добавляем внешние ключи, если есть
|
||||||
|
if (tableDefinition.foreign_keys && tableDefinition.foreign_keys.length > 0) {
|
||||||
|
createSQL += ',\n' + tableDefinition.foreign_keys.map(fk => ` ${fk}`).join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
createSQL += '\n)';
|
||||||
|
|
||||||
|
db.run(createSQL, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log(`${colors.red} ❌ Ошибка: ${err.message}${colors.reset}`);
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.green} ✅ Таблица создана${colors.reset}`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для создания индекса
|
||||||
|
function createIndex(indexSQL) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(indexSQL, function(err) {
|
||||||
|
if (err) {
|
||||||
|
// Игнорируем ошибки создания индексов, так как они могут уже существовать
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная функция проверки
|
||||||
|
async function checkDatabase() {
|
||||||
|
console.log(`${colors.cyan}🔍 Получение списка существующих таблиц...${colors.reset}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingTables = await getExistingTables();
|
||||||
|
console.log(`${colors.green}✅ Найдено таблиц: ${existingTables.length}${colors.reset}`);
|
||||||
|
|
||||||
|
// Проверяем наличие всех ожидаемых таблиц
|
||||||
|
const expectedTableNames = Object.keys(expectedTables);
|
||||||
|
const missingTables = expectedTableNames.filter(t => !existingTables.includes(t));
|
||||||
|
const extraTables = existingTables.filter(t => !expectedTableNames.includes(t) && !t.startsWith('sqlite_'));
|
||||||
|
|
||||||
|
console.log(`\n${colors.cyan}📊 СТАТИСТИКА ТАБЛИЦ:${colors.reset}`);
|
||||||
|
console.log(` Ожидаемых таблиц: ${expectedTableNames.length}`);
|
||||||
|
console.log(` Существующих таблиц: ${existingTables.length}`);
|
||||||
|
console.log(` Отсутствует таблиц: ${missingTables.length}`);
|
||||||
|
console.log(` Лишних таблиц: ${extraTables.length}`);
|
||||||
|
|
||||||
|
if (extraTables.length > 0) {
|
||||||
|
console.log(`\n${colors.yellow}⚠️ Лишние таблицы (не требуются, но можно оставить):${colors.reset}`);
|
||||||
|
extraTables.forEach(t => console.log(` - ${t}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем структуру каждой ожидаемой таблицы
|
||||||
|
console.log(`\n${colors.cyan}🔧 ПРОВЕРКА СТРУКТУРЫ ТАБЛИЦ:${colors.reset}`);
|
||||||
|
|
||||||
|
for (const tableName of expectedTableNames) {
|
||||||
|
console.log(`\n${colors.magenta}📋 Таблица: ${tableName}${colors.reset}`);
|
||||||
|
|
||||||
|
const tableDef = expectedTables[tableName];
|
||||||
|
|
||||||
|
if (!existingTables.includes(tableName)) {
|
||||||
|
// Таблица не существует - создаём
|
||||||
|
console.log(`${colors.yellow} ⚠️ Таблица не существует${colors.reset}`);
|
||||||
|
await createTable(tableName, tableDef);
|
||||||
|
|
||||||
|
// Создаём индексы для новой таблицы
|
||||||
|
if (tableDef.indexes && tableDef.indexes.length > 0) {
|
||||||
|
console.log(`${colors.yellow} 🔧 Создание индексов...${colors.reset}`);
|
||||||
|
for (const indexSQL of tableDef.indexes) {
|
||||||
|
await createIndex(indexSQL);
|
||||||
|
}
|
||||||
|
console.log(`${colors.green} ✅ Индексы созданы${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Таблица существует - проверяем колонки
|
||||||
|
const columns = await getTableInfo(tableName);
|
||||||
|
const existingColumnNames = columns.map(c => c.name.toLowerCase());
|
||||||
|
|
||||||
|
console.log(` 📊 Колонок в БД: ${columns.length}, требуется: ${tableDef.columns.length}`);
|
||||||
|
|
||||||
|
// Проверяем наличие всех необходимых колонок
|
||||||
|
for (const expectedCol of tableDef.columns) {
|
||||||
|
const colName = expectedCol.name.toLowerCase();
|
||||||
|
|
||||||
|
if (!existingColumnNames.includes(colName)) {
|
||||||
|
console.log(`${colors.yellow} ⚠️ Отсутствует колонка: ${expectedCol.name}${colors.reset}`);
|
||||||
|
await addColumn(tableName, expectedCol.name, expectedCol.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем типы данных колонок (базовая проверка)
|
||||||
|
for (const existingCol of columns) {
|
||||||
|
const expectedCol = tableDef.columns.find(c => c.name.toLowerCase() === existingCol.name.toLowerCase());
|
||||||
|
if (expectedCol) {
|
||||||
|
const expectedType = expectedCol.type.split(' ')[0].toUpperCase();
|
||||||
|
const existingType = existingCol.type.toUpperCase();
|
||||||
|
|
||||||
|
if (!existingType.includes(expectedType) && !expectedType.includes(existingType)) {
|
||||||
|
console.log(`${colors.yellow} ⚠️ Несоответствие типа: ${existingCol.name} - ожидается ${expectedType}, в БД ${existingType}${colors.reset}`);
|
||||||
|
console.log(` Ручное изменение типа данных может привести к потере данных. Пропускаем.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем индексы
|
||||||
|
try {
|
||||||
|
const indexes = await getTableIndexes(tableName);
|
||||||
|
const existingIndexNames = indexes.map(i => i.name.toLowerCase());
|
||||||
|
|
||||||
|
if (tableDef.indexes && tableDef.indexes.length > 0) {
|
||||||
|
console.log(` 🔍 Проверка индексов...`);
|
||||||
|
|
||||||
|
for (const indexSQL of tableDef.indexes) {
|
||||||
|
// Извлекаем имя индекса из SQL (упрощённо)
|
||||||
|
const match = indexSQL.match(/INDEX\s+IF NOT EXISTS\s+(\w+)/i) ||
|
||||||
|
indexSQL.match(/INDEX\s+(\w+)/i);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const indexName = match[1].toLowerCase();
|
||||||
|
if (!existingIndexNames.includes(indexName)) {
|
||||||
|
console.log(`${colors.yellow} ➕ Создание индекса: ${indexName}${colors.reset}`);
|
||||||
|
await createIndex(indexSQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`${colors.red} ❌ Ошибка проверки индексов: ${err.message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем внешние ключи (только для SQLite - ограниченная поддержка)
|
||||||
|
if (tableDef.foreign_keys && tableDef.foreign_keys.length > 0) {
|
||||||
|
// В SQLite сложно проверить внешние ключи через PRAGMA, просто удостоверимся что таблица создана правильно
|
||||||
|
console.log(` 🔍 Внешние ключи определены в структуре таблицы`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие директорий для файлов
|
||||||
|
console.log(`\n${colors.cyan}📁 ПРОВЕРКА ДИРЕКТОРИЙ:${colors.reset}`);
|
||||||
|
|
||||||
|
const dirsToCheck = [
|
||||||
|
path.join(__dirname, 'data', 'uploads'),
|
||||||
|
path.join(__dirname, 'data', 'uploads', 'tasks'),
|
||||||
|
path.join(__dirname, 'data', 'uploads', 'chat'),
|
||||||
|
path.join(__dirname, 'data', 'logs')
|
||||||
|
];
|
||||||
|
|
||||||
|
dirsToCheck.forEach(dir => {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
console.log(`${colors.yellow} 📁 Создание директории: ${path.basename(dir)}${colors.reset}`);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
console.log(`${colors.green} ✅ Создано${colors.reset}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.green} ✅ Директория существует: ${path.basename(dir)}${colors.reset}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Итоговый отчёт
|
||||||
|
console.log(`\n${colors.cyan}🏁 ИТОГОВЫЙ ОТЧЁТ:${colors.reset}`);
|
||||||
|
console.log('=' .repeat(60));
|
||||||
|
|
||||||
|
// Проверяем все ли таблицы теперь существуют
|
||||||
|
const finalTables = await getExistingTables();
|
||||||
|
const stillMissing = expectedTableNames.filter(t => !finalTables.includes(t));
|
||||||
|
|
||||||
|
if (stillMissing.length === 0) {
|
||||||
|
console.log(`${colors.green}✅ Все необходимые таблицы присутствуют в базе данных.${colors.reset}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.red}❌ Отсутствуют таблицы: ${stillMissing.join(', ')}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${colors.green}✨ Проверка базы данных завершена!${colors.reset}`);
|
||||||
|
console.log('=' .repeat(60));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red}❌ Критическая ошибка:${colors.reset}`, error);
|
||||||
|
} finally {
|
||||||
|
// Закрываем соединение с базой данных
|
||||||
|
db.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`${colors.red}❌ Ошибка закрытия БД:${colors.reset}`, err.message);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.green}✅ Соединение с БД закрыто${colors.reset}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем проверку
|
||||||
|
checkDatabase();
|
||||||
Reference in New Issue
Block a user