1197 lines
54 KiB
Plaintext
1197 lines
54 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Verwaltung - Stundenerfassung</title>
|
||
<link rel="icon" type="image/png" href="/images/favicon.png">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<%- include('header') %>
|
||
</head>
|
||
<body>
|
||
<div class="navbar">
|
||
<div class="container">
|
||
<div class="navbar-brand">
|
||
<img src="/images/header.png" alt="Logo" class="navbar-logo">
|
||
<h1>Stundenerfassung - Verwaltung</h1>
|
||
</div>
|
||
<div class="nav-right">
|
||
<span>Verwaltung: <%= user.firstname %> <%= user.lastname %></span>
|
||
<% if (user.roles && user.roles.length > 1) { %>
|
||
<select id="roleSwitcher" class="role-switcher" style="margin-right: 10px; padding: 5px 10px; border-radius: 4px; border: 1px solid #ddd;">
|
||
<% const roleLabels = { 'mitarbeiter': 'Mitarbeiter', 'verwaltung': 'Verwaltung', 'admin': 'Administrator' }; %>
|
||
<% user.roles.forEach(function(role) { %>
|
||
<option value="<%= role %>" <%= user.currentRole === role ? 'selected' : '' %>><%= roleLabels[role] || role %></option>
|
||
<% }); %>
|
||
</select>
|
||
<% } %>
|
||
<a href="/logout" class="btn btn-logout">Abmelden</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container verwaltung-container">
|
||
<div class="verwaltung-panel">
|
||
<h2>Postfach - Eingereichte Stundenzettel</h2>
|
||
|
||
<!-- Massendownload für Kalenderwoche -->
|
||
<div style="margin-bottom: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;">
|
||
<h3 style="margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #333;">Massendownload für Kalenderwoche</h3>
|
||
<div style="display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;">
|
||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||
<label for="bulkDownloadYear" style="font-size: 13px; color: #555; font-weight: 500;">Jahr:</label>
|
||
<input
|
||
type="number"
|
||
id="bulkDownloadYear"
|
||
min="2000"
|
||
max="2100"
|
||
value="<%= new Date().getFullYear() %>"
|
||
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 100px;"
|
||
placeholder="2024">
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||
<label for="bulkDownloadWeek" style="font-size: 13px; color: #555; font-weight: 500;">Kalenderwoche:</label>
|
||
<input
|
||
type="number"
|
||
id="bulkDownloadWeek"
|
||
min="1"
|
||
max="53"
|
||
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 100px;"
|
||
placeholder="5">
|
||
</div>
|
||
<button
|
||
id="bulkDownloadBtn"
|
||
class="btn btn-primary"
|
||
style="padding: 8px 20px; font-size: 14px; white-space: nowrap;">
|
||
Alle PDFs für KW herunterladen
|
||
</button>
|
||
</div>
|
||
<div id="bulkDownloadStatus" style="margin-top: 12px; font-size: 13px; color: #666; display: none;"></div>
|
||
</div>
|
||
|
||
<% if (!groupedByEmployee || groupedByEmployee.length === 0) { %>
|
||
<div class="empty-state">
|
||
<p>Keine eingereichten Stundenzettel vorhanden.</p>
|
||
</div>
|
||
<% } else { %>
|
||
<div class="timesheet-groups">
|
||
<% groupedByEmployee.forEach(function(employee, employeeIndex) { %>
|
||
<!-- Level 1: Mitarbeiter -->
|
||
<div class="employee-group" data-employee-id="<%= employee.user.id %>" data-employee-index="<%= employeeIndex %>">
|
||
<div class="employee-header">
|
||
<div class="employee-info">
|
||
<div class="employee-name">
|
||
<strong><%= employee.user.firstname %> <%= employee.user.lastname %></strong>
|
||
<% if (employee.user.personalnummer) { %>
|
||
<span style="margin-left: 10px; color: #666;">(Personalnummer: <%= employee.user.personalnummer %>)</span>
|
||
<% } %>
|
||
<% if (employee.has_new_version_after_download) { %>
|
||
<span class="employee-new-version-warning">ACHTUNG: Neue version eingereicht</span>
|
||
<% } %>
|
||
</div>
|
||
<div class="employee-details" style="margin-top: 10px;">
|
||
<div style="display: inline-block; margin-right: 20px;">
|
||
<strong>Aktuelle Überstunden:</strong> <span class="current-overtime-value" data-user-id="<%= employee.user.id %>">-</span> Stunden
|
||
</div>
|
||
<div style="display: inline-block; margin-right: 20px;">
|
||
<strong>Verbleibender Urlaub:</strong> <span class="remaining-vacation-value" data-user-id="<%= employee.user.id %>">-</span> Tage
|
||
</div>
|
||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||
<strong>Überstunden-Korrektur:</strong>
|
||
<input
|
||
type="number"
|
||
step="0.25"
|
||
class="overtime-offset-input"
|
||
data-user-id="<%= employee.user.id %>"
|
||
value="0"
|
||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||
title="Korrektur eingeben (z. B. +10 oder -20). Nach dem Speichern wird das Feld auf 0 gesetzt." />
|
||
<button
|
||
type="button"
|
||
class="btn btn-success btn-sm save-overtime-offset-btn"
|
||
data-user-id="<%= employee.user.id %>"
|
||
style="padding: 6px 10px; white-space: nowrap;"
|
||
title="Überstunden-Korrektur speichern (addiert/abzieht und protokolliert)">
|
||
Speichern
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary btn-sm toggle-overtime-corrections-btn"
|
||
data-user-id="<%= employee.user.id %>"
|
||
style="padding: 6px 10px; white-space: nowrap;"
|
||
title="Korrektur-Historie anzeigen/ausblenden">
|
||
Historie
|
||
</button>
|
||
</div>
|
||
<div class="overtime-corrections-container" data-user-id="<%= employee.user.id %>" style="display: none; margin-top: 10px; padding: 10px 12px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px;">
|
||
<div class="overtime-corrections-loading" style="color: #666; font-size: 13px;">Lade Korrekturen...</div>
|
||
<div class="overtime-corrections-empty" style="display: none; color: #999; font-size: 13px;">Keine Korrekturen vorhanden.</div>
|
||
<ul class="overtime-corrections-list" style="margin: 8px 0 0 0; padding-left: 18px; font-size: 13px;"></ul>
|
||
</div>
|
||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||
<strong>Urlaubstage-Offset:</strong>
|
||
<input
|
||
type="number"
|
||
step="0.5"
|
||
class="vacation-offset-input"
|
||
data-user-id="<%= employee.user.id %>"
|
||
value="<%= (employee.user.vacation_offset_days !== undefined && employee.user.vacation_offset_days !== null) ? employee.user.vacation_offset_days : 0 %>"
|
||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||
title="Manuelle Korrektur (positiv oder negativ) in Tagen" />
|
||
<button
|
||
type="button"
|
||
class="btn btn-success btn-sm save-vacation-offset-btn"
|
||
data-user-id="<%= employee.user.id %>"
|
||
style="padding: 6px 10px; white-space: nowrap;"
|
||
title="Urlaubstage-Offset speichern">
|
||
Speichern
|
||
</button>
|
||
</div>
|
||
<div style="display: inline-block; margin-right: 20px;">
|
||
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></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>
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm toggle-employee-btn" data-employee-index="<%= employeeIndex %>">
|
||
<span class="toggle-icon">▼</span> Kalenderwochen
|
||
</button>
|
||
</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
|
||
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>
|
||
|
||
<script>
|
||
async function loadStatsForDiv(statsDiv) {
|
||
const userId = statsDiv.dataset.userId;
|
||
const weekStart = statsDiv.dataset.weekStart;
|
||
const weekEnd = statsDiv.dataset.weekEnd;
|
||
|
||
return fetch(`/api/verwaltung/user/${userId}/stats?week_start=${weekStart}&week_end=${weekEnd}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const loadingDiv = statsDiv.querySelector('.stats-loading');
|
||
if (loadingDiv) {
|
||
loadingDiv.style.display = 'none';
|
||
}
|
||
|
||
// vorherige Stats entfernen (wenn reloaded)
|
||
statsDiv.querySelectorAll('.stats-inline').forEach(n => n.remove());
|
||
|
||
// Statistiken anzeigen
|
||
let statsHTML = '';
|
||
if (data.weekOvertimeHours !== undefined) {
|
||
const weekOvertimeColor = data.weekOvertimeHours < 0 ? '#dc3545' : (data.weekOvertimeHours > 0 ? '#28a745' : '#666');
|
||
const sign = data.weekOvertimeHours >= 0 ? '+' : '';
|
||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||
<strong>Überstunden:</strong> <span style="color: ${weekOvertimeColor};">${sign}${data.weekOvertimeHours.toFixed(2)} h</span>
|
||
</div>`;
|
||
}
|
||
if (data.overtimeOffsetHours !== undefined && data.overtimeOffsetHours !== 0) {
|
||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||
<strong>Offset:</strong> <span>${Number(data.overtimeOffsetHours).toFixed(2)} h</span>
|
||
${data.remainingOvertimeWithOffset !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${Number(data.remainingOvertimeWithOffset).toFixed(2)} h)</span>` : ''}
|
||
</div>`;
|
||
}
|
||
if (data.totalVacationDays !== undefined || data.vacationDays !== undefined) {
|
||
const totalTaken = data.totalVacationDays !== undefined ? data.totalVacationDays : 0;
|
||
const inWeek = data.vacationDays !== undefined ? data.vacationDays : 0;
|
||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||
<strong>Urlaub genommen (dieses Jahr):</strong> <span>${Number(totalTaken).toFixed(1)} Tag${totalTaken !== 1 ? 'e' : ''}</span>
|
||
${inWeek > 0 ? ` <span style="color: #666;">(diese Woche: ${inWeek.toFixed(1)})</span>` : ''}
|
||
${data.remainingVacation !== undefined ? ` <span style="color: #28a745;">(verbleibend: ${Number(data.remainingVacation).toFixed(1)} Tage)</span>` : ''}
|
||
</div>`;
|
||
}
|
||
if (data.vacationOffsetDays !== undefined && data.vacationOffsetDays !== 0) {
|
||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||
<strong>Urlaubstage-Offset:</strong> <span>${Number(data.vacationOffsetDays).toFixed(1)} Tag${Math.abs(data.vacationOffsetDays) !== 1 ? 'e' : ''}</span>
|
||
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${Number(data.remainingVacation).toFixed(1)} Tage)</span>` : ''}
|
||
</div>`;
|
||
}
|
||
if (data.sickDays !== undefined && data.sickDays > 0) {
|
||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||
<strong>Krankheitstage:</strong> <span style="color: #e74c3c;">${data.sickDays} Tag${data.sickDays !== 1 ? 'e' : ''}</span>
|
||
</div>`;
|
||
}
|
||
|
||
if (statsHTML) {
|
||
statsDiv.insertAdjacentHTML('beforeend', statsHTML);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Fehler beim Laden der Statistiken:', error);
|
||
const loadingDiv = statsDiv.querySelector('.stats-loading');
|
||
if (loadingDiv) {
|
||
loadingDiv.textContent = 'Fehler beim Laden';
|
||
loadingDiv.style.color = 'red';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Statistiken für alle Wochen initial laden
|
||
document.querySelectorAll('.group-stats').forEach(statsDiv => loadStatsForDiv(statsDiv));
|
||
|
||
// Krankheitstage für alle Mitarbeiter laden
|
||
async function loadSickDays() {
|
||
const sickDaysElements = document.querySelectorAll('.sick-days-count');
|
||
const userIds = Array.from(sickDaysElements).map(el => el.dataset.userId);
|
||
const uniqueUserIds = [...new Set(userIds)];
|
||
|
||
for (const userId of uniqueUserIds) {
|
||
try {
|
||
const response = await fetch(`/api/verwaltung/user/${userId}/sick-days`);
|
||
if (!response.ok) {
|
||
throw new Error('Fehler beim Laden der Krankheitstage');
|
||
}
|
||
const data = await response.json();
|
||
|
||
// Alle Elemente für diesen User aktualisieren
|
||
document.querySelectorAll(`.sick-days-count[data-user-id="${userId}"]`).forEach(el => {
|
||
el.textContent = data.sickDays || 0;
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der Krankheitstage für User', userId, ':', error);
|
||
document.querySelectorAll(`.sick-days-count[data-user-id="${userId}"]`).forEach(el => {
|
||
el.textContent = '-';
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Aktuelle Überstunden für alle Mitarbeiter-Header laden (wie im Dashboard)
|
||
async function loadCurrentOvertime() {
|
||
const elements = document.querySelectorAll('.current-overtime-value');
|
||
const userIds = [...new Set(Array.from(elements).map(el => el.dataset.userId).filter(Boolean))];
|
||
if (userIds.length === 0) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/verwaltung/employees/current-overtime?userIds=' + userIds.join(','));
|
||
if (!response.ok) throw new Error('Fehler beim Laden der Überstunden');
|
||
const data = await response.json();
|
||
|
||
elements.forEach(el => {
|
||
const userId = el.dataset.userId;
|
||
const value = data[userId] != null ? Number(data[userId]) : null;
|
||
if (value === null) {
|
||
el.textContent = '-';
|
||
el.style.color = '';
|
||
} else {
|
||
el.textContent = value >= 0 ? '+' + value.toFixed(2) : value.toFixed(2);
|
||
el.style.color = value >= 0 ? '#27ae60' : '#e74c3c';
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der aktuellen Überstunden:', error);
|
||
elements.forEach(el => {
|
||
el.textContent = '-';
|
||
el.style.color = '';
|
||
});
|
||
}
|
||
}
|
||
|
||
// Verbleibenden Urlaub (Tage) für alle Mitarbeiter-Header laden
|
||
async function loadRemainingVacation() {
|
||
const elements = document.querySelectorAll('.remaining-vacation-value');
|
||
const byUser = {};
|
||
|
||
elements.forEach(el => {
|
||
const userId = el.dataset.userId;
|
||
if (!userId) return;
|
||
if (!byUser[userId]) byUser[userId] = [];
|
||
byUser[userId].push(el);
|
||
});
|
||
|
||
const userIds = Object.keys(byUser);
|
||
if (userIds.length === 0) return;
|
||
|
||
for (const userId of userIds) {
|
||
try {
|
||
// Nimm die erste (neueste) Woche für diesen Mitarbeiter
|
||
const statsDiv = document.querySelector(`.group-stats[data-user-id="${userId}"]`);
|
||
if (!statsDiv) {
|
||
byUser[userId].forEach(el => { el.textContent = '-'; });
|
||
continue;
|
||
}
|
||
const weekStart = statsDiv.dataset.weekStart;
|
||
const weekEnd = statsDiv.dataset.weekEnd;
|
||
if (!weekStart || !weekEnd) {
|
||
byUser[userId].forEach(el => { el.textContent = '-'; });
|
||
continue;
|
||
}
|
||
|
||
const resp = await fetch(`/api/verwaltung/user/${userId}/stats?week_start=${weekStart}&week_end=${weekEnd}`);
|
||
if (!resp.ok) {
|
||
byUser[userId].forEach(el => { el.textContent = '-'; });
|
||
continue;
|
||
}
|
||
const data = await resp.json();
|
||
const value = data && typeof data.remainingVacation === 'number'
|
||
? data.remainingVacation
|
||
: null;
|
||
|
||
byUser[userId].forEach(el => {
|
||
if (value === null) {
|
||
el.textContent = '-';
|
||
} else {
|
||
el.textContent = value.toFixed(1);
|
||
}
|
||
});
|
||
} catch (err) {
|
||
console.error('Fehler beim Laden des verbleibenden Urlaubs für User', userId, err);
|
||
byUser[userId].forEach(el => { el.textContent = '-'; });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Krankheitstage beim Laden der Seite abrufen
|
||
loadSickDays();
|
||
loadCurrentOvertime();
|
||
loadRemainingVacation();
|
||
|
||
// Überstunden-Korrektur-Historie laden/anzeigen
|
||
function parseSqliteDatetime(value) {
|
||
if (!value) return null;
|
||
const s = String(value);
|
||
if (s.includes('T')) return new Date(s);
|
||
return new Date(s.replace(' ', 'T') + 'Z');
|
||
}
|
||
|
||
function formatHours(value) {
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n)) return '';
|
||
const sign = n > 0 ? '+' : '';
|
||
let s = sign + n.toFixed(2);
|
||
s = s.replace(/\.00$/, '');
|
||
s = s.replace(/(\.\d)0$/, '$1');
|
||
return s;
|
||
}
|
||
|
||
function showOvertimeCorrectionReasonModal(opts) {
|
||
const title = opts && opts.title ? String(opts.title) : 'Grund für die Korrektur';
|
||
const promptText = opts && opts.prompt ? String(opts.prompt) : 'Bitte geben Sie einen Grund an, warum die Korrektur vorgenommen wird.';
|
||
const onSubmit = opts && typeof opts.onSubmit === 'function' ? opts.onSubmit : null;
|
||
const onCancel = opts && typeof opts.onCancel === 'function' ? opts.onCancel : null;
|
||
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
const box = document.createElement('div');
|
||
box.style.cssText = `
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 520px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
padding: 18px 18px 14px 18px;
|
||
`;
|
||
|
||
box.innerHTML = `
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px;">
|
||
<h3 style="margin: 0; font-size: 16px; color: #2c3e50;">${title}</h3>
|
||
<button type="button" data-action="close" class="btn btn-secondary btn-sm" style="padding: 6px 10px;">✕</button>
|
||
</div>
|
||
<div style="color:#666; font-size: 13px; margin-bottom: 10px;">${promptText}</div>
|
||
<textarea data-role="reason" rows="4" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-family: inherit; font-size: 13px; resize: vertical;" placeholder="Grund..."></textarea>
|
||
<div data-role="error" style="display:none; margin-top: 8px; color:#dc3545; font-size: 13px;"></div>
|
||
<div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 12px;">
|
||
<button type="button" data-action="cancel" class="btn btn-secondary">Abbrechen</button>
|
||
<button type="button" data-action="submit" class="btn btn-success">Speichern</button>
|
||
</div>
|
||
`;
|
||
|
||
modal.appendChild(box);
|
||
document.body.appendChild(modal);
|
||
|
||
const textarea = box.querySelector('textarea[data-role="reason"]');
|
||
const errorEl = box.querySelector('[data-role="error"]');
|
||
|
||
function close() {
|
||
document.body.removeChild(modal);
|
||
}
|
||
|
||
function cancel() {
|
||
try {
|
||
if (onCancel) onCancel();
|
||
} finally {
|
||
close();
|
||
}
|
||
}
|
||
|
||
function setError(msg) {
|
||
if (!errorEl) return;
|
||
errorEl.textContent = msg;
|
||
errorEl.style.display = msg ? 'block' : 'none';
|
||
}
|
||
|
||
async function submit() {
|
||
const reason = textarea ? textarea.value.trim() : '';
|
||
if (!reason) {
|
||
setError('Bitte Grund angeben.');
|
||
if (textarea) textarea.focus();
|
||
return;
|
||
}
|
||
setError('');
|
||
if (onSubmit) {
|
||
await onSubmit(reason);
|
||
}
|
||
close();
|
||
}
|
||
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) cancel();
|
||
});
|
||
box.querySelectorAll('button[data-action="close"], button[data-action="cancel"]').forEach(btn => {
|
||
btn.addEventListener('click', cancel);
|
||
});
|
||
const submitBtn = box.querySelector('button[data-action="submit"]');
|
||
if (submitBtn) submitBtn.addEventListener('click', submit);
|
||
|
||
if (textarea) {
|
||
textarea.focus();
|
||
textarea.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') cancel();
|
||
if ((e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
|
||
e.preventDefault();
|
||
submit();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
async function loadOvertimeCorrectionsForUser(userId) {
|
||
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||
if (!container) return;
|
||
|
||
const loadingEl = container.querySelector('.overtime-corrections-loading');
|
||
const emptyEl = container.querySelector('.overtime-corrections-empty');
|
||
const listEl = container.querySelector('.overtime-corrections-list');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'block';
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
if (listEl) listEl.innerHTML = '';
|
||
|
||
try {
|
||
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-corrections`);
|
||
const data = await resp.json().catch(() => ({}));
|
||
const corrections = Array.isArray(data.corrections) ? data.corrections : [];
|
||
|
||
if (!resp.ok) {
|
||
throw new Error(data.error || 'Fehler beim Laden der Korrekturen');
|
||
}
|
||
|
||
if (corrections.length === 0) {
|
||
if (emptyEl) emptyEl.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
corrections.forEach(c => {
|
||
const dt = parseSqliteDatetime(c.corrected_at);
|
||
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||
const hoursText = formatHours(c.correction_hours);
|
||
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
||
const li = document.createElement('li');
|
||
li.textContent = reason
|
||
? `Korrektur am ${dateText} ${hoursText} h – ${reason}`
|
||
: `Korrektur am ${dateText} ${hoursText} h`;
|
||
if (listEl) listEl.appendChild(li);
|
||
});
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Überstunden-Korrekturen:', e);
|
||
if (emptyEl) {
|
||
emptyEl.textContent = 'Fehler beim Laden der Korrekturen.';
|
||
emptyEl.style.display = 'block';
|
||
}
|
||
} finally {
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('.toggle-overtime-corrections-btn').forEach(btn => {
|
||
btn.addEventListener('click', async function() {
|
||
const userId = this.dataset.userId;
|
||
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||
if (!container) return;
|
||
|
||
const isOpen = container.style.display !== 'none' && container.style.display !== '';
|
||
if (isOpen) {
|
||
container.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
container.style.display = 'block';
|
||
|
||
// Beim Öffnen immer neu laden (damit neue Korrekturen sofort sichtbar sind)
|
||
await loadOvertimeCorrectionsForUser(userId);
|
||
container.dataset.loaded = 'true';
|
||
});
|
||
});
|
||
|
||
// Überstunden-Offset speichern
|
||
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
||
btn.addEventListener('click', async function() {
|
||
const userId = this.dataset.userId;
|
||
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
||
if (!input) return;
|
||
|
||
if (this.dataset.modalOpen === 'true') return;
|
||
|
||
const originalText = this.textContent;
|
||
|
||
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||
const raw = (input.value || '').trim();
|
||
const value = raw === '' ? '' : Number(raw);
|
||
|
||
// Wenn keine Korrektur (0), nichts tun außer UI auf 0 zu normalisieren
|
||
if (value === 0) {
|
||
input.value = 0;
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
return;
|
||
}
|
||
|
||
this.dataset.modalOpen = 'true';
|
||
this.disabled = true;
|
||
|
||
// Modal: Grund ist Pflicht
|
||
showOvertimeCorrectionReasonModal({
|
||
title: 'Grund für die Überstunden-Korrektur',
|
||
prompt: `Korrektur: ${value > 0 ? '+' : ''}${value} h`,
|
||
onCancel: () => {
|
||
delete this.dataset.modalOpen;
|
||
this.disabled = false;
|
||
},
|
||
onSubmit: async (reason) => {
|
||
this.textContent = '...';
|
||
try {
|
||
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ overtime_offset_hours: value, reason })
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) {
|
||
alert(data.error || 'Fehler beim Speichern der Korrektur');
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
delete this.dataset.modalOpen;
|
||
return;
|
||
}
|
||
|
||
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
||
input.value = (data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null)
|
||
? Number(data.overtime_offset_hours)
|
||
: 0;
|
||
|
||
// Stats für diesen User neu laden
|
||
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||
statDivs.forEach(div => {
|
||
// loading indicator optional wieder anzeigen
|
||
const loading = div.querySelector('.stats-loading');
|
||
if (loading) {
|
||
loading.style.display = 'inline-block';
|
||
loading.style.color = '#666';
|
||
loading.textContent = 'Lade Statistiken...';
|
||
}
|
||
loadStatsForDiv(div);
|
||
});
|
||
loadCurrentOvertime();
|
||
|
||
// Historie (falls geöffnet) aktualisieren
|
||
const correctionsContainer = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||
if (correctionsContainer && correctionsContainer.style.display !== 'none') {
|
||
await loadOvertimeCorrectionsForUser(userId);
|
||
}
|
||
|
||
this.textContent = '✓';
|
||
setTimeout(() => {
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
delete this.dataset.modalOpen;
|
||
}, 900);
|
||
} catch (e) {
|
||
console.error('Fehler beim Speichern der Korrektur:', e);
|
||
alert('Fehler beim Speichern der Korrektur');
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
delete this.dataset.modalOpen;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// Urlaubstage-Offset speichern
|
||
document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => {
|
||
btn.addEventListener('click', async function() {
|
||
const userId = this.dataset.userId;
|
||
const input = document.querySelector(`.vacation-offset-input[data-user-id="${userId}"]`);
|
||
if (!input) return;
|
||
|
||
const originalText = this.textContent;
|
||
this.disabled = true;
|
||
this.textContent = '...';
|
||
|
||
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||
const raw = (input.value || '').trim();
|
||
const value = raw === '' ? '' : Number(raw);
|
||
|
||
try {
|
||
const resp = await fetch(`/api/verwaltung/user/${userId}/vacation-offset`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ vacation_offset_days: value })
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) {
|
||
alert(data.error || 'Fehler beim Speichern des Offsets');
|
||
return;
|
||
}
|
||
|
||
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
||
input.value = (data.vacation_offset_days !== undefined && data.vacation_offset_days !== null)
|
||
? Number(data.vacation_offset_days)
|
||
: 0;
|
||
|
||
// Stats für diesen User neu laden
|
||
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||
statDivs.forEach(div => {
|
||
// loading indicator optional wieder anzeigen
|
||
const loading = div.querySelector('.stats-loading');
|
||
if (loading) {
|
||
loading.style.display = 'inline-block';
|
||
loading.style.color = '#666';
|
||
loading.textContent = 'Lade Statistiken...';
|
||
}
|
||
loadStatsForDiv(div);
|
||
});
|
||
|
||
this.textContent = '✓';
|
||
setTimeout(() => {
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
}, 900);
|
||
} catch (e) {
|
||
console.error('Fehler beim Speichern des Offsets:', e);
|
||
alert('Fehler beim Speichern des Offsets');
|
||
} finally {
|
||
if (this.textContent === '...') {
|
||
this.textContent = originalText;
|
||
this.disabled = false;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen)
|
||
document.querySelectorAll('.toggle-employee-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const employeeIndex = this.dataset.employeeIndex;
|
||
const weeksContainer = document.querySelector(`.weeks-container[data-employee-index="${employeeIndex}"]`);
|
||
const toggleIcon = this.querySelector('.toggle-icon');
|
||
|
||
if (weeksContainer) {
|
||
if (weeksContainer.style.display === 'none' || !weeksContainer.style.display) {
|
||
weeksContainer.style.display = 'block';
|
||
toggleIcon.textContent = '▲';
|
||
this.classList.add('active');
|
||
} else {
|
||
weeksContainer.style.display = 'none';
|
||
toggleIcon.textContent = '▼';
|
||
this.classList.remove('active');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Versionen-Gruppen auf-/zuklappen (innerhalb einer Kalenderwoche)
|
||
document.querySelectorAll('.toggle-versions-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const employeeIndex = this.dataset.employeeIndex;
|
||
const weekIndex = this.dataset.weekIndex;
|
||
const versionsContainer = document.querySelector(`.versions-container[data-employee-index="${employeeIndex}"][data-week-index="${weekIndex}"]`);
|
||
const toggleIcon = this.querySelector('.toggle-icon');
|
||
|
||
if (versionsContainer) {
|
||
if (versionsContainer.style.display === 'none' || !versionsContainer.style.display) {
|
||
versionsContainer.style.display = 'block';
|
||
toggleIcon.textContent = '▲';
|
||
this.classList.add('active');
|
||
} else {
|
||
versionsContainer.style.display = 'none';
|
||
toggleIcon.textContent = '▼';
|
||
this.classList.remove('active');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// PDF-Download Marker aktualisieren
|
||
document.querySelectorAll('.pdf-download-link').forEach(link => {
|
||
link.addEventListener('click', function() {
|
||
const timesheetId = this.dataset.timesheetId;
|
||
const currentUser = '<%= user.firstname %> <%= user.lastname %>';
|
||
|
||
// Nach kurzer Verzögerung Marker aktualisieren (wenn Download erfolgreich war)
|
||
// Lade aktualisierte Daten vom Server
|
||
setTimeout(() => {
|
||
fetch(`/api/timesheet/download-info/${timesheetId}`)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
if (data.downloaded) {
|
||
const marker = document.querySelector(`.timesheet-row[data-timesheet-id="${timesheetId}"] .pdf-not-downloaded-marker`);
|
||
if (marker) {
|
||
// Verwende Server-Daten, oder Fallback auf aktuellen User
|
||
const downloadedBy = (data.downloaded_by_firstname && data.downloaded_by_lastname)
|
||
? `${data.downloaded_by_firstname} ${data.downloaded_by_lastname}`
|
||
: currentUser || 'Unbekannt';
|
||
const downloadedAt = data.downloaded_at
|
||
? new Date(data.downloaded_at).toLocaleString('de-DE')
|
||
: 'Gerade';
|
||
marker.outerHTML = `<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
|
||
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');
|
||
|
||
// Setze iframe src wenn noch nicht gesetzt (für besseres Laden)
|
||
if (iframe) {
|
||
const currentSrc = iframe.src || iframe.getAttribute('src');
|
||
if (!currentSrc || !currentSrc.includes('inline=true')) {
|
||
iframe.src = `/api/timesheet/pdf/${timesheetId}?inline=true`;
|
||
}
|
||
}
|
||
|
||
// 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();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Massendownload für Kalenderwoche
|
||
const bulkDownloadBtn = document.getElementById('bulkDownloadBtn');
|
||
const bulkDownloadYear = document.getElementById('bulkDownloadYear');
|
||
const bulkDownloadWeek = document.getElementById('bulkDownloadWeek');
|
||
const bulkDownloadStatus = document.getElementById('bulkDownloadStatus');
|
||
|
||
if (bulkDownloadBtn) {
|
||
bulkDownloadBtn.addEventListener('click', async function() {
|
||
const year = parseInt(bulkDownloadYear.value);
|
||
const week = parseInt(bulkDownloadWeek.value);
|
||
|
||
// Validierung
|
||
if (!year || year < 2000 || year > 2100) {
|
||
bulkDownloadStatus.textContent = 'Bitte geben Sie ein gültiges Jahr ein (2000-2100)';
|
||
bulkDownloadStatus.style.display = 'block';
|
||
bulkDownloadStatus.style.color = '#dc3545';
|
||
return;
|
||
}
|
||
|
||
if (!week || week < 1 || week > 53) {
|
||
bulkDownloadStatus.textContent = 'Bitte geben Sie eine gültige Kalenderwoche ein (1-53)';
|
||
bulkDownloadStatus.style.display = 'block';
|
||
bulkDownloadStatus.style.color = '#dc3545';
|
||
return;
|
||
}
|
||
|
||
// Button deaktivieren und Status anzeigen
|
||
bulkDownloadBtn.disabled = true;
|
||
bulkDownloadBtn.textContent = 'Lädt...';
|
||
bulkDownloadStatus.textContent = 'PDFs werden generiert und ZIP erstellt...';
|
||
bulkDownloadStatus.style.display = 'block';
|
||
bulkDownloadStatus.style.color = '#666';
|
||
|
||
try {
|
||
const response = await fetch(`/api/verwaltung/bulk-download/${year}/${week}`);
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }));
|
||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||
}
|
||
|
||
// ZIP-Download starten
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `Stundenzettel_KW${String(week).padStart(2, '0')}_${year}.zip`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
// Erfolgsmeldung
|
||
bulkDownloadStatus.textContent = `✓ ZIP erfolgreich heruntergeladen (KW ${week}/${year})`;
|
||
bulkDownloadStatus.style.color = '#28a745';
|
||
|
||
// Seite nach kurzer Verzögerung neu laden, um Download-Marker zu aktualisieren
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
|
||
} catch (error) {
|
||
console.error('Fehler beim Massendownload:', error);
|
||
bulkDownloadStatus.textContent = `Fehler: ${error.message || 'Unbekannter Fehler'}`;
|
||
bulkDownloadStatus.style.color = '#dc3545';
|
||
bulkDownloadBtn.disabled = false;
|
||
bulkDownloadBtn.textContent = 'Alle PDFs für KW herunterladen';
|
||
}
|
||
});
|
||
|
||
// Enter-Taste in Eingabefeldern
|
||
[bulkDownloadYear, bulkDownloadWeek].forEach(input => {
|
||
if (input) {
|
||
input.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
bulkDownloadBtn.click();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
<script>
|
||
// Rollenwechsel-Handler
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const roleSwitcher = document.getElementById('roleSwitcher');
|
||
if (roleSwitcher) {
|
||
roleSwitcher.addEventListener('change', async function() {
|
||
const newRole = this.value;
|
||
try {
|
||
const response = await fetch('/api/user/switch-role', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ role: newRole })
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Redirect basierend auf neuer Rolle
|
||
if (newRole === 'admin') {
|
||
window.location.href = '/admin';
|
||
} else if (newRole === 'verwaltung') {
|
||
window.location.href = '/verwaltung';
|
||
} else {
|
||
window.location.href = '/dashboard';
|
||
}
|
||
} else {
|
||
alert('Fehler beim Wechseln der Rolle: ' + (result.error || 'Unbekannter Fehler'));
|
||
// Wert zurücksetzen
|
||
this.value = '<%= user.currentRole || "verwaltung" %>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Rollenwechsel:', error);
|
||
alert('Fehler beim Wechseln der Rolle');
|
||
// Wert zurücksetzen
|
||
this.value = '<%= user.currentRole || "verwaltung" %>';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
<%- include('footer') %>
|
||
</body>
|
||
</html>
|