const $ = (q) => document.querySelector(q); const BASE_PATH = (() => { const p = window.location.pathname; if (p === "/" || p === "") return ""; return p.endsWith("/") ? p.slice(0, -1) : p.replace(/\/[^/]*$/, ""); })(); const U = (path) => (BASE_PATH || "") + path; const $$ = (q) => Array.from(document.querySelectorAll(q)); const state = { user: null, folders: [], tags: [], selectedFolder: '', currentView: 'photos', currentPhoto: null, }; async function api(path, opts = {}) { const res = await {fetch(U(path), { credentials: 'same-origin', headers: opts.body instanceof FormData ? {} : {'Content-Type':'application/json'}, ...opts, }); const type = res.headers.get('content-type') || ''; const data = type.includes('application/json') ? await res.json() : await res.text(); if (!res.ok || (data && data.ok === false)) throw new Error(data.error || res.statusText); return data; } function fmtBytes(n){ if(!n) return '0 MB'; const u=['B','KB','MB','GB','TB']; let i=0; while(n>1024&&i"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])) } function roleName(r){ return r==='admin'?'Admin':r==='user'?'Użytkownik':'Gość' } function canAdmin(){ return state.user?.role === 'admin' } function visibleAdmin(){ $$('.admin-only').forEach(x=>x.classList.toggle('hidden-role', !canAdmin())) } async function init(){ try { const me = await api('/api/me'); state.user = me.user; $('#login').classList.add('hidden'); $('#app').classList.remove('hidden'); $('#currentUser').innerHTML = `${esc(state.user.display_name)}
${roleName(state.user.role)}`; visibleAdmin(); await reloadAll(); } catch(e) { $('#login').classList.remove('hidden'); $('#app').classList.add('hidden'); } } async function reloadAll(){ await Promise.all([loadFolders(), loadTags(), loadStats()]); await loadPhotos(); if(canAdmin()) { loadUsers(); loadEvents(); } } async function loadStats(){ const r = await api('/api/stats'); $('#statPhotos').textContent = r.stats.photos ?? 0; $('#statPending').textContent = r.stats.pending ?? 0; $('#pendingBubble').textContent = r.stats.pending ?? 0; $('#statFolders').textContent = r.stats.folders ?? 0; $('#statBytes').textContent = fmtBytes(r.stats.bytes ?? 0); } async function loadFolders(){ const r = await api('/api/folders'); state.folders = r.folders || []; renderFolderTrees(); fillFolderSelects(); } async function loadTags(){ const r = await api('/api/tags'); state.tags = r.tags || []; const opt = [''].concat(state.tags.map(t=>``)); $('#tagFilter').innerHTML = opt.join(''); $('#tagCloud').innerHTML = state.tags.length ? state.tags.map(t=>``).join('') : '
Brak tagów
'; $('#quickTags').innerHTML = state.tags.slice(0,10).map(t=>``).join('') || 'Jeszcze brak tagów'; } function depth(path){ return Math.max(0, (path || '').split('/').filter(Boolean).length - 1) } function renderFolderTrees(){ const rows = state.folders.map(f => { const d = depth(f.path); const selected = state.selectedFolder === f.id ? 'selected' : ''; return `
${esc(f.name)}${esc(f.permission)}
`; }).join('') || '
Brak katalogów
'; $('#folderTree').innerHTML = rows; $('#miniFolderTree').innerHTML = rows; } function fillFolderSelects(){ const opts = [''].concat(state.folders.map(f=>``)); $('#folderParentSelect').innerHTML = opts.join(''); const normal = state.folders.map(f=>``).join(''); $('#uploadFolderSelect').innerHTML = normal; $('#moveFolderSelect').innerHTML = normal; if(state.selectedFolder) $('#uploadFolderSelect').value = state.selectedFolder; } async function loadPhotos(statusOverride){ const params = new URLSearchParams(); const q = $('#searchInput').value.trim(); const tag = $('#tagFilter').value; const status = statusOverride || $('#statusFilter').value || 'approved'; if(state.selectedFolder) params.set('folder_id', state.selectedFolder); if(q) params.set('q', q); if(tag) params.set('tag', tag); if($('#dateFrom').value) params.set('date_from', $('#dateFrom').value); if($('#dateTo').value) params.set('date_to', $('#dateTo').value); params.set('status', status); const r = await api('/api/photos?' + params.toString()); const grid = statusOverride === 'pending' ? $('#pendingGrid') : $('#photoGrid'); renderPhotos(grid, r.photos || []); const folder = state.folders.find(f=>f.id===state.selectedFolder); $('#folderBreadcrumb').textContent = folder ? folder.path : 'Wszystkie dostępne katalogi'; } function renderPhotos(grid, photos){ if(!photos.length){ grid.innerHTML = '
Brak zdjęć do pokazania
'; return; } grid.innerHTML = photos.map(p => { const isVid = p.is_video ? 'WIDEO' : ''; const pending = p.status === 'pending' ? 'OCZEKUJE' : ''; const tags = (p.tags || []).map(t=>`#${esc(t.name)}`).join(''); const img = p.is_video ? `
Film
` : `${esc(p.file_name)}`; return `
${img}${isVid}${pending}
${esc(p.file_name)}
${esc(p.folder_path)}
${tags}
`; }).join(''); } function switchView(name){ state.currentView = name; $$('.nav-item').forEach(b=>b.classList.toggle('active', b.dataset.view===name)); $$('.view').forEach(v=>v.classList.remove('active-view')); $(`#view${name[0].toUpperCase()+name.slice(1)}`).classList.add('active-view'); if(name==='pending') loadPhotos('pending'); if(name==='logs') loadEvents(); if(name==='users') loadUsers(); } async function loadUsers(){ if(!canAdmin()) return; const r = await api('/api/users'); const rows = (r.users||[]).map(u=>`${esc(u.display_name)}
${esc(u.username)}${roleName(u.role)}${u.can_create_folders?'katalogi ':''}${u.can_upload?'upload ':''}${u.can_tag?'tagi ':''}${u.active?'Aktywny':'Wyłączony'}`).join(''); $('#usersTable').innerHTML = `${rows}
KontoRolaMożeStatus
`; } async function loadEvents(){ if(!canAdmin()) return; const r = await api('/api/events'); const html = (r.events||[]).map(e=>`
${esc(e.message)}${esc(e.created_at)} • ${esc(e.actor_name||'system')} • ${esc(e.action)}
`).join('') || '
Brak zdarzeń
'; $('#logsList').innerHTML = html; $('#activityMini').innerHTML = (r.events||[]).slice(0,4).map(e=>`
${esc(e.message)}
`).join('') || 'Brak danych'; } function openPhoto(p){ state.currentPhoto = p; $('#photoPreview').src = p.is_video ? '' : p.url; $('#photoTitle').textContent = p.file_name; $('#photoMeta').innerHTML = `${esc(p.folder_path)}
${fmtBytes(p.size_bytes)} • ${esc(p.status)}
Wysłał: ${esc(p.submitted_by_name || '')}`; $('#photoTagsInput').value = (p.tags||[]).map(t=>t.name).join(', '); $('#moveFolderSelect').value = p.folder_id; $('#approvePhotoBtn').style.display = p.status === 'pending' ? '' : 'none'; $('#rejectPhotoBtn').style.display = p.status === 'pending' ? '' : 'none'; $('#photoDialog').showModal(); } $('#loginForm').addEventListener('submit', async e=>{ e.preventDefault(); $('#loginError').textContent = ''; const fd = new FormData(e.currentTarget); try { await api('/api/login', {method:'POST', body: JSON.stringify(Object.fromEntries(fd))}); await init(); } catch(err){ $('#loginError').textContent = err.message; } }); $('#logoutBtn').addEventListener('click', async()=>{ await api('/api/logout',{method:'POST',body:'{}'}); location.reload(); }); $$('.nav-item').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view))); $('#refreshBtn').addEventListener('click', reloadAll); ['searchInput','dateFrom','dateTo','tagFilter','statusFilter'].forEach(id=>$("#"+id).addEventListener('change',()=>loadPhotos())); $('#searchInput').addEventListener('input', debounce(()=>loadPhotos(), 350)); function debounce(fn, ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms) } } document.body.addEventListener('click', e=>{ const close = e.target.dataset.close; if(close) $('#'+close).close(); const nav = e.target.closest('[data-folder]'); if(nav){ state.selectedFolder = nav.dataset.folder; renderFolderTrees(); loadPhotos(); fillFolderSelects(); } const tagBtn = e.target.closest('[data-tag]'); if(tagBtn){ $('#tagFilter').value = tagBtn.dataset.tag; switchView('photos'); loadPhotos(); } const card = e.target.closest('.photo-card'); if(card){ openPhoto(JSON.parse(card.dataset.photo)); } }); $('#uploadOpenBtn').addEventListener('click',()=>$('#uploadDialog').showModal()); $('#newFolderBtn').addEventListener('click',()=>$('#folderDialog').showModal()); $('#newFolderBtn2').addEventListener('click',()=>$('#folderDialog').showModal()); $('#newUserBtn').addEventListener('click',()=>$('#userDialog').showModal()); $('#newTagBtn').addEventListener('click', async()=>{ const name=prompt('Nazwa tagu'); if(name){ await api('/api/tags',{method:'POST',body:JSON.stringify({name})}); await loadTags(); } }); $('#folderForm').addEventListener('submit', async e=>{ e.preventDefault(); const fd = new FormData(e.currentTarget); await api('/api/folders',{method:'POST',body:JSON.stringify({name:fd.get('name'), parent_id:fd.get('parent_id')||null, inherit_permissions:!!fd.get('inherit_permissions')})}); $('#folderDialog').close(); e.currentTarget.reset(); await loadFolders(); await loadStats(); }); $('#uploadForm').addEventListener('submit', async e=>{ e.preventDefault(); const fd = new FormData(e.currentTarget); $('#uploadStatus').textContent = 'Wysyłam...'; try { await api('/api/photos/upload',{method:'POST',body:fd}); $('#uploadStatus').textContent='Wysłane.'; setTimeout(()=>$('#uploadDialog').close(),500); await reloadAll(); } catch(err){ $('#uploadStatus').textContent = err.message; } }); $('#userForm').addEventListener('submit', async e=>{ e.preventDefault(); const fd = new FormData(e.currentTarget); const payload = Object.fromEntries(fd); payload.can_create_folders=!!fd.get('can_create_folders'); payload.can_upload=!!fd.get('can_upload'); payload.can_tag=!!fd.get('can_tag'); if(!payload.expires_at) delete payload.expires_at; const r = await api('/api/users',{method:'POST',body:JSON.stringify(payload)}); $('#userCreateResult').textContent = `Utworzono. Hasło: ${r.password}`; await loadUsers(); }); $('#saveTagsBtn').addEventListener('click', async()=>{ const p = state.currentPhoto; if(!p) return; const tags = $('#photoTagsInput').value.split(',').map(x=>x.trim()).filter(Boolean); await api(`/api/photos/${p.id}/tags`,{method:'POST',body:JSON.stringify({tags})}); $('#photoDialog').close(); await loadTags(); await loadPhotos(state.currentView==='pending'?'pending':undefined); }); $('#movePhotoBtn').addEventListener('click', async()=>{ const p = state.currentPhoto; if(!p) return; await api(`/api/photos/${p.id}/move`,{method:'POST',body:JSON.stringify({folder_id:$('#moveFolderSelect').value})}); $('#photoDialog').close(); await loadPhotos(state.currentView==='pending'?'pending':undefined); }); $('#approvePhotoBtn').addEventListener('click', async()=>{ const p = state.currentPhoto; if(!p) return; const tags = $('#photoTagsInput').value.split(',').map(x=>x.trim()).filter(Boolean); await api(`/api/photos/${p.id}/approve`,{method:'POST',body:JSON.stringify({folder_id:$('#moveFolderSelect').value,tags})}); $('#photoDialog').close(); await reloadAll(); if(state.currentView==='pending') loadPhotos('pending'); }); $('#rejectPhotoBtn').addEventListener('click', async()=>{ const p = state.currentPhoto; if(!p) return; const reason = prompt('Powód odrzucenia, opcjonalnie') || ''; await api(`/api/photos/${p.id}/reject`,{method:'POST',body:JSON.stringify({reason})}); $('#photoDialog').close(); await reloadAll(); if(state.currentView==='pending') loadPhotos('pending'); }); init();