Files
WhatIDo/public/js/journal.js
T

655 lines
28 KiB
JavaScript

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';
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|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: 'ИИ: возвращён оригинал',
};
function aiBadge(e) {
if (!e || !e.ai_status) return '';
const label = AI_STATUS_LABELS[e.ai_status] || `ИИ: ${e.ai_status}`;
return `<span class="ai-badge ai-badge-${esc(e.ai_status)}" title="${esc(label)}">${esc(label)}</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">
${mp ? `<img src="${API}${mp}" data-img="${API}${mp}" alt="">` : ''}
<div class="info">
<div class="name">${esc(e.student_name)}</div>${aiBadge(e)}
<div class="group">${esc(e.group_name)}</div>
<div class="desc">${esc(e.description)}</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">
${mp
? `<img src="${API}${mp}" data-img="${API}${mp}" 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>`}
<div class="body">
<div class="name">${esc(e.student_name)}</div>${aiBadge(e)}
<div class="group">${esc(e.group_name)}</div>
<div class="desc">${esc(e.description)}</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) {
grid.className = 'grid';
grid.innerHTML = '<div class="empty">Ничего не найдено</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('');
}
}
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();
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();
}
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="${API}${esc(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();
}
})();