чат2
This commit is contained in:
37
api-chat.js
37
api-chat.js
@@ -460,7 +460,44 @@ module.exports = function(app, db, upload) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// GET /api/chat/unread-summary – сводка непрочитанных сообщений по задачам
|
||||||
|
router.get('/api/chat/unread-summary', requireAuth, (req, res) => {
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
m.task_id,
|
||||||
|
t.title,
|
||||||
|
COUNT(*) as unread_count
|
||||||
|
FROM task_chat_messages m
|
||||||
|
JOIN tasks t ON m.task_id = t.id
|
||||||
|
LEFT JOIN task_chat_reads r ON m.id = r.message_id AND r.user_id = ?
|
||||||
|
WHERE m.user_id != ?
|
||||||
|
AND m.is_deleted = 0
|
||||||
|
AND r.id IS NULL
|
||||||
|
GROUP BY m.task_id, t.title
|
||||||
|
ORDER BY MAX(m.created_at) DESC
|
||||||
|
LIMIT 50
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(query, [userId, userId], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ Ошибка получения сводки непрочитанных:', err);
|
||||||
|
return res.status(500).json({ error: 'Ошибка получения сводки' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUnread = rows.reduce((sum, row) => sum + row.unread_count, 0);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
tasks: rows.map(r => ({
|
||||||
|
taskId: r.task_id,
|
||||||
|
title: r.title,
|
||||||
|
unreadCount: r.unread_count
|
||||||
|
})),
|
||||||
|
totalUnread
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
// Вспомогательная функция для уведомлений участников
|
// Вспомогательная функция для уведомлений участников
|
||||||
function notifyTaskParticipants(taskId, senderId, message) {
|
function notifyTaskParticipants(taskId, senderId, message) {
|
||||||
db.all(`
|
db.all(`
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ function reloadAllScripts() {
|
|||||||
'profile.js',
|
'profile.js',
|
||||||
'time-selector.js',
|
'time-selector.js',
|
||||||
'openTaskChat.js',
|
'openTaskChat.js',
|
||||||
|
'openTaskChat2.js',
|
||||||
'tasks_files.js',
|
'tasks_files.js',
|
||||||
'navbar.js',
|
'navbar.js',
|
||||||
'chat-ui.js',
|
'chat-ui.js',
|
||||||
|
|||||||
130
public/openTaskChat2.js
Normal file
130
public/openTaskChat2.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Показывает модальное окно со списком задач
|
||||||
|
function showUnreadNotification(tasks) {
|
||||||
|
const modalHtml = `
|
||||||
|
<div class="modal" id="unread-notification-modal">
|
||||||
|
<div class="modal-content" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>📬 У вас новые сообщения</h3>
|
||||||
|
<span class="close" onclick="closeUnreadNotification()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>В следующих задачах есть непрочитанные сообщения:</p>
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
${tasks.map(task => `
|
||||||
|
<li style="margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 5px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span><strong>${escapeHtml(task.title)}</strong> (${task.unreadCount} ${pluralize(task.unreadCount, ['новое сообщение', 'новых сообщения', 'новых сообщений'])})</span>
|
||||||
|
<button class="btn-primary" onclick="openTaskAndMarkRead(${task.taskId})">Открыть чат</button>
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-cancel" onclick="closeUnreadNotification()">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let modal = document.getElementById('unread-notification-modal');
|
||||||
|
if (modal) modal.remove();
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = modalHtml;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
modal = document.getElementById('unread-notification-modal');
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
// Добавляем стили (если ещё не добавлены)
|
||||||
|
if (!document.getElementById('unread-notification-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'unread-notification-styles';
|
||||||
|
style.textContent = `
|
||||||
|
#unread-notification-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1001;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
#unread-notification-modal .modal-content {
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateY(-50px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUnreadNotification() {
|
||||||
|
const modal = document.getElementById('unread-notification-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
setTimeout(() => modal.remove(), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательная функция для склонения
|
||||||
|
function pluralize(count, words) {
|
||||||
|
const cases = [2, 0, 1, 1, 1, 2];
|
||||||
|
return words[(count % 100 > 4 && count % 100 < 20) ? 2 : cases[Math.min(count % 10, 5)]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экранирование HTML в заголовке задачи (защита от XSS)
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открывает чат задачи и помечает все сообщения как прочитанные
|
||||||
|
function openTaskAndMarkRead(taskId) {
|
||||||
|
closeUnreadNotification();
|
||||||
|
// Опционально: сразу отмечаем все сообщения прочитанными
|
||||||
|
fetch(`/api/chat/tasks/${taskId}/mark-read`, { method: 'POST' })
|
||||||
|
.catch(err => console.warn('Не удалось отметить сообщения как прочитанные', err))
|
||||||
|
.finally(() => openTaskChat(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка наличия непрочитанных сообщений
|
||||||
|
function checkUnreadMessages() {
|
||||||
|
// Не беспокоим пользователя, если страница не активна
|
||||||
|
if (document.hidden) return;
|
||||||
|
|
||||||
|
fetch('/api/chat/unread-summary')
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) return null; // пользователь не авторизован
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.totalUnread > 0) {
|
||||||
|
showUnreadNotification(data.tasks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Ошибка проверки новых сообщений:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск периодической проверки (раз в 5 минут)
|
||||||
|
setInterval(checkUnreadMessages, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// Проверка при загрузке страницы
|
||||||
|
document.addEventListener('DOMContentLoaded', checkUnreadMessages);
|
||||||
Reference in New Issue
Block a user