// Overtime Service: Berechnung der aktuellen Überstunden-Bilanz pro User // Wird von /api/user/stats und /api/verwaltung/employees/current-overtime genutzt. const { getHolidaysForDateRange } = require('./feiertage-service'); /** * Berechnet die aktuelle Überstunden-Bilanz für einen User (nur eingereichte Wochen). * Entspricht der Anzeige "Aktuelle Überstunden" im Mitarbeiter-Dashboard. * * @param {number} userId - User-ID * @param {object} db - Database-Instanz (sqlite3) * @param {function} callback - (err, currentOvertime) currentOvertime in Stunden (kann negativ sein) */ function getCurrentOvertimeForUser(userId, db, callback) { db.get( 'SELECT wochenstunden, arbeitstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return callback(err, null); } if (!user) { return callback(null, null); } const wochenstunden = user.wochenstunden || 0; const arbeitstage = user.arbeitstage || 5; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; 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 callback(err, null); } if (!weeks || weeks.length === 0) { return callback(null, overtimeOffsetHours); } let totalOvertimeHours = 0; let totalOvertimeTaken = 0; let processedWeeks = 0; let hasError = false; let callbackDone = false; function done(value) { if (callbackDone) return; callbackDone = true; callback(null, value); } weeks.forEach((week) => { 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; if (!callbackDone) { callbackDone = true; callback(err, null); } return; } 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; } } }); const entries = Object.values(entriesByDate); getHolidaysForDateRange(week.week_start, week.week_end) .catch(() => new Set()) .then((holidaySet) => { const startDate = new Date(week.week_start); const endDate = new Date(week.week_end); let workdays = 0; let filledWorkdays = 0; const fullDayHoursForCheck = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const day = d.getDay(); if (day >= 1 && day <= 5) { 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 overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHoursForCheck) < 0.01; const hasStartAndEnd = entry.start_time && entry.end_time && entry.start_time.toString().trim() !== '' && entry.end_time.toString().trim() !== ''; if (isFullDayVacation || isSick || isFullDayOvertime || hasStartAndEnd) { filledWorkdays++; } } } } const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; let weekTotalHours = 0; let weekOvertimeTaken = 0; let weekVacationHours = 0; 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') { weekVacationHours += fullDayHours; } else if (entry.vacation_type === 'half') { weekVacationHours += fullDayHours / 2; if (entry.total_hours && !isFullDayOvertime) { weekTotalHours += parseFloat(entry.total_hours) || 0; } } else { if (entry.total_hours && !isFullDayOvertime) { weekTotalHours += parseFloat(entry.total_hours) || 0; } } }); 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 += fullDayHours; } } // Variante B: Soll immer vertraglich, Verbleibend ohne Abzug „genommen“ //const sollStunden = (wochenstunden / arbeitstage) * workdays; const sollStunden = wochenstunden; const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden; totalOvertimeHours += weekOvertimeHours; totalOvertimeTaken += weekOvertimeTaken; processedWeeks++; if (processedWeeks === weeks.length && !hasError) { done(totalOvertimeHours + overtimeOffsetHours); } }); } ); }); } ); } ); } module.exports = { getCurrentOvertimeForUser };