413 lines
21 KiB
Plaintext
413 lines
21 KiB
Plaintext
<!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-logout">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 style="margin-top: 10px;">
|
|
<a href="/overtime-breakdown" class="btn btn-primary" style="width: 100%; font-size: 13px; padding: 8px 12px;">Details anzeigen</a>
|
|
</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 style="margin-top: 12px;">
|
|
<a href="/api/dashboard/qr-pdf" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
|
</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>
|