Files
SDSStundenerfassung/services/overtime-service.js

199 lines
8.4 KiB
JavaScript

// 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++;
}
}
}
}
if (filledWorkdays < workdays) {
processedWeeks++;
if (processedWeeks === weeks.length && !hasError) {
done(totalOvertimeHours - totalOvertimeTaken + overtimeOffsetHours);
}
return;
}
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 weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
totalOvertimeHours += weekOvertimeHours;
totalOvertimeTaken += weekOvertimeTaken;
processedWeeks++;
if (processedWeeks === weeks.length && !hasError) {
done(totalOvertimeHours + overtimeOffsetHours);
}
});
}
);
});
}
);
}
);
}
module.exports = {
getCurrentOvertimeForUser
};