feat(groups): ZIP-архив группы с выбором категорий и режима фото
Форма архива файлов группы на /groups.html получила галочки выгрузки: - «Работы (файлы проектов)» — включена по умолчанию - «Фото записи» — выключена по умолчанию - режим фото «Последние» / «Все», по умолчанию «Последние»; радиокнопки блокируются, когда «Фото записи» выключена Дефолты периода: дата «с» пустая, дата «по» — сегодня. GET /api/groups/:id/export/files принимает include_files, include_photos и photos_mode: - latest — только entries.photo_path, то главное обработанное фото записи - all — плюс вся галерея записи из entry_photos - 400, если обе категории выключены Запросы к БД для отключённых категорий не выполняются; в data.json и в аудит добавлены options и раздельные счётчики works/photos. В аудите добавлена метка export.group_files. Проверено на стенде: группа с многофото-записями даёт 4 фото в режиме «последние» и 6 в режиме «все» (совпадает с БД: 4 главных + 2 галереи), категории комбинируются независимо. api.smoketest.js 57/57, diff.selftest.js 16/16.
This commit is contained in:
@@ -65,6 +65,7 @@ async function loadGroups() {
|
||||
<button class="photo-btn" data-group-id="${g.id}" data-group-name="${esc(g.name)}" title="Фото группы (хронология)">Фото</button>
|
||||
<button class="btn-link" data-export-group="${g.id}" data-group-name="${esc(g.name)}" title="Экспортировать записи группы">CSV</button>
|
||||
<button class="btn-link" data-export-urls="${g.id}" data-group-name="${esc(g.name)}" title="Скачать .url файлы учеников группы">URL</button>
|
||||
<button class="btn-link" data-export-files="${g.id}" data-group-name="${esc(g.name)}" title="Скачать ZIP-архив файлов группы за период">ZIP</button>
|
||||
</div>
|
||||
<div class="sched-status">${schedSet ? `${DAYS[g.day_of_week]} ${g.time_start.slice(0,5)}–${g.time_end.slice(0,5)}` : 'Не настроено'}</div>
|
||||
</div>
|
||||
@@ -297,6 +298,83 @@ async function exportGroupUrls(groupId, groupName) {
|
||||
showToast(`Экспортировано: ${list.length}`);
|
||||
}
|
||||
|
||||
function openGroupFilesModal(groupId, groupName) {
|
||||
const sel = document.getElementById('groupFilesGroup');
|
||||
sel.innerHTML = allGroups.map(g => `<option value="${g.id}" ${String(g.id) === String(groupId) ? 'selected' : ''}>${esc(g.name)}</option>`).join('');
|
||||
document.getElementById('groupFilesTitle').textContent = `Архив файлов — ${groupName}`;
|
||||
document.getElementById('groupFilesFrom').value = '';
|
||||
document.getElementById('groupFilesTo').value = todayISOLocal();
|
||||
document.getElementById('groupFilesIncludeWorks').checked = true;
|
||||
document.getElementById('groupFilesIncludePhotos').checked = false;
|
||||
const latest = document.querySelector('input[name="groupFilesMode"][value="latest"]');
|
||||
if (latest) latest.checked = true;
|
||||
onGroupFilesPhotosToggle();
|
||||
document.getElementById('groupFilesModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeGroupFilesModal() {
|
||||
document.getElementById('groupFilesModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function todayISOLocal() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function onGroupFilesPhotosToggle() {
|
||||
const on = document.getElementById('groupFilesIncludePhotos').checked;
|
||||
document.querySelectorAll('input[name="groupFilesMode"]').forEach(el => { el.disabled = !on; });
|
||||
}
|
||||
|
||||
function groupFilesMode() {
|
||||
const checked = document.querySelector('input[name="groupFilesMode"]:checked');
|
||||
return checked && checked.value === 'all' ? 'all' : 'latest';
|
||||
}
|
||||
|
||||
async function downloadGroupFiles() {
|
||||
const groupId = document.getElementById('groupFilesGroup').value;
|
||||
const dateFrom = document.getElementById('groupFilesFrom').value;
|
||||
const dateTo = document.getElementById('groupFilesTo').value;
|
||||
const includeWorks = document.getElementById('groupFilesIncludeWorks').checked;
|
||||
const includePhotos = document.getElementById('groupFilesIncludePhotos').checked;
|
||||
if (!groupId) { alert('Выберите группу'); return; }
|
||||
if (!includeWorks && !includePhotos) { alert('Выберите хотя бы одну категорию: работы или фото записи'); return; }
|
||||
if (dateFrom && dateTo && dateFrom > dateTo) { alert('Дата «С» позже даты «По»'); return; }
|
||||
const btn = document.getElementById('downloadGroupFilesBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Формируем...';
|
||||
try {
|
||||
const q = new URLSearchParams();
|
||||
if (dateFrom) q.set('date_from', dateFrom);
|
||||
if (dateTo) q.set('date_to', dateTo);
|
||||
q.set('include_files', includeWorks ? '1' : '0');
|
||||
q.set('include_photos', includePhotos ? '1' : '0');
|
||||
q.set('photos_mode', groupFilesMode());
|
||||
const res = await fetch(`${API}/api/groups/${groupId}/export/files?${q}`, { headers: hdr() });
|
||||
if (!res.ok) {
|
||||
let msg = 'Не удалось сформировать архив';
|
||||
try { const e = await res.json(); msg = e.error || msg; } catch {}
|
||||
alert(msg);
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
let disp = res.headers.get('Content-Disposition') || '';
|
||||
let fname = 'group_files.zip';
|
||||
const star = disp.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
const plain = disp.match(/filename="?([^";]+)"?/i);
|
||||
if (star) { try { fname = decodeURIComponent(star[1]); } catch { fname = plain ? plain[1] : fname; } }
|
||||
else if (plain) fname = plain[1];
|
||||
downloadBlob(blob, fname);
|
||||
showToast('Архив сформирован');
|
||||
closeGroupFilesModal();
|
||||
} catch (err) {
|
||||
alert('Ошибка при формировании архива: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Скачать ZIP';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('newGroup').addEventListener('keydown', e => { if (e.key === 'Enter') addGroup(); });
|
||||
document.getElementById('addGroupBtn').addEventListener('click', addGroup);
|
||||
document.getElementById('photoFile').addEventListener('change', previewPhotoFile);
|
||||
@@ -307,6 +385,10 @@ document.getElementById('photoForm').addEventListener('submit', e => {
|
||||
|
||||
document.getElementById('photosModal').addEventListener('click', e => { if (e.target === e.currentTarget) closePhotos(); });
|
||||
document.getElementById('photoFormModal').addEventListener('click', e => { if (e.target === e.currentTarget) closePhotoForm(); });
|
||||
document.getElementById('groupFilesModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGroupFilesModal(); });
|
||||
document.getElementById('closeGroupFilesBtn').addEventListener('click', closeGroupFilesModal);
|
||||
document.getElementById('groupFilesForm').addEventListener('submit', e => { e.preventDefault(); downloadGroupFiles(); });
|
||||
document.getElementById('groupFilesIncludePhotos').addEventListener('change', onGroupFilesPhotosToggle);
|
||||
document.getElementById('openPhotoBtn').addEventListener('click', openAddPhoto);
|
||||
document.getElementById('closePhotosBtn').addEventListener('click', closePhotos);
|
||||
document.getElementById('closePhotoFormBtn').addEventListener('click', closePhotoForm);
|
||||
@@ -320,6 +402,8 @@ document.getElementById('groupList').addEventListener('click', e => {
|
||||
if (eg) { exportGroup(parseInt(eg.dataset.exportGroup, 10), eg.dataset.groupName); return; }
|
||||
const eu = e.target.closest('[data-export-urls]');
|
||||
if (eu) { exportGroupUrls(parseInt(eu.dataset.exportUrls, 10), eu.dataset.groupName); return; }
|
||||
const ef = e.target.closest('[data-export-files]');
|
||||
if (ef) { openGroupFilesModal(parseInt(ef.dataset.exportFiles, 10), ef.dataset.groupName); return; }
|
||||
const photoBtn = e.target.closest('.photo-btn');
|
||||
if (photoBtn) {
|
||||
const id = parseInt(photoBtn.dataset.groupId, 10);
|
||||
|
||||
Reference in New Issue
Block a user