Corp/backend/static/js/components/chatMenuActions.js
Игорь Надеин 2b61564837 feat(communications): Реализация управления уведомлениями чатов и оптимизация
- Добавлен API endpoint для включения/выключения уведомлений чата
- Реализован frontend для управления уведомлениями через меню чата
- Обновлены сигналы для проверки настроек notifications_enabled
- Стандартизирована логика уведомлений для всех типов чатов
- Оптимизированы запросы к БД (1 запрос вместо N для ChatUserSettings)
- Переведены детальные логи с INFO на DEBUG уровень

Файлы:
- backend/api/v1/communications/views.py (новый endpoint)
- backend/api/v1/urls.py (новый маршрут)
- backend/static/js/components/chatMenuActions.js (полная реализация)
- backend/communications/notification_signals.py (проверка настроек + оптимизация)
- backend/notifications/services.py (оптимизация логирования)
2026-01-08 14:39:40 +03:00

394 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* chatMenuActions.js
* Обработчик действий меню чата (закрепить, уведомления, редактировать, удалить)
*/
export function initChatMenuActions() {
// Обновление текста "Закрепить"/"Открепить" при открытии dropdown
document.addEventListener('show.bs.dropdown', function(e) {
const dropdownButton = e.target;
const chatRow = dropdownButton.closest('.chat-row');
if (!chatRow) return;
const isPinned = chatRow.classList.contains('pinned');
const dropdownMenu = dropdownButton.nextElementSibling;
const pinTextElement = dropdownMenu?.querySelector('[data-pin-text]');
if (pinTextElement) {
pinTextElement.textContent = isPinned ? 'Открепить' : 'Закрепить';
}
});
// Обработка кликов по пунктам меню
document.addEventListener('click', function(e) {
const actionLink = e.target.closest('[data-action]');
if (!actionLink) return;
e.preventDefault();
e.stopPropagation();
const action = actionLink.dataset.action;
const chatRow = actionLink.closest('.chat-row');
if (!chatRow) return;
const chatId = chatRow.dataset.chatId;
const chatName = chatRow.querySelector('[data-chat-name]')?.textContent || 'этот чат';
// Закрываем dropdown после клика
const dropdownMenu = actionLink.closest('.dropdown-menu');
const dropdownButton = dropdownMenu?.previousElementSibling;
if (dropdownButton) {
const dropdownInstance = bootstrap.Dropdown.getInstance(dropdownButton);
if (dropdownInstance) {
dropdownInstance.hide();
}
}
// Выполняем действие
switch(action) {
case 'pin':
handlePinAction(chatId, chatName, chatRow);
break;
case 'notifications':
handleNotificationsAction(chatId, chatName);
break;
case 'edit':
handleEditAction(chatId, chatName);
break;
case 'delete':
handleDeleteAction(chatId, chatName);
break;
default:
console.warn('Unknown action:', action);
}
});
}
/**
* Обработка закрепления/открепления чата
*/
function handlePinAction(chatId, chatName, chatRow) {
const isPinned = chatRow.classList.contains('pinned');
const actionText = isPinned ? 'открепления' : 'закрепления';
showDevMessage(`Функция ${actionText} чата находится в разработке`, 'info');
// TODO: Реализовать API endpoint для закрепления
/*
fetch(`/communications/api/chat/${chatId}/pin/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ pinned: !isPinned })
})
.then(response => response.json())
.then(data => {
if (data.ok) {
if (data.pinned) {
chatRow.classList.add('pinned');
moveToPinnedSection(chatRow);
} else {
chatRow.classList.remove('pinned');
moveToUnpinnedSection(chatRow);
}
}
})
.catch(error => console.error('Pin error:', error));
*/
}
/**
* Обработка настройки уведомлений
*/
async function handleNotificationsAction(chatId, chatName) {
try {
// Получаем текущее состояние из data-атрибута
const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`);
const currentlyEnabled = chatRow?.dataset.notificationsEnabled !== 'false';
// Переключаем состояние
const newState = !currentlyEnabled;
const response = await fetch(`/api/v1/communications/chats/${chatId}/notifications/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ enabled: newState })
});
const data = await response.json();
if (data.ok) {
// Обновляем data-атрибут
if (chatRow) {
chatRow.dataset.notificationsEnabled = newState.toString();
}
// Показываем toast с результатом
const message = data.message || `Уведомления ${newState ? 'включены' : 'отключены'}`;
showSuccessMessage(message);
// Обновляем иконку в меню
updateNotificationsIcon(chatRow, newState);
} else {
showErrorMessage(data.error || 'Ошибка при изменении настроек');
}
} catch (error) {
console.error('Notifications toggle error:', error);
showErrorMessage('Ошибка при изменении настроек уведомлений');
}
}
/**
* Обработка редактирования чата
*/
function handleEditAction(chatId, chatName) {
showDevMessage('Функция редактирования чата находится в разработке', 'info');
// TODO: Создать страницу редактирования чата
// window.location.href = `/communications/chats/${chatId}/edit/`;
}
/**
* Обработка удаления чата
*/
function handleDeleteAction(chatId, chatName) {
showDevMessage('Функция удаления чата находится в разработке', 'warning');
// TODO: Реализовать API endpoint для удаления
/*
if (!confirm(`Вы уверены, что хотите удалить чат "${chatName}"? Это действие необратимо.`)) {
return;
}
fetch(`/communications/api/chat/${chatId}/delete/`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.ok) {
const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`);
if (chatRow) {
chatRow.style.transition = 'opacity 0.3s';
chatRow.style.opacity = '0';
setTimeout(() => chatRow.remove(), 300);
}
} else {
alert('Ошибка при удалении чата: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Delete error:', error);
alert('Ошибка при удалении чата');
});
*/
}
/**
* Показать сообщение о разработке функции
*/
function showDevMessage(message, type = 'info') {
// Создаем toast уведомление
const toastContainer = getOrCreateToastContainer();
const toastId = `toast-${Date.now()}`;
const iconClass = type === 'warning' ? 'bi-exclamation-triangle-fill' : 'bi-info-circle-fill';
const bgClass = type === 'warning' ? 'bg-warning' : 'bg-info';
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="${iconClass} me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: 3000
});
toast.show();
// Удаляем элемент после скрытия
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
/**
* Получить или создать контейнер для toast уведомлений
*/
function getOrCreateToastContainer() {
let container = document.getElementById('chat-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'chat-toast-container';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '9999';
document.body.appendChild(container);
}
return container;
}
/**
* Получить CSRF токен
*/
function getCsrfToken() {
const tokenInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (tokenInput) {
return tokenInput.value;
}
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
/**
* Переместить чат в секцию закрепленных
*/
function moveToPinnedSection(chatRow) {
const parent = chatRow.parentElement;
const firstUnpinned = Array.from(parent.children).find(row =>
!row.classList.contains('pinned')
);
if (firstUnpinned) {
parent.insertBefore(chatRow, firstUnpinned);
} else {
parent.insertBefore(chatRow, parent.firstChild);
}
}
/**
* Переместить чат в секцию обычных
*/
function moveToUnpinnedSection(chatRow) {
const parent = chatRow.parentElement;
parent.appendChild(chatRow);
}
/**
* Обновить иконку уведомлений в чате
*/
function updateNotificationsIcon(chatRow, enabled) {
if (!chatRow) return;
// Ищем или создаем иконку уведомлений
let notifIcon = chatRow.querySelector('.chat-notification-icon');
if (!enabled) {
// Уведомления отключены - показываем перечеркнутый колокольчик
if (!notifIcon) {
notifIcon = document.createElement('i');
notifIcon.className = 'bi-bell-slash text-muted chat-notification-icon ms-1';
notifIcon.title = 'Уведомления отключены';
// Вставляем после заголовка чата
const titleElement = chatRow.querySelector('[data-chat-name]');
if (titleElement) {
titleElement.appendChild(notifIcon);
}
}
} else {
// Уведомления включены - убираем иконку
if (notifIcon) {
notifIcon.remove();
}
}
}
/**
* Показать сообщение об успехе
*/
function showSuccessMessage(message) {
const toastContainer = getOrCreateToastContainer();
const toastId = `toast-${Date.now()}`;
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi-check-circle-fill me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: 3000
});
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
/**
* Показать сообщение об ошибке
*/
function showErrorMessage(message) {
const toastContainer = getOrCreateToastContainer();
const toastId = `toast-${Date.now()}`;
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-danger border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi-exclamation-triangle-fill me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: 5000
});
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}