Files
WhatIDo/public/js/notifications.js
T
dev 7e4b1a1324 fix(chat): просмотр фото вложений в модалке, доступ тьюторов к уведомлениям
- изображения во вложениях чата открываются в общей модалке #imgModal
  (data-img + общий обработчик в admin.js) вместо новой вкладки
- Escape и клик по фону закрывают модалку изображения
- .chat-side[hidden] скрывается — раньше правило display:none его не покрывало
- страница уведомлений открывается любому авторизованному пользователю
  (checkAuth вместо requireAdminPage), пункт меню виден всем
2026-10-05 17:51:37 +03:00

164 lines
6.9 KiB
JavaScript

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 checkAuth())) 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 `<div class="notify-card${n.read ? '' : ' unread'}${nfLevelClass(n.level)}" data-nf-card="${n.id}">
<span class="notify-ic"><i data-lucide="${nfIcon(n.type)}"></i></span>
<span class="notify-body">
<span class="notify-title">${esc(n.title)}</span>
${n.body ? `<span class="notify-text">${esc(n.body)}</span>` : ''}
<span class="notify-time">${esc(when)}${n.read ? '' : ' · не прочитано'}</span>
</span>
<span class="notify-card-actions">
${n.link ? `<a class="btn-primary ghost" style="padding:6px 12px;font-size:.78rem" href="${linkAttr}">Открыть</a>` : ''}
${n.read ? '' : `<button type="button" class="notify-link" data-nf-read="${n.id}">Прочитано</button>`}
${isAdmin() ? `<button type="button" class="notify-link" style="color:#ef4444" data-nf-del="${n.id}">Удалить</button>` : ''}
</span>
</div>`;
}
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('')
: `<div class="notify-empty">${nfUnreadOnly ? 'Непрочитанных уведомлений нет' : 'Уведомлений пока нет'}</div>`;
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;
}
}