async function init() { if (!(await checkAuth())) return; buildSidebar('shorts'); document.getElementById('addShortsBtn').addEventListener('click', () => openShortsModal(null)); document.getElementById('shortsCancel').addEventListener('click', closeModal); document.getElementById('shortsUsed').addEventListener('change', e => { const at = document.getElementById('shortsUsedAt'); if (e.target.checked && !at.value) at.value = todayIso(); if (!e.target.checked) at.value = ''; }); document.getElementById('shortsUsedAt').addEventListener('change', e => { document.getElementById('shortsUsed').checked = !!e.target.value; }); document.getElementById('shortsAiBtn').addEventListener('click', openShortsAi); document.getElementById('aiImproveBtn').addEventListener('click', improveShortsAi); document.getElementById('aiAcceptBtn').addEventListener('click', acceptShortsAi); document.getElementById('aiRejectBtn').addEventListener('click', closeShortsAi); document.getElementById('shortsSave').addEventListener('click', saveShorts); document.getElementById('shortsSearchBtn').addEventListener('click', () => { shortsPage = 1; loadShorts(); }); document.getElementById('shortsClearBtn').addEventListener('click', () => { document.getElementById('shortsSearch').value = ''; shortsPage = 1; loadShorts(); }); document.getElementById('shortsSearch').addEventListener('keydown', e => { if (e.key === 'Enter') { shortsPage = 1; loadShorts(); } }); connectShortsLive(); await loadShorts(); } let shortsLiveSource = null; let shortsRefreshTimer = null; function connectShortsLive() { if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } const connect = () => { const t = sessionStorage.getItem('authToken'); if (!t) return; shortsLiveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`); shortsLiveSource.addEventListener('shorts_changed', () => { if (shortsRefreshTimer) return; shortsRefreshTimer = setTimeout(() => { shortsRefreshTimer = null; loadShorts(); }, 300); }); shortsLiveSource.onerror = () => { if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } setTimeout(connect, 5000); }; }; connect(); window.addEventListener('beforeunload', () => { if (shortsLiveSource) shortsLiveSource.close(); }); } let shortsPage = 1; const SHORTS_PAGE_SIZE = 50; let shortsTotal = 0; let editingId = null; async function loadShorts() { const tbody = document.querySelector('#shortsTable tbody'); tbody.innerHTML = '…'; const p = new URLSearchParams(); p.set('limit', SHORTS_PAGE_SIZE); p.set('offset', (shortsPage - 1) * SHORTS_PAGE_SIZE); const search = document.getElementById('shortsSearch').value.trim(); if (search) p.set('search', search); try { const res = await fetch(`${API}/api/shorts?${p}`, { headers: hdr() }); if (!res.ok) { tbody.innerHTML = 'Ошибка загрузки'; return; } const data = await res.json(); shortsTotal = data.total || 0; const rows = data.items || []; const totalPages = Math.max(1, Math.ceil(shortsTotal / SHORTS_PAGE_SIZE)); const empty = document.getElementById('shortsEmpty'); if (!rows.length) { empty.style.display = 'block'; tbody.innerHTML = ''; } else { empty.style.display = 'none'; tbody.innerHTML = rows.map(r => { const dateStr = r.lesson_date ? fmtDateOnlyIso(r.lesson_date) : ''; const usedDateStr = r.used_at ? fmtDateOnlyIso(r.used_at) : '—'; const author = r.author_name || r.author_username || '—'; return ` ${esc(r.resident)} ${dateStr} ${esc(r.topic || '')} ${esc(r.message || '')} ${esc(author)}
${r.used ? esc(usedDateStr) : '—'}
`; }).join(''); } renderShortsPager(document.getElementById('shortsPager'), shortsPage, totalPages, goShortsPage); attachRowHandlers(); renderIcons(); } catch (e) { tbody.innerHTML = 'Ошибка: ' + esc(e.message) + ''; } } function attachRowHandlers() { const tbody = document.querySelector('#shortsTable tbody'); tbody.querySelectorAll('.used-check').forEach(cb => { cb.addEventListener('change', () => toggleUsed(Number(cb.dataset.id), cb.checked)); }); tbody.querySelectorAll('[data-edit]').forEach(b => { b.addEventListener('click', () => loadShortsRow(Number(b.dataset.edit))); }); tbody.querySelectorAll('[data-del]').forEach(b => { b.addEventListener('click', () => delShorts(Number(b.dataset.del))); }); } function renderShortsPager(container, page, totalPages, onPageChange) { if (totalPages <= 1) { container.innerHTML = ''; return; } const pages = []; const maxVisible = 5; let start = Math.max(1, page - Math.floor(maxVisible / 2)); let end = Math.min(totalPages, start + maxVisible - 1); if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1); for (let i = start; i <= end; i++) pages.push(i); let html = ''; if (page > 1) html += ``; if (start > 1) { html += ``; if (start > 2) html += `…`; } for (const p of pages) html += ``; if (end < totalPages) { if (end < totalPages - 1) html += `…`; html += ``; } if (page < totalPages) html += ``; container.innerHTML = html; container.querySelectorAll('button').forEach(b => { b.addEventListener('click', () => onPageChange(parseInt(b.dataset.page, 10))); }); } function goShortsPage(p) { shortsPage = p; loadShorts(); } function openShortsModal(row) { editingId = row ? row.id : null; document.getElementById('shortsModalTitle').textContent = row ? `Сообщение #${row.id}` : 'Новое сообщение'; document.getElementById('shortsLinesHint').textContent = row ? ' (только текст сообщения)' : ' (формат: ФИО - текст или ФИО — текст)'; document.getElementById('shortsDate').value = row ? row.lesson_date : todayIso(); document.getElementById('shortsTopic').value = row ? (row.topic || '') : ''; document.getElementById('shortsResident').value = row ? (row.resident || '') : ''; document.getElementById('shortsUsed').checked = row ? !!row.used : false; document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : ''; const usedDisplay = row ? '' : 'none'; document.getElementById('shortsUsedField').style.display = usedDisplay; document.getElementById('shortsUsedAtField').style.display = usedDisplay; if (row) { document.getElementById('shortsLines').value = row.message || ''; } else { document.getElementById('shortsLines').value = ''; } document.getElementById('shortsModal').classList.add('open'); } function closeModal() { document.getElementById('shortsModal').classList.remove('open'); } async function saveShorts() { const date = document.getElementById('shortsDate').value.trim(); const topic = document.getElementById('shortsTopic').value.trim() || null; const linesText = document.getElementById('shortsLines').value.trim(); const residentOverride = document.getElementById('shortsResident').value.trim() || null; const used = document.getElementById('shortsUsed').checked; let usedAt = document.getElementById('shortsUsedAt').value.trim() || null; if (used && !usedAt) usedAt = todayIso(); if (!used) usedAt = null; if (!date) { showToast('Укажите дату занятия'); return; } if (!linesText) { showToast('Укажите текст сообщения(я)'); return; } let residentFromLine = residentOverride || ''; const lines = linesText.split('\n'); const parsed = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const match = trimmed.match(/^(.+?)\s*[-—]\s*(.+)$/); if (match) { parsed.push({ resident: match[1].trim(), message: match[2].trim() }); if (!residentFromLine) residentFromLine = match[1].trim(); } else { parsed.push({ resident: residentOverride || 'Не указан', message: trimmed }); if (!residentFromLine) residentFromLine = residentOverride || 'Не указан'; } } if (!parsed.length) { showToast('Нет сообщений для добавления'); return; } try { let res; if (editingId) { const resident = residentOverride || parsed[0].resident || 'Не указан'; const message = parsed.length === 1 && !parsed[0].resident ? parsed[0].message : parsed.map(p => p.message).join('\n'); res = await fetch(`${API}/api/shorts/${editingId}`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ resident, lesson_date: date, topic, message, used, used_at: usedAt }) }); } else { res = await fetch(`${API}/api/shorts`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ lesson_date: date, topic, lines: linesText, resident: residentOverride, used }) }); } if (!res.ok) { const err = await res.json().catch(() => ({})); showToast(err.error || 'Ошибка'); return; } closeModal(); shortsPage = 1; await loadShorts(); showToast(editingId ? 'Сообщение обновлено' : 'Сообщения добавлены'); } catch (e) { showToast('Ошибка: ' + e.message); } } async function delShorts(id) { if (!confirm('Переместить сообщение в корзину?')) return; try { const res = await fetch(`${API}/api/shorts/${id}`, { method: 'DELETE', headers: hdr() }); if (!res.ok) { const err = await res.json().catch(() => ({})); showToast(err.error || 'Ошибка'); return; } await loadShorts(); showToast('Сообщение перемещено в корзину'); } catch (e) { showToast('Ошибка: ' + e.message); } } async function toggleUsed(id, checked) { try { const usedAt = checked ? todayIso() : null; const res = await fetch(`${API}/api/shorts/${id}`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ used: checked, used_at: usedAt }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); showToast(err.error || 'Ошибка'); return; } await loadShorts(); } catch (e) { showToast('Ошибка: ' + e.message); } } async function loadShortsRow(id) { try { const res = await fetch(`${API}/api/shorts/${id}`, { headers: hdr() }); if (!res.ok) { showToast('Не найдено'); return; } openShortsModal(await res.json()); } catch (e) { showToast('Ошибка: ' + e.message); } } let shortsAiBusy = false; async function openShortsAi() { const src = document.getElementById('aiSourceText'); src.value = document.getElementById('shortsLines').value; document.getElementById('aiSuggestion').textContent = ''; document.getElementById('aiCorrectModal').classList.add('open'); const sel = document.getElementById('aiModelSelect'); try { const res = await fetch(`${API}/api/ai/profiles`, { headers: hdr() }); if (!res.ok) throw new Error('Профили недоступны'); const data = await res.json(); const profiles = data.profiles || []; sel.innerHTML = `` + profiles.map(p => ``).join(''); sel.value = data.active || 'native'; } catch (e) { sel.innerHTML = ''; } } function closeShortsAi() { document.getElementById('aiCorrectModal').classList.remove('open'); } async function improveShortsAi() { if (shortsAiBusy) return; const text = document.getElementById('aiSourceText').value.trim(); if (!text) { showToast('Нет текста для улучшения'); return; } const suggestionEl = document.getElementById('aiSuggestion'); const btn = document.getElementById('aiImproveBtn'); shortsAiBusy = true; btn.disabled = true; suggestionEl.textContent = 'Обработка...'; try { const res = await fetch(`${API}/api/shorts/ai-correct`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ text, profile: document.getElementById('aiModelSelect').value }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Ошибка ИИ'); } const data = await res.json(); suggestionEl.textContent = data.suggestion || text; } catch (e) { suggestionEl.textContent = 'Ошибка: ' + e.message; showToast('ИИ недоступен: ' + e.message); } finally { shortsAiBusy = false; btn.disabled = false; } } function acceptShortsAi() { const suggestion = document.getElementById('aiSuggestion').textContent; if (suggestion && suggestion !== 'Обработка...' && !suggestion.startsWith('Ошибка:')) { document.getElementById('shortsLines').value = suggestion; } closeShortsAi(); } init();