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)); }
|
||||
});
|
||||
|
||||
@@ -53,6 +53,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="exportModal">
|
||||
<div class="edit-modal" style="max-width:480px">
|
||||
<h3>Экспорт отчёта: <span id="exportName" style="color:var(--accent)"></span></h3>
|
||||
<form id="exportForm" class="settings-stack" style="gap:12px">
|
||||
<div class="settings-row">
|
||||
<div class="settings-field" style="flex:1">
|
||||
<label>Период (необязательно)</label>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<input type="date" id="exportDateFrom" class="settings-input" style="min-width:0;flex:1">
|
||||
<input type="date" id="exportDateTo" class="settings-input" style="min-width:0;flex:1">
|
||||
</div>
|
||||
<span class="hint">Ограничить записи/фото/файлы датами</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expEntries" checked>
|
||||
Записи занятий (текст)
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expPhotos" checked>
|
||||
Фотографии
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expCaptions" checked>
|
||||
Подписи к фотографиям
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expFiles" checked>
|
||||
Файлы
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expDates" checked>
|
||||
Показывать даты
|
||||
</label>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="exportCancelBtn">Отмена</button>
|
||||
<button type="submit" class="btn-primary">Скачать ZIP</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="attachModal">
|
||||
<div class="edit-modal" style="max-width:480px">
|
||||
<h3>Прикрепить к группе</h3>
|
||||
|
||||
Reference in New Issue
Block a user