diff --git a/routes/dashboard.js b/routes/dashboard.js index b97f1d7..8b8beaf 100644 --- a/routes/dashboard.js +++ b/routes/dashboard.js @@ -39,6 +39,32 @@ function registerDashboardRoutes(app) { } }); }); + + // Überstunden-Auswertung für Mitarbeiter + app.get('/overtime-breakdown', requireAuth, (req, res) => { + // Prüfe ob User Mitarbeiter-Rolle hat + if (!hasRole(req, 'mitarbeiter')) { + // Wenn User keine Mitarbeiter-Rolle hat, aber andere Rollen, redirecte entsprechend + if (hasRole(req, 'admin')) { + return res.redirect('/admin'); + } + if (hasRole(req, 'verwaltung')) { + return res.redirect('/verwaltung'); + } + return res.status(403).send('Zugriff verweigert'); + } + + res.render('overtime-breakdown', { + user: { + id: req.session.userId, + firstname: req.session.firstname, + lastname: req.session.lastname, + username: req.session.username, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'mitarbeiter' + } + }); + }); } module.exports = registerDashboardRoutes; diff --git a/routes/user.js b/routes/user.js index c7a7ae4..9d015da 100644 --- a/routes/user.js +++ b/routes/user.js @@ -431,25 +431,15 @@ function registerUserRoutes(app) { weekVacationDays += 0.5; weekVacationHours += 4; // Halber Tag = 4 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) if (entry.total_hours && !isFullDayOvertime) { - let hours = entry.total_hours; - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) - const weekendPercentage = getWeekendPercentage(entry.date); - if (weekendPercentage >= 100 && hours > 0 && !entry.sick_status) { - hours = hours * (weekendPercentage / 100); - } - weekTotalHours += hours; + weekTotalHours += parseFloat(entry.total_hours) || 0; } } else { // Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden) + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) if (entry.total_hours && !isFullDayOvertime) { - let hours = entry.total_hours; - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Krankheit) - const weekendPercentage = getWeekendPercentage(entry.date); - if (weekendPercentage > 0 && hours > 0 && !entry.sick_status) { - hours = hours * (1 + weekendPercentage / 100); - } - weekTotalHours += hours; + weekTotalHours += parseFloat(entry.total_hours) || 0; } } }); @@ -511,6 +501,232 @@ function registerUserRoutes(app) { }); // db.get (user) }); // db.get (options) }); // app.get + + // API: Wöchentliche Überstunden-Aufschlüsselung + app.get('/api/user/overtime-breakdown', requireAuth, (req, res) => { + const userId = req.session.userId; + + // Wochenend-Prozentsätze laden + db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Laden der Optionen' }); + } + + const saturdayPercentage = options?.saturday_percentage || 100; + const sundayPercentage = options?.sunday_percentage || 100; + + // Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück + function getWeekendPercentage(dateStr) { + const date = new Date(dateStr); + const day = date.getDay(); + if (day === 6) { // Samstag + return saturdayPercentage; + } else if (day === 0) { // Sonntag + return sundayPercentage; + } + return 100; // Kein Wochenende = 100% (normal) + } + + // User-Daten abrufen + db.get('SELECT wochenstunden, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { + if (err || !user) { + return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); + } + + const wochenstunden = user.wochenstunden || 0; + const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; + + // Alle eingereichten Wochen abrufen + db.all(`SELECT DISTINCT week_start, week_end + FROM weekly_timesheets + WHERE user_id = ? AND status = 'eingereicht' + ORDER BY week_start DESC`, + [userId], + (err, weeks) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); + } + + // Wenn keine Wochen vorhanden + if (!weeks || weeks.length === 0) { + return res.json({ weeks: [] }); + } + + const { getCalendarWeek } = require('../helpers/utils'); + const weekData = []; + let processedWeeks = 0; + let hasError = false; + + // Für jede Woche die Statistiken berechnen + weeks.forEach((week) => { + // Einträge für diese Woche abrufen (nur neueste pro Tag) + db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, + [userId, week.week_start, week.week_end], + (err, allEntries) => { + if (hasError) return; + + if (err) { + hasError = true; + return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); + } + + // Filtere auf neuesten Eintrag pro Tag + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; + const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; + if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { + entriesByDate[entry.date] = entry; + } + } + }); + + // Konvertiere zurück zu Array + const entries = Object.values(entriesByDate); + + // Feiertage für die Woche laden + getHolidaysForDateRange(week.week_start, week.week_end) + .catch(() => new Set()) + .then((holidaySet) => { + // Prüfe alle 5 Werktage (Montag-Freitag) + const startDate = new Date(week.week_start); + const endDate = new Date(week.week_end); + let workdays = 0; + let filledWorkdays = 0; + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { // Montag bis Freitag + workdays++; + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) { + filledWorkdays++; + continue; + } + const entry = entriesByDate[dateStr]; + + if (entry) { + const isFullDayVacation = entry.vacation_type === 'full'; + const isSick = entry.sick_status === 1 || entry.sick_status === true; + const hasStartAndEnd = entry.start_time && entry.end_time && + entry.start_time.toString().trim() !== '' && + entry.end_time.toString().trim() !== ''; + + if (isFullDayVacation || isSick || hasStartAndEnd) { + filledWorkdays++; + } + } + } + } + + // Nur berechnen wenn alle Werktage ausgefüllt sind + if (filledWorkdays < workdays) { + processedWeeks++; + if (processedWeeks === weeks.length && !hasError) { + res.json({ weeks: weekData }); + } + return; + } + + // Berechnungen für diese Woche + let weekTotalHours = 0; + let weekOvertimeTaken = 0; + let weekVacationDays = 0; + let weekVacationHours = 0; + + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + let fullDayOvertimeDays = 0; + + entries.forEach(entry => { + const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + if (entry.overtime_taken_hours) { + weekOvertimeTaken += parseFloat(entry.overtime_taken_hours) || 0; + } + + if (isFullDayOvertime) { + fullDayOvertimeDays++; + } + + if (entry.vacation_type === 'full') { + weekVacationDays += 1; + weekVacationHours += 8; + } else if (entry.vacation_type === 'half') { + weekVacationDays += 0.5; + weekVacationHours += 4; + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) + if (entry.total_hours && !isFullDayOvertime) { + weekTotalHours += parseFloat(entry.total_hours) || 0; + } + } else { + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) + if (entry.total_hours && !isFullDayOvertime) { + weekTotalHours += parseFloat(entry.total_hours) || 0; + } + } + }); + + // Feiertagsstunden + let holidayHours = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + + // Sollstunden berechnen + const sollStunden = (wochenstunden / 5) * workdays; + const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; + const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); + const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; + + // Kalenderwoche berechnen + const calendarWeek = getCalendarWeek(week.week_start); + const year = new Date(week.week_start).getFullYear(); + + // Wochen-Daten hinzufügen + weekData.push({ + week_start: week.week_start, + week_end: week.week_end, + calendar_week: calendarWeek, + year: year, + overtime_hours: parseFloat(weekOvertimeHours.toFixed(2)), + overtime_taken: parseFloat(weekOvertimeTaken.toFixed(2)), + total_hours: parseFloat(weekTotalHoursWithVacation.toFixed(2)), + soll_stunden: parseFloat(adjustedSollStunden.toFixed(2)), + vacation_days: parseFloat(weekVacationDays.toFixed(1)), + workdays: workdays + }); + + processedWeeks++; + + // Wenn alle Wochen verarbeitet wurden, Antwort senden + if (processedWeeks === weeks.length && !hasError) { + // Sortiere nach Datum (neueste zuerst) + weekData.sort((a, b) => { + if (a.year !== b.year) return b.year - a.year; + if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week; + return new Date(b.week_start) - new Date(a.week_start); + }); + res.json({ weeks: weekData, overtime_offset_hours: overtimeOffsetHours }); + } + }); // getHolidaysForDateRange.then + }); // db.all (allEntries) + }); // weeks.forEach + }); // db.all (weeks) + }); // db.get (user) + }); // db.get (options) + }); // app.get } module.exports = registerUserRoutes; diff --git a/views/dashboard.ejs b/views/dashboard.ejs index d318924..af9790e 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -70,6 +70,9 @@