let currentEntries = []; let editId = null; let editGroups = []; let groupsLoaded = false; let studentsLoaded = false; const PAGE_SIZE = 20; let page = 1; let totalEntries = 0; let searchTimer = null; let view = localStorage.getItem('journalView') === 'cards' ? 'cards' : 'list'; function isImageFile(name) { return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || ''); } function filesHTML(files) { if (!files || !files.length) return ''; return `
${files.map(f => { const link = isImageFile(f.name) ? `${esc(f.name)}` : `${esc(f.name)}`; return `${link}`; }).join('')}
`; } function findFile(id) { for (const e of currentEntries) { const f = (e.files || []).find(x => x.id === id); if (f) return f; } return null; } async function detachFile(id) { const f = findFile(id); if (!f) return; if (!confirm(`Открепить файл «${f.name}» от записи? Он переместится в «Файлы → Откреплённые».`)) return; const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() }); if (res.ok) { showToast('Файл откреплён'); loadEntries(); } else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); } } function gridEntryHTML(e) { return `
${e.photo_path ? `` : ''}
${esc(e.student_name)}
${esc(e.group_name)}
${esc(e.description)}
${filesHTML(e.files)}
${new Date(e.created_at).toLocaleString('ru')}
`; } function cardEntryHTML(e) { return `
${e.photo_path ? `` : `
`}
${esc(e.student_name)}
${esc(e.group_name)}
${esc(e.description)}
${filesHTML(e.files)}
${new Date(e.created_at).toLocaleString('ru')}
`; } function renderGrid() { const grid = document.getElementById('grid'); if (!currentEntries.length) { grid.className = 'grid'; grid.innerHTML = '
Ничего не найдено
'; return; } if (view === 'cards') { grid.className = 'journal-cards'; grid.innerHTML = currentEntries.map(cardEntryHTML).join(''); } else { grid.className = 'grid'; grid.innerHTML = currentEntries.map(gridEntryHTML).join(''); } } function setView(v) { view = v === 'cards' ? 'cards' : 'list'; localStorage.setItem('journalView', view); document.querySelectorAll('#viewToggle button').forEach(b => { b.classList.toggle('active', b.dataset.view === view); }); renderGrid(); } function setDefaultDates() { const today = new Date().toISOString().slice(0, 10); document.getElementById('dateFrom').value = today; document.getElementById('dateTo').value = today; } 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 => ``).join(''); } sel.value = e.group_id; document.getElementById('editModal').classList.add('open'); } function closeEdit() { document.getElementById('editModal').classList.remove('open'); document.getElementById('editFiles').value = ''; document.getElementById('editFilesPreview').innerHTML = ''; editId = null; } async function saveEdit() { if (!editId) return; const name = document.getElementById('editName').value.trim(); const group_id = document.getElementById('editGroup').value; const description = document.getElementById('editDesc').value.trim(); if (!name || !group_id || !description) { alert('Заполните все поля'); return; } const res = await fetch(`${API}/api/entries/${editId}`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ student_name: name, group_id, description }) }); if (!res.ok) { const err = await res.json(); alert(err.error); return; } const files = document.getElementById('editFiles').files; if (files.length) { const formData = new FormData(); for (const f of files) formData.append('files', f); const uploadRes = await fetch(`${API}/api/entries/${editId}/files`, { method: 'POST', headers: { 'X-Admin-Token': token }, body: formData }); if (!uploadRes.ok) { const err = await uploadRes.json().catch(() => ({})); alert(err.error || 'Ошибка загрузки файлов'); return; } } closeEdit(); loadEntries(); showToast('Запись обновлена'); } async function delEntry(id) { if (!confirm('Переместить запись в корзину? Файлы и фото сохранятся.')) return; await fetch(`${API}/api/entries/${id}`, { method: 'DELETE', headers: hdr() }); showToast('Запись перемещена в корзину'); loadEntries(); } document.getElementById('studentFilter').addEventListener('change', () => { page = 1; loadEntries(); }); document.getElementById('groupFilter').addEventListener('change', () => { page = 1; loadEntries(); }); document.getElementById('dateFrom').addEventListener('change', () => { page = 1; loadEntries(); }); document.getElementById('dateTo').addEventListener('change', () => { page = 1; loadEntries(); }); document.getElementById('searchInput').addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { page = 1; loadEntries(); }, 300); }); document.getElementById('editForm').addEventListener('submit', (e) => { e.preventDefault(); saveEdit(); }); document.getElementById('editFiles').addEventListener('change', (e) => { const preview = document.getElementById('editFilesPreview'); preview.innerHTML = ''; Array.from(e.target.files).forEach((f, i) => { const div = document.createElement('div'); div.className = 'file-preview-item'; div.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:.8rem'; div.innerHTML = `${esc(f.name)} ${(f.size/1024).toFixed(1)} KB `; preview.appendChild(div); }); }); function removeEditFile(index) { const input = document.getElementById('editFiles'); const dt = new DataTransfer(); Array.from(input.files).forEach((f, i) => { if (i !== index) dt.items.add(f); }); input.files = dt.files; input.dispatchEvent(new Event('change')); } async function exportCSV() { const p = new URLSearchParams(); 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); const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() }); const entries = (await res.json()).entries; if (!entries.length) { alert('Нет данных для экспорта'); return; } const rows = [['Фамилия и имя', 'Группа', 'Описание', 'Дата', 'Фото']]; entries.forEach(e => { rows.push([ e.student_name, e.group_name, e.description.replace(/\n/g, ' '), new Date(e.created_at).toLocaleString('ru'), e.photo_path ? `${API}${e.photo_path}` : '' ]); }); const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(';')).join('\n'); const bom = '\uFEFF'; const blob = new Blob([bom + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const dateLabel = df && dt ? `${df}_${dt}` : 'all'; a.download = `journal_${dateLabel}.csv`; a.click(); URL.revokeObjectURL(url); } let linkGroups = []; let linkStudentsLoaded = false; let createdLinkUrl = ''; function fmtD(d) { return d ? d.split('-').reverse().join('.') : 'весь период'; } async function openLinkCreate() { const gid = document.getElementById('groupFilter').value; if (!gid) { alert('Сначала выберите группу в фильтре'); return; } const df = document.getElementById('dateFrom').value; const dt = document.getElementById('dateTo').value; if (!linkGroups.length) { const res = await fetch(`${API}/api/groups`, { headers: hdr() }); linkGroups = await res.json(); } if (!linkStudentsLoaded) { const res = await fetch(`${API}/api/students/names`, { headers: hdr() }); const names = await res.json(); const dl = document.getElementById('linkStudentList'); dl.innerHTML = names.map(n => `