static
This commit is contained in:
parent
b074c91549
commit
e2fe9fa66a
@ -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}"
|
||||
)
|
||||
|
||||
@ -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 = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi-exclamation-triangle"></i>
|
||||
Ошибка загрузки уведомлений: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="text-center py-5">
|
||||
<i class="bi-bell-slash" style="font-size: 3rem; color: #ccc;"></i>
|
||||
<p class="text-muted mt-3">Уведомлений нет</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="row row-cols-1 g-3">
|
||||
${notificationsHtml}
|
||||
</div>
|
||||
${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 `
|
||||
<div class="col">
|
||||
<div class="card ${readClass}" data-notification-id="${notification.id}">
|
||||
<div class="card ${readClass}" data-notification-id="${
|
||||
notification.id
|
||||
}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="flex-shrink-0">
|
||||
@ -192,22 +198,43 @@ class NotificationListManager {
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h6 class="mb-0">${this.escapeHtml(notification.title)}</h6>
|
||||
<small class="text-muted ms-2">${this.formatTime(notification.created_at)}</small>
|
||||
<h6 class="mb-0">${this.escapeHtml(
|
||||
notification.title
|
||||
)}</h6>
|
||||
<small class="text-muted ms-2">${this.formatTime(
|
||||
notification.created_at
|
||||
)}</small>
|
||||
</div>
|
||||
<p class="mb-2 text-muted">${this.escapeHtml(notification.message)}</p>
|
||||
<p class="mb-2 text-muted">${this.escapeHtml(
|
||||
notification.message
|
||||
)}</p>
|
||||
<div class="d-flex gap-2">
|
||||
${notification.action_url ? `
|
||||
<a href="${notification.action_url}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi-arrow-right-circle"></i> ${notification.action_text || 'Посмотреть'}
|
||||
${
|
||||
notification.action_url
|
||||
? `
|
||||
<a href="${
|
||||
notification.action_url
|
||||
}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi-arrow-right-circle"></i> ${
|
||||
notification.action_text ||
|
||||
"Посмотреть"
|
||||
}
|
||||
</a>
|
||||
` : ''}
|
||||
${!notification.is_read ? `
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
!notification.is_read
|
||||
? `
|
||||
<button class="btn btn-sm btn-outline-secondary mark-read-btn" data-id="${notification.id}">
|
||||
<i class="bi-check2"></i> Прочитано
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-outline-danger delete-btn" data-id="${notification.id}">
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<button class="btn btn-sm btn-outline-danger delete-btn" data-id="${
|
||||
notification.id
|
||||
}">
|
||||
<i class="bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -217,232 +244,248 @@ class NotificationListManager {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = '<nav class="mt-4"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Кнопка "Предыдущая"
|
||||
paginationHtml += `
|
||||
<li class="page-item ${this.currentPage === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" data-page="${this.currentPage - 1}">
|
||||
|
||||
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 =
|
||||
'<nav class="mt-4"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Кнопка "Предыдущая"
|
||||
paginationHtml += `
|
||||
<li class="page-item ${this.currentPage === 1 ? "disabled" : ""}">
|
||||
<a class="page-link" href="#" data-page="${
|
||||
this.currentPage - 1
|
||||
}">
|
||||
<i class="bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// Страницы
|
||||
let prevPage = 0;
|
||||
pages.forEach(page => {
|
||||
if (page - prevPage > 1) {
|
||||
paginationHtml += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
paginationHtml += `
|
||||
<li class="page-item ${page === this.currentPage ? 'active' : ''}">
|
||||
|
||||
// Страницы
|
||||
let prevPage = 0;
|
||||
pages.forEach((page) => {
|
||||
if (page - prevPage > 1) {
|
||||
paginationHtml +=
|
||||
'<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
paginationHtml += `
|
||||
<li class="page-item ${
|
||||
page === this.currentPage ? "active" : ""
|
||||
}">
|
||||
<a class="page-link" href="#" data-page="${page}">${page}</a>
|
||||
</li>
|
||||
`;
|
||||
prevPage = page;
|
||||
});
|
||||
|
||||
// Кнопка "Следующая"
|
||||
paginationHtml += `
|
||||
<li class="page-item ${this.currentPage === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" data-page="${this.currentPage + 1}">
|
||||
prevPage = page;
|
||||
});
|
||||
|
||||
// Кнопка "Следующая"
|
||||
paginationHtml += `
|
||||
<li class="page-item ${
|
||||
this.currentPage === totalPages ? "disabled" : ""
|
||||
}">
|
||||
<a class="page-link" href="#" data-page="${
|
||||
this.currentPage + 1
|
||||
}">
|
||||
<i class="bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
paginationHtml += '</ul></nav>';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paginationHtml += "</ul></nav>";
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi-exclamation-triangle"></i>
|
||||
Ошибка загрузки настроек: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<div class="row g-4">';
|
||||
|
||||
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 = '<div class="row g-4">';
|
||||
|
||||
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 += `
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
@ -80,66 +120,113 @@ class NotificationSettingsManager {
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h5 class="card-title mb-1">${cat.name}</h5>
|
||||
<p class="text-muted small mb-3">${cat.description}</p>
|
||||
|
||||
<p class="text-muted small mb-3">${
|
||||
cat.description
|
||||
}</p>
|
||||
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input setting-toggle"
|
||||
type="checkbox"
|
||||
<input class="form-check-input setting-toggle"
|
||||
type="checkbox"
|
||||
id="enable_${cat.code}"
|
||||
data-category="${cat.code}"
|
||||
data-field="is_enabled"
|
||||
${setting.is_enabled ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enable_${cat.code}">
|
||||
${
|
||||
setting.is_enabled
|
||||
? "checked"
|
||||
: ""
|
||||
}>
|
||||
<label class="form-check-label" for="enable_${
|
||||
cat.code
|
||||
}">
|
||||
<strong>Включить уведомления</strong>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="ps-4 ${setting.is_enabled ? '' : 'd-none'}" id="channels_${cat.code}">
|
||||
|
||||
<div class="ps-4 ${
|
||||
setting.is_enabled ? "" : "d-none"
|
||||
}" id="channels_${cat.code}">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
id="web_${cat.code}"
|
||||
data-category="${cat.code}"
|
||||
data-field="web_enabled"
|
||||
${setting.web_enabled ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="web_${cat.code}">
|
||||
${
|
||||
setting.web_enabled
|
||||
? "checked"
|
||||
: ""
|
||||
}>
|
||||
<label class="form-check-label" for="web_${
|
||||
cat.code
|
||||
}">
|
||||
<i class="bi-globe"></i> Веб-уведомления
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
id="email_${cat.code}"
|
||||
data-category="${cat.code}"
|
||||
data-field="email_enabled"
|
||||
${setting.email_enabled ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="email_${cat.code}">
|
||||
${
|
||||
setting.email_enabled
|
||||
? "checked"
|
||||
: ""
|
||||
}>
|
||||
<label class="form-check-label" for="email_${
|
||||
cat.code
|
||||
}">
|
||||
<i class="bi-envelope"></i> Email уведомления
|
||||
</label>
|
||||
</div>
|
||||
|
||||
${setting.email_enabled ? `
|
||||
|
||||
${
|
||||
setting.email_enabled
|
||||
? `
|
||||
<div class="ps-4 mb-2">
|
||||
<label class="form-label small text-muted mb-1">Частота отправки:</label>
|
||||
<select class="form-select form-select-sm email-frequency-select"
|
||||
<select class="form-select form-select-sm email-frequency-select"
|
||||
data-category="${cat.code}"
|
||||
id="email_freq_${cat.code}">
|
||||
<option value="instant" ${setting.email_frequency === 'instant' ? 'selected' : ''}>Мгновенно</option>
|
||||
<option value="daily" ${setting.email_frequency === 'daily' ? 'selected' : ''}>Ежедневный дайджест</option>
|
||||
<option value="weekly" ${setting.email_frequency === 'weekly' ? 'selected' : ''}>Еженедельный дайджест</option>
|
||||
<option value="instant" ${
|
||||
setting.email_frequency ===
|
||||
"instant"
|
||||
? "selected"
|
||||
: ""
|
||||
}>Мгновенно</option>
|
||||
<option value="daily" ${
|
||||
setting.email_frequency ===
|
||||
"daily"
|
||||
? "selected"
|
||||
: ""
|
||||
}>Ежедневный дайджест</option>
|
||||
<option value="weekly" ${
|
||||
setting.email_frequency ===
|
||||
"weekly"
|
||||
? "selected"
|
||||
: ""
|
||||
}>Еженедельный дайджест</option>
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
<input class="form-check-input channel-toggle"
|
||||
type="checkbox"
|
||||
id="telegram_${cat.code}"
|
||||
data-category="${cat.code}"
|
||||
data-field="telegram_enabled"
|
||||
${setting.telegram_enabled ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="telegram_${cat.code}">
|
||||
${
|
||||
setting.telegram_enabled
|
||||
? "checked"
|
||||
: ""
|
||||
}>
|
||||
<label class="form-check-label" for="telegram_${
|
||||
cat.code
|
||||
}">
|
||||
<i class="bi-telegram"></i> Telegram
|
||||
</label>
|
||||
</div>
|
||||
@ -150,176 +237,180 @@ class NotificationSettingsManager {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
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 += "</div>";
|
||||
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 = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h6 class="mb-3">
|
||||
<i class="bi-telegram text-primary"></i>
|
||||
Настройка Telegram уведомлений
|
||||
</h6>
|
||||
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>📱 Как получать уведомления в Telegram:</strong>
|
||||
<ol class="mb-0 mt-3">
|
||||
@ -349,20 +440,20 @@ class TelegramLinkManager {
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user