Overtime Corrections mit Historie und Grund

This commit is contained in:
2026-02-12 11:24:45 +01:00
parent e020aa4e46
commit 3edc0fe60c
6 changed files with 648 additions and 225 deletions

View File

@@ -587,194 +587,219 @@ function registerUserRoutes(app) {
const arbeitstage = user.arbeitstage || 5;
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`,
// Korrekturen durch die Verwaltung (Historie) laden
db.all(
`SELECT correction_hours, corrected_at, reason
FROM overtime_corrections
WHERE user_id = ?
ORDER BY corrected_at DESC`,
[userId],
(err, weeks) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
}
(correctionsErr, corrections) => {
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
const overtimeCorrections = correctionsErr ? [] : (corrections || []);
// Wenn keine Wochen vorhanden
if (!weeks || weeks.length === 0) {
return res.json({ weeks: [] });
}
// 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' });
}
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;
}
}
// Wenn keine Wochen vorhanden
if (!weeks || weeks.length === 0) {
return res.json({
weeks: [],
overtime_offset_hours: overtimeOffsetHours,
overtime_corrections: overtimeCorrections
});
}
// Konvertiere zurück zu Array
const entries = Object.values(entriesByDate);
const { getCalendarWeek } = require('../helpers/utils');
const weekData = [];
let processedWeeks = 0;
let hasError = false;
// 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;
// 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;
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;
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;
}
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() !== '';
// Konvertiere zurück zu Array
const entries = Object.values(entriesByDate);
if (isFullDayVacation || isSick || hasStartAndEnd) {
filledWorkdays++;
// 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 && arbeitstage > 0 ? wochenstunden / arbeitstage : 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 += fullDayHours;
} else if (entry.vacation_type === 'half') {
weekVacationDays += 0.5;
weekVacationHours += fullDayHours / 2;
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += parseFloat(entry.total_hours) || 0;
// Nur berechnen wenn alle Werktage ausgefüllt sind
if (filledWorkdays < workdays) {
processedWeeks++;
if (processedWeeks === weeks.length && !hasError) {
res.json({
weeks: weekData,
overtime_offset_hours: overtimeOffsetHours,
overtime_corrections: overtimeCorrections
});
}
return;
}
} else {
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += parseFloat(entry.total_hours) || 0;
// Berechnungen für diese Woche
let weekTotalHours = 0;
let weekOvertimeTaken = 0;
let weekVacationDays = 0;
let weekVacationHours = 0;
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 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 += fullDayHours;
} else if (entry.vacation_type === 'half') {
weekVacationDays += 0.5;
weekVacationHours += fullDayHours / 2;
// 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: (Wochenarbeitszeit / Arbeitstage) 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 += fullDayHours;
}
}
}
});
// Feiertagsstunden: (Wochenarbeitszeit / Arbeitstage) 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 += fullDayHours;
}
}
// Sollstunden berechnen
const sollStunden = (wochenstunden / arbeitstage) * workdays;
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
// Sollstunden berechnen
const sollStunden = (wochenstunden / arbeitstage) * 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();
// 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
});
// 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++;
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)
// 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,
overtime_corrections: overtimeCorrections
});
}
}); // getHolidaysForDateRange.then
}); // db.all (allEntries)
}); // weeks.forEach
}); // db.all (weeks)
}
);
}); // db.get (user)
}); // db.get (options)
}); // app.get

View File

@@ -132,6 +132,8 @@ function registerVerwaltungRoutes(app) {
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
const userId = req.params.id;
const raw = req.body ? req.body.overtime_offset_hours : undefined;
const reasonRaw = req.body ? req.body.reason : undefined;
const reason = (reasonRaw === null || reasonRaw === undefined) ? '' : String(reasonRaw).trim();
// Leere Eingabe => 0
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
@@ -139,12 +141,59 @@ function registerVerwaltungRoutes(app) {
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
}
db.run('UPDATE users SET overtime_offset_hours = ? WHERE id = ?', [normalized, userId], (err) => {
if (err) {
console.error('Fehler beim Speichern des Überstunden-Offsets:', err);
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
}
res.json({ success: true, overtime_offset_hours: normalized });
// Neue Logik: Korrektur protokollieren + kumulativ addieren
// Feld in der Verwaltung soll nach dem Speichern immer auf 0 zurückgesetzt werden.
if (normalized === 0) {
return res.json({ success: true, overtime_offset_hours: 0 });
}
if (!reason) {
return res.status(400).json({ error: 'Bitte geben Sie einen Grund für die Korrektur an.' });
}
db.serialize(() => {
db.run('BEGIN TRANSACTION');
db.run(
`INSERT INTO overtime_corrections (user_id, correction_hours, reason, corrected_at)
VALUES (?, ?, ?, datetime('now'))`,
[userId, normalized, reason],
(err) => {
if (err) {
console.error('Fehler beim Speichern der Überstunden-Korrektur:', err);
db.run('ROLLBACK', () => {
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
});
return;
}
db.run(
'UPDATE users SET overtime_offset_hours = COALESCE(overtime_offset_hours, 0) + ? WHERE id = ?',
[normalized, userId],
(err) => {
if (err) {
console.error('Fehler beim Aktualisieren des Überstunden-Offsets:', err);
db.run('ROLLBACK', () => {
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
});
return;
}
db.run('COMMIT', (err) => {
if (err) {
console.error('Fehler beim Commit der Überstunden-Korrektur:', err);
db.run('ROLLBACK', () => {
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
});
return;
}
res.json({ success: true, overtime_offset_hours: 0 });
});
}
);
}
);
});
});
@@ -168,6 +217,26 @@ function registerVerwaltungRoutes(app) {
});
});
// API: Überstunden-Korrektur-Historie für einen User abrufen
app.get('/api/verwaltung/user/:id/overtime-corrections', requireVerwaltung, (req, res) => {
const userId = req.params.id;
db.all(
`SELECT correction_hours, corrected_at, reason
FROM overtime_corrections
WHERE user_id = ?
ORDER BY corrected_at DESC`,
[userId],
(err, rows) => {
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
if (err) {
return res.json({ corrections: [] });
}
res.json({ corrections: rows || [] });
}
);
});
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
const userId = req.params.id;