feat(photos): раздел «Фото» — единая галерея всех загруженных фотографий

Новый read-only раздел для просмотра всех фотографий системы:
GET /api/photos собирает через UNION ALL пять источников — главное фото
записи, фото записи (entry_photos), фотохронику групп, фото учеников и фото
тем модулей. Фильтры search/student_name/group_id/date_from/date_to и
пагинация limit/offset, ответ { photos, total }. Не-admin ограничен
своими филиалами (branch_id), чужой group_id отдаёт 403.

Фронтенд: public/photos.html + public/js/photos.js — сетка превью
(/uploads/thumb/...), бейдж источника, описание, ученик/группа, дата,
ссылка на источник, lightbox по клику, поиск с debounce и пагинация.
Навигация: пункт «Фото» в сайдбаре и плитка в быстрых действиях дашборда.
This commit is contained in:
dev
2026-09-28 00:15:32 +03:00
parent 2afe676969
commit 31542de33b
6 changed files with 383 additions and 31 deletions
+60 -14
View File
@@ -49,9 +49,10 @@ function buildSidebar(active) {
const base = [
{ page: 'dashboard', label: 'Дашборд', icon: 'layout-dashboard' },
{ page: 'journal', label: 'Журнал', icon: 'book-open' },
{ page: 'notifications', label: 'Уведомления', icon: 'bell' },
{ page: 'notifications', label: 'Уведомления', icon: 'bell', badge: true },
{ page: 'students', label: 'Ученики', icon: 'graduation-cap' },
{ page: 'groups', label: 'Группы', icon: 'users' },
{ page: 'photos', label: 'Фото', icon: 'Image' },
{ page: 'files', label: 'Файлы', icon: 'folder' },
{ page: 'links', label: 'Ссылки', icon: 'link' },
{ page: 'trash', label: 'Корзина', icon: 'trash-2' },
@@ -65,26 +66,65 @@ function buildSidebar(active) {
{ page: 'bans', label: 'Блокировки', icon: 'shield-off' },
{ page: 'settings', label: 'Настройки', icon: 'settings' },
];
const nav = [...base, ...(isAdmin() ? adminNav : [])];
const userName = currentUser?.name || currentUser?.username || '';
const roleLabel = isAdmin() ? 'Администратор' : 'Тутор';
const renderItem = n => {
const isAct = n.page === active;
const badgeHtml = n.badge ? `<span class="sidebar-badge" id="sidebarNotifyBadge" ${notifyUnread > 0 ? '' : 'hidden'}>${notifyUnread > 99 ? '99+' : notifyUnread}</span>` : '';
return `<a href="${n.page}.html" data-nav="${n.page}" class="${isAct ? 'active' : ''}" title="${n.label}">
<i data-lucide="${n.icon}"></i>
<span class="nav-text">${n.label}</span>
${badgeHtml}
</a>`;
};
el.innerHTML = `
<div class="logo">
<span class="logo-ic" id="sidebarLogo"></span>
<span id="sidebarName">Админ-панель</span>
</div>
<div class="user-box">
<div class="user-name">${esc(userName)}</div>
<div class="user-role">${roleLabel}</div>
<div class="user-avatar"><i data-lucide="${isAdmin() ? 'shield-check' : 'user'}"></i></div>
<div class="user-meta">
<div class="user-name" title="${esc(userName)}">${esc(userName)}</div>
<div class="user-role">${roleLabel}</div>
</div>
</div>
<div class="nav-scroll-wrap">
<nav>
<div class="nav-group">
<div class="nav-group-title">Основное</div>
<div class="nav-items">
${base.map(renderItem).join('')}
</div>
</div>
${isAdmin() ? `
<div class="nav-group">
<div class="nav-group-title">Администрирование</div>
<div class="nav-items">
${adminNav.map(renderItem).join('')}
</div>
</div>` : ''}
</nav>
</div>
<nav>
${nav.map(n => `<a href="${n.page}.html" data-nav="${n.page}" class="${n.page === active ? 'active' : ''}"><i data-lucide="${n.icon}"></i>${n.label}</a>`).join('')}
</nav>
<div class="bottom">
<div class="version" id="sidebarVersion"></div>
<a href="#" class="notify-bell" id="notifyBell" title="Уведомления"><i data-lucide="bell"></i>Уведомления<span class="notify-badge" id="notifyBadge" hidden>0</span></a>
<a href="/" data-nav="home"><i data-lucide="home"></i>На главную</a>
<a class="logout" href="#" data-action="logout"><i data-lucide="log-out"></i>Выйти</a>
<div class="bottom-actions">
<a href="#" class="notify-bell" id="notifyBell" title="Быстрые уведомления">
<i data-lucide="bell"></i>
<span>Уведомления</span>
<span class="notify-badge" id="notifyBadge" ${notifyUnread > 0 ? '' : 'hidden'}>${notifyUnread > 99 ? '99+' : notifyUnread}</span>
</a>
<a href="/" data-nav="home" title="Перейти на главную страницу">
<i data-lucide="home"></i>
<span>На главную</span>
</a>
</div>
<div class="bottom-footer">
<a class="logout-btn" href="#" data-action="logout" title="Выйти из аккаунта">
<i data-lucide="log-out"></i>
<span>Выйти</span>
</a>
<span class="version" id="sidebarVersion"></span>
</div>
</div>`;
renderIcons();
loadVersion();
@@ -162,9 +202,15 @@ function notifyTime(ts) {
function setNotifyUnread(n) {
notifyUnread = Number(n) || 0;
const badge = document.getElementById('notifyBadge');
if (!badge) return;
badge.textContent = notifyUnread > 99 ? '99+' : String(notifyUnread);
badge.hidden = notifyUnread <= 0;
if (badge) {
badge.textContent = notifyUnread > 99 ? '99+' : String(notifyUnread);
badge.hidden = notifyUnread <= 0;
}
const navBadge = document.getElementById('sidebarNotifyBadge');
if (navBadge) {
navBadge.textContent = notifyUnread > 99 ? '99+' : String(notifyUnread);
navBadge.hidden = notifyUnread <= 0;
}
}
function notifyItemHtml(n, expanded) {