diff --git a/doc/DSGVO-Dokumentation.md b/doc/DSGVO-Dokumentation.md index deca6ba..1a0652e 100644 --- a/doc/DSGVO-Dokumentation.md +++ b/doc/DSGVO-Dokumentation.md @@ -443,7 +443,7 @@ Die automatische Berechnung von Arbeitszeiten und Überstunden dient lediglich d Diese DSGVO-Dokumentation wird bei Änderungen der Datenverarbeitung aktualisiert. Die aktuelle Version ist immer im System verfügbar. -**Letzte Aktualisierung:** [Datum] +**Letzte Aktualisierung:** [10.03.2026] **Version:** 1.0 diff --git a/public/css/style.css b/public/css/style.css index b21245a..1323f4c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -376,6 +376,40 @@ body { margin-bottom: 4px; } +/* Dashboard: Überstunden-Wert standardmäßig weichzeichnen */ +.stat-value-blurred { + filter: blur(5px); + transition: filter 0.2s ease; +} + +/* Wrapper nur um den Wert, damit Overlay nicht die ganze Karte bedeckt */ +.stat-card-overtime .stat-value-wrapper { + position: relative; + display: inline-block; +} + +/* Hinweistext direkt über dem geblurrten Überstunden-Wert */ +.stat-card-overtime .stat-blur-hint { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 500; + color: #555; + text-align: center; + padding: 0 8px; + pointer-events: none; + background: linear-gradient(to bottom, rgba(248, 249, 250, 0.95), rgba(248, 249, 250, 0.8)); + opacity: 1; + transition: opacity 0.2s ease; +} + +.stat-card-overtime:hover .stat-blur-hint { + opacity: 0; +} + .stat-unit { font-size: 11px; color: #999; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index b6bb9cf..4154733 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -113,6 +113,21 @@ async function loadUserStats() { // Beim Laden der Seite document.addEventListener('DOMContentLoaded', async function() { + // Blur-Effekt für aktuelle Überstunden: Standard = geblurrt, bei Hover klar + (function initOvertimeBlur() { + const overtimeCard = document.querySelector('.stat-card-overtime'); + if (!overtimeCard) return; + const valueEl = overtimeCard.querySelector('#currentOvertime'); + if (!valueEl) return; + + overtimeCard.addEventListener('mouseenter', () => { + valueEl.classList.remove('stat-value-blurred'); + }); + + overtimeCard.addEventListener('mouseleave', () => { + valueEl.classList.add('stat-value-blurred'); + }); + })(); // Letzte Woche vom Server laden try { const response = await fetch('/api/user/last-week'); diff --git a/routes/verwaltung-routes.js b/routes/verwaltung-routes.js index b72f470..93efd97 100644 --- a/routes/verwaltung-routes.js +++ b/routes/verwaltung-routes.js @@ -10,6 +10,18 @@ const { getCurrentOvertimeForUser } = require('../services/overtime-service'); // Routes registrieren function registerVerwaltungRoutes(app) { + // Helper: Minuten in Format h:mm (z. B. 90 -> "1:30", -45 -> "-0:45") + function minutesToHhMm(totalMinutes) { + if (totalMinutes == null || !Number.isFinite(Number(totalMinutes))) return '0:00'; + const n = Number(totalMinutes); + const sign = n < 0 ? -1 : 1; + const absVal = Math.abs(n); + let h = Math.floor(absVal / 60); + let min = Math.round(absVal - h * 60); + const prefix = sign < 0 ? '-' : ''; + return prefix + h + ':' + String(min).padStart(2, '0'); + } + // Verwaltungs-Bereich app.get('/verwaltung', requireVerwaltung, (req, res) => { db.all(` @@ -132,6 +144,204 @@ function registerVerwaltungRoutes(app) { }); }); + // Projektauswertung nach Mitarbeitern für eine Projektnummer + app.get('/verwaltung/projektauswertung', requireVerwaltung, (req, res) => { + const projectNumberRaw = req.query.project ? String(req.query.project).trim() : ''; + const projectNumber = projectNumberRaw || null; + + if (!projectNumber) { + // Nur Formular anzeigen, noch keine Auswertung + return res.render('projekt-auswertung', { + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'verwaltung' + }, + projectNumber: '', + results: [], + totalProjectHours: 0, + hasResults: false + }); + } + + // Aggregation der Projektstunden pro Mitarbeiter über alle 5 Aktivitäten + const sql = ` + SELECT + u.id AS user_id, + u.firstname, + u.lastname, + ( + SUM(CASE WHEN te.activity1_project_number = ? THEN COALESCE(te.activity1_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity2_project_number = ? THEN COALESCE(te.activity2_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity3_project_number = ? THEN COALESCE(te.activity3_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity4_project_number = ? THEN COALESCE(te.activity4_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity5_project_number = ? THEN COALESCE(te.activity5_hours, 0) ELSE 0 END) + ) AS total_hours, + ROUND( + ( + SUM(CASE WHEN te.activity1_project_number = ? THEN COALESCE(te.activity1_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity2_project_number = ? THEN COALESCE(te.activity2_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity3_project_number = ? THEN COALESCE(te.activity3_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity4_project_number = ? THEN COALESCE(te.activity4_hours, 0) ELSE 0 END) + + SUM(CASE WHEN te.activity5_project_number = ? THEN COALESCE(te.activity5_hours, 0) ELSE 0 END) + ) * 60 + ) AS total_minutes + FROM timesheet_entries te + JOIN users u ON u.id = te.user_id + GROUP BY u.id, u.firstname, u.lastname + HAVING total_minutes <> 0 + ORDER BY u.lastname, u.firstname + `; + + const params = [ + projectNumber, projectNumber, projectNumber, projectNumber, projectNumber, + projectNumber, projectNumber, projectNumber, projectNumber, projectNumber + ]; + + db.all(sql, params, (err, rows) => { + if (err) { + console.error('Fehler bei der Projektauswertung:', err); + return res.status(500).send('Fehler bei der Projektauswertung'); + } + + const rawResults = (rows || []).map((row) => { + const totalMinutes = row.total_minutes || 0; + const totalHours = row.total_hours || 0; + return { + userId: row.user_id, + firstname: row.firstname, + lastname: row.lastname, + totalHours, + totalMinutes, + totalHoursFormatted: minutesToHhMm(totalMinutes) + }; + }); + + const results = rawResults; + + const totalProjectMinutes = results.reduce((sum, r) => sum + (r.totalMinutes || 0), 0); + const totalProjectHours = totalProjectMinutes / 60; + const totalProjectHoursFormatted = minutesToHhMm(totalProjectMinutes); + + if (results.length === 0) { + return res.render('projekt-auswertung', { + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'verwaltung' + }, + projectNumber: projectNumberRaw, + results, + totalProjectHours, + totalProjectHoursFormatted, + hasResults: false, + breakdownByUser: {} + }); + } + + // Details pro Mitarbeiter (Aktivitäten) laden + const breakdownByUser = {}; + const userIds = results.map((r) => r.userId); + let pending = userIds.length; + + const detailSql = ` + SELECT + date, + activity1_desc, activity1_hours, activity1_project_number, + activity2_desc, activity2_hours, activity2_project_number, + activity3_desc, activity3_hours, activity3_project_number, + activity4_desc, activity4_hours, activity4_project_number, + activity5_desc, activity5_hours, activity5_project_number + FROM timesheet_entries + WHERE user_id = ? + AND ( + activity1_project_number = ? OR + activity2_project_number = ? OR + activity3_project_number = ? OR + activity4_project_number = ? OR + activity5_project_number = ? + ) + ORDER BY date + `; + + userIds.forEach((userId) => { + db.all( + detailSql, + [userId, projectNumber, projectNumber, projectNumber, projectNumber, projectNumber], + (detailErr, rowsDetail) => { + if (detailErr) { + console.error('Fehler beim Laden der Projekt-Aktivitäten:', detailErr); + breakdownByUser[userId] = []; + } else { + const activities = []; + (rowsDetail || []).forEach((row) => { + const date = row.date; + + const pushActivity = (desc, hours) => { + if (!hours || !Number.isFinite(Number(hours))) return; + const decimal = Number(hours); + const minutes = Math.round(decimal * 60); + if (minutes === 0) return; + activities.push({ + date, + description: desc || '', + hoursDecimal: decimal, + minutes, + formatted: minutesToHhMm(minutes) + }); + }; + + if (row.activity1_project_number === projectNumber) { + pushActivity(row.activity1_desc, row.activity1_hours); + } + if (row.activity2_project_number === projectNumber) { + pushActivity(row.activity2_desc, row.activity2_hours); + } + if (row.activity3_project_number === projectNumber) { + pushActivity(row.activity3_desc, row.activity3_hours); + } + if (row.activity4_project_number === projectNumber) { + pushActivity(row.activity4_desc, row.activity4_hours); + } + if (row.activity5_project_number === projectNumber) { + pushActivity(row.activity5_desc, row.activity5_hours); + } + }); + + // Nach Datum sortieren + activities.sort((a, b) => { + if (a.date === b.date) return 0; + return a.date < b.date ? -1 : 1; + }); + + breakdownByUser[userId] = activities; + } + + pending -= 1; + if (pending === 0) { + res.render('projekt-auswertung', { + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'verwaltung' + }, + projectNumber: projectNumberRaw, + results, + totalProjectHours, + totalProjectHoursFormatted, + hasResults: results.length > 0, + breakdownByUser + }); + } + } + ); + }); + }); + }); + // API: Überstunden-Offset für einen User setzen (positiv/negativ) app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => { const userId = req.params.id; diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 3261351..734e0b8 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -80,12 +80,15 @@
-
+
Aktuelle Überstunden ?
-
-
+
+
-
+
Zum Anzeigen Maus drüberziehen
+
Stunden
Details anzeigen diff --git a/views/projekt-auswertung.ejs b/views/projekt-auswertung.ejs new file mode 100644 index 0000000..1404051 --- /dev/null +++ b/views/projekt-auswertung.ejs @@ -0,0 +1,185 @@ + + + + + + Projektauswertung - Verwaltung + + + <%- include('header') %> + + + + +
+
+

Projektauswertung nach Mitarbeitern

+

Geben Sie eine Projektnummer ein, um alle erfassten Stunden pro Mitarbeiter für dieses Projekt auszuwerten.

+ + + +
+
+ + +
+ +
+ + <% if (projectNumber && !hasResults) { %> +
+

Für das Projekt <%= projectNumber %> wurden keine Stunden gefunden.

+
+ <% } %> + + <% if (hasResults) { %> +

Ergebnis für Projekt <%= projectNumber %>

+ + + + + + + + + + <% results.forEach(function(row) { + const activities = (breakdownByUser && breakdownByUser[row.userId]) || []; + %> + + + + + + <% if (activities.length > 0) { %> + + + + <% } %> + <% }); %> + + + + + + + + +
MitarbeiterGesamtzeit (h:mm)
<%= row.firstname %> <%= row.lastname %><%= row.totalHoursFormatted %> h + <% if (activities.length > 0) { %> + + <% } %> +
Gesamt Projektstunden<%= totalProjectHoursFormatted %> h
+ <% } %> +
+
+ + + <%- include('footer') %> + + + diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index 00fb0b5..878649e 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -17,6 +17,7 @@