Verwaltung: Backend auf Lazyloading der Kalenderwochen umgestellt
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
146
views/verwaltung-weeks-partial.ejs
Normal file
146
views/verwaltung-weeks-partial.ejs
Normal 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>
|
||||||
|
<% }); %>
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user