let currentEntries = []; let seenEntryIds = new Set(); let highlightNewIds = new Set(); let highlightClearTimer = null; 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'; let aiCorrecting = false; let editDeletePhoto = false; let linkSettings = null; async function loadLinkSettings() { if (!linkSettings) { const res = await fetch(`${API}/api/settings`, { headers: hdr() }); linkSettings = await res.json().catch(() => ({})); } return linkSettings; } function isImageFile(name) { return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || ''); } function mainPhotoPath(e) { if (e.photo_path) return e.photo_path; if (e.photos && e.photos.length) return e.photos[0].photo_path; return null; } 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('')}
`; } const AI_STATUS_LABELS = { pending: 'ИИ: в очереди', processing: 'ИИ: проверяет…', done: 'ИИ: проверено', skipped: 'ИИ: пропущено', error: 'ИИ: ошибка', reverted: 'ИИ: возвращён оригинал', }; const AI_STATUS_ICONS = { pending: 'clock', processing: 'loader', done: 'circle-check', skipped: 'skip-forward', error: 'circle-alert', reverted: 'undo', }; function aiBadge(e) { if (!e || !e.ai_status) return ''; const st = e.ai_status; const label = AI_STATUS_LABELS[st] || `ИИ: ${st}`; const icon = AI_STATUS_ICONS[st] || 'sparkles'; return ``; } 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('Файл откреплён'); if (f.entry_id) refreshEntryCard(f.entry_id); else loadEntries(); } else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); } } function gridEntryHTML(e) { const mp = mainPhotoPath(e); return `
${mp ? `` : ''}
${esc(e.student_name)}
${esc(e.group_name)}
${esc(e.description)}${aiBadge(e)}
${filesHTML(e.files)}
${new Date(e.created_at).toLocaleString('ru')}
${mp ? `` : ''}
`; } function cardEntryHTML(e) { const mp = mainPhotoPath(e); return `
${mp ? `` : `
`} ${e.group_name ? `${esc(e.group_name)}` : ''}
${esc(e.student_name)}
${esc(e.description)}${aiBadge(e)}
${filesHTML(e.files)}
${new Date(e.created_at).toLocaleString('ru')} ${mp ? `` : ''}
`; } function renderGrid() { const grid = document.getElementById('grid'); if (!currentEntries.length) { const fmt = d => d ? d.split('-').reverse().join('.') : ''; const from = fmt(document.getElementById('dateFrom').value); const to = fmt(document.getElementById('dateTo').value); let range = ''; if (from && to) range = ` за ${from} – ${to}`; else if (from) range = ` с ${from}`; else if (to) range = ` по ${to}`; grid.className = 'grid'; grid.innerHTML = `
Ничего не найдено${range}
`; return; } if (view === 'cards') { grid.className = 'journal-cards'; grid.innerHTML = currentEntries.map(cardEntryHTML).join(''); } else { grid.className = 'grid'; grid.innerHTML = currentEntries.map(gridEntryHTML).join(''); } const items = Array.from(grid.children); items.forEach((el, i) => { el.style.animationDelay = `${Math.min(i * 45, 500)}ms`; }); renderIcons(); } function refreshEntryCardDom(e) { const btn = document.querySelector(`[data-edit="${e.id}"]`); const card = btn ? btn.closest('.card, .journal-card') : null; if (!card) return; const tmp = document.createElement('div'); tmp.innerHTML = (view === 'cards' ? cardEntryHTML(e) : gridEntryHTML(e)).trim(); const fresh = tmp.firstElementChild; fresh.classList.remove('entry-enter', 'entry-appear'); fresh.style.animationDelay = ''; card.replaceWith(fresh); renderIcons(); } async function refreshEntryCard(id) { const res = await fetch(`${API}/api/entries/${id}`, { headers: hdr() }); if (!res.ok) return false; const e = await res.json(); const idx = currentEntries.findIndex(x => x.id === e.id); if (idx !== -1) currentEntries[idx] = e; refreshEntryCardDom(e); return true; } 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; renderEditPhotos(e.photos || [], e.photo_path); document.getElementById('editModal').classList.add('open'); } function renderEditPhotos(photos, mainPath) { const preview = document.getElementById('editPhotoPreview'); preview.innerHTML = ''; if (!photos.length) return; photos.forEach((p) => { const isMain = p.photo_path === mainPath; 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.dataset.photoId = p.id; const applyStyle = isMain ? 'background:#16a34a;border:none;color:#fff;border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:default;white-space:nowrap' : 'background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:pointer;white-space:nowrap'; div.innerHTML = ` ${esc(p.photo_path.split('/').pop())} `; preview.appendChild(div); const thumb = div.querySelector('img'); const showMissing = () => { const ph = document.createElement('span'); ph.textContent = 'нет файла'; ph.style.cssText = 'width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:var(--bg);border:1px dashed var(--border);border-radius:4px;font-size:.6rem;color:var(--muted);text-align:center;line-height:1.1;flex:0 0 auto'; thumb.replaceWith(ph); }; thumb.addEventListener('error', showMissing); if (thumb.complete && thumb.naturalWidth === 0) showMissing(); div.querySelector('.delete-entry-photo').addEventListener('click', async () => { if (!confirm('Удалить это фото?')) return; const res = await fetch(`${API}/api/entries/${editId}/photos/${p.id}`, { method: 'DELETE', headers: hdr() }); if (res.ok) { div.remove(); showToast('Фото удалено'); refreshEntryCard(editId); } else { alert('Ошибка удаления'); } }); }); } async function applyMainPhoto(btn) { const div = btn.closest('.file-preview-item'); const photoId = div?.dataset.photoId; if (!photoId) return; btn.disabled = true; const res = await fetch(`${API}/api/entries/${editId}/photos/${photoId}/main`, { method: 'PUT', headers: hdr() }); if (!res.ok) { btn.disabled = false; const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); return; } document.querySelectorAll('#editPhotoPreview .apply-photo-btn').forEach(b => { b.disabled = false; b.textContent = 'Применить'; b.title = 'Сделать главным фото записи'; b.style.background = 'var(--card)'; b.style.color = 'var(--text)'; }); btn.disabled = true; btn.textContent = 'Применено ✓'; btn.title = 'Главное фото записи'; btn.style.background = '#16a34a'; btn.style.color = '#fff'; showToast('Главное фото обновлено'); refreshEntryCard(editId); } function closeEdit() { document.getElementById('editModal').classList.remove('open'); document.getElementById('editFiles').value = ''; document.getElementById('editFilesPreview').innerHTML = ''; document.getElementById('editPhoto').value = ''; document.getElementById('editPhotoPreview').innerHTML = ''; document.getElementById('editAiStatus').style.display = 'none'; editId = null; editDeletePhoto = false; } let enhanceEntryId = null; let enhanceSourceImg = null; let enhanceTimer = null; let enhanceSaving = false; let enhanceEngine = 'auto'; let enhanceEngineLoaded = false; async function loadEnhanceEngine() { if (enhanceEngineLoaded) return; enhanceEngineLoaded = true; try { const r = await fetch(`${API}/api/public-settings`); const f = await r.json(); if (['auto', 'server', 'client'].includes(f.photo_enhance_engine)) enhanceEngine = f.photo_enhance_engine; } catch (e) {} } function openEnhance(id) { const e = currentEntries.find(x => x.id === id); if (!e) return; const mp = mainPhotoPath(e); if (!mp) { showToast('У записи нет фото'); return; } enhanceEntryId = id; enhanceSourceImg = null; ['Brightness', 'Contrast', 'Saturate'].forEach(k => { const el = document.getElementById('enhance' + k); el.value = 100; document.getElementById('enhance' + k + 'Val').textContent = '100%'; }); const sharp = document.getElementById('enhanceSharp'); sharp.value = 0; document.getElementById('enhanceSharpVal').textContent = '0%'; const orig = document.getElementById('enhanceOrig'); orig.src = `${API}${mp}`; const canvas = document.getElementById('enhanceCanvas'); canvas.style.display = 'none'; const loading = document.getElementById('enhanceLoading'); loading.style.display = ''; loading.textContent = 'Загрузка...'; const img = new Image(); img.onload = () => { enhanceSourceImg = img; renderEnhancePreview(); }; img.onerror = () => { loading.textContent = 'Не удалось загрузить фото'; }; img.src = `${API}${mp}`; document.getElementById('enhanceModal').classList.add('open'); } function closeEnhance() { document.getElementById('enhanceModal').classList.remove('open'); enhanceEntryId = null; enhanceSourceImg = null; } function medianFilterPass(ctx, w, h) { const data = ctx.getImageData(0, 0, w, h); const src = data.data; const out = new Uint8ClampedArray(src); const row = w * 4; const win = new Array(9); for (let y = 1; y < h - 1; y++) { for (let x = 1; x < w - 1; x++) { const i = y * row + x * 4; for (let ch = 0; ch < 3; ch++) { win[0] = src[i - row - 4 + ch]; win[1] = src[i - row + ch]; win[2] = src[i - row + 4 + ch]; win[3] = src[i - 4 + ch]; win[4] = src[i + ch]; win[5] = src[i + 4 + ch]; win[6] = src[i + row - 4 + ch]; win[7] = src[i + row + ch]; win[8] = src[i + row + 4 + ch]; win.sort((a, b) => a - b); out[i + ch] = win[4]; } } } data.data.set(out); ctx.putImageData(data, 0, 0); } function sharpenPass(ctx, w, h, sharp) { const data = ctx.getImageData(0, 0, w, h); const src = data.data; const out = new Uint8ClampedArray(src); const row = w * 4; const a = (sharp / 100) * 1.5; for (let y = 1; y < h - 1; y++) { for (let x = 1; x < w - 1; x++) { const i = y * row + x * 4; for (let ch = 0; ch < 3; ch++) { const v = (1 + 4 * a) * src[i + ch] - a * src[i - 4 + ch] - a * src[i + 4 + ch] - a * src[i - row + ch] - a * src[i + row + ch]; out[i + ch] = v; } } } data.data.set(out); ctx.putImageData(data, 0, 0); } function drawEnhanced(ctx, img, w, h) { const b = document.getElementById('enhanceBrightness').value; const c = document.getElementById('enhanceContrast').value; const s = document.getElementById('enhanceSaturate').value; const dn = parseInt(document.getElementById('enhanceDenoise').value, 10); const sp = parseInt(document.getElementById('enhanceSharp').value, 10); ctx.filter = `brightness(${b}%) contrast(${c}%) saturate(${s}%)`; ctx.drawImage(img, 0, 0, w, h); ctx.filter = 'none'; if (dn > 0) { medianFilterPass(ctx, w, h); if (dn > 70) medianFilterPass(ctx, w, h); } if (sp > 0) sharpenPass(ctx, w, h, sp); } function renderEnhancePreview() { if (!enhanceSourceImg) return; const img = enhanceSourceImg; const scale = Math.min(1, 1600 / img.naturalWidth); const w = Math.max(1, Math.round(img.naturalWidth * scale)); const h = Math.max(1, Math.round(img.naturalHeight * scale)); const canvas = document.getElementById('enhanceCanvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); drawEnhanced(ctx, img, w, h); canvas.style.display = 'block'; document.getElementById('enhanceLoading').style.display = 'none'; } function scheduleEnhancePreview() { document.getElementById('enhanceBrightnessVal').textContent = document.getElementById('enhanceBrightness').value + '%'; document.getElementById('enhanceContrastVal').textContent = document.getElementById('enhanceContrast').value + '%'; document.getElementById('enhanceSaturateVal').textContent = document.getElementById('enhanceSaturate').value + '%'; document.getElementById('enhanceDenoiseVal').textContent = document.getElementById('enhanceDenoise').value + '%'; document.getElementById('enhanceSharpVal').textContent = document.getElementById('enhanceSharp').value + '%'; if (enhanceTimer) clearTimeout(enhanceTimer); enhanceTimer = setTimeout(renderEnhancePreview, 120); } function enhanceParams() { return { brightness: parseInt(document.getElementById('enhanceBrightness').value, 10), contrast: parseInt(document.getElementById('enhanceContrast').value, 10), saturate: parseInt(document.getElementById('enhanceSaturate').value, 10), denoise: parseInt(document.getElementById('enhanceDenoise').value, 10), sharp: parseInt(document.getElementById('enhanceSharp').value, 10), }; } function estimateNoise(img) { const aw = 160; const ah = Math.max(1, Math.round(img.naturalHeight * aw / img.naturalWidth)); const ac = document.createElement('canvas'); ac.width = aw; ac.height = ah; const actx = ac.getContext('2d'); actx.drawImage(img, 0, 0, aw, ah); const d = actx.getImageData(0, 0, aw, ah).data; let sum = 0; let n = 0; for (let y = 0; y < ah - 1; y++) { for (let x = 0; x < aw - 1; x++) { const i = (y * aw + x) * 4; const l = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]; const lr = 0.299 * d[i + 4] + 0.587 * d[i + 5] + 0.114 * d[i + 6]; const lb = 0.299 * d[i + aw * 4] + 0.587 * d[i + aw * 4 + 1] + 0.114 * d[i + aw * 4 + 2]; sum += Math.abs(l - lr) + Math.abs(l - lb); n += 2; } } return n ? sum / n : 0; } function autoEnhance() { if (!enhanceSourceImg) return; const img = enhanceSourceImg; const aw = 160; const ah = Math.max(1, Math.round(img.naturalHeight * aw / img.naturalWidth)); const ac = document.createElement('canvas'); ac.width = aw; ac.height = ah; const actx = ac.getContext('2d'); actx.drawImage(img, 0, 0, aw, ah); const d = actx.getImageData(0, 0, aw, ah).data; const hist = new Array(256).fill(0); let total = 0; for (let i = 0; i < d.length; i += 4) { if (d[i + 3] < 16) continue; hist[Math.round(0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2])]++; total++; } const pct = p => { let acc = 0; const target = total * p; for (let v = 0; v < 256; v++) { acc += hist[v]; if (acc >= target) return v; } return 255; }; let lo = pct(0.005); let hi = pct(0.995); if (hi - lo < 30) { lo = Math.max(0, lo - 15); hi = Math.min(255, hi + 15); } const contrast = Math.round(Math.min(150, Math.max(100, 25500 / (hi - lo)))); const brightness = Math.round(Math.min(120, Math.max(85, 12800 / Math.max(1, (lo + hi) / 2)))); const noise = estimateNoise(img); let denoise = 0; let sharp = 30; if (noise > 6) { denoise = Math.min(50, Math.round(noise * 6)); sharp = 15; } const saturate = 106; document.getElementById('enhanceBrightness').value = brightness; document.getElementById('enhanceContrast').value = contrast; document.getElementById('enhanceSaturate').value = saturate; document.getElementById('enhanceDenoise').value = denoise; document.getElementById('enhanceSharp').value = sharp; scheduleEnhancePreview(); } async function saveEnhance() { if (!enhanceEntryId || !enhanceSourceImg || enhanceSaving) return; await loadEnhanceEngine(); const btn = document.getElementById('enhanceSaveBtn'); enhanceSaving = true; btn.disabled = true; btn.textContent = 'Сохранение...'; const entryId = enhanceEntryId; try { let done = false; let engineLabel = 'клиент'; if (enhanceEngine !== 'client') { try { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 8000); const res = await fetch(`${API}/api/entries/${entryId}/photo/enhance`, { method: 'PUT', headers: hdrJson(), body: JSON.stringify({ params: enhanceParams() }), signal: ctrl.signal, }); clearTimeout(timer); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Ошибка сервера'); } engineLabel = 'сервер'; done = true; } catch (e) { if (enhanceEngine === 'server') throw e; } } if (!done) { const img = enhanceSourceImg; const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); drawEnhanced(ctx, img, canvas.width, canvas.height); const blob = await new Promise((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/jpeg', 0.92)); const fd = new FormData(); fd.append('photo', blob, 'enhanced.jpg'); const res = await fetch(`${API}/api/entries/${entryId}/photo/enhance`, { method: 'PUT', headers: hdr(), body: fd }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Ошибка сохранения'); } } showToast(`Фото улучшено (${engineLabel}) и сохранено`); closeEnhance(); refreshEntryCard(entryId); } catch (e) { alert(e.message); } finally { enhanceSaving = false; btn.disabled = false; btn.textContent = 'Применить'; } } async function aiCorrect() { if (aiCorrecting) return; const textarea = document.getElementById('editDesc'); const text = textarea.value.trim(); if (!text) { showToast('Нет текста для исправления'); return; } const btn = document.getElementById('aiCorrectBtn'); const modal = document.getElementById('aiCorrectModal'); const suggestionEl = document.getElementById('aiSuggestion'); aiCorrecting = true; btn.disabled = true; btn.textContent = '⏳'; suggestionEl.textContent = 'Обработка...'; modal.classList.add('open'); try { const res = await fetch(`${API}/api/ai/correct`, { method: 'POST', headers: hdrJson(), body: JSON.stringify({ text }) }); if (!res.ok) { const err = await res.json(); 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 { aiCorrecting = false; btn.disabled = false; btn.textContent = '✨'; } } function closeAiCorrect() { document.getElementById('aiCorrectModal').classList.remove('open'); } function acceptAiCorrect() { const suggestion = document.getElementById('aiSuggestion').textContent; if (suggestion && suggestion !== 'Обработка...' && !suggestion.startsWith('Ошибка:')) { document.getElementById('editDesc').value = suggestion; } closeAiCorrect(); } function renderEditAiStatus(e) { const el = document.getElementById('editAiStatus'); if (!e || !e.ai_status) { el.style.display = 'none'; el.innerHTML = ''; return; } el.style.display = 'block'; const changed = e.description_ai && e.description_original && e.description_ai !== e.description_original; const parts = [AI_STATUS_LABELS[e.ai_status] || `ИИ: ${e.ai_status}`]; if (e.ai_checked_at) parts.push(new Date(e.ai_checked_at).toLocaleString('ru')); let html = `${esc(parts.join(' · '))}`; if (changed) html += ` `; html += ` `; el.innerHTML = html; const rb = document.getElementById('aiRevertBtn'); if (rb) rb.addEventListener('click', () => revertAi(e.id)); const cb = document.getElementById('aiRecheckBtn'); if (cb) cb.addEventListener('click', () => recheckAi(e.id)); } async function revertAi(id) { if (!confirm('Вернуть исходный текст записи (до ИИ-проверки)?')) return; const res = await fetch(`${API}/api/entries/${id}/ai/revert`, { method: 'POST', headers: hdr() }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); return; } const e = currentEntries.find(x => x.id === id); if (e) { if (e.description_original) e.description = e.description_original; e.description_ai = null; e.ai_status = 'reverted'; e.ai_error = null; } showToast('Оригинал восстановлен'); closeEdit(); refreshEntryCard(id); } async function recheckAi(id) { const res = await fetch(`${API}/api/entries/${id}/ai/recheck`, { method: 'POST', headers: hdr() }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); return; } const e = currentEntries.find(x => x.id === id); if (e) { e.ai_status = 'pending'; e.ai_error = null; } showToast('Отправлено на ИИ-проверку'); closeEdit(); refreshEntryCard(id); } 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 formData = new FormData(); formData.append('student_name', name); formData.append('group_id', group_id); formData.append('description', description); const photoFiles = document.getElementById('editPhoto').files; for (const f of photoFiles) formData.append('photo', f); const res = await fetch(`${API}/api/entries/${editId}`, { method: 'PUT', headers: hdr(), body: formData }); if (!res.ok) { const err = await res.json(); alert(err.error); return; } const files = document.getElementById('editFiles').files; if (files.length) { const filesFormData = new FormData(); for (const f of files) filesFormData.append('files', f); const uploadRes = await fetch(`${API}/api/entries/${editId}/files`, { method: 'POST', headers: hdr(), body: filesFormData }); if (!uploadRes.ok) { const err = await uploadRes.json().catch(() => ({})); alert(err.error || 'Ошибка загрузки файлов'); return; } } const id = editId; closeEdit(); const refreshed = await refreshEntryCard(id); if (!refreshed) 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); }); }); document.getElementById('editPhoto').addEventListener('change', (e) => { const preview = document.getElementById('editPhotoPreview'); Array.from(e.target.files).forEach(f => { 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'; const url = URL.createObjectURL(f); 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 => `