Verwaltung: Backend auf Lazyloading der Kalenderwochen umgestellt

This commit is contained in:
2026-03-12 18:24:37 +01:00
parent 8595b099db
commit 91603f1617
3 changed files with 544 additions and 380 deletions

View File

@@ -44,7 +44,7 @@ function registerVerwaltungRoutes(app) {
} }
// Gruppiere nach Mitarbeiter, dann nach Kalenderwoche // Gruppiere nach Mitarbeiter, dann nach Kalenderwoche
// Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } } // Struktur intern: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } }
const groupedByEmployee = {}; const groupedByEmployee = {};
(timesheets || []).forEach(ts => { (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 // Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
Object.values(groupedByEmployee).forEach(employee => { Object.values(groupedByEmployee).forEach(employee => {
Object.values(employee.weeks).forEach(week => { Object.values(employee.weeks).forEach(week => {
// Finde die neueste Version mit pdf_downloaded_at (letzter Download)
let lastDownloadTime = null; let lastDownloadTime = null;
week.versions.forEach(version => { week.versions.forEach(version => {
if (version.pdf_downloaded_at) { if (version.pdf_downloaded_at) {
@@ -97,7 +96,6 @@ function registerVerwaltungRoutes(app) {
} }
}); });
// Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde
let hasNewVersionAfterDownload = false; let hasNewVersionAfterDownload = false;
if (lastDownloadTime) { if (lastDownloadTime) {
week.versions.forEach(version => { week.versions.forEach(version => {
@@ -110,25 +108,28 @@ function registerVerwaltungRoutes(app) {
}); });
} }
// Setze Flag auf dem week-Objekt
week.has_new_version_after_download = hasNewVersionAfterDownload; week.has_new_version_after_download = hasNewVersionAfterDownload;
}); });
}); });
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst) // Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
const sortedEmployees = Object.values(groupedByEmployee).map(employee => { // Für das Initial-Rendering der Verwaltung werden nur Mitarbeiter-Header-Daten
// Wochen innerhalb jedes Mitarbeiters sortieren // benötigt. Wochen-/Versionslisten werden per AJAX nachgeladen.
const employeesForView = Object.values(groupedByEmployee).map(employee => {
const sortedWeeks = Object.values(employee.weeks).sort((a, b) => { const sortedWeeks = Object.values(employee.weeks).sort((a, b) => {
return new Date(b.week_start) - new Date(a.week_start); 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 hasNewVersionAfterDownload = sortedWeeks.some(w => w.has_new_version_after_download);
const weekCount = sortedWeeks.length;
const latestWeek = weekCount > 0 ? sortedWeeks[0] : null;
return { return {
...employee, user: employee.user,
has_new_version_after_download: hasNewVersionAfterDownload, 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) => { }).sort((a, b) => {
// Mitarbeiter nach Nachname, dann Vorname sortieren // Mitarbeiter nach Nachname, dann Vorname sortieren
@@ -138,7 +139,7 @@ function registerVerwaltungRoutes(app) {
}); });
res.render('verwaltung', { res.render('verwaltung', {
groupedByEmployee: sortedEmployees, groupedByEmployee: employeesForView,
user: { user: {
firstname: req.session.firstname, firstname: req.session.firstname,
lastname: req.session.lastname, 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 // Plausibilitätsprüfung Projektnummer: 7 Ziffern, beginnt mit 5, dann YY, dann 4 Ziffern
function isValidProjectNumber(value) { function isValidProjectNumber(value) {
if (!value || String(value).trim() === '') return false; if (!value || String(value).trim() === '') return false;

View File

@@ -0,0 +1,146 @@
<% employee.weeks.forEach(function(week, weekIndex) { %>
<div class="week-group" data-week-index="<%= weekIndex %>">
<div class="week-header">
<div class="week-info">
<div class="week-dates">
<%
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);
%>
<strong>Kalenderwoche <%= String(calendarWeek).padStart(2, '0') %>:</strong> <%= new Date(week.week_start).toLocaleDateString('de-DE') %> -
<%= new Date(week.week_end).toLocaleDateString('de-DE') %>
</div>
<div class="group-stats" data-user-id="<%= employee.user.id %>" data-week-start="<%= week.week_start %>" data-week-end="<%= week.week_end %>" style="margin-top: 10px;">
<div class="stats-loading" style="display: inline-block; color: #666;">Lade Statistiken...</div>
</div>
<div class="week-versions-info" style="margin-top: 5px;">
<span class="version-count"><%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %></span>
</div>
<% if (week.has_new_version_after_download) { %>
<div class="new-version-warning" style="margin-top: 10px;">
<strong>ACHTUNG: Neue version eingereicht</strong>
</div>
<% } %>
</div>
<button class="btn btn-secondary btn-sm toggle-versions-btn">
<span class="toggle-icon">▼</span> Versionen
</button>
</div>
<div class="versions-container" style="display: none;">
<table class="timesheet-table versions-table">
<thead>
<tr>
<th>Version</th>
<th>Eingereicht am</th>
<th>Grund</th>
<th>Kommentar</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% week.versions.forEach(function(ts) { %>
<tr class="timesheet-row" data-timesheet-id="<%= ts.id %>">
<td>
<span class="version-badge">
Version <%= ts.version || 1 %>
</span>
<% if (ts.pdf_downloaded_at) { %>
<%
let downloadedByName = 'Unbekannt';
if (ts.downloaded_by_firstname && ts.downloaded_by_lastname) {
downloadedByName = `${ts.downloaded_by_firstname} ${ts.downloaded_by_lastname}`;
} else if (ts.downloaded_by_firstname) {
downloadedByName = ts.downloaded_by_firstname;
} else if (ts.downloaded_by_lastname) {
downloadedByName = ts.downloaded_by_lastname;
}
%>
<span class="pdf-downloaded-marker" title="PDF wurde am <%= new Date(ts.pdf_downloaded_at).toLocaleString('de-DE') %> von <%= downloadedByName %> heruntergeladen">
✓ Heruntergeladen von <%= downloadedByName %>
</span>
<% } else { %>
<span class="pdf-not-downloaded-marker">⭕ Nicht heruntergeladen</span>
<% } %>
</td>
<td><%= new Date(ts.submitted_at).toLocaleString('de-DE') %></td>
<td>
<% if (ts.version_reason && ts.version_reason.trim() !== '') { %>
<span style="color: #666; font-size: 13px;" title="<%= ts.version_reason %>">
<%= ts.version_reason.length > 50 ? ts.version_reason.substring(0, 50) + '...' : ts.version_reason %>
</span>
<% } else { %>
<span style="color: #999; font-style: italic;">-</span>
<% } %>
</td>
<td>
<div class="admin-comment-cell" style="display: flex; gap: 8px; align-items: flex-start; min-width: 300px;">
<textarea
class="admin-comment-input"
data-timesheet-id="<%= ts.id %>"
rows="2"
style="flex: 1; min-width: 250px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; resize: vertical;"
placeholder="Kommentar..."><%= ts.admin_comment || '' %></textarea>
<button
class="btn btn-success btn-sm save-comment-btn"
data-timesheet-id="<%= ts.id %>"
style="white-space: nowrap; padding: 8px 12px;"
title="Kommentar speichern">
💾
</button>
</div>
</td>
<td>
<button class="btn btn-info btn-sm toggle-pdf-btn" data-timesheet-id="<%= ts.id %>">
<span class="arrow-icon">▶</span> PDF anzeigen
</button>
<a href="/api/timesheet/pdf/<%= ts.id %>" class="btn btn-primary btn-sm pdf-download-link" data-timesheet-id="<%= ts.id %>" target="_blank" download>
📥 PDF herunterladen
</a>
</td>
</tr>
<tr class="pdf-preview-row" data-timesheet-id="<%= ts.id %>" style="display: none;">
<td colspan="5">
<div class="pdf-preview-container">
<div class="pdf-preview-header">
<h3>PDF-Vorschau: <%= employee.user.firstname %> <%= employee.user.lastname %> - Version <%= ts.version || 1 %> - <%= new Date(ts.week_start).toLocaleDateString('de-DE') %> bis <%= new Date(ts.week_end).toLocaleDateString('de-DE') %></h3>
<button class="btn btn-secondary btn-sm close-pdf-btn" data-timesheet-id="<%= ts.id %>">
✕ Schließen
</button>
</div>
<div class="pdf-viewer-wrapper">
<iframe
data-src="/api/timesheet/pdf/<%= ts.id %>?inline=true"
class="pdf-iframe"
frameborder="0"
type="application/pdf"
allow="fullscreen">
<p>Ihr Browser unterstützt keine PDF-Vorschau.
<a href="/api/timesheet/pdf/<%= ts.id %>?inline=true" target="_blank">PDF in neuem Tab öffnen</a>
</p>
</iframe>
<div class="pdf-fallback">
<p>PDF wird geladen...</p>
<a href="/api/timesheet/pdf/<%= ts.id %>?inline=true" target="_blank" class="btn btn-primary">
PDF in neuem Tab öffnen
</a>
</div>
</div>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<% }); %>

View File

@@ -152,12 +152,17 @@
</button> </button>
</div> </div>
<div style="display: inline-block; margin-right: 20px;"> <div style="display: inline-block; margin-right: 20px;">
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></span> <strong>Kalenderwochen:</strong> <span><%= employee.weekCount %></span>
</div> </div>
<div style="display: inline-block; margin-right: 20px;"> <div style="display: inline-block; margin-right: 20px;">
<strong>Krankheitstage (<span class="sick-days-year"><%= new Date().getFullYear() %></span>):</strong> <strong>Krankheitstage (<span class="sick-days-year"><%= new Date().getFullYear() %></span>):</strong>
<span class="sick-days-count" data-user-id="<%= employee.user.id %>" style="color: #e74c3c;">-</span> <span class="sick-days-count" data-user-id="<%= employee.user.id %>" style="color: #e74c3c;">-</span>
</div> </div>
<% if (employee.latest_week_start && employee.latest_week_end) { %>
<div class="group-stats group-stats-header" data-user-id="<%= employee.user.id %>" data-week-start="<%= employee.latest_week_start %>" data-week-end="<%= employee.latest_week_end %>" style="display: none;">
<div class="stats-loading" style="display: inline-block; color: #666;">Lade Statistiken...</div>
</div>
<% } %>
</div> </div>
</div> </div>
<button class="btn btn-secondary btn-sm toggle-employee-btn" data-employee-index="<%= employeeIndex %>"> <button class="btn btn-secondary btn-sm toggle-employee-btn" data-employee-index="<%= employeeIndex %>">
@@ -166,155 +171,7 @@
</div> </div>
<!-- Level 2: Kalenderwochen --> <!-- Level 2: Kalenderwochen -->
<div class="weeks-container" data-employee-index="<%= employeeIndex %>" style="display: none;"> <div class="weeks-container" data-employee-index="<%= employeeIndex %>" data-user-id="<%= employee.user.id %>" style="display: none;" data-loaded="false"></div>
<% employee.weeks.forEach(function(week, weekIndex) { %>
<div class="week-group" data-employee-index="<%= employeeIndex %>" data-week-index="<%= weekIndex %>">
<div class="week-header">
<div class="week-info">
<div class="week-dates">
<%
// Kalenderwoche berechnen
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);
%>
<strong>Kalenderwoche <%= String(calendarWeek).padStart(2, '0') %>:</strong> <%= new Date(week.week_start).toLocaleDateString('de-DE') %> -
<%= new Date(week.week_end).toLocaleDateString('de-DE') %>
</div>
<div class="group-stats" data-user-id="<%= employee.user.id %>" data-week-start="<%= week.week_start %>" data-week-end="<%= week.week_end %>" style="margin-top: 10px;">
<div class="stats-loading" style="display: inline-block; color: #666;">Lade Statistiken...</div>
</div>
<div class="week-versions-info" style="margin-top: 5px;">
<span class="version-count"><%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %></span>
</div>
<% if (week.has_new_version_after_download) { %>
<div class="new-version-warning" style="margin-top: 10px;">
<strong>ACHTUNG: Neue version eingereicht</strong>
</div>
<% } %>
</div>
<button class="btn btn-secondary btn-sm toggle-versions-btn" data-employee-index="<%= employeeIndex %>" data-week-index="<%= weekIndex %>">
<span class="toggle-icon">▼</span> Versionen
</button>
</div>
<!-- Level 3: Versionen -->
<div class="versions-container" data-employee-index="<%= employeeIndex %>" data-week-index="<%= weekIndex %>" style="display: none;">
<table class="timesheet-table versions-table">
<thead>
<tr>
<th>Version</th>
<th>Eingereicht am</th>
<th>Grund</th>
<th>Kommentar</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% week.versions.forEach(function(ts) { %>
<tr class="timesheet-row" data-timesheet-id="<%= ts.id %>">
<td>
<span class="version-badge">
Version <%= ts.version || 1 %>
</span>
<% if (ts.pdf_downloaded_at) { %>
<%
let downloadedByName = 'Unbekannt';
if (ts.downloaded_by_firstname && ts.downloaded_by_lastname) {
downloadedByName = `${ts.downloaded_by_firstname} ${ts.downloaded_by_lastname}`;
} else if (ts.downloaded_by_firstname) {
downloadedByName = ts.downloaded_by_firstname;
} else if (ts.downloaded_by_lastname) {
downloadedByName = ts.downloaded_by_lastname;
}
%>
<span class="pdf-downloaded-marker" title="PDF wurde am <%= new Date(ts.pdf_downloaded_at).toLocaleString('de-DE') %> von <%= downloadedByName %> heruntergeladen">
✓ Heruntergeladen von <%= downloadedByName %>
</span>
<% } else { %>
<span class="pdf-not-downloaded-marker">⭕ Nicht heruntergeladen</span>
<% } %>
</td>
<td><%= new Date(ts.submitted_at).toLocaleString('de-DE') %></td>
<td>
<% if (ts.version_reason && ts.version_reason.trim() !== '') { %>
<span style="color: #666; font-size: 13px;" title="<%= ts.version_reason %>">
<%= ts.version_reason.length > 50 ? ts.version_reason.substring(0, 50) + '...' : ts.version_reason %>
</span>
<% } else { %>
<span style="color: #999; font-style: italic;">-</span>
<% } %>
</td>
<td>
<div class="admin-comment-cell" style="display: flex; gap: 8px; align-items: flex-start; min-width: 300px;">
<textarea
class="admin-comment-input"
data-timesheet-id="<%= ts.id %>"
rows="2"
style="flex: 1; min-width: 250px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; resize: vertical;"
placeholder="Kommentar..."><%= ts.admin_comment || '' %></textarea>
<button
class="btn btn-success btn-sm save-comment-btn"
data-timesheet-id="<%= ts.id %>"
style="white-space: nowrap; padding: 8px 12px;"
title="Kommentar speichern">
💾
</button>
</div>
</td>
<td>
<button class="btn btn-info btn-sm toggle-pdf-btn" data-timesheet-id="<%= ts.id %>">
<span class="arrow-icon">▶</span> PDF anzeigen
</button>
<a href="/api/timesheet/pdf/<%= ts.id %>" class="btn btn-primary btn-sm pdf-download-link" data-timesheet-id="<%= ts.id %>" target="_blank" download>
📥 PDF herunterladen
</a>
</td>
</tr>
<tr class="pdf-preview-row" data-timesheet-id="<%= ts.id %>" style="display: none;">
<td colspan="5">
<div class="pdf-preview-container">
<div class="pdf-preview-header">
<h3>PDF-Vorschau: <%= employee.user.firstname %> <%= employee.user.lastname %> - Version <%= ts.version || 1 %> - <%= new Date(ts.week_start).toLocaleDateString('de-DE') %> bis <%= new Date(ts.week_end).toLocaleDateString('de-DE') %></h3>
<button class="btn btn-secondary btn-sm close-pdf-btn" data-timesheet-id="<%= ts.id %>">
✕ Schließen
</button>
</div>
<div class="pdf-viewer-wrapper">
<iframe
data-src="/api/timesheet/pdf/<%= ts.id %>?inline=true"
class="pdf-iframe"
frameborder="0"
type="application/pdf"
allow="fullscreen">
<p>Ihr Browser unterstützt keine PDF-Vorschau.
<a href="/api/timesheet/pdf/<%= ts.id %>?inline=true" target="_blank">PDF in neuem Tab öffnen</a>
</p>
</iframe>
<div class="pdf-fallback">
<p>PDF wird geladen...</p>
<a href="/api/timesheet/pdf/<%= ts.id %>?inline=true" target="_blank" class="btn btn-primary">
PDF in neuem Tab öffnen
</a>
</div>
</div>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<% }); %>
</div>
</div> </div>
<% }); %> <% }); %>
</div> </div>
@@ -390,8 +247,227 @@
}); });
} }
// Statistiken für alle Wochen initial laden function initWeeksInContainer(root) {
document.querySelectorAll('.group-stats').forEach(statsDiv => loadStatsForDiv(statsDiv)); 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 = `<span class="pdf-downloaded-marker" title="PDF wurde am ${downloadedAt} von ${downloadedBy} heruntergeladen">✓ Heruntergeladen von ${downloadedBy}</span>`;
}
}
})
.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 = `<span class="pdf-downloaded-marker" title="PDF wurde von ${currentUser} heruntergeladen">✓ Heruntergeladen von ${currentUser}</span>`;
}
});
}, 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 // Krankheitstage für alle Mitarbeiter laden
async function loadSickDays() { 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 => { document.querySelectorAll('.toggle-employee-btn').forEach(btn => {
btn.addEventListener('click', function() { btn.addEventListener('click', async function() {
const employeeIndex = this.dataset.employeeIndex; const employeeGroup = this.closest('.employee-group');
const weeksContainer = document.querySelector(`.weeks-container[data-employee-index="${employeeIndex}"]`); if (!employeeGroup) return;
const weeksContainer = employeeGroup.querySelector('.weeks-container');
const toggleIcon = this.querySelector('.toggle-icon'); const toggleIcon = this.querySelector('.toggle-icon');
const userId = employeeGroup.dataset.employeeId;
if (weeksContainer) { if (!weeksContainer) return;
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) // Wochen bei erstem Öffnen per AJAX nachladen
document.querySelectorAll('.toggle-versions-btn').forEach(btn => { if (weeksContainer.dataset.loaded !== 'true' && userId) {
btn.addEventListener('click', function() { weeksContainer.style.display = 'block';
const employeeIndex = this.dataset.employeeIndex; weeksContainer.innerHTML = '<div class="weeks-loading" style="padding: 10px; color: #666;">Lade Kalenderwochen...</div>';
const weekIndex = this.dataset.weekIndex; toggleIcon.textContent = '▲';
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 = `<span class="pdf-downloaded-marker" title="PDF wurde am ${downloadedAt} von ${downloadedBy} heruntergeladen">✓ Heruntergeladen von ${downloadedBy}</span>`;
}
}
})
.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 = `<span class="pdf-downloaded-marker" title="PDF wurde von ${currentUser} heruntergeladen">✓ Heruntergeladen von ${currentUser}</span>`;
}
});
}, 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'); this.classList.add('active');
// iframe-src erst jetzt setzen (lazy load), falls noch nicht gesetzt try {
if (iframe) { const response = await fetch(`/api/verwaltung/employee/${userId}/weeks`);
const currentSrc = iframe.getAttribute('src'); if (!response.ok) {
if (!currentSrc) { throw new Error('Fehler beim Laden der Wochen');
const dataSrc = iframe.getAttribute('data-src') || `/api/timesheet/pdf/${timesheetId}?inline=true`;
iframe.setAttribute('src', dataSrc);
} }
const html = await response.text();
weeksContainer.innerHTML = html || '<div style="padding: 10px; color: #666;">Keine eingereichten Wochen vorhanden.</div>';
weeksContainer.dataset.loaded = 'true';
initWeeksInContainer(weeksContainer);
} catch (error) {
console.error('Fehler beim Nachladen der Wochen:', error);
weeksContainer.innerHTML = '<div class="weeks-error" style="padding: 10px; color: #dc3545;">Fehler beim Laden der Wochen. Bitte erneut versuchen.</div>';
} }
// 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');
return; return;
} }
const comment = commentInput.value.trim(); // Nur Anzeige toggeln, wenn bereits geladen
const originalButtonText = this.innerHTML; if (weeksContainer.style.display === 'none' || !weeksContainer.style.display) {
weeksContainer.style.display = 'block';
// Button deaktivieren während des Speicherns toggleIcon.textContent = '▲';
this.disabled = true; this.classList.add('active');
this.innerHTML = '...'; } else {
this.title = 'Speichere...'; weeksContainer.style.display = 'none';
toggleIcon.textContent = '▼';
try { this.classList.remove('active');
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();
}
} }
}); });
}); });