Files
WhatIDo/public/js/journal.js
T

741 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `<div class="entry-files">${files.map(f => {
const link = isImageFile(f.name)
? `<a class="f-link" href="#" data-img="${API}/api/files/${f.token}" title="Открыть">${esc(f.name)}</a>`
: `<a class="f-link" href="${API}/api/files/${f.token}" download title="Скачать">${esc(f.name)}</a>`;
return `<span class="f-item">${link}<button class="f-detach" data-detach="${f.id}" title="Открепить файл">✕</button></span>`;
}).join('')}</div>`;
}
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 `<span class="ai-badge ai-badge-ic ai-badge-${esc(st)}" data-ai-badge="${e.id}" title="${esc(label)}" role="img" aria-label="${esc(label)}"><i data-lucide="${icon}"></i></span>`;
}
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) {
const mp = mainPhotoPath(e);
return `
<div class="card${highlightNewIds.has(e.id) ? ' entry-appear' : ' entry-enter'}">
${mp ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` : ''}
<div class="info">
<div class="name">${esc(e.student_name)}</div>
<div class="group">${esc(e.group_name)}</div>
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
${filesHTML(e.files)}
<div class="time">${new Date(e.created_at).toLocaleString('ru')}</div>
</div>
<button class="edit" data-edit="${e.id}" title="Редактировать">✎</button>
<button class="del" data-del="${e.id}" title="Удалить">&times;</button>
</div>
`;
}
function cardEntryHTML(e) {
const mp = mainPhotoPath(e);
return `
<div class="journal-card${highlightNewIds.has(e.id) ? ' entry-appear' : ' entry-enter'}">
${mp
? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">`
: `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`}
${e.group_name ? `<span class="group-badge" title="${esc(e.group_name)}">${esc(e.group_name)}</span>` : ''}
<div class="body">
<div class="name">${esc(e.student_name)}</div>
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
${filesHTML(e.files)}
</div>
<div class="foot">
<span>${new Date(e.created_at).toLocaleString('ru')}</span>
<span>
<button class="edit" data-edit="${e.id}" title="Редактировать">✎</button>
<button class="del" data-del="${e.id}" title="Удалить">&times;</button>
</span>
</div>
</div>
`;
}
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 = `<div class="empty">Ничего не найдено${range}</div>`;
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 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 => `<option value="${esc(n)}">`).join('');
studentsLoaded = true;
}
async function loadEntries() {
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);
p.set('limit', PAGE_SIZE);
p.set('offset', (page - 1) * PAGE_SIZE);
const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() });
const data = await res.json();
const firstLoad = seenEntryIds.size === 0;
const newIds = firstLoad ? [] : data.entries.filter(e => !seenEntryIds.has(e.id)).map(e => e.id);
if (newIds.length) {
highlightNewIds = new Set([...highlightNewIds, ...newIds]);
if (highlightClearTimer) clearTimeout(highlightClearTimer);
highlightClearTimer = setTimeout(() => { highlightNewIds = new Set(); }, 8000);
}
data.entries.forEach(e => seenEntryIds.add(e.id));
currentEntries = data.entries;
totalEntries = data.total;
renderGrid();
renderPager(document.getElementById('pager'), page, Math.max(1, Math.ceil(totalEntries / PAGE_SIZE)), goPage);
}
function goPage(p) {
page = p;
loadEntries();
}
let liveSource = null;
let liveReconnectTimer = null;
let aiRefreshTimer = null;
let entriesRefreshTimer = null;
function applyAiUpdate(update) {
const e = currentEntries.find(x => x.id === update.id);
if (!e) return;
e.ai_status = update.ai_status;
e.ai_error = update.ai_error || null;
if (update.ai_status === 'done') {
if (aiRefreshTimer) return;
aiRefreshTimer = setTimeout(() => { aiRefreshTimer = null; loadEntries(); }, 300);
return;
}
document.querySelectorAll(`[data-ai-badge="${update.id}"]`).forEach(badgeEl => {
badgeEl.outerHTML = aiBadge(e);
});
renderIcons();
const editBadge = document.getElementById('editAiStatus');
if (editId === update.id && editBadge && editBadge.style.display !== 'none') {
renderEditAiStatus(e);
}
}
function connectLiveUpdates() {
if (liveSource) { liveSource.close(); liveSource = null; }
const connect = () => {
const t = sessionStorage.getItem('authToken');
if (!t) return;
liveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`);
liveSource.addEventListener('entries_changed', () => {
if (entriesRefreshTimer) return;
entriesRefreshTimer = setTimeout(() => {
entriesRefreshTimer = null;
showToast('Поступила новая запись — список обновлён');
page = 1;
loadEntries();
}, 300);
});
liveSource.addEventListener('ai_status', ev => {
try { applyAiUpdate(JSON.parse(ev.data)); } catch (err) {}
});
liveSource.onerror = () => {
if (liveSource) { liveSource.close(); liveSource = null; }
if (liveReconnectTimer) clearTimeout(liveReconnectTimer);
liveReconnectTimer = setTimeout(connect, 5000);
};
};
connect();
window.addEventListener('beforeunload', () => { if (liveSource) liveSource.close(); });
}
async function openEdit(id) {
const e = currentEntries.find(x => x.id === id);
if (!e) return;
editId = id;
editDeletePhoto = false;
fillAutocomplete();
document.getElementById('editName').value = e.student_name;
document.getElementById('editDesc').value = e.description;
renderEditAiStatus(e);
const sel = document.getElementById('editGroup');
if (!editGroups.length) {
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
editGroups = await res.json();
sel.innerHTML = editGroups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).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 = `<img src="${thumbSrc(p.photo_path)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px"> <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.photo_path.split('/').pop())}</span> <button type="button" class="apply-photo-btn" style="${applyStyle}" title="${isMain ? 'Главное фото записи' : 'Сделать главным фото записи'}" ${isMain ? 'disabled' : ''}>${isMain ? 'Применено ✓' : 'Применить'}</button> <button type="button" class="delete-entry-photo" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:1.1rem;line-height:1" title="Удалить фото">✕</button>`;
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('Фото удалено');
loadEntries();
} 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('Главное фото обновлено');
loadEntries();
}
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;
}
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 = `<span>${esc(parts.join(' · '))}</span>`;
if (changed) html += ` <button type="button" id="aiRevertBtn" class="ai-btn" style="font-size:.75rem" title="Вернуть исходный текст (до ИИ)">↩ Оригинал</button>`;
html += ` <button type="button" id="aiRecheckBtn" class="ai-btn" style="font-size:.75rem" title="Проверить заново">⟳ Проверить</button>`;
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; }
showToast('Оригинал восстановлен');
closeEdit();
loadEntries();
}
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; }
showToast('Отправлено на ИИ-проверку');
closeEdit();
loadEntries();
}
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;
}
}
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 = `<span>${esc(f.name)}</span> <span style="color:var(--muted)">${(f.size/1024).toFixed(1)} KB</span> <button type="button" data-remove-file="${i}" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;line-height:1">✕</button>`;
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 = `<img src="${url}" style="width:48px;height:48px;object-fit:cover;border-radius:4px"> <span>${esc(f.name)}</span> <span style="color:var(--muted)">${(f.size/1024).toFixed(1)} KB</span> <button type="button" data-remove-new-photo="${f.name}" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;line-height:1">✕</button>`;
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 => `<option value="${esc(n)}">`).join('');
linkStudentsLoaded = true;
}
const currentStudent = document.getElementById('studentFilter').value;
const g = linkGroups.find(x => String(x.id) === String(gid));
document.getElementById('linkPreview').innerHTML =
`Будут показаны записи: <b>${esc(g ? g.name : '')}</b><br>Период: <b>${fmtD(df)}</b> — <b>${fmtD(dt)}</b>`;
document.getElementById('linkGroupPreview').value = g ? g.name : '';
const dfl = df.split('-').reverse().join('.');
document.getElementById('linkNameInput').value = `${g ? g.name : ''} ${dfl}`;
document.getElementById('linkStudent').value = currentStudent;
document.getElementById('linkExpiresDays').value = '7';
document.getElementById('linkShowNames').checked = false;
document.getElementById('linkPassword').value = '';
document.getElementById('linkMessage').value = '';
document.getElementById('linkUrl').value = '';
const s = await loadLinkSettings();
document.getElementById('linkShowNames').checked = s.share_show_student_names !== 'false';
document.getElementById('linkShowMessage').checked = s.share_show_student_message === 'true';
document.getElementById('linkShowDate').checked = s.share_show_entry_date === 'true';
document.getElementById('linkShowPhotos').checked = s.share_show_group_photos !== 'false';
document.getElementById('linkCreateView').style.display = '';
document.getElementById('linkResultView').style.display = 'none';
document.getElementById('linkModal').classList.add('open');
}
function closeLinkCreate() {
document.getElementById('linkModal').classList.remove('open');
}
async function createLink() {
const name = document.getElementById('linkNameInput').value.trim();
const group_id = document.getElementById('groupFilter').value;
const student_name = document.getElementById('linkStudent').value;
const date_from = document.getElementById('dateFrom').value;
const date_to = document.getElementById('dateTo').value;
const expires_days = parseInt(document.getElementById('linkExpiresDays').value, 10);
const show_student_names = document.getElementById('linkShowNames').checked;
const access_password = document.getElementById('linkPassword').value;
const message = document.getElementById('linkMessage').value.trim();
const link_url = document.getElementById('linkUrl').value.trim();
if (!name) { alert('Введите название'); return; }
if (expires_days < 1 || expires_days > 365) { alert('Дней истечения: 1-365'); return; }
const expires_at = new Date(Date.now() + expires_days * 24 * 60 * 60 * 1000).toISOString();
const res = await fetch(`${API}/api/links`, {
method: 'POST',
headers: hdrJson(),
body: JSON.stringify({ name, group_id, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, show_student_names, expires_at, access_password: access_password || null, message: message || null, link_url: link_url || null, show_student_message: document.getElementById('linkShowMessage').checked, show_entry_date: document.getElementById('linkShowDate').checked, show_group_photos: document.getElementById('linkShowPhotos').checked })
});
if (!res.ok) { const err = await res.json(); alert(err.error || 'Ошибка'); return; }
const link = await res.json();
createdLinkUrl = `${location.origin}/s/${link.token}`;
document.getElementById('linkUrlResult').value = createdLinkUrl;
document.getElementById('linkCreateView').style.display = 'none';
document.getElementById('linkResultView').style.display = '';
showToast('Ссылка создана');
}
function copyLink() { copyText(createdLinkUrl); }
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
document.getElementById('linkModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeLinkCreate(); });
document.getElementById('grid').addEventListener('click', e => {
const det = e.target.closest('[data-detach]');
if (det) { detachFile(parseInt(det.dataset.detach, 10)); return; }
const ed = e.target.closest('[data-edit]');
if (ed) { openEdit(parseInt(ed.dataset.edit, 10)); return; }
const dl = e.target.closest('[data-del]');
if (dl) { delEntry(parseInt(dl.dataset.del, 10)); }
});
document.getElementById('editFilesPreview').addEventListener('click', e => {
const btn = e.target.closest('[data-remove-file]');
if (btn) removeEditFile(parseInt(btn.dataset.removeFile, 10));
});
document.getElementById('editPhotoPreview').addEventListener('click', e => {
const removeBtn = e.target.closest('[data-remove-photo]');
if (removeBtn) {
document.getElementById('editPhoto').value = '';
document.getElementById('editPhotoPreview').innerHTML = '';
return;
}
const applyBtn = e.target.closest('.apply-photo-btn');
if (applyBtn) { applyMainPhoto(applyBtn); return; }
const removeNewBtn = e.target.closest('[data-remove-new-photo]');
if (removeNewBtn) {
const fileName = removeNewBtn.dataset.removeNewPhoto;
const input = document.getElementById('editPhoto');
const dt = new DataTransfer();
Array.from(input.files).forEach(f => { if (f.name !== fileName) dt.items.add(f); });
input.files = dt.files;
removeNewBtn.closest('.file-preview-item').remove();
}
});
document.getElementById('aiCorrectBtn').addEventListener('click', aiCorrect);
document.getElementById('aiAcceptBtn').addEventListener('click', acceptAiCorrect);
document.getElementById('aiRejectBtn').addEventListener('click', closeAiCorrect);
document.getElementById('aiCorrectModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeAiCorrect();
});
document.getElementById('exportCsvBtn').addEventListener('click', exportCSV);
document.getElementById('createLinkBtn').addEventListener('click', openLinkCreate);
document.querySelectorAll('#viewToggle button').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
document.getElementById('editCancelBtn').addEventListener('click', closeEdit);
document.getElementById('linkCreateCloseBtn').addEventListener('click', closeLinkCreate);
document.getElementById('linkCreateSaveBtn').addEventListener('click', createLink);
document.getElementById('linkCopyBtn').addEventListener('click', copyLink);
(async () => {
if (await checkAuth()) {
buildSidebar(document.body.dataset.page);
document.querySelectorAll('#viewToggle button').forEach(b => {
b.classList.toggle('active', b.dataset.view === view);
});
setDefaultDates();
loadGroupOptions();
loadStudentOptions();
loadEntries();
connectLiveUpdates();
}
})();