FirstCommit

This commit is contained in:
Carsten Graf
2026-01-22 01:13:28 +01:00
commit 17838c4f1e
17 changed files with 12225 additions and 0 deletions

348
views/admin.ejs Normal file
View File

@@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - Stundenerfassung</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="navbar">
<div class="container">
<h1>Stundenerfassung - Admin</h1>
<div class="nav-right">
<span>Admin: <%= 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-secondary">Abmelden</a>
</div>
</div>
</div>
<div class="container">
<div class="admin-panel">
<h2>Benutzerverwaltung</h2>
<div class="add-user-form">
<h3>Neuen Benutzer anlegen</h3>
<form id="addUserForm">
<div class="form-row">
<div class="form-group">
<label for="firstname">Vorname</label>
<input type="text" id="firstname" name="firstname" required>
</div>
<div class="form-group">
<label for="lastname">Nachname</label>
<input type="text" id="lastname" name="lastname" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="username">Benutzername</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" required>
</div>
</div>
<div class="form-group">
<label>Rollen</label>
<div class="roles-checkbox-group">
<label class="role-checkbox-label">
<input type="checkbox" name="roles" value="mitarbeiter" class="role-checkbox-input">
<span class="role-checkbox-text">Mitarbeiter</span>
</label>
<label class="role-checkbox-label">
<input type="checkbox" name="roles" value="verwaltung" class="role-checkbox-input">
<span class="role-checkbox-text">Verwaltung</span>
</label>
<label class="role-checkbox-label">
<input type="checkbox" name="roles" value="admin" class="role-checkbox-input">
<span class="role-checkbox-text">Administrator</span>
</label>
</div>
<small class="form-help-text">Wählen Sie eine oder mehrere Rollen aus</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="personalnummer">Personalnummer</label>
<input type="text" id="personalnummer" name="personalnummer">
</div>
<div class="form-group">
<label for="wochenstunden">Wochenstunden</label>
<input type="number" id="wochenstunden" name="wochenstunden" step="0.5" min="0" placeholder="z.B. 40">
</div>
<div class="form-group">
<label for="urlaubstage">Urlaubstage</label>
<input type="number" id="urlaubstage" name="urlaubstage" step="0.5" min="0" placeholder="z.B. 25">
</div>
</div>
<button type="submit" class="btn btn-primary">Benutzer anlegen</button>
</form>
</div>
<div class="user-list">
<h3>Benutzer-Liste</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Benutzername</th>
<th>Vorname</th>
<th>Nachname</th>
<th>Rolle</th>
<th>Personalnummer</th>
<th>Wochenstunden</th>
<th>Urlaubstage</th>
<th>Erstellt am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<% users.forEach(function(u) { %>
<tr data-user-id="<%= u.id %>">
<td><%= u.id %></td>
<td><%= u.username %></td>
<td><%= u.firstname %></td>
<td><%= u.lastname %></td>
<td>
<div class="user-field-display" data-field="roles">
<%
const roleLabels = { 'mitarbeiter': 'Mitarbeiter', 'verwaltung': 'Verwaltung', 'admin': 'Admin' };
const userRoles = u.roles || [];
if (userRoles.length > 0) {
userRoles.forEach(function(role, idx) { %>
<span class="role-badge role-<%= role %>" style="margin-right: 5px;"><%= roleLabels[role] || role %></span>
<% });
} else {
%>
<span class="role-badge role-mitarbeiter">Mitarbeiter</span>
<% } %>
</div>
<div class="user-field-edit" data-field="roles" data-user-id="<%= u.id %>" style="display: none;">
<div class="roles-checkbox-group roles-checkbox-group-inline">
<label class="role-checkbox-label role-checkbox-label-small">
<input type="checkbox" class="role-checkbox role-checkbox-input" data-role="mitarbeiter" value="mitarbeiter" <%= userRoles.includes('mitarbeiter') ? 'checked' : '' %>>
<span class="role-checkbox-text">Mitarbeiter</span>
</label>
<label class="role-checkbox-label role-checkbox-label-small">
<input type="checkbox" class="role-checkbox role-checkbox-input" data-role="verwaltung" value="verwaltung" <%= userRoles.includes('verwaltung') ? 'checked' : '' %>>
<span class="role-checkbox-text">Verwaltung</span>
</label>
<label class="role-checkbox-label role-checkbox-label-small">
<input type="checkbox" class="role-checkbox role-checkbox-input" data-role="admin" value="admin" <%= userRoles.includes('admin') ? 'checked' : '' %>>
<span class="role-checkbox-text">Admin</span>
</label>
</div>
</div>
</td>
<td>
<span class="user-field-display" data-field="personalnummer"><%= u.personalnummer || '-' %></span>
<input type="text" class="user-field-edit" data-field="personalnummer" data-user-id="<%= u.id %>" value="<%= u.personalnummer || '' %>" style="display: none; width: 100px;">
</td>
<td>
<span class="user-field-display" data-field="wochenstunden"><%= u.wochenstunden || '-' %></span>
<input type="number" step="0.5" class="user-field-edit" data-field="wochenstunden" data-user-id="<%= u.id %>" value="<%= u.wochenstunden || '' %>" style="display: none; width: 80px;">
</td>
<td>
<span class="user-field-display" data-field="urlaubstage"><%= u.urlaubstage || '-' %></span>
<input type="number" step="0.5" class="user-field-edit" data-field="urlaubstage" data-user-id="<%= u.id %>" value="<%= u.urlaubstage || '' %>" style="display: none; width: 80px;">
</td>
<td><%= new Date(u.created_at).toLocaleDateString('de-DE') %></td>
<td>
<button onclick="editUser(<%= u.id %>)" class="btn btn-primary btn-sm edit-user-btn" data-user-id="<%= u.id %>">Bearbeiten</button>
<button onclick="saveUser(<%= u.id %>)" class="btn btn-success btn-sm save-user-btn" data-user-id="<%= u.id %>" style="display: none;">Speichern</button>
<button onclick="cancelEditUser(<%= u.id %>)" class="btn btn-secondary btn-sm cancel-user-btn" data-user-id="<%= u.id %>" style="display: none;">Abbrechen</button>
<% if (u.id > 2) { %>
<button onclick="deleteUser(<%= u.id %>, '<%= u.username %>')" class="btn btn-danger btn-sm">Löschen</button>
<% } else { %>
<span class="text-muted">System</span>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<div class="ldap-sync-section" style="margin-top: 40px;">
<h2>LDAP-Synchronisation</h2>
<div class="ldap-config-form">
<h3>LDAP-Konfiguration</h3>
<form id="ldapConfigForm">
<div class="form-group">
<label>
<input type="checkbox" id="ldapEnabled" name="enabled">
LDAP-Synchronisation aktivieren
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="ldapUrl">LDAP-Server URL</label>
<input type="text" id="ldapUrl" name="url" placeholder="ldap://ldap.example.com:389">
</div>
<div class="form-group">
<label for="ldapBaseDn">Base DN</label>
<input type="text" id="ldapBaseDn" name="base_dn" placeholder="dc=example,dc=com">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="ldapBindDn">Bind DN (optional)</label>
<input type="text" id="ldapBindDn" name="bind_dn" placeholder="cn=admin,dc=example,dc=com">
</div>
<div class="form-group">
<label for="ldapBindPassword">Bind Passwort (optional)</label>
<input type="password" id="ldapBindPassword" name="bind_password" placeholder="Leer lassen um nicht zu ändern">
</div>
</div>
<div class="form-group">
<label for="ldapSearchFilter">User Search Filter</label>
<input type="text" id="ldapSearchFilter" name="user_search_filter" placeholder="(objectClass=person)" value="(objectClass=person)">
</div>
<div class="form-row">
<div class="form-group">
<label for="ldapUsernameAttr">Username-Attribut</label>
<input type="text" id="ldapUsernameAttr" name="username_attribute" placeholder="cn" value="cn">
</div>
<div class="form-group">
<label for="ldapFirstnameAttr">Vorname-Attribut</label>
<input type="text" id="ldapFirstnameAttr" name="firstname_attribute" placeholder="givenName" value="givenName">
</div>
<div class="form-group">
<label for="ldapLastnameAttr">Nachname-Attribut</label>
<input type="text" id="ldapLastnameAttr" name="lastname_attribute" placeholder="sn" value="sn">
</div>
</div>
<div class="form-group">
<label for="ldapSyncInterval">Sync-Intervall (Minuten)</label>
<input type="number" id="ldapSyncInterval" name="sync_interval" min="0" value="0" placeholder="0 = nur manuell">
<small>0 = nur manuelle Synchronisation</small>
</div>
<button type="submit" class="btn btn-primary">Konfiguration speichern</button>
</form>
</div>
<div class="ldap-sync-actions" style="margin-top: 30px;">
<h3>Synchronisation</h3>
<div style="margin-bottom: 15px;">
<button id="syncNowBtn" class="btn btn-primary">Synchronisation jetzt starten</button>
<span id="syncStatus" style="margin-left: 15px;"></span>
</div>
<% if (ldapConfig && ldapConfig.last_sync) { %>
<p><strong>Letzte Synchronisation:</strong> <%= new Date(ldapConfig.last_sync).toLocaleString('de-DE') %></p>
<% } else { %>
<p><strong>Letzte Synchronisation:</strong> Noch keine Synchronisation durchgeführt</p>
<% } %>
</div>
<div class="ldap-sync-log" style="margin-top: 30px;">
<h3>Sync-Log (letzte 10 Einträge)</h3>
<table>
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Typ</th>
<th>Status</th>
<th>Benutzer synchronisiert</th>
<th>Fehlermeldung</th>
</tr>
</thead>
<tbody>
<% if (syncLogs && syncLogs.length > 0) { %>
<% syncLogs.forEach(function(log) { %>
<tr>
<td><%= new Date(log.sync_started_at).toLocaleString('de-DE') %></td>
<td><%= log.sync_type === 'manual' ? 'Manuell' : 'Automatisch' %></td>
<td>
<span class="role-badge role-<%= log.status === 'success' ? 'mitarbeiter' : 'admin' %>">
<%= log.status === 'success' ? 'Erfolg' : 'Fehler' %>
</span>
</td>
<td><%= log.users_synced %></td>
<td><%= log.error_message || '-' %></td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="5" style="text-align: center;">Keine Log-Einträge vorhanden</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="/js/admin.js"></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 || "admin" %>';
}
} catch (error) {
console.error('Fehler beim Rollenwechsel:', error);
alert('Fehler beim Wechseln der Rolle');
// Wert zurücksetzen
this.value = '<%= user.currentRole || "admin" %>';
}
});
}
});
</script>
</body>
</html>

167
views/dashboard.ejs Normal file
View File

@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="de-DE">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Language" content="de-DE">
<title>Dashboard - Stundenerfassung</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="navbar">
<div class="container">
<h1>Stundenerfassung</h1>
<div class="nav-right">
<span>Willkommen, <%= 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-secondary">Abmelden</a>
</div>
</div>
</div>
<div class="container dashboard-container">
<div class="dashboard-layout">
<div class="dashboard">
<div class="week-selector">
<button id="prevWeek" class="btn btn-secondary">◀ Vorherige Woche</button>
<h2 id="weekTitle">Kalenderwoche</h2>
<button id="nextWeek" class="btn btn-secondary">Nächste Woche ▶</button>
</div>
<div id="timesheetTable">
<!-- Wird mit JavaScript gefüllt -->
</div>
<div class="summary">
<div class="summary-item">
<strong>Gesamtstunden diese Woche:</strong>
<span id="totalHours">0.00 h</span>
</div>
</div>
<div class="actions">
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)">Woche abschicken</button>
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
</div>
</div>
<!-- Auswertung: Überstunden und Urlaubstage -->
<div class="user-stats-panel" id="userStatsPanel">
<h3>Ihre Auswertung</h3>
<div class="stat-card stat-overtime">
<div class="stat-label">Aktuelle Überstunden</div>
<div class="stat-value" id="currentOvertime">-</div>
<div class="stat-unit">Stunden</div>
</div>
<div class="stat-card stat-vacation">
<div class="stat-label">Verbleibende Urlaubstage</div>
<div class="stat-value" id="remainingVacation">-</div>
<div class="stat-unit">von <span id="totalVacation">-</span> Tagen</div>
</div>
<!-- API-URLs für Zeiterfassung -->
<div class="stat-card stat-api-urls" style="margin-top: 20px; padding: 15px; box-sizing: border-box; width: 100%;">
<h4 style="margin-bottom: 15px; font-size: 14px; color: #555;">Schnelle Zeiterfassung</h4>
<div style="margin-bottom: 12px;">
<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Kommen (Check-in):</label>
<div style="display: flex; gap: 8px; align-items: center; width: 100%;">
<input type="text" id="checkinUrl" readonly value=""
style="flex: 1; min-width: 0; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; background-color: #f9f9f9;">
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-secondary btn-sm" style="padding: 6px 12px; font-size: 12px; flex-shrink: 0;">Kopieren</button>
</div>
</div>
<div>
<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Gehen (Check-out):</label>
<div style="display: flex; gap: 8px; align-items: center; width: 100%;">
<input type="text" id="checkoutUrl" readonly value=""
style="flex: 1; min-width: 0; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; background-color: #f9f9f9;">
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-secondary btn-sm" style="padding: 6px 12px; font-size: 12px; flex-shrink: 0;">Kopieren</button>
</div>
</div>
<p style="margin-top: 10px; font-size: 11px; color: #888; line-height: 1.4;">
Diese URLs können Sie in einer App eintragen oder direkt im Browser aufrufen, um Ihre Arbeitszeiten zu erfassen.
</p>
</div>
</div>
</div>
</div>
<script src="/js/dashboard.js"></script>
<script>
// URL-Kopier-Funktion
function copyToClipboard(inputId) {
const input = document.getElementById(inputId);
input.select();
input.setSelectionRange(0, 99999); // Für mobile Geräte
try {
document.execCommand('copy');
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Kopiert!';
button.style.backgroundColor = '#27ae60';
setTimeout(() => {
button.textContent = originalText;
button.style.backgroundColor = '';
}, 2000);
} catch (err) {
alert('Fehler beim Kopieren. Bitte manuell kopieren.');
}
}
// URLs mit aktueller Domain aktualisieren
document.addEventListener('DOMContentLoaded', function() {
const userId = '<%= user.id %>';
const baseUrl = window.location.origin;
const checkinInput = document.getElementById('checkinUrl');
const checkoutInput = document.getElementById('checkoutUrl');
if (checkinInput) checkinInput.value = `${baseUrl}/api/checkin/${userId}`;
if (checkoutInput) checkoutInput.value = `${baseUrl}/api/checkout/${userId}`;
// Rollenwechsel-Handler
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 || "mitarbeiter" %>';
}
} catch (error) {
console.error('Fehler beim Rollenwechsel:', error);
alert('Fehler beim Wechseln der Rolle');
// Wert zurücksetzen
this.value = '<%= user.currentRole || "mitarbeiter" %>';
}
});
}
});
</script>
</body>
</html>

35
views/login.ejs Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Stundenerfassung</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h1>Stundenerfassung</h1>
<h2>Anmeldung</h2>
<% if (error) { %>
<div class="error-message"><%= error %></div>
<% } %>
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Benutzername</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Anmelden</button>
</form>
</div>
</div>
</body>
</html>

515
views/verwaltung.ejs Normal file
View File

@@ -0,0 +1,515 @@
<!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="stylesheet" href="/css/style.css">
</head>
<body>
<div class="navbar">
<div class="container">
<h1>Stundenerfassung - Verwaltung</h1>
<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-secondary">Abmelden</a>
</div>
</div>
</div>
<div class="container verwaltung-container">
<div class="verwaltung-panel">
<h2>Postfach - Eingereichte Stundenzettel</h2>
<% 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>
<% } %>
</div>
<div class="employee-details" style="margin-top: 10px;">
<div style="display: inline-block; margin-right: 20px;">
<strong>Wochenstunden:</strong> <span><%= employee.user.wochenstunden || '-' %></span>
</div>
<div style="display: inline-block; margin-right: 20px;">
<strong>Urlaubstage:</strong> <span><%= employee.user.urlaubstage || '-' %></span>
</div>
<div style="display: inline-block; margin-right: 20px;">
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></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">
<strong>Kalenderwoche:</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>
</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>Status</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><span class="status-badge status-<%= ts.status %>"><%= ts.status %></span></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="6">
<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>
// Statistiken für alle Wochen laden
document.querySelectorAll('.group-stats').forEach(statsDiv => {
const userId = statsDiv.dataset.userId;
const weekStart = statsDiv.dataset.weekStart;
const weekEnd = statsDiv.dataset.weekEnd;
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';
}
// Statistiken anzeigen
let statsHTML = '';
if (data.overtimeHours !== undefined) {
statsHTML += `<div style="display: inline-block; margin-right: 20px;">
<strong>Überstunden:</strong> <span>${data.overtimeHours.toFixed(2)} h</span>
${data.overtimeTaken > 0 ? `<span style="color: #666;">(davon genommen: ${data.overtimeTaken.toFixed(2)} h)</span>` : ''}
${data.remainingOvertime !== data.overtimeHours ? `<span style="color: #28a745;">(verbleibend: ${data.remainingOvertime.toFixed(2)} h)</span>` : ''}
</div>`;
}
if (data.vacationDays !== undefined) {
statsHTML += `<div style="display: inline-block; margin-right: 20px;">
<strong>Urlaub genommen:</strong> <span>${data.vacationDays.toFixed(1)} Tag${data.vacationDays !== 1 ? 'e' : ''}</span>
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend: ${data.remainingVacation.toFixed(1)} Tage)</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';
}
});
});
// 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();
}
}
});
});
</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>
</body>
</html>