diff --git a/server/src/main/resources/static/admin/index.html b/server/src/main/resources/static/admin/index.html index e05a1b9..7dbd258 100644 --- a/server/src/main/resources/static/admin/index.html +++ b/server/src/main/resources/static/admin/index.html @@ -83,6 +83,9 @@ #inv-table th { cursor: pointer; user-select: none; white-space: nowrap; } #inv-table th:hover { background: #3A3428; } #inv-table th .sort-arrow { margin-left: 4px; color: #6B5B3E; font-size: .8rem; } + #resources-table th { cursor: pointer; user-select: none; white-space: nowrap; } + #resources-table th:hover { background: #3A3428; } + #resources-table th .sort-arrow { margin-left: 4px; color: #6B5B3E; font-size: .8rem; } .paging-bar { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; } .paging-bar button { padding: 4px 10px; font-size: .85rem; } .paging-bar span { font-size: .85rem; color: #A89070; } @@ -282,20 +285,46 @@ +
+ + + + + + +
- + - + - - + +
TitelTitel FormatGrößeGröße TagsAutorHochgeladenAutor Hochgeladen Aktionen
Tab auswählen um zu laden…
+
@@ -1089,37 +1118,181 @@ } } + // ── Ressourcen: Paging, Suche, Sortierung, Filter, Gruppierung ────────── + let allResources = []; + let resPage = 0; + let resPageSize = 20; + let resSortKey = 'createdAt'; + let resSortDir = 'desc'; + + function applyResFilter() { + resPage = 0; + renderResTable(); + } + + function sortRes(key) { + if (resSortKey === key) { resSortDir = resSortDir === 'asc' ? 'desc' : 'asc'; } + else { resSortKey = key; resSortDir = 'asc'; } + resPage = 0; + renderResTable(); + } + + function renderResTable() { + const search = (document.getElementById('res-search-filter')?.value || '').toLowerCase(); + const filterFormat = document.getElementById('res-filter-format')?.value || ''; + const filterAuthor = document.getElementById('res-filter-author')?.value || ''; + const filterTag = document.getElementById('res-filter-tag')?.value || ''; + const groupBy = document.getElementById('res-group-by')?.value || ''; + + // 1. Filter + let filtered = allResources.filter(r => { + if (search && !r.title.toLowerCase().includes(search) && !(r.author || '').toLowerCase().includes(search)) return false; + if (filterFormat && r.fileFormat !== filterFormat) return false; + if (filterAuthor && (r.author || '') !== filterAuthor) return false; + if (filterTag && !(r.tags || []).includes(filterTag)) return false; + return true; + }); + + // 2. Sort + filtered.sort((a, b) => { + let av = a[resSortKey], bv = b[resSortKey]; + if (typeof av === 'string') av = av.toLowerCase(); + if (typeof bv === 'string') bv = bv.toLowerCase(); + if (av == null) av = typeof bv === 'number' ? 0 : ''; + if (bv == null) bv = typeof av === 'number' ? 0 : ''; + return resSortDir === 'asc' ? (av > bv ? 1 : av < bv ? -1 : 0) : (av < bv ? 1 : av > bv ? -1 : 0); + }); + + // 3. Update sort arrows + ['title', 'author', 'createdAt', 'fileSize'].forEach(k => { + const el = document.getElementById('rssort-' + k); + if (el) el.textContent = resSortKey === k ? (resSortDir === 'asc' ? '↑' : '↓') : ''; + }); + + // 4. Paging + const total = filtered.length; + const totalPages = Math.max(1, Math.ceil(total / resPageSize)); + if (resPage >= totalPages) resPage = totalPages - 1; + const start = resPage * resPageSize; + const end = Math.min(start + resPageSize, total); + const page = filtered.slice(start, end); + + // 5. Paging bar + const pagingBar = document.getElementById('res-paging-bar'); + pagingBar.innerHTML = ''; + const info = document.createElement('span'); + info.textContent = total === 0 ? 'Keine Einträge' : `Zeigt ${start + 1}–${end} von ${total} · Seite ${resPage + 1} von ${totalPages}`; + pagingBar.appendChild(info); + if (resPage > 0) { + const prev = document.createElement('button'); prev.className = 'secondary'; prev.textContent = '← Zurück'; + prev.onclick = () => { resPage--; renderResTable(); }; + pagingBar.appendChild(prev); + } + const pageRange = resPagingRange(resPage, totalPages, 5); + pageRange.forEach(p => { + const btn = document.createElement('button'); + btn.className = p === resPage ? 'primary' : 'secondary'; + btn.textContent = String(p + 1); + btn.onclick = () => { resPage = p; renderResTable(); }; + pagingBar.appendChild(btn); + }); + if (resPage < totalPages - 1) { + const next = document.createElement('button'); next.className = 'secondary'; next.textContent = 'Weiter →'; + next.onclick = () => { resPage++; renderResTable(); }; + pagingBar.appendChild(next); + } + + // 6. Render table + const tbody = document.getElementById('resources-body'); + if (!page.length) { + tbody.innerHTML = 'Keine Einträge gefunden'; + return; + } + tbody.innerHTML = ''; + if (groupBy) { + const groups = {}; + page.forEach(r => { + let key; + if (groupBy === 'fileFormat') key = (r.fileFormat || '').toUpperCase(); + else if (groupBy === 'author') key = r.author || '(kein Autor)'; + else key = (r.tags && r.tags.length) ? r.tags[0] : '(kein Tag)'; + if (!groups[key]) groups[key] = []; + groups[key].push(r); + }); + Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])).forEach(([groupKey, items]) => { + const groupRow = document.createElement('tr'); + groupRow.innerHTML = `▣ ${escHtml(groupKey)} (${items.length})`; + tbody.appendChild(groupRow); + items.forEach(r => tbody.appendChild(buildResRow(r))); + }); + } else { + page.forEach(r => tbody.appendChild(buildResRow(r))); + } + } + + function buildResRow(r) { + const tr = document.createElement('tr'); + const tdTitle = document.createElement('td'); tdTitle.textContent = r.title; + const tdFormat = document.createElement('td'); tdFormat.textContent = (r.fileFormat || '').toUpperCase(); + const tdSize = document.createElement('td'); tdSize.textContent = formatBytes(r.fileSize || 0); + const tdTags = document.createElement('td'); tdTags.textContent = (r.tags || []).join(', '); + const tdAuthor = document.createElement('td'); tdAuthor.textContent = r.author || '–'; + const tdDate = document.createElement('td'); + tdDate.textContent = r.createdAt ? new Date(r.createdAt).toLocaleDateString('de-DE') : '–'; + const tdAct = document.createElement('td'); tdAct.className = 'actions'; + const btnDl = document.createElement('button'); btnDl.className = 'secondary'; btnDl.textContent = 'Download'; + btnDl.onclick = () => { window.open('/api/resources/' + r.guid + '/download?token=' + accessToken, '_blank'); }; + const btnDel = document.createElement('button'); btnDel.className = 'danger'; btnDel.textContent = 'Löschen'; + btnDel.onclick = () => deleteResource(r.guid, r.title); + tdAct.append(btnDl, btnDel); + tr.append(tdTitle, tdFormat, tdSize, tdTags, tdAuthor, tdDate, tdAct); + return tr; + } + + function resPagingRange(current, total, maxVisible) { + if (total <= maxVisible) return Array.from({ length: total }, (_, i) => i); + const half = Math.floor(maxVisible / 2); + let start = Math.max(0, current - half); + let end = start + maxVisible; + if (end > total) { end = total; start = Math.max(0, end - maxVisible); } + return Array.from({ length: end - start }, (_, i) => start + i); + } + + function updateResFilterDropdowns() { + const authors = [...new Set(allResources.map(r => r.author).filter(Boolean))].sort(); + const tags = [...new Set(allResources.flatMap(r => r.tags || []))].sort(); + const authorSel = document.getElementById('res-filter-author'); + const tagSel = document.getElementById('res-filter-tag'); + if (authorSel) { + const cur = authorSel.value; + authorSel.innerHTML = ''; + authors.forEach(a => { + const opt = document.createElement('option'); opt.value = a; opt.textContent = a; + if (a === cur) opt.selected = true; + authorSel.appendChild(opt); + }); + } + if (tagSel) { + const cur = tagSel.value; + tagSel.innerHTML = ''; + tags.forEach(t => { + const opt = document.createElement('option'); opt.value = t; opt.textContent = t; + if (t === cur) opt.selected = true; + tagSel.appendChild(opt); + }); + } + } + async function loadResources() { const tbody = document.getElementById('resources-body'); tbody.innerHTML = 'Wird geladen…'; try { const res = await fetch('/api/resources', { headers: { 'Authorization': 'Bearer ' + accessToken } }); if (!res.ok) { tbody.innerHTML = 'Fehler beim Laden'; return; } - const resources = await res.json(); + allResources = await res.json(); resourcesLoaded = true; - if (!resources.length) { - tbody.innerHTML = 'Keine Ressourcen vorhanden'; - return; - } - tbody.innerHTML = ''; - resources.forEach(r => { - const tr = document.createElement('tr'); - const tdTitle = document.createElement('td'); tdTitle.textContent = r.title; - const tdFormat = document.createElement('td'); tdFormat.textContent = (r.fileFormat || '').toUpperCase(); - const tdSize = document.createElement('td'); tdSize.textContent = formatBytes(r.fileSize || 0); - const tdTags = document.createElement('td'); tdTags.textContent = (r.tags || []).join(', '); - const tdAuthor = document.createElement('td'); tdAuthor.textContent = r.author || '–'; - const tdDate = document.createElement('td'); - tdDate.textContent = r.createdAt ? new Date(r.createdAt).toLocaleDateString('de-DE') : '–'; - const tdAct = document.createElement('td'); tdAct.className = 'actions'; - const btnDl = document.createElement('button'); btnDl.className = 'secondary'; btnDl.textContent = 'Download'; - btnDl.onclick = () => { window.open('/api/resources/' + r.guid + '/download?token=' + accessToken, '_blank'); }; - const btnDel = document.createElement('button'); btnDel.className = 'danger'; btnDel.textContent = 'Löschen'; - btnDel.onclick = () => deleteResource(r.guid, r.title); - tdAct.append(btnDl, btnDel); - tr.append(tdTitle, tdFormat, tdSize, tdTags, tdAuthor, tdDate, tdAct); - tbody.appendChild(tr); - }); + updateResFilterDropdowns(); + renderResTable(); } catch (e) { tbody.innerHTML = 'Verbindungsfehler'; }