// ----------------------------- // Projektplan — Dateien (Upload, Freigabe, Kommentare) // ----------------------------- // Dieses Modul stellt den Bereich "Dateien" des Projektbereichs bereit // (Upload-Maske und Dateiliste, siehe js/plan/plan-sections.js; eine // Kompakt-Karte im Dashboard, #planFilesSummary, zeigt die zuletzt // hochgeladene Datei und ausstehende Prüfungen und springt per Klick über // openFilesPanel() in diesen Bereich): Upload (roher // Binär-Body wie js/import.js), Anzeige aller Dateien eines Projekts samt // Prüferstatus, Genehmigen-/Ablehnen-Aktionen für den eingeloggten Prüfer // sowie einen einfachen Kommentar-Thread je Datei. Dateien werden nur noch // über project_nr allein mit dem Projekt verknüpft (NICHT project_id — siehe // server.js-Abschnitt "Datei-Upload, Freigabe, Kommentare"), daher ist der // Upload-Bereich deaktiviert und die Liste zeigt nur einen Hinweis, solange // das aktuelle Projekt keine Projektnummer (app.project.nr) hat. // // Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp // (plan-state.js) für app.project, currentUser (js/auth.js) für die // Sichtbarkeit der Genehmigen-/Ablehnen-Buttons. (function(){ const app = window.PlanApp; // filesCache: zuletzt geladene Dateiliste (für Re-Renders nach Aktionen, // ohne jedes Mal neu zu fetchen); openComments: Set der Datei-IDs, deren // Kommentar-Thread gerade aufgeklappt ist. let filesCache = []; const openComments = new Set(); // statusLabel/statusClass: deutsche Anzeige- bzw. CSS-Klasse für den // Datei-/Prüfstatus. function statusLabel(status){ if(status === 'approved') return 'Freigegeben'; if(status === 'rejected') return 'Abgelehnt'; return 'Ausstehend'; } function statusClass(status){ if(status === 'approved') return 'approved'; if(status === 'rejected') return 'rejected'; return 'pending'; } // formatDateTimeDE: formatiert einen ISO-Zeitstempel als deutsches // Datum+Uhrzeit (Fallback: leerer String bei ungültigem Wert). function formatDateTimeDE(iso){ if(!iso) return ''; const d = new Date(iso); if(isNaN(d.getTime())) return ''; return formatDateDE(d) + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0'); } // initPlanFiles: initialisiert den Dateien-Bereich beim Laden der Seite // (Prüfer-Checkboxen befüllen, Dateiliste laden) — Nachfolger des früheren // openFilesModal(), seit der Bereich fest auf der Seite sichtbar ist statt // in einem Modal. Wird von renderPlanPage() (plan-state.js) aufgerufen. window.initPlanFiles = function(){ const hasNr = !!(app.project && app.project.nr); const hint = document.getElementById('filesNoProjectHint'); const uploadSection = document.getElementById('filesUploadSection'); if(hint) hint.style.display = hasNr ? 'none' : ''; if(uploadSection) uploadSection.style.display = (hasNr && hatRecht('proj.dateien')) ? '' : 'none'; const fileInput = document.getElementById('fileUploadInput'); if(fileInput) fileInput.value = ''; if(hasNr) loadReviewerCheckboxes(); renderProjectFiles(); }; // openFilesPanel: springt in den Bereich "Dateien" des Projektbereichs // (js/plan/plan-sections.js), in dem Upload-Maske und Dateiliste stehen. // Früher öffnete die Funktion ein eigenes Modal; der Name bleibt, weil die // Kompakt-Karte "Dateien & Freigaben" im Dashboard darauf verweist. window.openFilesPanel = function(){ if(typeof showPlanSection === 'function') showPlanSection('dateien'); }; // loadReviewerCheckboxes: lädt alle aktiven Benutzerkonten (GET /users, // nicht die Roster-Personen des Projekts — Prüfer müssen echte Logins // sein) und baut die Mehrfachauswahl-Checkbox-Liste (gleiches Muster wie // todoEditAssignees in plan-modals.js). function loadReviewerCheckboxes(){ const container = document.getElementById('fileUploadReviewers'); if(!container) return; container.innerHTML = 'Lade Benutzer…'; fetch(baseUrl() + '/users') .then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); }) .then(data => { const users = Array.isArray(data.users) ? data.users : []; if(users.length === 0){ container.innerHTML = 'Keine Benutzer vorhanden.'; return; } // Das eigene Konto herausfiltern: Der Server entfernt den Uploader // still aus der Prüferliste — wer nur sich selbst ankreuzte, bekam // deshalb die verwirrende Meldung "Mindestens ein Prüfer erforderlich". const selectable = users.filter(u => !(typeof currentUser !== 'undefined' && currentUser && u.id === currentUser.id)); if(selectable.length === 0){ container.innerHTML = 'Keine weiteren Benutzer vorhanden, die prüfen könnten.'; return; } container.innerHTML = selectable.map(u => `` ).join(''); }) .catch(err => { container.innerHTML = 'Benutzer konnten nicht geladen werden.'; console.error('Benutzerliste laden fehlgeschlagen:', err); }); } // submitFileUpload: liest die gewählte Datei und die angehakten Prüfer, // schickt die Datei als rohen Binär-Body (wie js/import.js) an // POST /project-files und lädt danach die Liste neu. window.submitFileUpload = function(){ if(!app.project || !app.project.nr){ alert('Dieses Projekt hat keine Projektnummer.'); return; } const fileInput = document.getElementById('fileUploadInput'); const file = fileInput && fileInput.files && fileInput.files[0]; if(!file){ alert('Bitte eine Datei auswählen.'); return; } const reviewersContainer = document.getElementById('fileUploadReviewers'); const reviewerIds = reviewersContainer ? Array.from(reviewersContainer.querySelectorAll('input:checked')).map(cb => cb.value) : []; if(reviewerIds.length === 0){ alert('Bitte mindestens einen Prüfer auswählen.'); return; } const url = baseUrl() + '/project-files' + '?projectNr=' + encodeURIComponent(app.project.nr) + '&projectName=' + encodeURIComponent(app.project.name || '') + '&filename=' + encodeURIComponent(file.name) + '&reviewers=' + reviewerIds.join(','); fetch(url, { method: 'POST', headers: { 'Content-Type': file.type || 'application/octet-stream' }, body: file }) .then(r => r.json().then(json => ({ ok: r.ok, json }))) .then(({ ok, json }) => { if(!ok || !json.ok) throw new Error((json && json.error) || 'Upload fehlgeschlagen.'); if(fileInput) fileInput.value = ''; if(reviewersContainer) reviewersContainer.querySelectorAll('input:checked').forEach(cb => cb.checked = false); app.showToast('Datei hochgeladen.'); renderProjectFiles(); }) .catch(err => { console.error('Datei-Upload fehlgeschlagen:', err); alert('Datei-Upload fehlgeschlagen: ' + (err.message || err)); }); }; // renderProjectFiles: lädt die Dateiliste des aktuellen Projekts // (GET /project-files?projectNr=) und rendert sie in #filesListContainer. // Ohne Projektnummer wird nur ein Hinweistext gezeigt. window.renderProjectFiles = function(){ const container = document.getElementById('filesListContainer'); if(!container) return; if(!app.project || !app.project.nr){ container.innerHTML = 'Dieses Projekt hat keine Projektnummer und kann daher keine Dateien erhalten.'; const summaryEl = document.getElementById('planFilesSummary'); if(summaryEl) summaryEl.innerHTML = 'Keine Projektnummer.'; return; } container.innerHTML = 'Lade Dateien…'; const url = baseUrl() + '/project-files' + '?projectNr=' + encodeURIComponent(app.project.nr); fetch(url) .then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); }) .then(data => { filesCache = Array.isArray(data.files) ? data.files : []; renderFilesList(); }) .catch(err => { container.innerHTML = 'Dateien konnten nicht geladen werden.'; console.error('Dateiliste laden fehlgeschlagen:', err); }); }; // renderFilesSummary: füllt die Kompakt-Vorschau der "Dateien & Freigaben"- // Karte unter dem GANTT (Cockpit-Layout, #planFilesSummary in // App.html) aus filesCache — Name der zuletzt hochgeladenen Datei // (Server liefert /project-files bereits nach created_at DESC sortiert, // filesCache[0] ist also die neueste) sowie einen Hinweis auf noch // ausstehende Prüfungen. function renderFilesSummary(){ const summaryEl = document.getElementById('planFilesSummary'); if(!summaryEl) return; if(filesCache.length === 0){ summaryEl.innerHTML = 'Noch keine Dateien.'; return; } const pendingCount = filesCache.filter(f => f.status === 'pending').length; const chip = pendingCount > 0 ? `${pendingCount} ausstehend` : 'Alle geprüft'; summaryEl.innerHTML = `