@@ -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
+ )}
@@ -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
+ }
+
-
-
-
-
`;
- });
-
- 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 = `
`;
-
- 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();
+ }
});