add soft delete with trash page and group photo chronology with styling
This commit is contained in:
+182
-1
@@ -18,6 +18,44 @@
|
||||
<div class="list" id="groupList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="photosModal" onclick="if(event.target===this)closePhotos()">
|
||||
<div class="gallery-modal">
|
||||
<div class="gallery-head">
|
||||
<h3 id="photosTitle">Фото</h3>
|
||||
<button class="upload-btn" onclick="openAddPhoto()">Загрузить фото</button>
|
||||
</div>
|
||||
<div class="photo-grid" id="photoGrid"><div class="empty">Загрузка...</div></div>
|
||||
<div class="pager" id="photosPager"></div>
|
||||
<div class="gallery-foot">
|
||||
<button class="cancel" onclick="closePhotos()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="photoFormModal" onclick="if(event.target===this)closePhotoForm()">
|
||||
<div class="edit-modal">
|
||||
<h3 id="photoFormTitle">Добавить фото</h3>
|
||||
<div>
|
||||
<label>Выбрать файл (jpg/png и т.п.)</label>
|
||||
<input type="file" id="photoFile" accept="image/*">
|
||||
</div>
|
||||
<div id="photoFormPreview" class="photo-preview" style="display:none"><img id="photoPreviewImg" alt=""></div>
|
||||
<div>
|
||||
<label>Подпись</label>
|
||||
<textarea id="photoCaption" placeholder="Например: выступление, тренировка..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Дата съёмки</label>
|
||||
<input type="date" id="photoTakenAt">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="cancel" onclick="closePhotoForm()">Отмена</button>
|
||||
<button class="save" onclick="savePhotoForm()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="admin.js"></script>
|
||||
<script src="vendor/jszip.min.js"></script>
|
||||
@@ -34,6 +72,9 @@ async function loadGroups() {
|
||||
const schedSet = g.day_of_week != null && g.time_start && g.time_end;
|
||||
return `
|
||||
<div class="group-card">
|
||||
${g.cover_path
|
||||
? `<img class="group-cover" src="${API}${g.cover_path}" onclick="openPhotos(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Фото группы (хронология)" alt="">`
|
||||
: `<div class="group-cover empty-cover" onclick="openPhotos(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Фото группы (хронология)">📷</div>`}
|
||||
<div class="group-head">
|
||||
<input class="name-input" id="name-${g.id}" value="${esc(g.name)}">
|
||||
<button class="del" onclick="delGroup(${g.id})" title="Удалить">×</button>
|
||||
@@ -55,6 +96,7 @@ async function loadGroups() {
|
||||
<div class="group-foot">
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="save-sched" onclick="saveGroup(${g.id})">Сохранить</button>
|
||||
<button class="photo-btn" onclick="openPhotos(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Фото группы (хронология)">Фото</button>
|
||||
<button class="btn-link" onclick="exportGroup(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Экспортировать записи группы">CSV</button>
|
||||
<button class="btn-link" onclick="exportGroupUrls(${g.id}, '${esc(g.name).replace(/'/g, ''')}')" title="Скачать .url файлы учеников группы">URL</button>
|
||||
</div>
|
||||
@@ -93,11 +135,149 @@ async function addGroup() {
|
||||
}
|
||||
|
||||
async function delGroup(id) {
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
if (!confirm('Удалить группу? Фото группы также будут удалены.')) return;
|
||||
await fetch(`${API}/api/groups/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
// --- Фото группы (хронология) ---
|
||||
const PHOTO_PAGE_SIZE = 24;
|
||||
let currentGroupId = null;
|
||||
let currentGroupName = '';
|
||||
let photos = [];
|
||||
let photoPage = 1;
|
||||
let totalPhotos = 0;
|
||||
let editingPhotoId = null;
|
||||
|
||||
async function openPhotos(groupId, groupName) {
|
||||
currentGroupId = groupId;
|
||||
currentGroupName = groupName;
|
||||
photoPage = 1;
|
||||
editingPhotoId = null;
|
||||
document.getElementById('photosTitle').textContent = `Фото — ${groupName}`;
|
||||
document.getElementById('photosModal').classList.add('open');
|
||||
await loadGroupPhotos();
|
||||
}
|
||||
|
||||
function closePhotos() {
|
||||
document.getElementById('photosModal').classList.remove('open');
|
||||
currentGroupId = null;
|
||||
}
|
||||
|
||||
async function loadGroupPhotos() {
|
||||
const p = new URLSearchParams();
|
||||
p.set('limit', PHOTO_PAGE_SIZE);
|
||||
p.set('offset', (photoPage - 1) * PHOTO_PAGE_SIZE);
|
||||
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos?${p}`, { headers: hdr() });
|
||||
const data = await res.json();
|
||||
photos = data.photos;
|
||||
totalPhotos = data.total;
|
||||
document.getElementById('photoGrid').className = 'photo-grid';
|
||||
document.getElementById('photoGrid').innerHTML = photos.length
|
||||
? photos.map(photoHTML).join('')
|
||||
: '<div class="empty" style="grid-column:1/-1">Фото пока нет. Загрузите первое фото.</div>';
|
||||
renderPager(document.getElementById('photosPager'), photoPage, Math.max(1, Math.ceil(totalPhotos / PHOTO_PAGE_SIZE)), goPhotoPage);
|
||||
}
|
||||
|
||||
function goPhotoPage(p) { photoPage = p; loadGroupPhotos(); }
|
||||
|
||||
function fmtPhotoDate(d) { return d ? d.split('-').reverse().join('.') : ''; }
|
||||
|
||||
function photoHTML(p) {
|
||||
const taken = p.taken_at ? `Дата: ${fmtPhotoDate(p.taken_at)}` : '';
|
||||
const uploaded = `Загружено: ${new Date(p.created_at).toLocaleDateString('ru')}`;
|
||||
return `
|
||||
<div class="photo-item">
|
||||
<img src="${API}${p.photo_path}" onclick="showImg('${API}${p.photo_path}')" alt="">
|
||||
<div class="p-cap">${esc(p.caption || '')}</div>
|
||||
<div class="p-date">${taken ? taken + ' · ' : ''}${uploaded}</div>
|
||||
<div class="p-actions">
|
||||
<button class="p-edit" onclick="openEditPhoto(${p.id})" title="Редактировать">✎</button>
|
||||
<button class="p-del" onclick="deletePhoto(${p.id})" title="Удалить">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function openAddPhoto() {
|
||||
editingPhotoId = null;
|
||||
document.getElementById('photoFormTitle').textContent = `Добавить фото — ${currentGroupName}`;
|
||||
document.getElementById('photoFile').value = '';
|
||||
document.getElementById('photoCaption').value = '';
|
||||
document.getElementById('photoTakenAt').value = '';
|
||||
document.getElementById('photoFormPreview').style.display = 'none';
|
||||
document.getElementById('photoFormModal').classList.add('open');
|
||||
}
|
||||
|
||||
async function openEditPhoto(id) {
|
||||
const p = photos.find(x => x.id === id);
|
||||
if (!p) return;
|
||||
editingPhotoId = id;
|
||||
document.getElementById('photoFormTitle').textContent = 'Редактировать фото';
|
||||
document.getElementById('photoFile').value = '';
|
||||
document.getElementById('photoCaption').value = p.caption || '';
|
||||
document.getElementById('photoTakenAt').value = p.taken_at || '';
|
||||
const prev = document.getElementById('photoFormPreview');
|
||||
const img = document.getElementById('photoPreviewImg');
|
||||
img.src = `${API}${p.photo_path}`;
|
||||
prev.style.display = 'block';
|
||||
document.getElementById('photoFormModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closePhotoForm() {
|
||||
document.getElementById('photoFormModal').classList.remove('open');
|
||||
editingPhotoId = null;
|
||||
}
|
||||
|
||||
function previewPhotoFile() {
|
||||
const file = document.getElementById('photoFile').files[0];
|
||||
const prev = document.getElementById('photoFormPreview');
|
||||
if (!file) { prev.style.display = 'none'; return; }
|
||||
const img = document.getElementById('photoPreviewImg');
|
||||
img.src = URL.createObjectURL(file);
|
||||
prev.style.display = 'block';
|
||||
}
|
||||
|
||||
async function savePhotoForm() {
|
||||
const caption = document.getElementById('photoCaption').value.trim();
|
||||
const taken_at = document.getElementById('photoTakenAt').value;
|
||||
if (editingPhotoId) {
|
||||
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos/${editingPhotoId}`, {
|
||||
method: 'PUT',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ caption: caption || null, taken_at: taken_at || null })
|
||||
});
|
||||
if (!res.ok) { const e = await res.json(); alert(e.error || 'Ошибка'); return; }
|
||||
showToast('Фото обновлено');
|
||||
} else {
|
||||
const file = document.getElementById('photoFile').files[0];
|
||||
if (!file) { alert('Выберите файл'); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('photo', file);
|
||||
fd.append('caption', caption);
|
||||
fd.append('taken_at', taken_at);
|
||||
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos`, {
|
||||
method: 'POST',
|
||||
headers: hdr(),
|
||||
body: fd
|
||||
});
|
||||
if (!res.ok) { const e = await res.json(); alert(e.error || 'Ошибка'); return; }
|
||||
showToast('Фото добавлено в хронологию');
|
||||
}
|
||||
closePhotoForm();
|
||||
loadGroupPhotos();
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
async function deletePhoto(id) {
|
||||
if (!confirm('Удалить это фото безвозвратно?')) return;
|
||||
const res = await fetch(`${API}/api/groups/${currentGroupId}/photos/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
if (!res.ok) { const e = await res.json(); alert(e.error || 'Ошибка'); return; }
|
||||
showToast('Фото удалено');
|
||||
loadGroupPhotos();
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
async function exportGroup(id, name) {
|
||||
const res = await fetch(`${API}/api/entries?group_id=${id}`, { headers: hdr() });
|
||||
const { entries } = await res.json();
|
||||
@@ -133,6 +313,7 @@ async function exportGroupUrls(groupId, groupName) {
|
||||
}
|
||||
|
||||
document.getElementById('newGroup').addEventListener('keydown', e => { if (e.key === 'Enter') addGroup(); });
|
||||
document.getElementById('photoFile').addEventListener('change', previewPhotoFile);
|
||||
|
||||
(async () => {
|
||||
if (await checkAuth()) {
|
||||
|
||||
Reference in New Issue
Block a user