const PAGE_SIZE = 25; let nfItems = []; let nfTotal = 0; let nfUnread = 0; let nfUnreadOnly = false; let nfLoading = false; async function nfMarkRead(id) { const res = await fetch(`${API}/api/notifications/${id}/read`, { method: 'POST', headers: hdr() }); if (!res.ok) { showToast('Не удалось отметить уведомление'); return; } const item = nfItems.find(n => n.id === id); if (item) item.read = true; nfUnread = Math.max(0, nfUnread - 1); if (typeof notifyList !== 'undefined') { const shared = notifyList.find(n => n.id === id); if (shared) shared.read = true; } if (nfUnreadOnly) { nfItems = nfItems.filter(n => n.id !== id); nfTotal = Math.max(0, nfTotal - 1); loadNotifyPage(true); return; } renderNotifyPage(); if (typeof setNotifyUnread === 'function') setNotifyUnread(nfUnread); } async function nfDelete(id) { if (!confirm('Удалить уведомление у всех пользователей?')) return; const res = await fetch(`${API}/api/notifications/${id}`, { method: 'DELETE', headers: hdr() }); if (!res.ok) { showToast('Не удалось удалить уведомление'); return; } if (typeof notifyList !== 'undefined') notifyList = notifyList.filter(n => n.id !== id); loadNotifyPage(true); } async function nfMarkAllRead() { const res = await fetch(`${API}/api/notifications/read-all`, { method: 'POST', headers: hdr() }); if (!res.ok) { showToast('Не удалось отметить уведомления'); return; } showToast('Все уведомления прочитаны'); if (typeof notifyList !== 'undefined') notifyList.forEach(n => { n.read = true; }); if (typeof setNotifyUnread === 'function') setNotifyUnread(0); loadNotifyPage(true); } async function nfClearAll() { if (!confirm('Удалить все уведомления у всех пользователей? Действие необратимо.')) return; const res = await fetch(`${API}/api/notifications`, { method: 'DELETE', headers: hdr() }); if (!res.ok) { showToast('Не удалось очистить уведомления'); return; } const data = await res.json().catch(() => ({})); showToast(`Удалено уведомлений: ${data.deleted || 0}`); if (typeof notifyList !== 'undefined') notifyList = []; if (typeof setNotifyUnread === 'function') setNotifyUnread(0); loadNotifyPage(true); } function onNotificationArrived() { loadNotifyPage(true); } function onNotifyListChanged() { loadNotifyPage(true); } document.getElementById('nfList').addEventListener('click', e => { const readBtn = e.target.closest('[data-nf-read]'); if (readBtn) { e.preventDefault(); nfMarkRead(parseInt(readBtn.dataset.nfRead, 10)); return; } const delBtn = e.target.closest('[data-nf-del]'); if (delBtn) { e.preventDefault(); nfDelete(parseInt(delBtn.dataset.nfDel, 10)); return; } if (e.target.closest('a[href]')) { const card = e.target.closest('[data-nf-card]'); if (card) nfMarkRead(parseInt(card.dataset.nfCard, 10)); } }); document.getElementById('notifyFilter').addEventListener('click', e => { const btn = e.target.closest('button[data-filter]'); if (!btn) return; nfUnreadOnly = btn.dataset.filter === 'unread'; document.querySelectorAll('#notifyFilter button').forEach(b => b.classList.toggle('active', b === btn)); loadNotifyPage(true); }); document.getElementById('nfReadAllBtn').addEventListener('click', nfMarkAllRead); document.getElementById('nfClearBtn').addEventListener('click', nfClearAll); document.getElementById('nfMoreBtn').addEventListener('click', () => loadNotifyPage(false)); document.getElementById('nfReloadBtn').addEventListener('click', () => loadNotifyPage(true)); (async () => { if (!(await requireAdminPage())) return; buildSidebar(document.body.dataset.page); const clearBtn = document.getElementById('nfClearBtn'); if (clearBtn) clearBtn.hidden = !isAdmin(); const authStatus = document.getElementById('authStatus'); if (authStatus) authStatus.classList.add('loaded'); await loadNotifyPage(true); })(); function nfIcon(type) { return typeof NOTIFY_ICONS !== 'undefined' && NOTIFY_ICONS[type] ? NOTIFY_ICONS[type] : 'bell'; } function nfLevelClass(level) { return level === 'warning' || level === 'critical' ? ` level-${level}` : ''; } function nfCardHtml(n) { const when = typeof notifyTime === 'function' ? notifyTime(n.created_at) : ''; const linkAttr = typeof attrEsc === 'function' ? attrEsc(n.link) : esc(n.link); return `
${esc(n.title)} ${n.body ? `${esc(n.body)}` : ''} ${esc(when)}${n.read ? '' : ' · не прочитано'} ${n.link ? `Открыть` : ''} ${n.read ? '' : ``} ${isAdmin() ? `` : ''}
`; } function renderNotifyPage() { const list = document.getElementById('nfList'); const state = document.getElementById('nfState'); const more = document.getElementById('nfMoreBtn'); if (!list) return; list.innerHTML = nfItems.length ? nfItems.map(nfCardHtml).join('') : `
${nfUnreadOnly ? 'Непрочитанных уведомлений нет' : 'Уведомлений пока нет'}
`; if (state) { state.textContent = nfItems.length ? `Показано ${nfItems.length} из ${nfTotal} · непрочитанных: ${nfUnread}` : `Непрочитанных: ${nfUnread}`; } if (more) more.hidden = nfItems.length >= nfTotal; renderIcons(); } async function loadNotifyPage(reset) { if (nfLoading) return; nfLoading = true; const offset = reset ? 0 : nfItems.length; try { const q = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset) }); if (nfUnreadOnly) q.set('unread', '1'); const res = await fetch(`${API}/api/notifications?${q.toString()}`, { headers: hdr() }); if (!res.ok) { showToast('Ошибка загрузки уведомлений'); return; } const data = await res.json(); const items = data.items || []; nfItems = reset ? items : nfItems.concat(items); nfTotal = Number(data.total) || 0; nfUnread = Number(data.unread) || 0; renderNotifyPage(); if (typeof setNotifyUnread === 'function') setNotifyUnread(nfUnread); } catch { showToast('Ошибка сети'); } finally { nfLoading = false; } }