// 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 = ` `; // Добавляем стили для чата 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 = ` `; 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)}) `; 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 = ''; 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 = `
↪ Ответ ${message.reply_to_user_name}: "${message.reply_to_message.substring(0, 30)}${message.reply_to_message.length > 30 ? '...' : ''}"
`; } let filesHtml = ''; if (message.files && message.files.length > 0) { filesHtml = '
'; message.files.forEach(file => { filesHtml += `
📎 ${file.original_name} (${this.formatFileSize(file.file_size)})
`; }); filesHtml += '
'; } let actionsHtml = ''; if (isOwn || window.currentUserRole === 'admin') { actionsHtml = `
${isOwn ? `` : ''}
`; } div.innerHTML = ` ${replyHtml}
${message.user_name} ${time} ${message.is_edited ? '(ред.)' : ''} ${actionsHtml}
${this.escapeHtml(message.message)}
${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 = `
💬 Нет сообщений. Напишите что-нибудь...
`; } 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)); });