/* =================================================================== Dateiablage. Hochgeladen wird der rohe Dateiinhalt, der Name steht im Kopf -- deshalb braucht der Server kein multipart/form-data zu zerlegen. Wer was sehen und freigeben darf, entscheidet ausschliesslich er. =================================================================== */ (() => { 'use strict'; const $ = (id) => document.getElementById(id); const STATUSNAME = { entwurf: 'Entwurf', review: 'Review', freigegeben: 'Freigegeben' }; const WEITER = { entwurf: 'review', review: 'freigegeben' }; const ZURUECK = { review: 'entwurf', freigegeben: 'review' }; let ich = null; let filter = ''; let maxBytes = 25 * 1024 * 1024; const el = (tag, klasse, text) => { const k = document.createElement(tag); if (klasse) k.className = klasse; if (text !== undefined) k.textContent = text; return k; }; const melde = (t) => { $('fehler').textContent = t || ''; }; async function hole(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } return a; } function groesse(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB'; return (bytes / 1024 / 1024).toFixed(1).replace('.', ',') + ' MB'; } const datum = (iso) => (iso ? iso.slice(0, 10).split('-').reverse().join('.') : '—'); /* ---------- Hochladen -------------------------------------------------- */ async function schicke(datei) { melde(''); if (datei.size > maxBytes) { melde(`„${datei.name}" ist ${groesse(datei.size)} groß – erlaubt sind ${groesse(maxBytes)}.`); return; } $('fortschritt').hidden = false; $('fortschritt-text').textContent = `„${datei.name}" wird hochgeladen …`; try { const kopf = { 'Content-Type': datei.type || 'application/octet-stream', /* Kodiert, damit Umlaute und Leerzeichen im Kopf heil ankommen. */ 'X-Dateiname': encodeURIComponent(datei.name), }; if (ich.rolle === 'admin' && $('f-creator').value) kopf['X-Creator'] = $('f-creator').value; const a = await hole('/workspace/api/dateien', { method: 'POST', headers: kopf, body: datei }); if (!a.ok) { const d = await a.json().catch(() => ({})); melde(d.fehler || 'Hochladen hat nicht geklappt.'); return; } await laden(); } catch { /* umgeleitet */ } finally { $('fortschritt').hidden = true; $('feld-datei').value = ''; } } const ablage = $('ablage'); ablage.addEventListener('click', () => $('feld-datei').click()); ablage.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); $('feld-datei').click(); } }); $('feld-datei').addEventListener('change', () => { for (const d of $('feld-datei').files) schicke(d); }); for (const art of ['dragenter', 'dragover']) { ablage.addEventListener(art, (e) => { e.preventDefault(); ablage.dataset.bereit = 'ja'; }); } for (const art of ['dragleave', 'drop']) { ablage.addEventListener(art, () => { delete ablage.dataset.bereit; }); } ablage.addEventListener('drop', (e) => { e.preventDefault(); for (const d of e.dataTransfer.files) schicke(d); }); /* Ohne diese beiden öffnet der Browser eine daneben fallengelassene Datei einfach als Seite -- die Arbeit wäre dann weg. */ addEventListener('dragover', (e) => e.preventDefault()); addEventListener('drop', (e) => e.preventDefault()); /* ---------- Liste ------------------------------------------------------ */ function tuKnopf(text, tu) { const b = el('button', 'schritt', text); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; melde(''); try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; } }); return b; } async function statusSetzen(d, neu) { const a = await hole(`/workspace/api/dateien/${d.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: neu }), }); if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ändern hat nicht geklappt.'); return; } await laden(); } function zeile(d) { const k = el('div', 'datei'); k.dataset.status = d.status; const zeichen = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); zeichen.setAttribute('viewBox', '0 0 24 24'); zeichen.setAttribute('class', 'datei__zeichen'); zeichen.setAttribute('aria-hidden', 'true'); const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pfad.setAttribute('d', 'M13.5 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5L13.5 3Zm0 0V8a.5.5 0 0 0 .5.5h5'); zeichen.append(pfad); k.append(zeichen); k.append(el('div', 'datei__name', d.name_original)); const knoepfe = el('div', 'datei__knoepfe'); const marke = el('span', 'marke-status', STATUSNAME[d.status] || d.status); marke.dataset.status = d.status; knoepfe.append(marke); const holen = el('a', 'schritt', 'öffnen'); holen.href = `/workspace/api/dateien/${d.id}/inhalt`; knoepfe.append(holen); if (WEITER[d.status]) { const darf = d.status !== 'review' || ich.rolle === 'admin'; if (darf) knoepfe.append(tuKnopf( d.status === 'entwurf' ? 'zum Review ▶' : 'freigeben ▶', () => statusSetzen(d, WEITER[d.status]))); } if (ZURUECK[d.status] && (d.status !== 'freigegeben' || ich.rolle === 'admin')) { knoepfe.append(tuKnopf('◀ zurück', () => statusSetzen(d, ZURUECK[d.status]))); } if (ich.rolle === 'admin' || (d.hochgeladen_von === ich.id && d.status !== 'freigegeben')) { knoepfe.append(tuKnopf('löschen', async () => { if (!confirm(`„${d.name_original}" wirklich löschen?`)) return; const a = await hole(`/workspace/api/dateien/${d.id}`, { method: 'DELETE' }); if (!a.ok) { melde('Löschen hat nicht geklappt.'); return; } await laden(); })); } k.append(knoepfe); const zeile2 = el('div', 'datei__zeile'); zeile2.append(el('span', null, groesse(d.groesse))); zeile2.append(el('span', null, '· ' + datum(d.erstellt))); if (d.hochgeladen_name) zeile2.append(el('span', null, '· von ' + d.hochgeladen_name)); if (ich.rolle === 'admin' && d.creator_name) { zeile2.append(el('span', null, '· Bereich ' + d.creator_name)); } if (d.notiz) zeile2.append(el('span', null, '· ' + d.notiz)); k.append(zeile2); return k; } async function laden() { try { const a = await hole('/workspace/api/dateien'); if (!a.ok) { melde('Dateien konnten nicht geladen werden.'); return; } const daten = await a.json(); maxBytes = daten.max_bytes || maxBytes; $('ablage-grenze').textContent = 'bis ' + groesse(maxBytes); const ziel = $('liste'); ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); const sichtbar = daten.dateien.filter((d) => !filter || d.status === filter); if (!sichtbar.length) { ziel.append(el('p', 'leer-hinweis', daten.dateien.length ? 'In diesem Zustand liegt gerade nichts.' : 'Noch keine Datei abgelegt.')); return; } for (const d of sichtbar) ziel.append(zeile(d)); } catch { /* umgeleitet */ } } for (const b of $('filter').querySelectorAll('.schritt')) { b.addEventListener('click', async () => { filter = b.dataset.status; for (const x of $('filter').querySelectorAll('.schritt')) { x.setAttribute('aria-pressed', String(x === b)); } await laden(); }); } /* ---------- Start ------------------------------------------------------- */ (async () => { try { const a = await hole('/workspace/api/ich'); if (!a.ok) { location.assign('/workspace/'); return; } ich = await a.json(); } catch { return; } $('wer').textContent = ich.name + ' · ' + ich.rolle; /* Creator laden nichts hoch (Wunsch 28.08.2026). Das Ablegefeld verschwindet, stattdessen steht dort, warum -- ein einfach fehlendes Feld wirkt wie ein Fehler. Der Server weist einen Versuch ohnehin ab; das hier ist nur die Anzeige. */ if (ich.rolle === 'creator') { $('ablage').hidden = true; $('nur-lesen').hidden = false; } if (ich.rolle === 'admin') { $('zuordnung').hidden = false; try { const { personen } = await (await hole('/workspace/api/personen')).json(); for (const p of personen.filter((p) => p.rolle === 'creator')) { const o = document.createElement('option'); o.value = String(p.id); o.textContent = p.name; $('f-creator').append(o); } } catch { /* Zuordnung bleibt leer */ } } await laden(); })(); })();