// 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}
${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));
});