Update project files
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user