feat: редизайн страницы отчёта ученика — портфолио-стиль, галерея с листанием и видео
- renderStudentReport: детский учебный дизайн (крупные скругления, sticky-навигация, секции-карточки) - журнал занятий в две колонки (одна на мобильных) - единая галерея фото и видео: листание кнопками/стрелками, счётчик - воспроизведение видео прямо в лайтбоксе - кнопка закрытия ✕, закрытие по фону и Esc - модалка экспорта отчёта ученика (период и выбор содержимого)
This commit is contained in:
@@ -47,6 +47,7 @@ function renderStudents() {
|
||||
<select class="group-select" id="sgroup-${s.id}"></select>
|
||||
<div class="student-actions">
|
||||
<button class="btn-save" data-save-student="${s.id}">Сохранить</button>
|
||||
<button class="btn-link" data-export-student="${esc(s.name)}" title="Сформировать ZIP-отчёт: фотографии и файлы">Отчёт</button>
|
||||
<button class="btn-link" data-copy-student="${esc(s.name)}" title="Скопировать ссылку с подстановкой имени">Ссылка</button>
|
||||
<button class="btn-del" data-del-student="${s.id}" title="Удалить">×</button>
|
||||
</div>
|
||||
@@ -80,6 +81,81 @@ async function saveStudent(id) {
|
||||
else { const e = await res.json(); alert(e.error); }
|
||||
}
|
||||
|
||||
let exportPendingName = null;
|
||||
|
||||
function openExportModal(name) {
|
||||
exportPendingName = name;
|
||||
document.getElementById('exportName').textContent = name;
|
||||
document.getElementById('exportForm').reset();
|
||||
document.getElementById('exportDateFrom').value = '';
|
||||
document.getElementById('expEntries').checked = true;
|
||||
document.getElementById('expPhotos').checked = true;
|
||||
document.getElementById('expCaptions').checked = true;
|
||||
document.getElementById('expFiles').checked = true;
|
||||
document.getElementById('expDates').checked = true;
|
||||
document.getElementById('exportModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeExportModal() {
|
||||
document.getElementById('exportModal').classList.remove('open');
|
||||
exportPendingName = null;
|
||||
}
|
||||
|
||||
function onExportPhotosToggle() {
|
||||
const on = document.getElementById('expPhotos').checked;
|
||||
document.getElementById('expCaptions').disabled = !on;
|
||||
}
|
||||
|
||||
async function exportStudent(name, opts) {
|
||||
showToast(`Формируем отчёт: ${name}...`);
|
||||
try {
|
||||
const q = new URLSearchParams({ name });
|
||||
if (opts.dateFrom) q.set('date_from', opts.dateFrom);
|
||||
if (opts.dateTo) q.set('date_to', opts.dateTo);
|
||||
q.set('include_entries', opts.entries ? '1' : '0');
|
||||
q.set('include_photos', opts.photos ? '1' : '0');
|
||||
q.set('include_captions', opts.captions ? '1' : '0');
|
||||
q.set('include_files', opts.files ? '1' : '0');
|
||||
q.set('show_dates', opts.dates ? '1' : '0');
|
||||
const res = await fetch(`${API}/api/export/student?${q}`, { headers: hdr() });
|
||||
if (!res.ok) {
|
||||
const e = await res.json();
|
||||
alert(e.error || 'Не удалось сформировать отчёт');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
downloadBlob(blob, `student_${safeWindowsName(name)}_report.zip`);
|
||||
showToast(`Готово: ${name}`);
|
||||
} catch (err) {
|
||||
alert('Ошибка при формировании отчёта');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitExport() {
|
||||
const name = exportPendingName;
|
||||
if (!name) return;
|
||||
const captionsEl = document.getElementById('expCaptions');
|
||||
const opts = {
|
||||
entries: document.getElementById('expEntries').checked,
|
||||
photos: document.getElementById('expPhotos').checked,
|
||||
files: document.getElementById('expFiles').checked,
|
||||
captions: captionsEl.checked && !captionsEl.disabled,
|
||||
dates: document.getElementById('expDates').checked,
|
||||
dateFrom: document.getElementById('exportDateFrom').value,
|
||||
dateTo: document.getElementById('exportDateTo').value,
|
||||
};
|
||||
if (!opts.entries && !opts.photos && !opts.files) {
|
||||
alert('Выберите хотя бы одну категорию: записи, фото или файлы');
|
||||
return;
|
||||
}
|
||||
if (opts.dateFrom && opts.dateTo && opts.dateFrom > opts.dateTo) {
|
||||
alert('Дата «С» позже даты «По»');
|
||||
return;
|
||||
}
|
||||
closeExportModal();
|
||||
await exportStudent(name, opts);
|
||||
}
|
||||
|
||||
async function delStudent(id) {
|
||||
if (!confirm('Удалить ученика из списка? Старые записи останутся.')) return;
|
||||
await fetch(`${API}/api/students/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
@@ -217,6 +293,14 @@ document.getElementById('attachSearch').addEventListener('input', () => {
|
||||
aSearchTimer = setTimeout(renderAttachList, 300);
|
||||
});
|
||||
|
||||
document.getElementById('exportForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
submitExport();
|
||||
});
|
||||
document.getElementById('exportCancelBtn').addEventListener('click', closeExportModal);
|
||||
document.getElementById('expPhotos').addEventListener('change', onExportPhotosToggle);
|
||||
document.getElementById('exportModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeExportModal(); });
|
||||
|
||||
document.getElementById('addStudentBtn').addEventListener('click', addStudent);
|
||||
document.getElementById('batchAddBtn').addEventListener('click', openBatchAdd);
|
||||
document.getElementById('batchAttachBtn').addEventListener('click', openBatchAttach);
|
||||
@@ -232,6 +316,8 @@ document.getElementById('studentList').addEventListener('click', e => {
|
||||
if (sv) { saveStudent(parseInt(sv.dataset.saveStudent, 10)); return; }
|
||||
const cp = e.target.closest('[data-copy-student]');
|
||||
if (cp) { copyStudentLink(cp.dataset.copyStudent); return; }
|
||||
const ex = e.target.closest('[data-export-student]');
|
||||
if (ex) { openExportModal(ex.dataset.exportStudent.trim()); return; }
|
||||
const dl = e.target.closest('[data-del-student]');
|
||||
if (dl) { delStudent(parseInt(dl.dataset.delStudent, 10)); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user