196 lines
11 KiB
JavaScript
196 lines
11 KiB
JavaScript
let links = [];
|
|
let groups = [];
|
|
let editingId = null;
|
|
let studentsLoaded = false;
|
|
let linkSettings = null;
|
|
let linkPage = 1;
|
|
const LINK_PAGE_SIZE = 20;
|
|
|
|
async function loadLinkSettings() {
|
|
if (!linkSettings) {
|
|
const res = await fetch(`${API}/api/settings`, { headers: hdr() });
|
|
linkSettings = await res.json().catch(() => ({}));
|
|
}
|
|
return linkSettings;
|
|
}
|
|
|
|
function linkUrl(token) { return `${location.origin}/s/${token}`; }
|
|
|
|
async function loadLinks() {
|
|
await loadLinkSettings();
|
|
const p = new URLSearchParams();
|
|
p.set('limit', LINK_PAGE_SIZE);
|
|
p.set('offset', (linkPage - 1) * LINK_PAGE_SIZE);
|
|
const res = await fetch(`${API}/api/links?${p}`, { headers: hdr() });
|
|
const data = await res.json();
|
|
links = data.items;
|
|
const total = data.total;
|
|
const el = document.getElementById('linksList');
|
|
if (!links.length) { el.innerHTML = `<div class="empty">${linkPage > 1 ? 'На этой странице ссылок нет.' : 'Ссылок пока нет. Создайте её в Журнале кнопкой «Ссылка».'}</div>`; renderPager(document.getElementById('pager'), 1, 1, goLinkPage); return; }
|
|
const now = Date.now();
|
|
el.innerHTML = links.map(l => {
|
|
const namesHidden = l.show_student_names != null ? !l.show_student_names : linkSettings.share_show_student_names === 'false';
|
|
const expired = l.expires_at && new Date(l.expires_at).getTime() < now;
|
|
const daysLeft = l.expires_at ? Math.ceil((new Date(l.expires_at).getTime() - now) / 86400000) : null;
|
|
const soon = !expired && daysLeft !== null && daysLeft <= 1;
|
|
return `
|
|
<div class="item${expired ? ' expired' : ''}">
|
|
<div style="min-width:0">
|
|
<div class="name">${esc(l.name)}${expired ? ' <span class="badge-expired">истекла</span>' : soon ? ' <span class="badge-soon">истекает</span>' : ''}</div>
|
|
<div style="font-size:.8rem;color:var(--muted);margin-top:2px">
|
|
${l.group_name ? `Группа: <b>${esc(l.group_name)}</b>` : ''}
|
|
${l.student_name ? `${l.group_name ? ' · ' : ''}Ученик: <b>${esc(l.student_name)}</b>` : ''}
|
|
${l.date_from || l.date_to ? ` · период: <b>${l.date_from ? l.date_from.split('-').reverse().join('.') : '…'}</b> — <b>${l.date_to ? l.date_to.split('-').reverse().join('.') : '…'}</b>` : ' · весь период'}
|
|
${expired ? ` · <span style="color:#ef4444;font-weight:600">срок действия истёк ${new Date(l.expires_at).toLocaleDateString('ru')}</span>` : l.expires_at ? ` · истекает ${new Date(l.expires_at).toLocaleDateString('ru')}` : ''}
|
|
${namesHidden ? ` · <span style="color:#f59e0b">имена скрыты</span>` : ''}
|
|
${l.access_password_hash ? ` · <span style="color:#2563eb">🔒 пароль</span>` : ''}
|
|
· создана ${new Date(l.created_at).toLocaleString('ru')}
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-shrink:0;align-items:center">
|
|
<a class="btn-link" style="text-decoration:none" href="/s/${l.token}" target="_blank" title="Открыть">Открыть</a>
|
|
<button class="btn-link" data-copy="${esc(linkUrl(l.token))}" title="Копировать ссылку">Копировать</button>
|
|
<button class="btn-link" data-edit-link="${l.id}" title="Редактировать">✎</button>
|
|
<button class="btn-link" style="color:var(--muted)" data-del-link="${l.id}" title="Удалить">×</button>
|
|
</div>
|
|
</div>
|
|
`;}).join('');
|
|
renderPager(document.getElementById('pager'), linkPage, Math.max(1, Math.ceil(total / LINK_PAGE_SIZE)), goLinkPage);
|
|
}
|
|
|
|
function goLinkPage(p) {
|
|
linkPage = p;
|
|
loadLinks();
|
|
}
|
|
|
|
function fmtDate(d) { const [y, m, day] = (d || '').split('-'); return d ? `${day}.${m}.${y}` : ''; }
|
|
|
|
async function openLinkModal() {
|
|
editingId = null;
|
|
document.getElementById('linkModalTitle').textContent = 'Новая ссылка';
|
|
document.getElementById('linkName').value = '';
|
|
document.getElementById('linkDateFrom').value = '';
|
|
document.getElementById('linkDateTo').value = '';
|
|
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';
|
|
if (!groups.length) {
|
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
|
groups = await res.json();
|
|
document.getElementById('linkGroup').innerHTML = '<option value="">— без группы —</option>' +
|
|
groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
|
}
|
|
if (!studentsLoaded) {
|
|
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('');
|
|
studentsLoaded = true;
|
|
}
|
|
document.getElementById('linkModal').classList.add('open');
|
|
}
|
|
|
|
async function openLinkEdit(id) {
|
|
const l = links.find(x => x.id === id);
|
|
if (!l) return;
|
|
editingId = id;
|
|
document.getElementById('linkModalTitle').textContent = 'Редактирование ссылки';
|
|
document.getElementById('linkName').value = l.name;
|
|
document.getElementById('linkDateFrom').value = l.date_from || '';
|
|
document.getElementById('linkDateTo').value = l.date_to || '';
|
|
document.getElementById('linkExpiresDays').value = l.expires_at ? Math.ceil((new Date(l.expires_at) - new Date()) / (1000 * 60 * 60 * 24)) : '7';
|
|
document.getElementById('linkShowNames').checked = l.show_student_names != null ? !!l.show_student_names : true;
|
|
document.getElementById('linkPassword').value = '';
|
|
document.getElementById('linkMessage').value = l.message || '';
|
|
document.getElementById('linkUrl').value = l.link_url || '';
|
|
const s = await loadLinkSettings();
|
|
document.getElementById('linkShowNames').checked = l.show_student_names != null ? !!l.show_student_names : s.share_show_student_names !== 'false';
|
|
document.getElementById('linkShowMessage').checked = l.show_student_message != null ? !!l.show_student_message : s.share_show_student_message === 'true';
|
|
document.getElementById('linkShowDate').checked = l.show_entry_date != null ? !!l.show_entry_date : s.share_show_entry_date === 'true';
|
|
document.getElementById('linkShowPhotos').checked = l.show_group_photos != null ? !!l.show_group_photos : s.share_show_group_photos !== 'false';
|
|
if (!groups.length) {
|
|
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
|
groups = await res.json();
|
|
document.getElementById('linkGroup').innerHTML = '<option value="">— без группы —</option>' +
|
|
groups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
|
}
|
|
document.getElementById('linkGroup').value = l.group_id || '';
|
|
if (!studentsLoaded) {
|
|
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('');
|
|
studentsLoaded = true;
|
|
}
|
|
document.getElementById('linkStudent').value = l.student_name || '';
|
|
document.getElementById('linkModal').classList.add('open');
|
|
}
|
|
|
|
function closeLinkModal() {
|
|
document.getElementById('linkModal').classList.remove('open');
|
|
editingId = null;
|
|
}
|
|
|
|
async function saveLink() {
|
|
const name = document.getElementById('linkName').value.trim();
|
|
const group_id = document.getElementById('linkGroup').value;
|
|
const student_name = document.getElementById('linkStudent').value;
|
|
const date_from = document.getElementById('linkDateFrom').value;
|
|
const date_to = document.getElementById('linkDateTo').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 body = JSON.stringify({ name, group_id: group_id || null, 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 });
|
|
const res = await fetch(editingId ? `${API}/api/links/${editingId}` : `${API}/api/links`, {
|
|
method: editingId ? 'PUT' : 'POST',
|
|
headers: hdrJson(),
|
|
body
|
|
});
|
|
if (res.ok) {
|
|
closeLinkModal();
|
|
loadLinks();
|
|
showToast(editingId ? 'Ссылка обновлена' : 'Ссылка создана');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error || 'Ошибка');
|
|
}
|
|
}
|
|
|
|
async function delLink(id) {
|
|
if (!confirm('Удалить ссылку?')) return;
|
|
await fetch(`${API}/api/links/${id}`, { method: 'DELETE', headers: hdr() });
|
|
loadLinks();
|
|
showToast('Ссылка удалена');
|
|
}
|
|
|
|
document.getElementById('linkModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeLinkModal(); });
|
|
document.getElementById('linkCancelBtn').addEventListener('click', closeLinkModal);
|
|
document.getElementById('linkSaveBtn').addEventListener('click', saveLink);
|
|
|
|
document.getElementById('linksList').addEventListener('click', e => {
|
|
const cp = e.target.closest('[data-copy]');
|
|
if (cp) { copyText(cp.dataset.copy); return; }
|
|
const ed = e.target.closest('[data-edit-link]');
|
|
if (ed) { openLinkEdit(parseInt(ed.dataset.editLink, 10)); return; }
|
|
const dl = e.target.closest('[data-del-link]');
|
|
if (dl) { delLink(parseInt(dl.dataset.delLink, 10)); }
|
|
});
|
|
|
|
(async () => {
|
|
if (await checkAuth()) {
|
|
buildSidebar(document.body.dataset.page);
|
|
loadLinks();
|
|
}
|
|
})(); |