let allStudents = []; let allGroups = []; let groupOptionsLoaded = false; let sPage = 1; const S_PAGE_SIZE = 25; let sSearchTimer = null; let aSearchTimer = null; let attachGroupLoaded = false; async function loadGroupOptions(sel, selected) { if (!groupOptionsLoaded) { const res = await fetch(`${API}/api/groups`, { headers: hdr() }); allGroups = await res.json(); groupOptionsLoaded = true; const filt = document.getElementById('studentGroupFilter'); if (filt && filt.options.length <= 1) { filt.innerHTML = `` + allGroups.map(g => ``).join(''); } } if (sel) { sel.innerHTML = `` + allGroups.map(g => ``).join(''); } } async function loadStudents() { const res = await fetch(`${API}/api/students`, { headers: hdr() }); allStudents = await res.json(); renderStudents(); } function renderStudents() { const q = document.getElementById('studentSearch').value.trim().toLowerCase(); const gid = document.getElementById('studentGroupFilter').value; let filtered = q ? allStudents.filter(s => s.name.toLowerCase().includes(q)) : allStudents; if (gid) filtered = filtered.filter(s => String(s.group_id) === gid); const totalPages = Math.max(1, Math.ceil(filtered.length / S_PAGE_SIZE)); if (sPage > totalPages) sPage = 1; const slice = filtered.slice((sPage - 1) * S_PAGE_SIZE, sPage * S_PAGE_SIZE); const list = document.getElementById('studentList'); if (!slice.length) { list.innerHTML = '
Ничего не найдено
'; } else { list.innerHTML = slice.map(s => `
`).join(''); slice.forEach(s => loadGroupOptions(document.getElementById(`sgroup-${s.id}`), s.group_id)); } renderPager(document.getElementById('pager'), sPage, totalPages, goStudentPage); } function goStudentPage(p) { sPage = p; renderStudents(); } async function addStudent() { const input = document.getElementById('newStudent'); const name = input.value.trim(); if (!name) return; const res = await fetch(`${API}/api/students`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ name }) }); if (res.ok) { input.value = ''; loadStudents(); fillAutocomplete(); showToast(`Добавлен: ${name}`); } else { const e = await res.json(); alert(e.error); } } async function saveStudent(id) { const name = document.getElementById(`sname-${id}`).value.trim(); if (!name) return; const group_id = document.getElementById(`sgroup-${id}`).value; const res = await fetch(`${API}/api/students/${id}`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ name, group_id }) }); if (res.ok) { loadStudents(); fillAutocomplete(); showToast(`Сохранено: ${name}`); } else { const e = await res.json(); alert(e.error); } } async function delStudent(id) { if (!confirm('Удалить ученика из списка? Старые записи останутся.')) return; await fetch(`${API}/api/students/${id}`, { method: 'DELETE', headers: hdr() }); loadStudents(); fillAutocomplete(); } function openBatchAdd() { document.getElementById('batchText').value = ''; loadGroupOptions(document.getElementById('batchGroup'), ''); document.getElementById('batchModal').classList.add('open'); } function closeBatchAdd() { document.getElementById('batchModal').classList.remove('open'); } async function batchAdd() { const raw = document.getElementById('batchText').value; const names = [...new Set(raw.split('\n').map(s => s.trim()).filter(Boolean))]; if (!names.length) return; const group_id = document.getElementById('batchGroup').value; let added = 0, dupes = 0; for (const name of names) { const res = await fetch(`${API}/api/students`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ name, group_id: group_id || null }) }); if (res.ok) added++; else if (res.status === 409) dupes++; } closeBatchAdd(); loadStudents(); fillAutocomplete(); showToast(`Добавлено: ${added}${dupes ? `, дубликатов: ${dupes}` : ''}`); } document.getElementById('newStudent').addEventListener('keydown', e => { if (e.key === 'Enter') addStudent(); }); document.getElementById('batchForm').addEventListener('submit', e => { e.preventDefault(); batchAdd(); }); document.getElementById('studentSearch').addEventListener('input', () => { clearTimeout(sSearchTimer); sSearchTimer = setTimeout(() => { sPage = 1; renderStudents(); }, 300); }); document.getElementById('studentGroupFilter').addEventListener('change', () => { sPage = 1; renderStudents(); }); function exportStudentsCSV() { const gid = document.getElementById('studentGroupFilter').value; const list = gid ? allStudents.filter(s => String(s.group_id) === gid) : allStudents; if (!list.length) { alert('Нет данных для экспорта'); return; } const rows = [['Фамилия Имя', 'Группа', 'Ссылка на страницу']]; list.forEach(s => { rows.push([ s.name, s.group_name || '', `${location.origin}/?nameInput=${encodeURIComponent(s.name)}` ]); }); const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(';')).join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const groupName = gid ? (allGroups.find(g => g.id == gid)?.name || 'group') : 'all'; a.download = `students_${groupName.replace(/[\\/:*?"<>|]/g, '_')}.csv`; a.click(); URL.revokeObjectURL(url); showToast(`Экспортировано: ${list.length}`); } async function exportStudentsUrls() { const gid = document.getElementById('studentGroupFilter').value; const list = gid ? allStudents.filter(s => String(s.group_id) === gid) : allStudents; if (!list.length) { alert('Нет данных для экспорта'); return; } const groupName = gid ? (allGroups.find(g => g.id == gid)?.name || 'group') : 'all'; await exportStudentUrlZip(list, `students_${groupName.replace(/[\\/:*?"<>|]/g, '_')}.zip`); showToast(`Экспортировано: ${list.length}`); } async function openBatchAttach() { const sel = document.getElementById('attachGroup'); if (!attachGroupLoaded) { const res = await fetch(`${API}/api/groups`, { headers: hdr() }); const groups = await res.json(); sel.innerHTML = `` + groups.map(g => ``).join(''); attachGroupLoaded = true; } document.getElementById('attachSearch').value = ''; renderAttachList(); document.getElementById('attachModal').classList.add('open'); } function closeBatchAttach() { document.getElementById('attachModal').classList.remove('open'); } function renderAttachList() { const q = document.getElementById('attachSearch').value.trim().toLowerCase(); const filtered = q ? allStudents.filter(s => s.name.toLowerCase().includes(q)) : allStudents; const list = document.getElementById('attachList'); if (!filtered.length) { list.innerHTML = '
Никого не найдено
'; return; } list.innerHTML = filtered.map(s => ` `).join(''); } async function batchAttach() { const group_id = document.getElementById('attachGroup').value; const ids = [...document.querySelectorAll('#attachList input[type=checkbox]:checked')].map(c => c.value); if (!group_id) { alert('Выберите группу'); return; } if (!ids.length) { alert('Выберите учеников'); return; } const res = await fetch(`${API}/api/students/batch-group`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ group_id, student_ids: ids }) }); if (res.ok) { const r = await res.json(); closeBatchAttach(); loadStudents(); showToast(`Прикреплено: ${r.updated}`); } else { const e = await res.json(); alert(e.error); } } document.getElementById('attachForm').addEventListener('submit', e => { e.preventDefault(); batchAttach(); }); document.getElementById('attachSearch').addEventListener('input', () => { clearTimeout(aSearchTimer); aSearchTimer = setTimeout(renderAttachList, 300); }); document.getElementById('addStudentBtn').addEventListener('click', addStudent); document.getElementById('batchAddBtn').addEventListener('click', openBatchAdd); document.getElementById('batchAttachBtn').addEventListener('click', openBatchAttach); document.getElementById('exportStudentsCsvBtn').addEventListener('click', exportStudentsCSV); document.getElementById('exportStudentsUrlsBtn').addEventListener('click', exportStudentsUrls); document.getElementById('batchModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeBatchAdd(); }); document.getElementById('attachModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeBatchAttach(); }); document.getElementById('batchCancelBtn').addEventListener('click', closeBatchAdd); document.getElementById('attachCancelBtn').addEventListener('click', closeBatchAttach); document.getElementById('studentList').addEventListener('click', e => { const sv = e.target.closest('[data-save-student]'); if (sv) { saveStudent(parseInt(sv.dataset.saveStudent, 10)); return; } const cp = e.target.closest('[data-copy-student]'); if (cp) { copyStudentLink(cp.dataset.copyStudent); return; } const dl = e.target.closest('[data-del-student]'); if (dl) { delStudent(parseInt(dl.dataset.delStudent, 10)); } }); (async () => { if (await checkAuth()) { buildSidebar(document.body.dataset.page); fillAutocomplete(); loadGroupOptions(); loadStudents(); } })();