172 lines
6.8 KiB
JavaScript
172 lines
6.8 KiB
JavaScript
const PAGE_SIZE = 20;
|
|
let page = 1;
|
|
let totalFiles = 0;
|
|
let currentFiles = [];
|
|
let groupsLoaded = false;
|
|
let studentsLoaded = false;
|
|
let searchTimer = null;
|
|
let mode = 'attached';
|
|
|
|
function setMode(m) {
|
|
mode = m === 'detached' ? 'detached' : 'attached';
|
|
document.querySelectorAll('#tabToggle button').forEach(b => {
|
|
b.classList.toggle('active', b.dataset.mode === mode);
|
|
});
|
|
const show = mode === 'attached';
|
|
const keep = ['searchInput', 'studentFilter', 'groupFilter', 'dateFrom', 'dateTo'].map(id => document.getElementById(id));
|
|
if (!show) { keep.forEach(el => { el.value = ''; }); }
|
|
keep.forEach(el => { el.parentElement.style.display = show ? '' : 'none'; });
|
|
page = 1;
|
|
loadFiles();
|
|
}
|
|
|
|
function isImageFile(name) {
|
|
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
|
|
}
|
|
|
|
function humanSize(b) {
|
|
if (!b) return '';
|
|
if (b < 1024) return b + ' Б';
|
|
if (b < 1048576) return (b / 1024).toFixed(1) + ' КБ';
|
|
return (b / 1048576).toFixed(1) + ' МБ';
|
|
}
|
|
|
|
function fileCardHTML(f) {
|
|
const url = `${API}/api/files/${f.token}`;
|
|
const thumb = isImageFile(f.name)
|
|
? `<img src="${thumbSrc(url)}" data-img="${esc(url)}" loading="lazy" decoding="async" alt="">`
|
|
: `<div class="file-icon">📄</div>`;
|
|
const target = isImageFile(f.name)
|
|
? `href="#" data-img="${esc(url)}"`
|
|
: `href="${url}" download`;
|
|
const meta = mode === 'detached'
|
|
? `<div class="group" style="color:#f59e0b">Откреплён от записи, не привязан</div>`
|
|
: `<div class="group">${esc(f.student_name)} · ${esc(f.group_name)}</div>`;
|
|
const action = mode === 'detached'
|
|
? `<button class="f-action f-del" data-delete="${f.id}" title="Удалить файл навсегда">Удалить</button>`
|
|
: `<button class="f-action" data-detach="${f.id}" title="Открепить файл от записи">Открепить</button>`;
|
|
return `
|
|
<div class="card">
|
|
${thumb}
|
|
<div class="info">
|
|
<div class="file-name"><a ${target}>${esc(f.name)}</a></div>
|
|
${meta}
|
|
<div class="time">${new Date(f.created_at).toLocaleString('ru')}${f.size ? ' · ' + humanSize(f.size) : ''}</div>
|
|
</div>
|
|
<div class="f-actions">${action}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderGrid() {
|
|
const grid = document.getElementById('grid');
|
|
if (!currentFiles.length) {
|
|
grid.className = 'grid';
|
|
grid.innerHTML = mode === 'detached'
|
|
? '<div class="empty">Откреплённых файлов нет</div>'
|
|
: '<div class="empty">Файлов не найдено</div>';
|
|
return;
|
|
}
|
|
grid.className = 'grid';
|
|
grid.innerHTML = currentFiles.map(fileCardHTML).join('');
|
|
}
|
|
|
|
async function detachFile(id) {
|
|
if (!confirm('Открепить файл от записи? Он переместится во вкладку «Откреплённые».')) return;
|
|
const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() });
|
|
if (res.ok) { showToast('Файл откреплён'); loadFiles(); }
|
|
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
|
}
|
|
|
|
async function deleteFile(id) {
|
|
if (!confirm('Удалить файл навсегда? Отменить это действие нельзя.')) return;
|
|
const res = await fetch(`${API}/api/files/${id}`, { method: 'DELETE', headers: hdr() });
|
|
if (res.ok) { showToast('Файл удалён'); loadFiles(); }
|
|
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
|
}
|
|
|
|
async function loadGroupOptions() {
|
|
if (groupsLoaded) return;
|
|
const sel = document.getElementById('groupFilter');
|
|
while (sel.options.length > 1) sel.remove(1);
|
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
|
const groups = await res.json();
|
|
groups.forEach(g => {
|
|
const o = document.createElement('option');
|
|
o.value = g.id; o.textContent = g.name;
|
|
sel.appendChild(o);
|
|
});
|
|
groupsLoaded = true;
|
|
}
|
|
|
|
async function loadStudentOptions() {
|
|
if (studentsLoaded) return;
|
|
const dl = document.getElementById('studentFilterList');
|
|
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
|
const names = await res.json();
|
|
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
|
studentsLoaded = true;
|
|
}
|
|
|
|
async function loadFiles() {
|
|
const p = new URLSearchParams();
|
|
if (mode === 'attached') {
|
|
const q = document.getElementById('searchInput').value;
|
|
const sn = document.getElementById('studentFilter').value;
|
|
const gid = document.getElementById('groupFilter').value;
|
|
const df = document.getElementById('dateFrom').value;
|
|
const dt = document.getElementById('dateTo').value;
|
|
if (q) p.set('search', q);
|
|
if (sn) p.set('student_name', sn);
|
|
if (gid) p.set('group_id', gid);
|
|
if (df) p.set('date_from', df);
|
|
if (dt) p.set('date_to', dt);
|
|
}
|
|
p.set('limit', PAGE_SIZE);
|
|
p.set('offset', (page - 1) * PAGE_SIZE);
|
|
const res = await fetch(`${API}/api/files${mode === 'detached' ? '/detached' : ''}?${p}`, { headers: hdr() });
|
|
const data = await res.json();
|
|
currentFiles = data.files;
|
|
totalFiles = data.total;
|
|
renderGrid();
|
|
renderPager(document.getElementById('pager'), page, Math.max(1, Math.ceil(totalFiles / PAGE_SIZE)), goPage);
|
|
}
|
|
|
|
function goPage(p) {
|
|
page = p;
|
|
loadFiles();
|
|
}
|
|
|
|
document.getElementById('studentFilter').addEventListener('change', () => { page = 1; loadFiles(); });
|
|
document.getElementById('groupFilter').addEventListener('change', () => { page = 1; loadFiles(); });
|
|
document.getElementById('dateFrom').addEventListener('change', () => { page = 1; loadFiles(); });
|
|
document.getElementById('dateTo').addEventListener('change', () => { page = 1; loadFiles(); });
|
|
document.getElementById('searchInput').addEventListener('input', () => {
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => { page = 1; loadFiles(); }, 300);
|
|
});
|
|
|
|
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
|
|
document.querySelectorAll('#tabToggle button').forEach(b => b.addEventListener('click', () => setMode(b.dataset.mode)));
|
|
|
|
document.getElementById('grid').addEventListener('click', e => {
|
|
const dt = e.target.closest('[data-detach]');
|
|
if (dt) { detachFile(parseInt(dt.dataset.detach, 10)); return; }
|
|
const del = e.target.closest('[data-delete]');
|
|
if (del) { deleteFile(parseInt(del.dataset.delete, 10)); }
|
|
});
|
|
|
|
(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);
|
|
});
|
|
loadGroupOptions();
|
|
loadStudentOptions();
|
|
loadFiles();
|
|
}
|
|
})(); |