Update project files

This commit is contained in:
dev
2026-09-10 11:26:51 +03:00
parent 7ccc9199e0
commit 3850fe35e4
19 changed files with 511 additions and 155 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ document.getElementById('branchesGrid').addEventListener('click', e => {
});
(async () => {
if (await checkAuth()) {
if (await requireAdminPage()) {
buildSidebar(document.body.dataset.page);
loadBranches();
}
+14 -2
View File
@@ -6,13 +6,25 @@ function plural(n, one, few, many) {
return many;
}
function renderStats(s) {
function renderStats(s, disk) {
let diskHtml = '';
if (disk) {
const pct = disk.used_pct;
const color = pct > 90 ? '#ef4444' : pct > 70 ? '#f59e0b' : '#16a34a';
diskHtml = `
<div class="stat">
<div>${disk.used} <span class="stat-sub">/ ${disk.total}</span></div>
<div class="label">Диск: свободно ${disk.free} (${100 - pct}%)</div>
<div class="stat-bar"><i style="width:${pct}%;background:${color}"></i></div>
</div>`;
}
document.getElementById('stats').innerHTML = `
<div class="stat"><div class="num">${s.entries}</div><div class="label">Всего записей</div></div>
<div class="stat"><div class="num">${s.students}</div><div class="label">Учеников</div></div>
<div class="stat"><div class="num">${s.groups}</div><div class="label">Групп</div></div>
<div class="stat"><div class="num">${s.today}</div><div class="label">Сегодня</div></div>
<div class="stat"><div class="num" style="color:${s.trash ? '#ef4444' : 'var(--muted)'}">${s.trash}</div><div class="label"><a href="trash.html">В корзине →</a></div></div>
${diskHtml}
`;
}
@@ -92,7 +104,7 @@ async function loadDashboard() {
const res = await fetch(`${API}/api/dashboard`, { headers: hdr() });
if (!res.ok) { document.getElementById('stats').innerHTML = '<div class="dash-empty">Не удалось загрузить данные</div>'; return; }
const d = await res.json();
renderStats(d.stats);
renderStats(d.stats, d.disk);
renderActivity(d.activity);
renderRecent(d.recent_entries);
renderActive(d.active_groups);
+3
View File
@@ -159,6 +159,9 @@ document.getElementById('grid').addEventListener('click', e => {
(async () => {
if (await checkAuth()) {
buildSidebar(document.body.dataset.page);
const detachedTab = document.getElementById('detachedTab');
if (detachedTab) detachedTab.style.display = isAdmin() ? '' : 'none';
if (mode === 'detached' && !isAdmin()) { mode = 'attached'; }
document.querySelectorAll('#tabToggle button').forEach(b => {
b.classList.toggle('active', b.dataset.mode === mode);
});
+75 -3
View File
@@ -23,7 +23,7 @@ async function loadGroups() {
list.innerHTML = `<div class="group-grid">` + groups.map(g => {
const schedSet = g.day_of_week != null && g.time_start && g.time_end;
return `
<div class="group-card">
<div class="group-card" data-group-id="${g.id}">
${g.cover_path
? `<img class="group-cover" src="${API}${g.cover_path}" data-group-id="${g.id}" data-group-name="${esc(g.name)}" title="Фото группы (хронология)" alt="">`
: `<div class="group-cover empty-cover" data-group-id="${g.id}" data-group-name="${esc(g.name)}" title="Фото группы (хронология)">📷</div>`}
@@ -149,7 +149,8 @@ function photoHTML(p) {
const uploaded = `Загружено: ${new Date(p.created_at).toLocaleDateString('ru')}`;
const isCover = currentCover && p.photo_path === currentCover;
return `
<div class="photo-item" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}">
<div class="photo-item" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}" draggable="true">
<div class="drag-hint" title="Перетащите, чтобы изменить порядок">⠿</div>
<img src="${API}${p.photo_path}" data-img="${API}${p.photo_path}" alt="">
<div class="p-cap">${esc(p.caption || '')}</div>
<div class="p-date">${taken ? taken + ' · ' : ''}${uploaded}</div>
@@ -236,6 +237,8 @@ async function deletePhoto(id) {
if (!confirm('Удалить это фото безвозвратно?')) return;
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos/${id}`, { method: 'DELETE', headers: hdr() });
if (!res.ok) { const e = await res.json(); alert(e.error || 'Ошибка'); return; }
const p = photos.find(x => x.id === id);
if (p && currentCover === p.photo_path) currentCover = null;
showToast('Фото удалено');
loadGroupPhotos();
loadGroups();
@@ -306,6 +309,73 @@ document.getElementById('groupList').addEventListener('click', e => {
}
});
function updateGroupCoverUI(coverPath) {
const card = document.querySelector(`.group-card[data-group-id="${currentGroupId}"]`);
if (!card) return;
const g = allGroups.find(x => String(x.id) === String(currentGroupId));
const name = g ? g.name : currentGroupName;
const existing = card.querySelector('.group-cover');
if (existing && existing.tagName === 'IMG') { existing.src = API + coverPath; return; }
const el = document.createElement('img');
el.className = 'group-cover';
el.src = API + coverPath;
el.dataset.groupId = String(currentGroupId);
el.dataset.groupName = name;
el.title = 'Фото группы (хронология)';
el.alt = '';
if (existing) existing.replaceWith(el);
else card.insertBefore(el, card.firstChild);
}
let dragPhotoId = null;
async function persistPhotoOrder() {
const items = [...document.querySelectorAll('#photoGrid .photo-item')];
const order = items.map(el => parseInt(el.dataset.photoId, 10));
if (JSON.stringify(order) === JSON.stringify(photos.map(p => p.id))) return;
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos/reorder`, {
method: 'PUT',
headers: hdrJson(),
body: JSON.stringify({ order })
});
if (res.ok) showToast('Порядок фото сохранён');
else {
try { const err = await res.json(); alert(err.error || 'Ошибка'); } catch { alert('Ошибка'); }
loadGroupPhotos();
}
}
document.getElementById('photoGrid').addEventListener('dragstart', e => {
const item = e.target.closest('.photo-item');
if (!item) return;
e.stopPropagation();
dragPhotoId = parseInt(item.dataset.photoId, 10);
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(dragPhotoId));
});
document.getElementById('photoGrid').addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.photo-item');
const dragged = dragPhotoId != null ? document.querySelector(`.photo-item[data-photo-id="${dragPhotoId}"]`) : null;
if (!target || !dragged || target === dragged) return;
if (target.contains(dragged)) return;
const rect = target.getBoundingClientRect();
const grid = document.getElementById('photoGrid');
grid.insertBefore(dragged, e.clientX > rect.left + rect.width / 2 ? target.nextSibling : target);
});
document.getElementById('photoGrid').addEventListener('dragend', e => {
const item = e.target.closest('.photo-item');
if (item) item.classList.remove('dragging');
if (dragPhotoId != null) persistPhotoOrder();
dragPhotoId = null;
});
document.getElementById('photoGrid').addEventListener('drop', e => e.preventDefault());
document.getElementById('photoGrid').addEventListener('click', async e => {
const coverBtn = e.target.closest('.p-cover');
if (coverBtn) {
@@ -320,9 +390,11 @@ document.getElementById('photoGrid').addEventListener('click', async e => {
if (res.ok) {
const g = await res.json();
currentCover = g.cover_path || null;
const grp = allGroups.find(x => String(x.id) === String(currentGroupId));
if (grp) grp.cover_path = currentCover;
showToast('Обложка обновлена');
updateGroupCoverUI(currentCover);
loadGroupPhotos();
loadGroups();
} else {
const err = await res.json();
alert(err.error || 'Ошибка');
+18 -7
View File
@@ -1,20 +1,31 @@
const API = location.origin;
async function doLogin() {
const pwd = document.getElementById('pwdInput').value;
if (!pwd) return;
const username = document.getElementById('userInput').value.trim();
const password = document.getElementById('pwdInput').value;
if (!username || !password) return;
const err = document.getElementById('loginErr');
err.style.display = 'none';
try {
const res = await fetch(`${API}/api/stats`, { headers: { 'X-Admin-Token': pwd } });
if (res.ok) {
sessionStorage.setItem('adminToken', pwd);
const res = await fetch(`${API}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json().catch(() => ({}));
if (res.ok && data.token) {
sessionStorage.setItem('authToken', data.token);
location.href = 'dashboard.html';
} else {
document.getElementById('loginErr').style.display = 'block';
err.textContent = data.error || 'Неверный логин или пароль';
err.style.display = 'block';
}
} catch {
document.getElementById('loginErr').style.display = 'block';
err.textContent = 'Ошибка соединения';
err.style.display = 'block';
}
}
document.getElementById('pwdInput').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.getElementById('userInput').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
document.querySelector('.login button').addEventListener('click', doLogin);
+4 -4
View File
@@ -102,9 +102,9 @@ function renderSystemInfo(si) {
</div>
<div class="sys-card">
<div class="sys-title">Фото</div>
<div class="sys-item"><span class="sys-label">Фото групп</span><span class="sys-value">${photos.group_photos?.count || 0}</span></div>
<div class="sys-item"><span class="sys-label">Фото в записях</span><span class="sys-value">${photos.entry_photos?.count || 0}</span></div>
<div class="sys-item"><span class="sys-label">Всего</span><span class="sys-value"><b>${photos.total_count || 0}</b></span></div>
<div class="sys-item"><span class="sys-label">Фото групп</span><span class="sys-value">${photos.group_photos?.count || 0} · ${photos.group_photos?.size || '0 B'}</span></div>
<div class="sys-item"><span class="sys-label">Фото в записях</span><span class="sys-value">${photos.entry_photos?.count || 0} · ${photos.entry_photos?.size || '0 B'}</span></div>
<div class="sys-item"><span class="sys-label">Всего</span><span class="sys-value"><b>${photos.total_count || 0}</b> · <b>${photos.total_size || '0 B'}</b></span></div>
</div>
<div class="sys-card">
<div class="sys-title">Файлы</div>
@@ -213,7 +213,7 @@ document.getElementById('restoreBtn').addEventListener('click', () => document.g
document.getElementById('saveBtn').addEventListener('click', saveSettings);
(async () => {
if (await checkAuth()) {
if (await requireAdminPage()) {
buildSidebar(document.body.dataset.page);
bindDirty();
loadSettings();