From 91603f16175409f11bb8be60e4de9c414d28f50b Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Thu, 12 Mar 2026 18:24:37 +0100 Subject: [PATCH] Verwaltung: Backend auf Lazyloading der Kalenderwochen umgestellt --- routes/verwaltung-routes.js | 145 ++++++- views/verwaltung-weeks-partial.ejs | 146 +++++++ views/verwaltung.ejs | 633 ++++++++++++----------------- 3 files changed, 544 insertions(+), 380 deletions(-) create mode 100644 views/verwaltung-weeks-partial.ejs diff --git a/routes/verwaltung-routes.js b/routes/verwaltung-routes.js index 9b7860e..4764dcc 100644 --- a/routes/verwaltung-routes.js +++ b/routes/verwaltung-routes.js @@ -44,7 +44,7 @@ function registerVerwaltungRoutes(app) { } // Gruppiere nach Mitarbeiter, dann nach Kalenderwoche - // Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } } + // Struktur intern: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } } const groupedByEmployee = {}; (timesheets || []).forEach(ts => { @@ -86,7 +86,6 @@ function registerVerwaltungRoutes(app) { // Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde Object.values(groupedByEmployee).forEach(employee => { Object.values(employee.weeks).forEach(week => { - // Finde die neueste Version mit pdf_downloaded_at (letzter Download) let lastDownloadTime = null; week.versions.forEach(version => { if (version.pdf_downloaded_at) { @@ -96,8 +95,7 @@ function registerVerwaltungRoutes(app) { } } }); - - // Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde + let hasNewVersionAfterDownload = false; if (lastDownloadTime) { week.versions.forEach(version => { @@ -109,26 +107,29 @@ function registerVerwaltungRoutes(app) { } }); } - - // Setze Flag auf dem week-Objekt + week.has_new_version_after_download = hasNewVersionAfterDownload; }); }); - + // Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst) - const sortedEmployees = Object.values(groupedByEmployee).map(employee => { - // Wochen innerhalb jedes Mitarbeiters sortieren + // Für das Initial-Rendering der Verwaltung werden nur Mitarbeiter-Header-Daten + // benötigt. Wochen-/Versionslisten werden per AJAX nachgeladen. + const employeesForView = Object.values(groupedByEmployee).map(employee => { const sortedWeeks = Object.values(employee.weeks).sort((a, b) => { return new Date(b.week_start) - new Date(a.week_start); }); - // Flag: Gibt es in irgendeiner Woche eine neue Version nach Download? const hasNewVersionAfterDownload = sortedWeeks.some(w => w.has_new_version_after_download); - + const weekCount = sortedWeeks.length; + const latestWeek = weekCount > 0 ? sortedWeeks[0] : null; + return { - ...employee, + user: employee.user, has_new_version_after_download: hasNewVersionAfterDownload, - weeks: sortedWeeks + weekCount, + latest_week_start: latestWeek ? latestWeek.week_start : null, + latest_week_end: latestWeek ? latestWeek.week_end : null }; }).sort((a, b) => { // Mitarbeiter nach Nachname, dann Vorname sortieren @@ -138,7 +139,7 @@ function registerVerwaltungRoutes(app) { }); res.render('verwaltung', { - groupedByEmployee: sortedEmployees, + groupedByEmployee: employeesForView, user: { firstname: req.session.firstname, lastname: req.session.lastname, @@ -149,6 +150,122 @@ function registerVerwaltungRoutes(app) { }); }); + // API: Wochen + Versionen für einen Mitarbeiter (für Lazy-Loading in der Verwaltung) + app.get('/api/verwaltung/employee/:id/weeks', requireVerwaltung, (req, res) => { + const userId = parseInt(req.params.id, 10); + if (!Number.isFinite(userId)) { + return res.status(400).send('Ungültige User-ID'); + } + + db.all(` + SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days, u.arbeitstage, + dl.firstname as downloaded_by_firstname, + dl.lastname as downloaded_by_lastname, + (SELECT COUNT(*) FROM weekly_timesheets wt2 + WHERE wt2.user_id = wt.user_id + AND wt2.week_start = wt.week_start + AND wt2.week_end = wt.week_end) as total_versions + FROM weekly_timesheets wt + JOIN users u ON wt.user_id = u.id + LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id + WHERE wt.status = 'eingereicht' + AND wt.user_id = ? + ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC + `, [userId], (err, timesheets) => { + if (err) { + console.error('Fehler beim Laden der Stundenzettel (Verwaltung-API /employee/:id/weeks):', err); + return res.status(500).send('Fehler beim Laden der Verwaltungsdaten.'); + } + + if (!timesheets || timesheets.length === 0) { + // Kein Inhalt, aber 200, damit das Frontend eine leere Anzeige darstellen kann + return res.send(''); + } + + // Gruppierung wie in /verwaltung, aber nur für einen Mitarbeiter + const groupedByEmployee = {}; + + timesheets.forEach(ts => { + const uid = ts.user_id; + const weekKey = `${ts.week_start}_${ts.week_end}`; + + if (!groupedByEmployee[uid]) { + groupedByEmployee[uid] = { + user: { + id: ts.user_id, + firstname: ts.firstname, + lastname: ts.lastname, + username: ts.username, + personalnummer: ts.personalnummer, + wochenstunden: ts.wochenstunden, + urlaubstage: ts.urlaubstage, + overtime_offset_hours: ts.overtime_offset_hours, + vacation_offset_days: ts.vacation_offset_days + }, + weeks: {} + }; + } + + if (!groupedByEmployee[uid].weeks[weekKey]) { + groupedByEmployee[uid].weeks[weekKey] = { + week_start: ts.week_start, + week_end: ts.week_end, + total_versions: ts.total_versions, + versions: [] + }; + } + + groupedByEmployee[uid].weeks[weekKey].versions.push(ts); + }); + + const employee = Object.values(groupedByEmployee)[0]; + if (!employee) { + return res.send(''); + } + + // Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde + Object.values(employee.weeks).forEach(week => { + let lastDownloadTime = null; + week.versions.forEach(version => { + if (version.pdf_downloaded_at) { + const downloadTime = new Date(version.pdf_downloaded_at).getTime(); + if (!lastDownloadTime || downloadTime > lastDownloadTime) { + lastDownloadTime = downloadTime; + } + } + }); + + let hasNewVersionAfterDownload = false; + if (lastDownloadTime) { + week.versions.forEach(version => { + if (version.submitted_at) { + const submittedTime = new Date(version.submitted_at).getTime(); + if (submittedTime > lastDownloadTime) { + hasNewVersionAfterDownload = true; + } + } + }); + } + + week.has_new_version_after_download = hasNewVersionAfterDownload; + }); + + const weeks = Object.values(employee.weeks).sort((a, b) => { + return new Date(b.week_start) - new Date(a.week_start); + }); + + const viewUser = { + firstname: req.session.firstname, + lastname: req.session.lastname + }; + + res.render('verwaltung-weeks-partial', { + employee: { user: employee.user, weeks }, + user: viewUser + }); + }); + }); + // Plausibilitätsprüfung Projektnummer: 7 Ziffern, beginnt mit 5, dann YY, dann 4 Ziffern function isValidProjectNumber(value) { if (!value || String(value).trim() === '') return false; diff --git a/views/verwaltung-weeks-partial.ejs b/views/verwaltung-weeks-partial.ejs new file mode 100644 index 0000000..da0502f --- /dev/null +++ b/views/verwaltung-weeks-partial.ejs @@ -0,0 +1,146 @@ +<% employee.weeks.forEach(function(week, weekIndex) { %> +
+
+
+
+ <% + function getCalendarWeek(dateStr) { + const date = new Date(dateStr); + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + return weekNo; + } + const calendarWeek = getCalendarWeek(week.week_start); + %> + Kalenderwoche <%= String(calendarWeek).padStart(2, '0') %>: <%= new Date(week.week_start).toLocaleDateString('de-DE') %> - + <%= new Date(week.week_end).toLocaleDateString('de-DE') %> +
+
+
Lade Statistiken...
+
+
+ <%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %> +
+ <% if (week.has_new_version_after_download) { %> +
+ ACHTUNG: Neue version eingereicht +
+ <% } %> +
+ +
+ + +
+<% }); %> + diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index 6703961..94c4da7 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -152,12 +152,17 @@
- Kalenderwochen: <%= employee.weeks.length %> + Kalenderwochen: <%= employee.weekCount %>
Krankheitstage (<%= new Date().getFullYear() %>): -
+ <% if (employee.latest_week_start && employee.latest_week_end) { %> + + <% } %> - - - - - - <% }); %> - + <% }); %> @@ -390,8 +247,227 @@ }); } - // Statistiken für alle Wochen initial laden - document.querySelectorAll('.group-stats').forEach(statsDiv => loadStatsForDiv(statsDiv)); + function initWeeksInContainer(root) { + if (!root) return; + + // Statistiken für alle Wochen/Container laden (Header + nachgeladene Wochen) + root.querySelectorAll('.group-stats').forEach(statsDiv => { + if (!statsDiv.dataset.statsInitialized) { + statsDiv.dataset.statsInitialized = 'true'; + loadStatsForDiv(statsDiv); + } + }); + + // Versionen-Gruppen auf-/zuklappen (innerhalb einer Kalenderwoche) + root.querySelectorAll('.toggle-versions-btn').forEach(btn => { + if (btn.dataset.initialized === 'true') return; + btn.dataset.initialized = 'true'; + btn.addEventListener('click', function() { + const weekGroup = this.closest('.week-group'); + if (!weekGroup) return; + const versionsContainer = weekGroup.querySelector('.versions-container'); + const toggleIcon = this.querySelector('.toggle-icon'); + + if (versionsContainer) { + if (versionsContainer.style.display === 'none' || !versionsContainer.style.display) { + versionsContainer.style.display = 'block'; + toggleIcon.textContent = '▲'; + this.classList.add('active'); + } else { + versionsContainer.style.display = 'none'; + toggleIcon.textContent = '▼'; + this.classList.remove('active'); + } + } + }); + }); + + // PDF-Download Marker aktualisieren + root.querySelectorAll('.pdf-download-link').forEach(link => { + if (link.dataset.initialized === 'true') return; + link.dataset.initialized = 'true'; + link.addEventListener('click', function() { + const timesheetId = this.dataset.timesheetId; + const currentUser = '<%= user.firstname %> <%= user.lastname %>'; + + // Nach kurzer Verzögerung Marker aktualisieren (wenn Download erfolgreich war) + setTimeout(() => { + fetch(`/api/timesheet/download-info/${timesheetId}`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.downloaded) { + const marker = document.querySelector(`.timesheet-row[data-timesheet-id="${timesheetId}"] .pdf-not-downloaded-marker`); + if (marker) { + const downloadedBy = (data.downloaded_by_firstname && data.downloaded_by_lastname) + ? `${data.downloaded_by_firstname} ${data.downloaded_by_lastname}` + : currentUser || 'Unbekannt'; + const downloadedAt = data.downloaded_at + ? new Date(data.downloaded_at).toLocaleString('de-DE') + : 'Gerade'; + marker.outerHTML = `✓ Heruntergeladen von ${downloadedBy}`; + } + } + }) + .catch(err => { + console.error('Fehler beim Laden der Download-Info:', err); + const marker = document.querySelector(`.timesheet-row[data-timesheet-id="${timesheetId}"] .pdf-not-downloaded-marker`); + if (marker && currentUser) { + marker.outerHTML = `✓ Heruntergeladen von ${currentUser}`; + } + }); + }, 1500); + }); + }); + + // PDF-Vorschau ein-/ausblenden (PDF erst bei Klick laden) + root.querySelectorAll('.toggle-pdf-btn').forEach(btn => { + if (btn.dataset.initialized === 'true') return; + btn.dataset.initialized = 'true'; + btn.addEventListener('click', function() { + const timesheetId = this.dataset.timesheetId; + const previewRow = root.querySelector(`.pdf-preview-row[data-timesheet-id="${timesheetId}"]`); + const arrowIcon = this.querySelector('.arrow-icon'); + const iframe = previewRow ? previewRow.querySelector('.pdf-iframe') : null; + + if (previewRow && (previewRow.style.display === 'none' || !previewRow.style.display)) { + // Alle anderen PDF-Vorschauen schließen (nur innerhalb root relevant) + root.querySelectorAll('.pdf-preview-row').forEach(row => { + if (row.dataset.timesheetId !== timesheetId) { + row.style.display = 'none'; + const otherBtn = root.querySelector(`.toggle-pdf-btn[data-timesheet-id="${row.dataset.timesheetId}"]`); + if (otherBtn) { + const otherIcon = otherBtn.querySelector('.arrow-icon'); + if (otherIcon) otherIcon.textContent = '▶'; + otherBtn.classList.remove('active'); + } + } + }); + + previewRow.style.display = 'table-row'; + arrowIcon.textContent = '▼'; + this.classList.add('active'); + + if (iframe) { + const currentSrc = iframe.getAttribute('src'); + if (!currentSrc) { + const dataSrc = iframe.getAttribute('data-src') || `/api/timesheet/pdf/${timesheetId}?inline=true`; + iframe.setAttribute('src', dataSrc); + } + } + + setTimeout(() => { + previewRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, 100); + } else { + if (previewRow) { + previewRow.style.display = 'none'; + } + arrowIcon.textContent = '▶'; + this.classList.remove('active'); + } + }); + }); + + // Schließen-Button für PDF-Vorschau + root.querySelectorAll('.close-pdf-btn').forEach(btn => { + if (btn.dataset.initialized === 'true') return; + btn.dataset.initialized = 'true'; + btn.addEventListener('click', function() { + const timesheetId = this.dataset.timesheetId; + const previewRow = root.querySelector(`.pdf-preview-row[data-timesheet-id="${timesheetId}"]`); + const toggleBtn = root.querySelector(`.toggle-pdf-btn[data-timesheet-id="${timesheetId}"]`); + + if (previewRow) previewRow.style.display = 'none'; + if (toggleBtn) { + const icon = toggleBtn.querySelector('.arrow-icon'); + if (icon) icon.textContent = '▶'; + toggleBtn.classList.remove('active'); + } + }); + }); + + // Kommentar speichern + root.querySelectorAll('.save-comment-btn').forEach(btn => { + if (btn.dataset.initialized === 'true') return; + btn.dataset.initialized = 'true'; + btn.addEventListener('click', async function() { + const timesheetId = this.dataset.timesheetId; + const commentInput = root.querySelector(`.admin-comment-input[data-timesheet-id="${timesheetId}"]`); + + if (!commentInput) { + console.error('Kommentar-Input nicht gefunden'); + return; + } + + const comment = commentInput.value.trim(); + const originalButtonText = this.innerHTML; + + this.disabled = true; + this.innerHTML = '...'; + this.title = 'Speichere...'; + + try { + const response = await fetch(`/api/verwaltung/timesheet/${timesheetId}/comment`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ comment: comment }) + }); + + const result = await response.json(); + + if (result.success) { + this.innerHTML = '✓'; + this.title = 'Gespeichert!'; + this.style.backgroundColor = '#28a745'; + + setTimeout(() => { + this.innerHTML = originalButtonText; + this.title = 'Kommentar speichern'; + this.style.backgroundColor = ''; + this.disabled = false; + }, 2000); + } else { + alert('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler')); + this.innerHTML = originalButtonText; + this.title = 'Kommentar speichern'; + this.disabled = false; + } + } catch (error) { + console.error('Fehler beim Speichern des Kommentars:', error); + alert('Fehler beim Speichern des Kommentars'); + this.innerHTML = originalButtonText; + this.title = 'Kommentar speichern'; + this.disabled = false; + } + }); + }); + + // Kommentar auch per Enter-Taste speichern (Strg+Enter) + root.querySelectorAll('.admin-comment-input').forEach(input => { + if (input.dataset.initialized === 'true') return; + input.dataset.initialized = 'true'; + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + const timesheetId = this.dataset.timesheetId; + const saveBtn = root.querySelector(`.save-comment-btn[data-timesheet-id="${timesheetId}"]`); + if (saveBtn) { + saveBtn.click(); + } + } + }); + }); + } + + + // Header-Statistiken (verbleibender Urlaub / Basiswerte) initial laden + initWeeksInContainer(document); // Krankheitstage für alle Mitarbeiter laden async function loadSickDays() { @@ -853,224 +929,49 @@ }); }); - // Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen) + // Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen, lazy load) document.querySelectorAll('.toggle-employee-btn').forEach(btn => { - btn.addEventListener('click', function() { - const employeeIndex = this.dataset.employeeIndex; - const weeksContainer = document.querySelector(`.weeks-container[data-employee-index="${employeeIndex}"]`); - const toggleIcon = this.querySelector('.toggle-icon'); - - if (weeksContainer) { - if (weeksContainer.style.display === 'none' || !weeksContainer.style.display) { - weeksContainer.style.display = 'block'; - toggleIcon.textContent = '▲'; - this.classList.add('active'); - } else { - weeksContainer.style.display = 'none'; - toggleIcon.textContent = '▼'; - this.classList.remove('active'); - } - } - }); - }); - - // Versionen-Gruppen auf-/zuklappen (innerhalb einer Kalenderwoche) - document.querySelectorAll('.toggle-versions-btn').forEach(btn => { - btn.addEventListener('click', function() { - const employeeIndex = this.dataset.employeeIndex; - const weekIndex = this.dataset.weekIndex; - const versionsContainer = document.querySelector(`.versions-container[data-employee-index="${employeeIndex}"][data-week-index="${weekIndex}"]`); - const toggleIcon = this.querySelector('.toggle-icon'); - - if (versionsContainer) { - if (versionsContainer.style.display === 'none' || !versionsContainer.style.display) { - versionsContainer.style.display = 'block'; - toggleIcon.textContent = '▲'; - this.classList.add('active'); - } else { - versionsContainer.style.display = 'none'; - toggleIcon.textContent = '▼'; - this.classList.remove('active'); - } - } - }); - }); - - // PDF-Download Marker aktualisieren - document.querySelectorAll('.pdf-download-link').forEach(link => { - link.addEventListener('click', function() { - const timesheetId = this.dataset.timesheetId; - const currentUser = '<%= user.firstname %> <%= user.lastname %>'; - - // Nach kurzer Verzögerung Marker aktualisieren (wenn Download erfolgreich war) - // Lade aktualisierte Daten vom Server - setTimeout(() => { - fetch(`/api/timesheet/download-info/${timesheetId}`) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - if (data.downloaded) { - const marker = document.querySelector(`.timesheet-row[data-timesheet-id="${timesheetId}"] .pdf-not-downloaded-marker`); - if (marker) { - // Verwende Server-Daten, oder Fallback auf aktuellen User - const downloadedBy = (data.downloaded_by_firstname && data.downloaded_by_lastname) - ? `${data.downloaded_by_firstname} ${data.downloaded_by_lastname}` - : currentUser || 'Unbekannt'; - const downloadedAt = data.downloaded_at - ? new Date(data.downloaded_at).toLocaleString('de-DE') - : 'Gerade'; - marker.outerHTML = `✓ Heruntergeladen von ${downloadedBy}`; - } - } - }) - .catch(err => { - console.error('Fehler beim Laden der Download-Info:', err); - // Fallback: Verwende aktuellen User - const marker = document.querySelector(`.timesheet-row[data-timesheet-id="${timesheetId}"] .pdf-not-downloaded-marker`); - if (marker && currentUser) { - marker.outerHTML = `✓ Heruntergeladen von ${currentUser}`; - } - }); - }, 1500); - }); - }); - - // PDF-Vorschau ein-/ausblenden (PDF erst bei Klick laden) - document.querySelectorAll('.toggle-pdf-btn').forEach(btn => { - btn.addEventListener('click', function() { - const timesheetId = this.dataset.timesheetId; - const previewRow = document.querySelector(`.pdf-preview-row[data-timesheet-id="${timesheetId}"]`); - const arrowIcon = this.querySelector('.arrow-icon'); - const iframe = previewRow ? previewRow.querySelector('.pdf-iframe') : null; - - if (previewRow && (previewRow.style.display === 'none' || !previewRow.style.display)) { - // Alle anderen PDF-Vorschauen schließen - document.querySelectorAll('.pdf-preview-row').forEach(row => { - if (row.dataset.timesheetId !== timesheetId) { - row.style.display = 'none'; - const otherBtn = document.querySelector(`.toggle-pdf-btn[data-timesheet-id="${row.dataset.timesheetId}"]`); - if (otherBtn) { - otherBtn.querySelector('.arrow-icon').textContent = '▶'; - otherBtn.classList.remove('active'); - } - } - }); - - // Diese PDF-Vorschau öffnen - previewRow.style.display = 'table-row'; - arrowIcon.textContent = '▼'; - this.classList.add('active'); - - // iframe-src erst jetzt setzen (lazy load), falls noch nicht gesetzt - if (iframe) { - const currentSrc = iframe.getAttribute('src'); - if (!currentSrc) { - const dataSrc = iframe.getAttribute('data-src') || `/api/timesheet/pdf/${timesheetId}?inline=true`; - iframe.setAttribute('src', dataSrc); - } - } - - // Scroll zur PDF-Vorschau - setTimeout(() => { - previewRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - }, 100); - } else { - // PDF-Vorschau schließen - if (previewRow) { - previewRow.style.display = 'none'; - } - arrowIcon.textContent = '▶'; - this.classList.remove('active'); - } - }); - }); - - // Schließen-Button - document.querySelectorAll('.close-pdf-btn').forEach(btn => { - btn.addEventListener('click', function() { - const timesheetId = this.dataset.timesheetId; - const previewRow = document.querySelector(`.pdf-preview-row[data-timesheet-id="${timesheetId}"]`); - const toggleBtn = document.querySelector(`.toggle-pdf-btn[data-timesheet-id="${timesheetId}"]`); - - previewRow.style.display = 'none'; - if (toggleBtn) { - toggleBtn.querySelector('.arrow-icon').textContent = '▶'; - toggleBtn.classList.remove('active'); - } - }); - }); - - // Kommentar speichern - document.querySelectorAll('.save-comment-btn').forEach(btn => { btn.addEventListener('click', async function() { - const timesheetId = this.dataset.timesheetId; - const commentInput = document.querySelector(`.admin-comment-input[data-timesheet-id="${timesheetId}"]`); - - if (!commentInput) { - console.error('Kommentar-Input nicht gefunden'); + const employeeGroup = this.closest('.employee-group'); + if (!employeeGroup) return; + const weeksContainer = employeeGroup.querySelector('.weeks-container'); + const toggleIcon = this.querySelector('.toggle-icon'); + const userId = employeeGroup.dataset.employeeId; + + if (!weeksContainer) return; + + // Wochen bei erstem Öffnen per AJAX nachladen + if (weeksContainer.dataset.loaded !== 'true' && userId) { + weeksContainer.style.display = 'block'; + weeksContainer.innerHTML = '
Lade Kalenderwochen...
'; + toggleIcon.textContent = '▲'; + this.classList.add('active'); + + try { + const response = await fetch(`/api/verwaltung/employee/${userId}/weeks`); + if (!response.ok) { + throw new Error('Fehler beim Laden der Wochen'); + } + const html = await response.text(); + weeksContainer.innerHTML = html || '
Keine eingereichten Wochen vorhanden.
'; + weeksContainer.dataset.loaded = 'true'; + initWeeksInContainer(weeksContainer); + } catch (error) { + console.error('Fehler beim Nachladen der Wochen:', error); + weeksContainer.innerHTML = '
Fehler beim Laden der Wochen. Bitte erneut versuchen.
'; + } return; } - - const comment = commentInput.value.trim(); - const originalButtonText = this.innerHTML; - - // Button deaktivieren während des Speicherns - this.disabled = true; - this.innerHTML = '...'; - this.title = 'Speichere...'; - - try { - const response = await fetch(`/api/verwaltung/timesheet/${timesheetId}/comment`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ comment: comment }) - }); - - const result = await response.json(); - - if (result.success) { - // Erfolgs-Feedback - this.innerHTML = '✓'; - this.title = 'Gespeichert!'; - this.style.backgroundColor = '#28a745'; - - // Nach 2 Sekunden zurücksetzen - setTimeout(() => { - this.innerHTML = originalButtonText; - this.title = 'Kommentar speichern'; - this.style.backgroundColor = ''; - }, 2000); - } else { - alert('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler')); - this.innerHTML = originalButtonText; - this.title = 'Kommentar speichern'; - this.disabled = false; - } - } catch (error) { - console.error('Fehler beim Speichern des Kommentars:', error); - alert('Fehler beim Speichern des Kommentars'); - this.innerHTML = originalButtonText; - this.title = 'Kommentar speichern'; - this.disabled = false; - } - }); - }); - - // Kommentar auch per Enter-Taste speichern (Strg+Enter) - document.querySelectorAll('.admin-comment-input').forEach(input => { - input.addEventListener('keydown', function(e) { - if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { - const timesheetId = this.dataset.timesheetId; - const saveBtn = document.querySelector(`.save-comment-btn[data-timesheet-id="${timesheetId}"]`); - if (saveBtn) { - saveBtn.click(); - } + + // Nur Anzeige toggeln, wenn bereits geladen + if (weeksContainer.style.display === 'none' || !weeksContainer.style.display) { + weeksContainer.style.display = 'block'; + toggleIcon.textContent = '▲'; + this.classList.add('active'); + } else { + weeksContainer.style.display = 'none'; + toggleIcon.textContent = '▼'; + this.classList.remove('active'); } }); });