From c396fe7d0effc92bbd9ade1252f5a285332bfe40 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Fri, 6 Feb 2026 16:17:50 +0100 Subject: [PATCH] Anpassung in der Verwaltung mit der Berechnung --- .cursor/worktrees.json | 5 ++ routes/verwaltung-routes.js | 140 +++++++++++++++++++++++++++++++++--- views/verwaltung.ejs | 14 ++-- 3 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 .cursor/worktrees.json diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..77e9744 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,5 @@ +{ + "setup-worktree": [ + "npm install" + ] +} diff --git a/routes/verwaltung-routes.js b/routes/verwaltung-routes.js index 310cfae..5fe579d 100644 --- a/routes/verwaltung-routes.js +++ b/routes/verwaltung-routes.js @@ -243,12 +243,19 @@ function registerVerwaltungRoutes(app) { // Nur Wochen bis Ende der angezeigten Kalenderwoche (Stand Urlaub = Ende dieser KW) const weeksUpToDisplayed = (weeks || []).filter((w) => w.week_end <= week_end); + // Wochen VOR der aktuellen Woche für kumulative Überstunden-Berechnung + const weeksBeforeCurrent = (weeks || []).filter((w) => w.week_end < week_end); let processedWeeks = 0; let totalVacationDays = 0; const vacationByDate = {}; + // Kumulative Überstunden über alle Wochen VOR der aktuellen Woche + let cumulativeOvertimeHours = 0; + let cumulativeOvertimeTaken = 0; + + // Urlaubstage für alle Wochen bis zur aktuellen Woche (inklusive) if (weeksUpToDisplayed.length === 0) { - processCurrentWeek(0); + processCurrentWeek(0, 0, 0); } else { weeksUpToDisplayed.forEach((week) => { db.all(`SELECT date, vacation_type, updated_at, id @@ -285,13 +292,111 @@ function registerVerwaltungRoutes(app) { totalVacationDays += 0.5; } }); - processCurrentWeek(totalVacationDays); + + // Berechne Überstunden für alle Wochen VOR der aktuellen Woche + if (weeksBeforeCurrent.length === 0) { + processCurrentWeek(totalVacationDays, 0, 0); + } else { + let processedOvertimeWeeks = 0; + weeksBeforeCurrent.forEach((week) => { + calculateWeekOvertime(week.week_start, week.week_end, (weekOvertime, weekOvertimeTaken) => { + cumulativeOvertimeHours += weekOvertime; + cumulativeOvertimeTaken += weekOvertimeTaken; + + processedOvertimeWeeks++; + if (processedOvertimeWeeks === weeksBeforeCurrent.length) { + processCurrentWeek(totalVacationDays, cumulativeOvertimeHours, cumulativeOvertimeTaken); + } + }); + }); + } } }); }); } - function processCurrentWeek(totalVacationDays) { + function calculateWeekOvertime(weekStart, weekEnd, callback) { + db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, + [userId, weekStart, weekEnd], + (err, allEntries) => { + if (err) { + return callback(0, 0); + } + + // Nur neuesten Eintrag pro Tag zählen + 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); + + let weekTotalHours = 0; + let weekOvertimeTaken = 0; + let weekVacationHours = 0; + + entries.forEach(entry => { + if (entry.overtime_taken_hours) { + weekOvertimeTaken += parseFloat(entry.overtime_taken_hours) || 0; + } + + if (entry.vacation_type === 'full') { + weekVacationHours += fullDayHours; + } else if (entry.vacation_type === 'half') { + weekVacationHours += fullDayHours / 2; + if (entry.total_hours) { + weekTotalHours += parseFloat(entry.total_hours) || 0; + } + } else { + if (entry.total_hours) { + weekTotalHours += parseFloat(entry.total_hours) || 0; + } + } + }); + + getHolidaysForDateRange(weekStart, weekEnd) + .catch(() => new Set()) + .then((holidaySet) => { + const startDate = new Date(weekStart); + const endDate = new Date(weekEnd); + let workdays = 0; + let holidayDays = 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) { + workdays++; + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) { + holidayDays++; + // Feiertagsstunden für alle Feiertage hinzufügen (wie im PDF) + holidayHours += fullDayHours; + } + } + } + + // Sollstunden = Wochenstunden (Feiertage reduzieren Soll nicht, da bezahlt) + const sollStunden = wochenstunden; + const totalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; + const weekOvertimeHours = totalHoursWithVacation - sollStunden; + + callback(weekOvertimeHours, weekOvertimeTaken); + }); + }); + } + + function processCurrentWeek(totalVacationDays, cumulativeOvertimeHours, cumulativeOvertimeTaken) { // Einträge für die Woche abrufen (id/updated_at für neuesten pro Tag) db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status FROM timesheet_entries @@ -366,23 +471,35 @@ function registerVerwaltungRoutes(app) { const startDate = new Date(week_start); const endDate = new Date(week_end); let workdays = 0; + let holidayDays = 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) { // Montag bis Freitag workdays++; const dateStr = d.toISOString().split('T')[0]; - if (holidaySet.has(dateStr)) holidayHours += fullDayHours; + if (holidaySet.has(dateStr)) { + holidayDays++; + // Feiertagsstunden für alle Feiertage hinzufügen (wie im PDF) + holidayHours += fullDayHours; + } } } - // Sollstunden berechnen - const sollStunden = (wochenstunden / arbeitstage) * workdays; + // Sollstunden = Wochenstunden (Feiertage reduzieren Soll nicht, da bezahlt) + const sollStunden = wochenstunden; - // Überstunden: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden + // Überstunden für die aktuelle Woche: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden const totalHoursWithVacation = totalHours + vacationHours + holidayHours; - const overtimeHours = totalHoursWithVacation - sollStunden; - const remainingOvertime = overtimeHours - overtimeTaken; + const weekOvertimeHours = totalHoursWithVacation - sollStunden; + + // Kumulative Überstunden: Summe aller Wochen bis zur aktuellen Woche + // cumulativeOvertimeHours enthält bereits alle vorherigen Wochen + const totalCumulativeOvertimeHours = cumulativeOvertimeHours + weekOvertimeHours; + const totalCumulativeOvertimeTaken = cumulativeOvertimeTaken + overtimeTaken; + + // Verbleibende Überstunden = kumulative Überstunden - kumulative genommene Überstunden + const remainingOvertime = totalCumulativeOvertimeHours - totalCumulativeOvertimeTaken; const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; // Verbleibende Urlaubstage (berücksichtigt alle eingereichten Wochen, nicht nur die aktuelle) @@ -393,8 +510,9 @@ function registerVerwaltungRoutes(app) { urlaubstage, totalHours, sollStunden, - overtimeHours, - overtimeTaken, + weekOvertimeHours, // Überstunden nur für diese Woche + overtimeHours: totalCumulativeOvertimeHours, // Kumulative Überstunden + overtimeTaken: totalCumulativeOvertimeTaken, // Kumulative genommene Überstunden remainingOvertime, overtimeOffsetHours, remainingOvertimeWithOffset, diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index 5ac7ff3..cd31c39 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -321,11 +321,11 @@ // Statistiken anzeigen let statsHTML = ''; - if (data.overtimeHours !== undefined) { + if (data.weekOvertimeHours !== undefined) { + const weekOvertimeColor = data.weekOvertimeHours < 0 ? '#dc3545' : (data.weekOvertimeHours > 0 ? '#28a745' : '#666'); + const sign = data.weekOvertimeHours >= 0 ? '+' : ''; statsHTML += `
- Überstunden: ${data.overtimeHours.toFixed(2)} h - ${data.overtimeTaken > 0 ? `(davon genommen: ${data.overtimeTaken.toFixed(2)} h)` : ''} - ${data.remainingOvertime !== data.overtimeHours ? `(verbleibend: ${data.remainingOvertime.toFixed(2)} h)` : ''} + Überstunden: ${sign}${data.weekOvertimeHours.toFixed(2)} h
`; } if (data.overtimeOffsetHours !== undefined && data.overtimeOffsetHours !== 0) { @@ -338,9 +338,9 @@ const totalTaken = data.totalVacationDays !== undefined ? data.totalVacationDays : 0; const inWeek = data.vacationDays !== undefined ? data.vacationDays : 0; statsHTML += `
- Urlaub genommen (kumuliert bis Ende KW): ${Number(totalTaken).toFixed(1)} Tag${totalTaken !== 1 ? 'e' : ''} - ${inWeek > 0 ? ` (davon in dieser Woche: ${inWeek.toFixed(1)})` : ''} - ${data.remainingVacation !== undefined ? ` (verbleibend Stand Ende KW: ${Number(data.remainingVacation).toFixed(1)} Tage)` : ''} + Urlaub genommen (dieses Jahr): ${Number(totalTaken).toFixed(1)} Tag${totalTaken !== 1 ? 'e' : ''} + ${inWeek > 0 ? ` (diese Woche: ${inWeek.toFixed(1)})` : ''} + ${data.remainingVacation !== undefined ? ` (verbleibend: ${Number(data.remainingVacation).toFixed(1)} Tage)` : ''}
`; } if (data.vacationOffsetDays !== undefined && data.vacationOffsetDays !== 0) {