// User API Routes const { db } = require('../database'); const { hasRole, getCurrentDate } = require('../helpers/utils'); const { requireAuth } = require('../middleware/auth'); const { getHolidaysForDateRange } = require('../services/feiertage-service'); // Routes registrieren function registerUserRoutes(app) { // API: Letzte bearbeitete Woche abrufen app.get('/api/user/last-week', requireAuth, (req, res) => { const userId = req.session.userId; db.get('SELECT last_week_start FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der letzten Woche' }); } res.json({ last_week_start: user?.last_week_start || null }); }); }); // API: Letzte bearbeitete Woche speichern app.post('/api/user/last-week', requireAuth, (req, res) => { const userId = req.session.userId; const { week_start } = req.body; if (!week_start) { return res.status(400).json({ error: 'week_start ist erforderlich' }); } db.run('UPDATE users SET last_week_start = ? WHERE id = ?', [week_start, userId], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Speichern der letzten Woche' }); } res.json({ success: true }); }); }); // API: Wochenend-Prozentsätze abrufen app.get('/api/user/weekend-percentages', requireAuth, (req, res) => { 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 Abrufen der Wochenend-Prozentsätze' }); } // Wenn keine Optionen vorhanden, Standardwerte zurückgeben res.json({ saturday_percentage: options?.saturday_percentage || 100, sunday_percentage: options?.sunday_percentage || 100 }); }); }); // API: User-Daten abrufen (Wochenstunden) app.get('/api/user/data', requireAuth, (req, res) => { const userId = req.session.userId; db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } res.json({ wochenstunden: user?.wochenstunden || 0 }); }); }); // API: Client-IP abrufen app.get('/api/user/client-ip', requireAuth, (req, res) => { // Versuche verschiedene Methoden, um die Client-IP zu erhalten const clientIp = req.ip || req.connection.remoteAddress || req.socket.remoteAddress || (req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) || req.headers['x-real-ip'] || 'unknown'; // Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1) const cleanIp = clientIp.replace(/^::ffff:/, ''); res.json({ client_ip: cleanIp }); }); // API: Ping-IP abrufen app.get('/api/user/ping-ip', requireAuth, (req, res) => { const userId = req.session.userId; db.get('SELECT ping_ip FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der IP-Adresse' }); } res.json({ ping_ip: user?.ping_ip || null }); }); }); // API: Ping-IP speichern app.post('/api/user/ping-ip', requireAuth, (req, res) => { const userId = req.session.userId; const { ping_ip } = req.body; // Validierung: IPv4 Format (einfache Prüfung) const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; if (ping_ip && ping_ip.trim() !== '' && !ipv4Regex.test(ping_ip.trim())) { return res.status(400).json({ error: 'Ungültige IP-Adresse. Bitte geben Sie eine gültige IPv4-Adresse ein.' }); } // Normalisiere: Leere Strings werden zu null const normalizedPingIp = (ping_ip && ping_ip.trim() !== '') ? ping_ip.trim() : null; db.run('UPDATE users SET ping_ip = ? WHERE id = ?', [normalizedPingIp, userId], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Speichern der IP-Adresse' }); } // Wenn IP entfernt wurde, lösche auch den Ping-Status für heute if (!normalizedPingIp) { const currentDate = getCurrentDate(); db.run('DELETE FROM ping_status WHERE user_id = ? AND date = ?', [userId, currentDate], (err) => { // Fehler ignorieren }); } res.json({ success: true, ping_ip: normalizedPingIp }); }); }); // API: Rollenwechsel app.post('/api/user/switch-role', requireAuth, (req, res) => { const { role } = req.body; if (!role) { return res.status(400).json({ error: 'Rolle ist erforderlich' }); } // Prüfe ob User diese Rolle hat if (!hasRole(req, role)) { return res.status(403).json({ error: 'Sie haben keine Berechtigung für diese Rolle' }); } // Validiere dass die Rolle eine gültige Rolle ist const validRoles = ['mitarbeiter', 'verwaltung', 'admin']; if (!validRoles.includes(role)) { return res.status(400).json({ error: 'Ungültige Rolle' }); } // Setze aktuelle Rolle req.session.currentRole = role; res.json({ success: true, currentRole: role }); }); // API: Verplante Urlaubstage (alle Wochen, auch nicht-eingereichte) app.get('/api/user/planned-vacation', requireAuth, (req, res) => { const userId = req.session.userId; const { getCalendarWeek } = require('../helpers/utils'); db.all(`SELECT date, vacation_type FROM timesheet_entries WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, [userId], (err, entries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); } let plannedDays = 0; const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } entries.forEach(entry => { const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; plannedDays += dayValue; // Berechne Kalenderwoche const date = new Date(entry.date); const year = date.getFullYear(); const week = getCalendarWeek(entry.date); const weekKey = `${year}-KW${week}`; if (!weeksMap[weekKey]) { weeksMap[weekKey] = { year, week, days: 0 }; } weeksMap[weekKey].days += dayValue; }); // Konvertiere zu sortiertem Array const weeks = Object.values(weeksMap).sort((a, b) => { if (a.year !== b.year) return a.year - b.year; return a.week - b.week; }); res.json({ plannedVacationDays: plannedDays, weeks: weeks }); } ); }); // API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage) app.get('/api/user/stats', 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, urlaubstage, 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 urlaubstage = user.urlaubstage || 0; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; // Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte) const { getCalendarWeek } = require('../helpers/utils'); db.all(`SELECT date, vacation_type FROM timesheet_entries WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, [userId], (err, allVacationEntries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); } let plannedVacationDays = 0; const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } (allVacationEntries || []).forEach(entry => { const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; plannedVacationDays += dayValue; // Berechne Kalenderwoche const date = new Date(entry.date); const year = date.getFullYear(); const week = getCalendarWeek(entry.date); const weekKey = `${year}-KW${week}`; if (!weeksMap[weekKey]) { weeksMap[weekKey] = { year, week, days: 0 }; } weeksMap[weekKey].days += dayValue; }); // Konvertiere zu sortiertem Array const plannedWeeks = Object.values(weeksMap).sort((a, b) => { if (a.year !== b.year) return a.year - b.year; return a.week - b.week; }); // 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`, [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({ currentOvertime: overtimeOffsetHours, remainingVacation: urlaubstage, totalOvertimeHours: 0, totalOvertimeTaken: 0, totalVacationDays: 0, plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, overtimeOffsetHours: overtimeOffsetHours }); } let totalOvertimeHours = 0; let totalOvertimeTaken = 0; let totalVacationDays = 0; 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; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse 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 { // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) 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); // Prüfe ob Woche vollständig ausgefüllt ist (alle 5 Werktage) // Feiertage für die Woche laden (Feiertag zählt als ausgefüllt) 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]; // Tag gilt als ausgefüllt wenn: // - Ganzer Tag Urlaub (vacation_type = 'full') // - Krank (sick_status = 1) // - ODER Start- und End-Zeit vorhanden sind 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) { // Woche nicht vollständig - überspringe diese Woche processedWeeks++; if (processedWeeks === weeks.length && !hasError) { const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; const remainingVacation = urlaubstage - totalVacationDays; res.json({ currentOvertime: currentOvertime, remainingVacation: remainingVacation, totalOvertimeHours: totalOvertimeHours, totalOvertimeTaken: totalOvertimeTaken, totalVacationDays: totalVacationDays, plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, overtimeOffsetHours: overtimeOffsetHours }); } return; // Überspringe diese Woche } // Berechnungen für diese Woche (nur wenn vollständig ausgefüllt) let weekTotalHours = 0; let weekOvertimeTaken = 0; let weekVacationDays = 0; let weekVacationHours = 0; const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden entries.forEach(entry => { // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind 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 += entry.overtime_taken_hours; } // Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden // Diese Tage werden separat gezählt, um die Sollstunden anzupassen if (isFullDayOvertime) { fullDayOvertimeDays++; } // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden if (entry.vacation_type === 'full') { weekVacationDays += 1; weekVacationHours += 8; // Ganzer Tag = 8 Stunden // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt } else if (entry.vacation_type === 'half') { weekVacationDays += 0.5; weekVacationHours += 4; // Halber Tag = 4 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein 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; } } else { // Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden) 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; } } }); // Feiertagsstunden: 8h pro Werktag der ein Feiertag ist 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; // Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); // weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet) const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; // Kumulativ addieren // WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden) // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) // Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden totalOvertimeHours += weekOvertimeHours; totalOvertimeTaken += weekOvertimeTaken; totalVacationDays += weekVacationDays; processedWeeks++; // Wenn alle Wochen verarbeitet wurden, Antwort senden if (processedWeeks === weeks.length && !hasError) { // Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset // weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend) // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; const remainingVacation = urlaubstage - totalVacationDays; res.json({ currentOvertime: currentOvertime, remainingVacation: remainingVacation, totalOvertimeHours: totalOvertimeHours, totalOvertimeTaken: totalOvertimeTaken, totalVacationDays: totalVacationDays, plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, overtimeOffsetHours: overtimeOffsetHours }); } }); // getHolidaysForDateRange.then }); // db.all (allEntries) }); // weeks.forEach }); // db.all (weeks) }); // db.all (allVacationEntries) }); // db.get (user) }); // db.get (options) }); // app.get } module.exports = registerUserRoutes;