diff --git a/backend/requests_app/notification_signals.py b/backend/requests_app/notification_signals.py index 79219340..79352b9c 100644 --- a/backend/requests_app/notification_signals.py +++ b/backend/requests_app/notification_signals.py @@ -10,6 +10,7 @@ Signals для автоматической генерации уведомле """ import logging + from django.contrib.auth import get_user_model from django.db import transaction from django.db.models.signals import m2m_changed, post_save, pre_save @@ -51,7 +52,9 @@ def create_request_notifications(sender, instance, created, **kwargs): ) _pending_new_requests.add(request_obj.id) else: - print(f"⏭️ [SIGNAL] Пропускаем: created={created}, status={request_obj.status}") + print( + f"⏭️ [SIGNAL] Пропускаем: created={created}, status={request_obj.status}" + ) if not created: # Проверяем изменение статуса через сохраненный атрибут _old_status @@ -216,26 +219,30 @@ def notify_new_request(request_obj): При sent_to_all_department=True отправляет всем сотрудникам отделов """ logger.info( - f"\n{'='*80}\n" + f"\n{'=' * 80}\n" f"[notify_new_request] 📨 НАЧАЛО ОТПРАВКИ УВЕДОМЛЕНИЙ О НОВОМ ЗАЯВЛЕНИИ\n" f" Request ID: {request_obj.id}\n" f" Тип: {request_obj.get_type_display()}\n" f" Сотрудник: {request_obj.employee}\n" f" Статус: {request_obj.status}\n" - f"{'='*80}" + f"{'=' * 80}" ) - + print(f"📨 [notify_new_request] Начинаем обработку заявления #{request_obj.id}") recipients_set = set() # 1. Основные получатели recipients_count = request_obj.recipients.count() - logger.info(f"[notify_new_request] Основные получатели (recipients): {recipients_count}") + logger.info( + f"[notify_new_request] Основные получатели (recipients): {recipients_count}" + ) print(f" Recipients в БД: {recipients_count}") for recipient in request_obj.recipients.filter(is_active=True): recipients_set.add(recipient) - logger.info(f"[notify_new_request] ✅ Основной получатель: {recipient.username} (ID={recipient.id}, email={recipient.email})") + logger.info( + f"[notify_new_request] ✅ Основной получатель: {recipient.username} (ID={recipient.id}, email={recipient.email})" + ) print( f" ✅ Добавлен основной получатель: {recipient.username} (ID: {recipient.id})" ) @@ -246,7 +253,9 @@ def notify_new_request(request_obj): print(f" CC users в БД: {cc_count}") for cc_user in request_obj.cc_users.filter(is_active=True): recipients_set.add(cc_user) - logger.info(f"[notify_new_request] ✅ CC получатель: {cc_user.username} (ID={cc_user.id}, email={cc_user.email})") + logger.info( + f"[notify_new_request] ✅ CC получатель: {cc_user.username} (ID={cc_user.id}, email={cc_user.email})" + ) print(f" ✅ Добавлен CC: {cc_user.username} (ID: {cc_user.id})") # 3. Если sent_to_all_department - все сотрудники отделов @@ -297,9 +306,7 @@ def notify_new_request(request_obj): recipients_set.update(dept_processors) # Итоговое логирование - logger.info( - f"[notify_new_request] 📊 ИТОГО получателей: {len(recipients_set)}" - ) + logger.info(f"[notify_new_request] 📊 ИТОГО получателей: {len(recipients_set)}") print( f"📊 [notify_new_request] Всего получателей для заявления #{request_obj.id}: {len(recipients_set)}" ) @@ -313,7 +320,9 @@ def notify_new_request(request_obj): return for r in recipients_set: - logger.info(f"[notify_new_request] 👤 {r.username} (ID={r.id}, email={r.email})") + logger.info( + f"[notify_new_request] 👤 {r.username} (ID={r.id}, email={r.email})" + ) print(f" 👤 {r.username} (ID: {r.id})") # Определяем тип уведомления для каждого получателя @@ -322,7 +331,7 @@ def notify_new_request(request_obj): comment_preview = ( request_obj.comment[:150] if request_obj.comment else "Без комментария" ) - + logger.info( f"[notify_new_request] Начало отправки {len(recipients_set)} уведомлений..." ) @@ -362,7 +371,7 @@ def notify_new_request(request_obj): title = f"📝 Новое заявление в отделе от {author_name}" message = f'Тип: "{request_type}". {comment_preview}' notification_type = "request_new" # Используем общий тип - + logger.info( f"[notify_new_request] ➡️ Вызов NotificationService.create_notification для {recipient.username}" ) diff --git a/backend/static/js/notifications/notification-list.js b/backend/static/js/notifications/notification-list.js index aae78f11..d2b54c7d 100644 --- a/backend/static/js/notifications/notification-list.js +++ b/backend/static/js/notifications/notification-list.js @@ -2,187 +2,193 @@ * Управление страницей списка уведомлений */ class NotificationListManager { - constructor() { + constructor() { + this.currentPage = 1; + this.pageSize = 20; + this.currentFilter = { + category: "", + is_read: "", + search: "", + }; + this.init(); + } + + init() { + this.setupFilters(); + this.setupSearch(); + this.setupMarkAllButton(); + this.loadNotifications(); + } + + setupFilters() { + // Фильтр по категории + const categoryFilter = document.getElementById("categoryFilter"); + if (categoryFilter) { + categoryFilter.addEventListener("change", (e) => { + this.currentFilter.category = e.target.value; this.currentPage = 1; - this.pageSize = 20; - this.currentFilter = { - category: '', - is_read: '', - search: '' - }; - this.init(); - } - - init() { - this.setupFilters(); - this.setupSearch(); - this.setupMarkAllButton(); this.loadNotifications(); + }); } - - setupFilters() { - // Фильтр по категории - const categoryFilter = document.getElementById('categoryFilter'); - if (categoryFilter) { - categoryFilter.addEventListener('change', (e) => { - this.currentFilter.category = e.target.value; - this.currentPage = 1; - this.loadNotifications(); - }); - } - - // Фильтр по статусу прочитанности - const statusFilter = document.getElementById('statusFilter'); - if (statusFilter) { - statusFilter.addEventListener('change', (e) => { - this.currentFilter.is_read = e.target.value; - this.currentPage = 1; - this.loadNotifications(); - }); - } + + // Фильтр по статусу прочитанности + const statusFilter = document.getElementById("statusFilter"); + if (statusFilter) { + statusFilter.addEventListener("change", (e) => { + this.currentFilter.is_read = e.target.value; + this.currentPage = 1; + this.loadNotifications(); + }); } - - setupSearch() { - const searchInput = document.getElementById('notificationSearch'); - const searchBtn = document.getElementById('searchBtn'); - - if (searchInput && searchBtn) { - searchBtn.addEventListener('click', () => { - this.currentFilter.search = searchInput.value.trim(); - this.currentPage = 1; - this.loadNotifications(); - }); - - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.currentFilter.search = searchInput.value.trim(); - this.currentPage = 1; - this.loadNotifications(); - } - }); + } + + setupSearch() { + const searchInput = document.getElementById("notificationSearch"); + const searchBtn = document.getElementById("searchBtn"); + + if (searchInput && searchBtn) { + searchBtn.addEventListener("click", () => { + this.currentFilter.search = searchInput.value.trim(); + this.currentPage = 1; + this.loadNotifications(); + }); + + searchInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + this.currentFilter.search = searchInput.value.trim(); + this.currentPage = 1; + this.loadNotifications(); } + }); } - - setupMarkAllButton() { - const markAllBtn = document.getElementById('markAllReadBtn'); - if (markAllBtn) { - markAllBtn.addEventListener('click', async () => { - await this.markAllAsRead(); - }); - } + } + + setupMarkAllButton() { + const markAllBtn = document.getElementById("markAllReadBtn"); + if (markAllBtn) { + markAllBtn.addEventListener("click", async () => { + await this.markAllAsRead(); + }); } - - async loadNotifications() { - const container = document.getElementById('notificationListContainer'); - if (!container) return; - - // Показать загрузку - container.innerHTML = ` + } + + async loadNotifications() { + const container = document.getElementById("notificationListContainer"); + if (!container) return; + + // Показать загрузку + container.innerHTML = `
Загрузка...
`; - - try { - const params = new URLSearchParams({ - page: this.currentPage, - page_size: this.pageSize - }); - - if (this.currentFilter.category) { - params.append('category', this.currentFilter.category); - } - if (this.currentFilter.is_read !== '') { - params.append('is_read', this.currentFilter.is_read); - } - if (this.currentFilter.search) { - params.append('search', this.currentFilter.search); - } - - const response = await fetch(`/api/v1/notifications/?${params}`, { - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - this.renderNotifications(data); - } catch (error) { - console.error('[NotificationList] Error loading:', error); - container.innerHTML = ` + + try { + const params = new URLSearchParams({ + page: this.currentPage, + page_size: this.pageSize, + }); + + if (this.currentFilter.category) { + params.append("category", this.currentFilter.category); + } + if (this.currentFilter.is_read !== "") { + params.append("is_read", this.currentFilter.is_read); + } + if (this.currentFilter.search) { + params.append("search", this.currentFilter.search); + } + + const response = await fetch(`/api/v1/notifications/?${params}`, { + credentials: "same-origin", + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + this.renderNotifications(data); + } catch (error) { + console.error("[NotificationList] Error loading:", error); + container.innerHTML = `
Ошибка загрузки уведомлений: ${error.message}
`; - } } - - renderNotifications(data) { - const container = document.getElementById('notificationListContainer'); - if (!container) return; - - if (!data.notifications || data.notifications.length === 0) { - container.innerHTML = ` + } + + renderNotifications(data) { + const container = document.getElementById("notificationListContainer"); + if (!container) return; + + if (!data.notifications || data.notifications.length === 0) { + container.innerHTML = `

Уведомлений нет

`; - return; - } - - const notificationsHtml = data.notifications.map(n => this.renderNotificationCard(n)).join(''); - const paginationHtml = this.renderPagination(data); - - container.innerHTML = ` + return; + } + + const notificationsHtml = data.notifications + .map((n) => this.renderNotificationCard(n)) + .join(""); + const paginationHtml = this.renderPagination(data); + + container.innerHTML = `
${notificationsHtml}
${paginationHtml} `; - - // Добавить обработчики событий - this.attachEventHandlers(); - } - - renderNotificationCard(notification) { - const categoryColors = { - 'communications': 'primary', - 'documents': 'success', - 'requests': 'warning', - 'calendar': 'info', - 'department': 'secondary', - 'profile': 'dark', - 'feed': 'danger', - 'system': 'light' - }; - - const categoryIcons = { - 'communications': 'bi-chat-dots', - 'documents': 'bi-file-earmark-text', - 'requests': 'bi-clipboard-check', - 'calendar': 'bi-calendar-event', - 'department': 'bi-people', - 'profile': 'bi-person', - 'feed': 'bi-newspaper', - 'system': 'bi-gear' - }; - - const color = categoryColors[notification.category] || 'secondary'; - const icon = categoryIcons[notification.category] || 'bi-bell'; - const readClass = notification.is_read ? 'notification-read' : 'notification-unread'; - - return ` + + // Добавить обработчики событий + this.attachEventHandlers(); + } + + renderNotificationCard(notification) { + const categoryColors = { + communications: "primary", + documents: "success", + requests: "warning", + calendar: "info", + department: "secondary", + profile: "dark", + feed: "danger", + system: "light", + }; + + const categoryIcons = { + communications: "bi-chat-dots", + documents: "bi-file-earmark-text", + requests: "bi-clipboard-check", + calendar: "bi-calendar-event", + department: "bi-people", + profile: "bi-person", + feed: "bi-newspaper", + system: "bi-gear", + }; + + const color = categoryColors[notification.category] || "secondary"; + const icon = categoryIcons[notification.category] || "bi-bell"; + const readClass = notification.is_read + ? "notification-read" + : "notification-unread"; + + return `
-
+
@@ -192,22 +198,43 @@ class NotificationListManager {
-
${this.escapeHtml(notification.title)}
- ${this.formatTime(notification.created_at)} +
${this.escapeHtml( + notification.title + )}
+ ${this.formatTime( + notification.created_at + )}
-

${this.escapeHtml(notification.message)}

+

${this.escapeHtml( + notification.message + )}

- ${notification.action_url ? ` - - ${notification.action_text || 'Посмотреть'} + ${ + notification.action_url + ? ` + + ${ + notification.action_text || + "Посмотреть" + } - ` : ''} - ${!notification.is_read ? ` + ` + : "" + } + ${ + !notification.is_read + ? ` - ` : ''} -
@@ -217,232 +244,248 @@ class NotificationListManager {
`; + } + + renderPagination(data) { + if (data.total <= this.pageSize) { + return ""; } - - renderPagination(data) { - if (data.total <= this.pageSize) { - return ''; - } - - const totalPages = Math.ceil(data.total / this.pageSize); - const pages = []; - - // Всегда показываем первую страницу - pages.push(1); - - // Показываем страницы вокруг текущей - for (let i = Math.max(2, this.currentPage - 1); i <= Math.min(totalPages - 1, this.currentPage + 1); i++) { - if (!pages.includes(i)) { - pages.push(i); - } - } - - // Всегда показываем последнюю страницу - if (!pages.includes(totalPages)) { - pages.push(totalPages); - } - - let paginationHtml = '"; + + return paginationHtml; + } + + attachEventHandlers() { + // Обработчики для кнопок "Прочитано" + document.querySelectorAll(".mark-read-btn").forEach((btn) => { + btn.addEventListener("click", async (e) => { + e.preventDefault(); + const id = btn.dataset.id; + await this.markAsRead(id); + }); + }); + + // Обработчики для кнопок удаления + document.querySelectorAll(".delete-btn").forEach((btn) => { + btn.addEventListener("click", async (e) => { + e.preventDefault(); + const id = btn.dataset.id; + if (confirm("Удалить это уведомление?")) { + await this.deleteNotification(id); + } + }); + }); + + // Обработчики для пагинации + document.querySelectorAll(".pagination .page-link").forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); + const page = parseInt(link.dataset.page); + if (page && page !== this.currentPage) { + this.currentPage = page; + this.loadNotifications(); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + }); + }); + } + + async markAsRead(id) { + try { + const response = await fetch(`/api/v1/notifications/${id}/read/`, { + method: "POST", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": this.getCsrfToken(), + }, + }); + + if (response.ok) { + // Обновить карточку + const card = document.querySelector(`[data-notification-id="${id}"]`); + if (card) { + card.classList.remove("notification-unread"); + card.classList.add("notification-read"); + card.querySelector(".mark-read-btn")?.remove(); + } + + // Обновить счетчик в navbar + if (window.notificationManager) { + const badge = document.getElementById("notificationBadge"); + if (badge) { + const currentCount = parseInt(badge.textContent) || 0; + if (currentCount > 0) { + window.notificationManager.updateBadge(currentCount - 1); } - } catch (error) { - console.error('[NotificationList] Error marking as read:', error); - alert('Ошибка при обновлении уведомления'); + } } + } + } catch (error) { + console.error("[NotificationList] Error marking as read:", error); + alert("Ошибка при обновлении уведомления"); } - - async markAllAsRead() { - if (!confirm('Отметить все уведомления как прочитанные?')) { - return; + } + + async markAllAsRead() { + if (!confirm("Отметить все уведомления как прочитанные?")) { + return; + } + + try { + const response = await fetch("/api/v1/notifications/read-all/", { + method: "POST", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": this.getCsrfToken(), + }, + }); + + if (response.ok) { + // Перезагрузить список + this.loadNotifications(); + + // Обновить счетчик + if (window.notificationManager) { + window.notificationManager.updateBadge(0); } - - try { - const response = await fetch('/api/v1/notifications/read-all/', { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - } - }); - - if (response.ok) { - // Перезагрузить список - this.loadNotifications(); - - // Обновить счетчик - if (window.notificationManager) { - window.notificationManager.updateBadge(0); - } - } - } catch (error) { - console.error('[NotificationList] Error marking all as read:', error); - alert('Ошибка при обновлении уведомлений'); + } + } catch (error) { + console.error("[NotificationList] Error marking all as read:", error); + alert("Ошибка при обновлении уведомлений"); + } + } + + async deleteNotification(id) { + try { + const response = await fetch(`/api/v1/notifications/${id}/`, { + method: "DELETE", + credentials: "same-origin", + headers: { + "X-CSRFToken": this.getCsrfToken(), + }, + }); + + if (response.ok) { + // Удалить карточку + const card = document + .querySelector(`[data-notification-id="${id}"]`) + ?.closest(".col"); + if (card) { + card.remove(); } - } - - async deleteNotification(id) { - try { - const response = await fetch(`/api/v1/notifications/${id}/`, { - method: 'DELETE', - credentials: 'same-origin', - headers: { - 'X-CSRFToken': this.getCsrfToken() - } - }); - - if (response.ok) { - // Удалить карточку - const card = document.querySelector(`[data-notification-id="${id}"]`)?.closest('.col'); - if (card) { - card.remove(); - } - - // Если список пуст, показать заглушку - const container = document.getElementById('notificationListContainer'); - if (container && !container.querySelector('.card')) { - this.loadNotifications(); - } - } - } catch (error) { - console.error('[NotificationList] Error deleting:', error); - alert('Ошибка при удалении уведомления'); + + // Если список пуст, показать заглушку + const container = document.getElementById("notificationListContainer"); + if (container && !container.querySelector(".card")) { + this.loadNotifications(); } + } + } catch (error) { + console.error("[NotificationList] Error deleting:", error); + alert("Ошибка при удалении уведомления"); } - - getCsrfToken() { - return document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - formatTime(dateString) { - const date = new Date(dateString); - const now = new Date(); - const diff = Math.floor((now - date) / 1000); // секунды - - if (diff < 60) return 'только что'; - if (diff < 3600) return `${Math.floor(diff / 60)} мин назад`; - if (diff < 86400) return `${Math.floor(diff / 3600)} ч назад`; - if (diff < 604800) return `${Math.floor(diff / 86400)} дн назад`; - - return date.toLocaleDateString('ru-RU', { - day: 'numeric', - month: 'short', - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined - }); - } + } + + getCsrfToken() { + return document.querySelector("[name=csrfmiddlewaretoken]")?.value || ""; + } + + escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + formatTime(dateString) { + const date = new Date(dateString); + const now = new Date(); + const diff = Math.floor((now - date) / 1000); // секунды + + if (diff < 60) return "только что"; + if (diff < 3600) return `${Math.floor(diff / 60)} мин назад`; + if (diff < 86400) return `${Math.floor(diff / 3600)} ч назад`; + if (diff < 604800) return `${Math.floor(diff / 86400)} дн назад`; + + return date.toLocaleDateString("ru-RU", { + day: "numeric", + month: "short", + year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, + }); + } } // Инициализация при загрузке страницы -document.addEventListener('DOMContentLoaded', () => { - if (document.getElementById('notificationListContainer')) { - window.notificationListManager = new NotificationListManager(); - } +document.addEventListener("DOMContentLoaded", () => { + if (document.getElementById("notificationListContainer")) { + window.notificationListManager = new NotificationListManager(); + } }); diff --git a/backend/static/js/notifications/notification-settings.js b/backend/static/js/notifications/notification-settings.js index 3ae0cdb7..fc92318d 100644 --- a/backend/static/js/notifications/notification-settings.js +++ b/backend/static/js/notifications/notification-settings.js @@ -2,73 +2,113 @@ * Управление настройками уведомлений */ class NotificationSettingsManager { - constructor() { - this.settings = {}; - this.init(); - } - - init() { - this.loadSettings(); - this.setupSoundToggle(); - } - - async loadSettings() { - const container = document.getElementById('settingsContainer'); - if (!container) return; - - try { - const response = await fetch('/api/v1/notifications/settings/', { - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - this.settings = data.settings || {}; - this.renderSettings(); - } catch (error) { - console.error('[Settings] Error loading:', error); - container.innerHTML = ` + constructor() { + this.settings = {}; + this.init(); + } + + init() { + this.loadSettings(); + this.setupSoundToggle(); + } + + async loadSettings() { + const container = document.getElementById("settingsContainer"); + if (!container) return; + + try { + const response = await fetch("/api/v1/notifications/settings/", { + credentials: "same-origin", + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + this.settings = data.settings || {}; + this.renderSettings(); + } catch (error) { + console.error("[Settings] Error loading:", error); + container.innerHTML = `
Ошибка загрузки настроек: ${error.message}
`; - } } - - renderSettings() { - const container = document.getElementById('settingsContainer'); - if (!container) return; - - const categories = [ - { code: 'communications', name: 'Коммуникации', icon: 'bi-chat-dots', description: 'Сообщения, упоминания, ответы в чатах' }, - { code: 'documents', name: 'Документы', icon: 'bi-file-earmark-text', description: 'Новые документы, ознакомления' }, - { code: 'requests', name: 'Заявления', icon: 'bi-clipboard-check', description: 'Заявки, согласования, комментарии' }, - { code: 'calendar', name: 'Календарь', icon: 'bi-calendar-event', description: 'События, напоминания, изменения' }, - { code: 'department', name: 'Отдел', icon: 'bi-people', description: 'Изменения в структуре отдела' }, - { code: 'profile', name: 'Профиль', icon: 'bi-person', description: 'Изменения профиля и безопасности' }, - { code: 'feed', name: 'Новости', icon: 'bi-newspaper', description: 'Публикации, комментарии, реакции' }, - { code: 'system', name: 'Система', icon: 'bi-gear', description: 'Системные уведомления и обновления' } - ]; - - let html = '
'; - - categories.forEach(cat => { - const setting = this.settings[cat.code] || { - is_enabled: true, - web_enabled: true, - email_enabled: false, - email_frequency: 'instant', - telegram_enabled: false - }; - - html += ` + } + + renderSettings() { + const container = document.getElementById("settingsContainer"); + if (!container) return; + + const categories = [ + { + code: "communications", + name: "Коммуникации", + icon: "bi-chat-dots", + description: "Сообщения, упоминания, ответы в чатах", + }, + { + code: "documents", + name: "Документы", + icon: "bi-file-earmark-text", + description: "Новые документы, ознакомления", + }, + { + code: "requests", + name: "Заявления", + icon: "bi-clipboard-check", + description: "Заявки, согласования, комментарии", + }, + { + code: "calendar", + name: "Календарь", + icon: "bi-calendar-event", + description: "События, напоминания, изменения", + }, + { + code: "department", + name: "Отдел", + icon: "bi-people", + description: "Изменения в структуре отдела", + }, + { + code: "profile", + name: "Профиль", + icon: "bi-person", + description: "Изменения профиля и безопасности", + }, + { + code: "feed", + name: "Новости", + icon: "bi-newspaper", + description: "Публикации, комментарии, реакции", + }, + { + code: "system", + name: "Система", + icon: "bi-gear", + description: "Системные уведомления и обновления", + }, + ]; + + let html = '
'; + + categories.forEach((cat) => { + const setting = this.settings[cat.code] || { + is_enabled: true, + web_enabled: true, + email_enabled: false, + email_frequency: "instant", + telegram_enabled: false, + }; + + html += `
@@ -80,66 +120,113 @@ class NotificationSettingsManager {
${cat.name}
-

${cat.description}

- +

${ + cat.description + }

+
- -
- -
+ +
- -
- +
- -
- - ${setting.email_enabled ? ` + + ${ + setting.email_enabled + ? `
-
- ` : ''} - + ` + : "" + } +
- -
@@ -150,176 +237,180 @@ class NotificationSettingsManager {
`; - }); - - html += '
'; - container.innerHTML = html; - - // Добавить обработчики - this.attachEventHandlers(); - } - - attachEventHandlers() { - // Обработчики для главных переключателей - document.querySelectorAll('.setting-toggle').forEach(toggle => { - toggle.addEventListener('change', async (e) => { - const category = toggle.dataset.category; - const field = toggle.dataset.field; - const value = toggle.checked; - - // Показать/скрыть каналы - const channels = document.getElementById(`channels_${category}`); - if (channels) { - if (value) { - channels.classList.remove('d-none'); - } else { - channels.classList.add('d-none'); - } - } - - await this.updateSetting(category, field, value); - }); - }); - - // Обработчики для каналов - document.querySelectorAll('.channel-toggle').forEach(toggle => { - toggle.addEventListener('change', async (e) => { - const category = toggle.dataset.category; - const field = toggle.dataset.field; - const value = toggle.checked; - - // Если включили email, перерисовать карточку для показа частоты - if (field === 'email_enabled') { - await this.updateSetting(category, field, value); - this.renderSettings(); // Перерисовать для показа select - } else { - await this.updateSetting(category, field, value); - } - }); - }); - - // Обработчики для частоты email - document.querySelectorAll('.email-frequency-select').forEach(select => { - select.addEventListener('change', async (e) => { - const category = select.dataset.category; - const value = select.value; - - await this.updateSetting(category, 'email_frequency', value); - }); - }); - } - - async updateSetting(category, field, value) { - try { - const response = await fetch('/api/v1/notifications/settings/category/update/', { - method: 'PUT', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - category: category, - [field]: value - }) - }); - - if (!response.ok) { - throw new Error('Failed to update setting'); - } - - // Обновить локальные настройки - if (!this.settings[category]) { - this.settings[category] = {}; - } - this.settings[category][field] = value; - - // Показать уведомление об успехе - this.showToast('Настройки сохранены', 'success'); - } catch (error) { - console.error('[Settings] Error updating:', error); - this.showToast('Ошибка сохранения настроек', 'danger'); + }); + + html += "
"; + container.innerHTML = html; + + // Добавить обработчики + this.attachEventHandlers(); + } + + attachEventHandlers() { + // Обработчики для главных переключателей + document.querySelectorAll(".setting-toggle").forEach((toggle) => { + toggle.addEventListener("change", async (e) => { + const category = toggle.dataset.category; + const field = toggle.dataset.field; + const value = toggle.checked; + + // Показать/скрыть каналы + const channels = document.getElementById(`channels_${category}`); + if (channels) { + if (value) { + channels.classList.remove("d-none"); + } else { + channels.classList.add("d-none"); + } } - } - - setupSoundToggle() { - const soundToggle = document.getElementById('soundToggle'); - if (soundToggle && window.notificationManager) { - soundToggle.checked = window.notificationManager.soundEnabled; - - soundToggle.addEventListener('change', (e) => { - window.notificationManager.saveSoundPreference(e.target.checked); - this.showToast( - e.target.checked ? 'Звук включен' : 'Звук выключен', - 'info' - ); - }); + + await this.updateSetting(category, field, value); + }); + }); + + // Обработчики для каналов + document.querySelectorAll(".channel-toggle").forEach((toggle) => { + toggle.addEventListener("change", async (e) => { + const category = toggle.dataset.category; + const field = toggle.dataset.field; + const value = toggle.checked; + + // Если включили email, перерисовать карточку для показа частоты + if (field === "email_enabled") { + await this.updateSetting(category, field, value); + this.renderSettings(); // Перерисовать для показа select + } else { + await this.updateSetting(category, field, value); } + }); + }); + + // Обработчики для частоты email + document.querySelectorAll(".email-frequency-select").forEach((select) => { + select.addEventListener("change", async (e) => { + const category = select.dataset.category; + const value = select.value; + + await this.updateSetting(category, "email_frequency", value); + }); + }); + } + + async updateSetting(category, field, value) { + try { + const response = await fetch( + "/api/v1/notifications/settings/category/update/", + { + method: "PUT", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": this.getCsrfToken(), + }, + body: JSON.stringify({ + category: category, + [field]: value, + }), + } + ); + + if (!response.ok) { + throw new Error("Failed to update setting"); + } + + // Обновить локальные настройки + if (!this.settings[category]) { + this.settings[category] = {}; + } + this.settings[category][field] = value; + + // Показать уведомление об успехе + this.showToast("Настройки сохранены", "success"); + } catch (error) { + console.error("[Settings] Error updating:", error); + this.showToast("Ошибка сохранения настроек", "danger"); } - - getCsrfToken() { - return document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + } + + setupSoundToggle() { + const soundToggle = document.getElementById("soundToggle"); + if (soundToggle && window.notificationManager) { + soundToggle.checked = window.notificationManager.soundEnabled; + + soundToggle.addEventListener("change", (e) => { + window.notificationManager.saveSoundPreference(e.target.checked); + this.showToast( + e.target.checked ? "Звук включен" : "Звук выключен", + "info" + ); + }); } - - showToast(message, type = 'info') { - // Создать toast уведомление - const toastContainer = document.getElementById('toastContainer') || this.createToastContainer(); - - const toast = document.createElement('div'); - toast.className = `toast align-items-center text-white bg-${type} border-0`; - toast.setAttribute('role', 'alert'); - toast.innerHTML = ` + } + + getCsrfToken() { + return document.querySelector("[name=csrfmiddlewaretoken]")?.value || ""; + } + + showToast(message, type = "info") { + // Создать toast уведомление + const toastContainer = + document.getElementById("toastContainer") || this.createToastContainer(); + + const toast = document.createElement("div"); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute("role", "alert"); + toast.innerHTML = `
${message}
`; - - toastContainer.appendChild(toast); - - const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); - bsToast.show(); - - toast.addEventListener('hidden.bs.toast', () => { - toast.remove(); - }); - } - - createToastContainer() { - const container = document.createElement('div'); - container.id = 'toastContainer'; - container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; - document.body.appendChild(container); - return container; - } + + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); + bsToast.show(); + + toast.addEventListener("hidden.bs.toast", () => { + toast.remove(); + }); + } + + createToastContainer() { + const container = document.createElement("div"); + container.id = "toastContainer"; + container.className = "toast-container position-fixed bottom-0 end-0 p-3"; + document.body.appendChild(container); + return container; + } } /** * Управление привязкой Telegram (упрощенная версия) */ class TelegramLinkManager { - constructor() { - this.container = document.getElementById('telegramLinkContainer'); - if (!this.container) return; - - this.init(); - } - - async init() { - this.renderInstructions(); - } - - renderInstructions() { - const botUsername = window.TELEGRAM_BOT_USERNAME || 'eusrr_bot'; - - this.container.innerHTML = ` + constructor() { + this.container = document.getElementById("telegramLinkContainer"); + if (!this.container) return; + + this.init(); + } + + async init() { + this.renderInstructions(); + } + + renderInstructions() { + const botUsername = window.TELEGRAM_BOT_USERNAME || "eusrr_bot"; + + this.container.innerHTML = `
Настройка Telegram уведомлений
- +
📱 Как получать уведомления в Telegram:
    @@ -349,20 +440,20 @@ class TelegramLinkManager {
- +
`; - } + } } // Инициализация -document.addEventListener('DOMContentLoaded', () => { - if (document.getElementById('settingsContainer')) { - window.notificationSettingsManager = new NotificationSettingsManager(); - } - if (document.getElementById('telegramLinkContainer')) { - window.telegramLinkManager = new TelegramLinkManager(); - } +document.addEventListener("DOMContentLoaded", () => { + if (document.getElementById("settingsContainer")) { + window.notificationSettingsManager = new NotificationSettingsManager(); + } + if (document.getElementById("telegramLinkContainer")) { + window.telegramLinkManager = new TelegramLinkManager(); + } });