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
|
||||
// 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) {
|
||||
@@ -97,7 +96,6 @@ 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 => {
|
||||
@@ -110,25 +108,28 @@ 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;
|
||||
|
||||
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>
|
||||
</div>
|
||||
<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 style="display: inline-block; margin-right: 20px;">
|
||||
<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>
|
||||
</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>
|
||||
<button class="btn btn-secondary btn-sm toggle-employee-btn" data-employee-index="<%= employeeIndex %>">
|
||||
@@ -166,155 +171,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Level 2: Kalenderwochen -->
|
||||
<div class="weeks-container" data-employee-index="<%= employeeIndex %>" style="display: none;">
|
||||
<% 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 class="weeks-container" data-employee-index="<%= employeeIndex %>" data-user-id="<%= employee.user.id %>" style="display: none;" data-loaded="false"></div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
@@ -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 = `<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
|
||||
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}"]`);
|
||||
btn.addEventListener('click', async function() {
|
||||
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) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!weeksContainer) return;
|
||||
|
||||
// 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 = `<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 = '▼';
|
||||
// Wochen bei erstem Öffnen per AJAX nachladen
|
||||
if (weeksContainer.dataset.loaded !== 'true' && userId) {
|
||||
weeksContainer.style.display = 'block';
|
||||
weeksContainer.innerHTML = '<div class="weeks-loading" style="padding: 10px; color: #666;">Lade Kalenderwochen...</div>';
|
||||
toggleIcon.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);
|
||||
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 || '<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;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user