Init
This commit is contained in:
419
views/admin.ejs
Normal file
419
views/admin.ejs
Normal file
@@ -0,0 +1,419 @@
|
||||
<!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="icon" type="image/png" href="/images/favicon.png">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<img src="/images/header.png" alt="Logo" class="navbar-logo">
|
||||
<h1>Stundenerfassung - Admin</h1>
|
||||
</div>
|
||||
<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="admin-container">
|
||||
<div class="container">
|
||||
<div class="admin-panel">
|
||||
<h2>Benutzerverwaltung</h2>
|
||||
|
||||
<!-- Benutzer anlegen - Zusammenklappbar -->
|
||||
<div class="add-user-section" style="margin-top: 20px;">
|
||||
<div class="collapsible-header" onclick="toggleAddUserSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 style="margin: 0;">Neuen Benutzer anlegen</h3>
|
||||
<span id="addUserToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
|
||||
</div>
|
||||
|
||||
<div id="addUserContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
|
||||
<div class="add-user-form">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Liste - Zusammenklappbar -->
|
||||
<div class="user-list-section" style="margin-top: 20px;">
|
||||
<div class="collapsible-header" onclick="toggleUserListSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 style="margin: 0;">Benutzer-Liste</h3>
|
||||
<span id="userListToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
|
||||
</div>
|
||||
|
||||
<div id="userListContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff; overflow-x: auto; max-width: 100%;">
|
||||
<div class="user-list" style="min-width: 100%;">
|
||||
<table style="width: 100%; min-width: 900px;">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="ldap-sync-section" style="margin-top: 40px;">
|
||||
<div class="collapsible-header" onclick="toggleLDAPSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2 style="margin: 0;">LDAP-Synchronisation</h2>
|
||||
<span id="ldapToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
|
||||
</div>
|
||||
|
||||
<div id="ldapContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
<script>
|
||||
// Benutzer anlegen Sektion ein-/ausklappen
|
||||
function toggleAddUserSection() {
|
||||
const content = document.getElementById('addUserContent');
|
||||
const icon = document.getElementById('addUserToggleIcon');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// Benutzer-Liste Sektion ein-/ausklappen
|
||||
function toggleUserListSection() {
|
||||
const content = document.getElementById('userListContent');
|
||||
const icon = document.getElementById('userListToggleIcon');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// LDAP-Sektion ein-/ausklappen
|
||||
function toggleLDAPSection() {
|
||||
const content = document.getElementById('ldapContent');
|
||||
const icon = document.getElementById('ldapToggleIcon');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
406
views/dashboard.ejs
Normal file
406
views/dashboard.ejs
Normal file
@@ -0,0 +1,406 @@
|
||||
<!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="icon" type="image/png" href="/images/favicon.png">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<img src="/images/header.png" alt="Logo" class="navbar-logo">
|
||||
<h1>Stundenerfassung</h1>
|
||||
</div>
|
||||
<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 class="summary-item" id="overtimeSummaryItem" style="display: none;">
|
||||
<strong>Überstunden diese Woche:</strong>
|
||||
<span id="overtimeHours">0.00 h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)" disabled>Woche abschicken</button>
|
||||
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Seitenleiste mit Statistiken und Erfassungs-URLs -->
|
||||
<div class="user-stats-panel">
|
||||
<!-- Statistik-Karten -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||
Aktuelle Überstunden
|
||||
<span class="help-icon" onclick="showHelpModal('overtime-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
</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" style="display: flex; align-items: center; gap: 5px;">
|
||||
Verbleibende Urlaubstage
|
||||
<span class="help-icon" onclick="showHelpModal('remaining-vacation-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
</div>
|
||||
<div class="stat-value" id="remainingVacation">-</div>
|
||||
<div class="stat-unit">von <span id="totalVacation">-</span> Tagen</div>
|
||||
</div>
|
||||
<div class="stat-card stat-planned">
|
||||
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||
Verplante Urlaubstage
|
||||
<span class="help-icon" onclick="showHelpModal('planned-vacation-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
</div>
|
||||
<div class="stat-value" id="plannedVacation">-</div>
|
||||
<div class="stat-unit">Tage</div>
|
||||
<div id="plannedWeeks" style="font-size: 11px; color: #666; margin-top: 8px; line-height: 1.4;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Zeiterfassung (URL & IP) -->
|
||||
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
|
||||
<h3 style="font-size: 14px; margin-bottom: 0; color: #2c3e50; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleTimeCapture()">
|
||||
<span class="toggle-icon-time-capture" style="display: inline-block; transition: transform 0.3s;">▶</span>
|
||||
Automatische Zeiterfassung
|
||||
</h3>
|
||||
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
||||
<!-- URL-Erfassung -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
||||
Zeiterfassung per URL
|
||||
<span class="help-icon" onclick="showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
</h4>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkinUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkoutUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP-Erfassung -->
|
||||
<div style="padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
||||
IP-basierte Zeiterfassung
|
||||
<span class="help-icon" onclick="showHelpModal('ip-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
</h4>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Ping-IP Adresse</label>
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
|
||||
<input type="text" id="pingIpInput" placeholder="z.B. 192.168.1.100" style="flex: 1; padding: 8px; font-size: 12px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<button onclick="window.savePingIP()" class="btn btn-sm btn-success" style="padding: 8px 12px;">Speichern</button>
|
||||
</div>
|
||||
<button onclick="window.detectClientIP()" class="btn btn-sm" style="padding: 6px 12px; background-color: #3498db; color: white; border: none; border-radius: 4px; font-size: 11px; cursor: pointer; margin-bottom: 5px;">Aktuelle IP ermitteln</button>
|
||||
<p style="font-size: 11px; color: #666; margin-top: 5px; font-style: italic;">Ihre IP-Adresse für automatische Zeiterfassung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/dashboard.js"></script>
|
||||
<script>
|
||||
// Wochenende-Sektion ein-/ausklappen
|
||||
function toggleWeekendSection() {
|
||||
const content = document.getElementById('weekendContent');
|
||||
const icon = document.getElementById('weekendToggleIcon');
|
||||
|
||||
if (content && icon) {
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeiterfassung ein-/ausklappen
|
||||
function toggleTimeCapture() {
|
||||
const content = document.getElementById('timeCaptureContent');
|
||||
const icon = document.querySelector('.toggle-icon-time-capture');
|
||||
|
||||
if (content && icon) {
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(90deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (Port 3334 für Check-in)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userId = '<%= user.id %>';
|
||||
const baseUrl = window.location.origin;
|
||||
// Check-in URLs verwenden Port 3334
|
||||
// Ersetze Port in URL oder füge Port hinzu falls nicht vorhanden
|
||||
let checkinBaseUrl;
|
||||
if (baseUrl.match(/:\d+$/)) {
|
||||
// Port vorhanden - ersetze ihn
|
||||
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||
} else {
|
||||
// Kein Port - füge Port hinzu
|
||||
const url = new URL(baseUrl);
|
||||
checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||
}
|
||||
const checkinInput = document.getElementById('checkinUrl');
|
||||
const checkoutInput = document.getElementById('checkoutUrl');
|
||||
if (checkinInput) checkinInput.value = `${checkinBaseUrl}/api/checkin/${userId}`;
|
||||
if (checkoutInput) checkoutInput.value = `${checkinBaseUrl}/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>
|
||||
|
||||
<!-- Help Modal -->
|
||||
<div id="helpModal" style="display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5);">
|
||||
<div style="position: relative; background-color: #fff; margin: 10% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||
<span onclick="closeHelpModal()" style="position: absolute; right: 15px; top: 15px; color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; line-height: 1;">×</span>
|
||||
<div id="helpModalContent" style="padding-right: 30px;">
|
||||
<!-- Content wird dynamisch eingefügt -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Help Modal Funktionen
|
||||
function showHelpModal(type) {
|
||||
const modal = document.getElementById('helpModal');
|
||||
const content = document.getElementById('helpModalContent');
|
||||
|
||||
let title = '';
|
||||
let text = '';
|
||||
|
||||
if (type === 'url-help') {
|
||||
title = 'Zeiterfassung per URL';
|
||||
text = `
|
||||
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Mit den Check-in und Check-out URLs können Sie Ihre Arbeitszeit automatisch erfassen,
|
||||
indem Sie die URLs in Ihrem Browser aufrufen oder als Lesezeichen speichern.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Check-in URL:</strong> Öffnen Sie diese URL, um Ihre Start-Zeit zu erfassen.<br>
|
||||
<strong>Check-out URL:</strong> Öffnen Sie diese URL, um Ihre End-Zeit zu erfassen.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Die URLs sind personalisiert und funktionieren nur für Ihren Account. Sie können sie
|
||||
kopieren und als Lesezeichen in Ihrem Browser speichern.
|
||||
</p>
|
||||
`;
|
||||
} else if (type === 'ip-help') {
|
||||
title = 'IP-basierte Zeiterfassung';
|
||||
text = `
|
||||
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Die IP-basierte Zeiterfassung erkennt automatisch, wenn Sie sich im Firmennetzwerk befinden,
|
||||
indem Ihre IP-Adresse regelmäßig geprüft wird.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>So funktioniert es:</strong>
|
||||
</p>
|
||||
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||
<li>Tragen Sie Ihre IP-Adresse ein (z.B. 192.168.1.100)</li>
|
||||
<li>Das System prüft regelmäßig, ob diese IP-Adresse erreichbar ist</li>
|
||||
<li>Wenn die IP erreichbar ist, wird automatisch eine Start-Zeit erfasst</li>
|
||||
<li>Wenn die IP nicht mehr erreichbar ist, wird automatisch eine End-Zeit erfasst</li>
|
||||
</ul>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Tipp:</strong> Verwenden Sie den Button "Aktuelle IP ermitteln", um Ihre aktuelle
|
||||
IP-Adresse automatisch zu erkennen.
|
||||
</p>
|
||||
`;
|
||||
} else if (type === 'remaining-vacation-help') {
|
||||
title = 'Verbleibende Urlaubstage';
|
||||
text = `
|
||||
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Die <strong>verbleibenden Urlaubstage</strong> zeigen an, wie viele Urlaubstage Sie noch
|
||||
zur Verfügung haben.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Wichtig:</strong> Diese Zahl berücksichtigt nur Urlaubstage aus Wochen, die bereits
|
||||
<strong>eingereicht</strong> wurden. Urlaubstage, die Sie nur geplant, aber noch nicht
|
||||
abgeschickt haben, werden hier nicht abgezogen.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Beispiel:</strong> Wenn Sie 25 Urlaubstage haben und bereits 5 Tage in eingereichten
|
||||
Wochen genommen haben, zeigt diese Anzeige 20 verbleibende Tage.
|
||||
</p>
|
||||
`;
|
||||
} else if (type === 'planned-vacation-help') {
|
||||
title = 'Verplante Urlaubstage';
|
||||
text = `
|
||||
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Die <strong>verplanten Urlaubstage</strong> zeigen alle Urlaubstage an, die Sie in irgendeiner
|
||||
Woche eingetragen haben - unabhängig davon, ob die Woche bereits eingereicht wurde oder nicht.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Unterschied zu "Verbleibende Urlaubstage":</strong>
|
||||
</p>
|
||||
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||
<li><strong>Verbleibende Urlaubstage:</strong> Nur von eingereichten Wochen</li>
|
||||
<li><strong>Verplante Urlaubstage:</strong> Alle geplanten Tage (auch nicht-eingereichte Wochen)</li>
|
||||
</ul>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Beispiel:</strong> Wenn Sie in einer noch nicht eingereichten Woche 3 Tage Urlaub
|
||||
eintragen, erscheinen diese sofort in "Verplante Urlaubstage", aber noch nicht in
|
||||
"Verbleibende Urlaubstage". Erst nach dem Abschicken der Woche werden sie auch von den
|
||||
verbleibenden Tagen abgezogen.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||
<strong>Hinweis:</strong> Unter dieser Anzeige sehen Sie, in welchen Kalenderwochen
|
||||
(KW) Sie Urlaub geplant haben.
|
||||
</p>
|
||||
`;
|
||||
} else if (type === 'overtime-help') {
|
||||
title = 'Aktuelle Überstunden';
|
||||
text = `
|
||||
<h3 style="margin-top: 0; color: #2c3e50; font-size: 18px;">${title}</h3>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
Die <strong>aktuellen Überstunden</strong> zeigen Ihre gesamten Überstunden an, die sich aus
|
||||
allen bereits eingereichten Wochen ergeben.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>Wichtig:</strong> Überstunden werden erst berechnet und angezeigt, wenn die entsprechende
|
||||
Woche <strong>abgeschickt</strong> wurde. Überstunden aus Wochen, die Sie nur geplant, aber noch
|
||||
nicht abgeschickt haben, werden hier nicht berücksichtigt.
|
||||
</p>
|
||||
<p style="color: #555; line-height: 1.6;">
|
||||
<strong>So funktioniert die Berechnung:</strong>
|
||||
</p>
|
||||
<ul style="color: #555; line-height: 1.8; padding-left: 20px;">
|
||||
<li>Für jede eingereichte Woche werden Ihre tatsächlichen Arbeitsstunden mit den Sollstunden verglichen</li>
|
||||
<li>Die Differenz ergibt die Überstunden (positiv) oder Minusstunden (negativ) für diese Woche</li>
|
||||
<li>Alle Überstunden aus eingereichten Wochen werden summiert</li>
|
||||
<li>Zusätzlich können manuelle Korrekturen (Offset) durch die Verwaltung hinzugefügt werden</li>
|
||||
</ul>
|
||||
<p style="color: #555; line-height: 1.6; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||
<strong>Beispiel:</strong> Wenn Sie diese Woche 42 Stunden arbeiten, aber nur 40 Stunden Soll haben,
|
||||
entstehen 2 Überstunden. Diese werden jedoch erst nach dem Abschicken der Woche zu Ihren
|
||||
aktuellen Überstunden hinzugefügt.
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
content.innerHTML = text;
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeHelpModal() {
|
||||
document.getElementById('helpModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Modal schließen wenn außerhalb geklickt wird
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('helpModal');
|
||||
if (event.target === modal) {
|
||||
closeHelpModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
46
views/login.ejs
Normal file
46
views/login.ejs
Normal file
@@ -0,0 +1,46 @@
|
||||
<!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="icon" type="image/png" href="/images/favicon.png">
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: flex-start; cursor: pointer;">
|
||||
<input type="checkbox" name="remember_me" id="remember_me" style="margin-right: 8px; margin-top: 2px;">
|
||||
<span style="display: flex; flex-direction: column;">
|
||||
<span>Angemeldet bleiben</span>
|
||||
<span style="font-size: 0.9em; color: #666;">(30 Tage)</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
780
views/verwaltung.ejs
Normal file
780
views/verwaltung.ejs
Normal file
@@ -0,0 +1,780 @@
|
||||
<!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">
|
||||
</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-secondary">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>
|
||||
<% } %>
|
||||
</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-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||
<strong>Überstunden-Offset:</strong>
|
||||
<input
|
||||
type="number"
|
||||
step="0.25"
|
||||
class="overtime-offset-input"
|
||||
data-user-id="<%= employee.user.id %>"
|
||||
value="<%= (employee.user.overtime_offset_hours !== undefined && employee.user.overtime_offset_hours !== null) ? employee.user.overtime_offset_hours : 0 %>"
|
||||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||||
title="Manuelle Korrektur (positiv oder negativ) in Stunden" />
|
||||
<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-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>
|
||||
</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>
|
||||
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.overtimeHours !== undefined) {
|
||||
statsHTML += `<div class="stats-inline" 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.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.vacationDays !== undefined) {
|
||||
statsHTML += `<div class="stats-inline" 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 (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 = '-';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Krankheitstage beim Laden der Seite abrufen
|
||||
loadSickDays();
|
||||
|
||||
// Ü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;
|
||||
|
||||
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}/overtime-offset`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ overtime_offset_hours: 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.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);
|
||||
});
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user