192 lines
8.1 KiB
JavaScript
192 lines
8.1 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++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
};
|