From fac668567e6e8d81407f222b5aff57918c0afe61 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 12 Sep 2026 23:55:35 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B?= =?UTF-8?q?=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B0=20=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B0=20=E2=80=94=20=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=D1=84=D0=BE=D0=BB=D0=B8=D0=BE-=D1=81=D1=82=D0=B8=D0=BB=D1=8C,?= =?UTF-8?q?=20=D0=B3=D0=B0=D0=BB=D0=B5=D1=80=D0=B5=D1=8F=20=D1=81=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B4=D0=B5=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renderStudentReport: детский учебный дизайн (крупные скругления, sticky-навигация, секции-карточки) - журнал занятий в две колонки (одна на мобильных) - единая галерея фото и видео: листание кнопками/стрелками, счётчик - воспроизведение видео прямо в лайтбоксе - кнопка закрытия ✕, закрытие по фону и Esc - модалка экспорта отчёта ученика (период и выбор содержимого) --- public/js/students.js | 86 +++++++ public/students.html | 52 ++++ server.js | 568 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 706 insertions(+) diff --git a/public/js/students.js b/public/js/students.js index 336c643..a647429 100644 --- a/public/js/students.js +++ b/public/js/students.js @@ -47,6 +47,7 @@ function renderStudents() {
+
@@ -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)); } }); diff --git a/public/students.html b/public/students.html index f4d0fb1..85e077f 100644 --- a/public/students.html +++ b/public/students.html @@ -53,6 +53,58 @@ + +