const PAGE_SIZE = 20; let currentEntries = []; let currentGroups = []; let pendingEntries = []; let pendingGroups = []; let page = 1; let totalEntries = 0; let totalGroups = 0; let pendingTotalEntries = 0; let pendingTotalGroups = 0; let purgeDays = 30; function isImageFile(name) { return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || ''); } function filesHTML(files) { if (!files || !files.length) return ''; return `
${files.map(f => isImageFile(f.name) ? `${esc(f.name)}` : `${esc(f.name)}` ).join('')}
`; } function entryHTML(e) { return `
${e.photo_path ? `` : ''}
${esc(e.student_name)}
${esc(e.group_name)}
${esc(e.description)}
${filesHTML(e.files)}
Удалена: ${new Date(e.deleted_at).toLocaleString('ru')}
`; } function groupHTML(g) { return `
${g.cover_path ? `` : ''}
${esc(g.name)}
${g.branch_name ? `
${esc(g.branch_name)}
` : ''}
Удалена: ${new Date(g.deleted_at).toLocaleString('ru')}
`; } function pendingEntryHTML(e) { return `
${e.photo_path ? `` : ''}
${esc(e.student_name)}
${esc(e.group_name)}
${esc(e.description)}
${filesHTML(e.files)}
Удаление через ${purgeDays} дн.: ${new Date(e.purge_at).toLocaleString('ru')}
`; } function pendingGroupHTML(g) { return `
${g.cover_path ? `` : ''}
${esc(g.name)}
${g.branch_name ? `
${esc(g.branch_name)}
` : ''}
Удаление через ${purgeDays} дн.: ${new Date(g.purge_at).toLocaleString('ru')}
`; } function renderGroups() { const grid = document.getElementById('groupsGrid'); grid.className = 'grid'; grid.innerHTML = currentGroups.length ? currentGroups.map(groupHTML).join('') : '
Групп в корзине нет
'; document.getElementById('groupsInfo').textContent = totalGroups > 0 ? `— групп в корзине: ${totalGroups}` : ''; } function renderGrid() { const grid = document.getElementById('grid'); grid.className = 'grid'; grid.innerHTML = currentEntries.length ? currentEntries.map(entryHTML).join('') : '
Записей в корзине нет
'; document.getElementById('entriesInfo').textContent = totalEntries > 0 ? `— записей в корзине: ${totalEntries}` : ''; document.getElementById('trashInfo').textContent = totalEntries + totalGroups > 0 ? `В корзине: записей ${totalEntries}, групп ${totalGroups}` : 'Корзина пуста'; document.getElementById('emptyBtn').disabled = totalEntries + totalGroups === 0; renderPager(document.getElementById('pager'), page, Math.max(1, Math.ceil(totalEntries / PAGE_SIZE)), goPage); } function renderPending() { document.getElementById('pendingGroupsGrid').className = 'grid'; document.getElementById('pendingGroupsGrid').innerHTML = pendingGroups.length ? pendingGroups.map(pendingGroupHTML).join('') : '
Нет групп, помеченных на удаление
'; document.getElementById('pendingGrid').className = 'grid'; document.getElementById('pendingGrid').innerHTML = pendingEntries.length ? pendingEntries.map(pendingEntryHTML).join('') : '
Нет записей, помеченных на удаление
'; document.getElementById('pendingGroupsInfo').textContent = pendingTotalGroups > 0 ? `— ${pendingTotalGroups}` : ''; document.getElementById('pendingEntriesInfo').textContent = pendingTotalEntries > 0 ? `— ${pendingTotalEntries}` : ''; const n = pendingTotalEntries + pendingTotalGroups; document.getElementById('pendingInfo').textContent = n > 0 ? `— будет удалено: ${n}` : ''; const hint = document.getElementById('pendingToggleHint'); if (hint) { hint.textContent = n > 0 ? `Помечено: записей ${pendingTotalEntries}, групп ${pendingTotalGroups} — безвозвратно удалятся через ${purgeDays} дней` : 'Данных, помеченных на удаление, нет'; } document.getElementById('pendingSection').hidden = !document.getElementById('pendingToggle').checked; } async function loadTrash() { const p = new URLSearchParams(); p.set('limit', PAGE_SIZE); p.set('offset', (page - 1) * PAGE_SIZE); const [trashRes, pendRes] = await Promise.all([ fetch(`${API}/api/trash?${p}`, { headers: hdr() }), fetch(`${API}/api/trash/pending`, { headers: hdr() }), ]); const data = await trashRes.json(); const pend = await pendRes.json(); currentEntries = data.entries; totalEntries = data.total; currentGroups = data.groups; totalGroups = data.total_groups; pendingEntries = pend.entries; pendingTotalEntries = pend.total; pendingGroups = pend.groups; pendingTotalGroups = pend.total_groups; if (pend.purge_days > 0) purgeDays = pend.purge_days; renderGroups(); renderGrid(); renderPending(); } function goPage(p) { page = p; loadTrash(); } async function restoreEntry(id) { const res = await fetch(`${API}/api/entries/${id}/restore`, { method: 'PUT', headers: hdr() }); if (res.ok) { showToast('Запись восстановлена'); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка восстановления'); } } async function hardDelete(id) { if (!confirm(`Пометить запись на удаление? Она будет безвозвратно удалена через ${purgeDays} дней вместе с файлами и фото.`)) return; const res = await fetch(`${API}/api/entries/${id}/permanent`, { method: 'DELETE', headers: hdr() }); if (res.ok) { showToast(`Запись помечена на удаление через ${purgeDays} дн.`); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка'); } } async function restoreGroup(id) { const res = await fetch(`${API}/api/groups/${id}/restore`, { method: 'PUT', headers: hdr() }); if (res.ok) { showToast('Группа восстановлена'); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка восстановления'); } } async function hardDeleteGroup(id, name) { if (!confirm(`Пометить группу «${name}» на удаление? Записи журнала, фото и файлы группы будут безвозвратно удалены через ${purgeDays} дней, ученики открепятся.`)) return; const res = await fetch(`${API}/api/groups/${id}/permanent`, { method: 'DELETE', headers: hdr() }); if (res.ok) { showToast(`Группа помечена на удаление через ${purgeDays} дн.`); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка'); } } async function unscheduleEntry(id) { const res = await fetch(`${API}/api/entries/${id}/unschedule`, { method: 'PUT', headers: hdr() }); if (res.ok) { showToast('Запись возвращена в корзину'); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка'); } } async function unscheduleGroup(id) { const res = await fetch(`${API}/api/groups/${id}/unschedule`, { method: 'PUT', headers: hdr() }); if (res.ok) { showToast('Группа возвращена в корзину'); loadTrash(); } else { const err = await res.json(); showToast(err.error || 'Ошибка'); } } async function emptyTrash() { const parts = []; if (totalEntries) parts.push(`записей: ${totalEntries}`); if (totalGroups) parts.push(`групп: ${totalGroups}`); if (!parts.length) return; if (!confirm(`Пометить всё из корзины (${parts.join(', ')}) на удаление? Данные будут безвозвратно удалены через ${purgeDays} дней.`)) return; const res = await fetch(`${API}/api/trash`, { method: 'DELETE', headers: hdr() }); if (res.ok) { showToast(`Всё помечено на удаление через ${purgeDays} дн.`); page = 1; loadTrash(); } else { const err = await res.json(); alert(err.error || 'Ошибка'); } } document.getElementById('emptyBtn').addEventListener('click', emptyTrash); document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open')); document.getElementById('pendingToggle').addEventListener('change', () => { const open = document.getElementById('pendingToggle').checked; document.getElementById('pendingSection').hidden = !open; if (open) loadTrash(); }); document.getElementById('grid').addEventListener('click', e => { const card = e.target.closest('.card'); if (!card) return; const rs = card.querySelector('[data-restore]'); if (rs && (e.target === rs || rs.contains(e.target))) { restoreEntry(parseInt(rs.dataset.restore, 10)); return; } const hd = card.querySelector('[data-hard]'); if (hd && (e.target === hd || hd.contains(e.target))) { hardDelete(parseInt(hd.dataset.hard, 10)); } }); document.getElementById('groupsGrid').addEventListener('click', e => { const card = e.target.closest('.card'); if (!card) return; const rs = card.querySelector('[data-restore-group]'); if (rs && (e.target === rs || rs.contains(e.target))) { restoreGroup(parseInt(rs.dataset.restoreGroup, 10)); return; } const hd = card.querySelector('[data-hard-group]'); if (hd && (e.target === hd || hd.contains(e.target))) { const id = parseInt(hd.dataset.hardGroup, 10); const g = currentGroups.find(x => String(x.id) === String(id)); hardDeleteGroup(id, g ? g.name : id); } }); document.getElementById('pendingGrid').addEventListener('click', e => { const card = e.target.closest('.card'); if (!card) return; const us = card.querySelector('[data-unschedule]'); if (us && (e.target === us || us.contains(e.target))) { unscheduleEntry(parseInt(us.dataset.unschedule, 10)); } }); document.getElementById('pendingGroupsGrid').addEventListener('click', e => { const card = e.target.closest('.card'); if (!card) return; const us = card.querySelector('[data-unschedule-group]'); if (us && (e.target === us || us.contains(e.target))) { unscheduleGroup(parseInt(us.dataset.unscheduleGroup, 10)); } }); (async () => { if (await checkAuth()) { buildSidebar(document.body.dataset.page); loadTrash(); } })();