Umstellung auf anzeige x h y min

This commit is contained in:
2026-02-13 12:21:43 +01:00
parent 23c255438b
commit af1f6efb40
8 changed files with 117 additions and 94 deletions

View File

@@ -3,7 +3,7 @@
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const { db } = require('./database'); const { db } = require('./database');
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); const { getCurrentDate, getCurrentTime, updateTotalHours, formatHoursMin } = require('./helpers/utils');
const checkinApp = express(); const checkinApp = express();
const CHECKIN_PORT = 3334; const CHECKIN_PORT = 3334;
@@ -139,7 +139,7 @@ checkinApp.get('/api/checkout/:userId', (req, res) => {
return sendResponse(req, res, false, { error: 'Fehler beim Aktualisieren', status: 500 }); return sendResponse(req, res, false, { error: 'Fehler beim Aktualisieren', status: 500 });
} }
const successTitle = 'Schönen Feierabend, du wurdest erfolgreich ausgecheckt'; const successTitle = 'Schönen Feierabend, du wurdest erfolgreich ausgecheckt';
const successMessage = `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`; const successMessage = `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${formatHoursMin(totalHours)}`;
sendResponse(req, res, true, { sendResponse(req, res, true, {
title: successTitle, title: successTitle,
message: successMessage, message: successMessage,

View File

@@ -132,6 +132,22 @@ function getWeekStart(dateStr) {
return `${year}-${month}-${dayOfMonth}`; return `${year}-${month}-${dayOfMonth}`;
} }
// Helper: Dezimalstunden in Anzeige "X h Y min" (z. B. 8.25 -> "8 h 15 min", -1.5 -> "-1 h 30 min")
function formatHoursMin(decimalHours) {
if (decimalHours == null || !Number.isFinite(Number(decimalHours))) return '0 h 0 min';
const n = Number(decimalHours);
const sign = n < 0 ? -1 : 1;
const absVal = Math.abs(n);
let h = Math.floor(absVal);
let min = Math.round((absVal - h) * 60);
if (min >= 60) {
h += 1;
min = 0;
}
const prefix = sign < 0 ? '-' : '';
return prefix + h + ' h ' + min + ' min';
}
module.exports = { module.exports = {
hasRole, hasRole,
getDefaultRole, getDefaultRole,
@@ -143,5 +159,6 @@ module.exports = {
formatDateTime, formatDateTime,
getCalendarWeek, getCalendarWeek,
getWeekDatesFromCalendarWeek, getWeekDatesFromCalendarWeek,
getWeekStart getWeekStart,
formatHoursMin
}; };

View File

@@ -51,7 +51,7 @@ async function loadUserStats() {
const currentOvertimeEl = document.getElementById('currentOvertime'); const currentOvertimeEl = document.getElementById('currentOvertime');
if (currentOvertimeEl) { if (currentOvertimeEl) {
const overtime = stats.currentOvertime || 0; const overtime = stats.currentOvertime || 0;
currentOvertimeEl.textContent = overtime >= 0 ? `+${overtime.toFixed(2)}` : overtime.toFixed(2); currentOvertimeEl.textContent = (overtime >= 0 ? '+' : '') + formatHoursMin(overtime);
currentOvertimeEl.style.color = overtime >= 0 ? '#27ae60' : '#e74c3c'; currentOvertimeEl.style.color = overtime >= 0 ? '#27ae60' : '#e74c3c';
// Auch die Border-Farbe des Cards anpassen // Auch die Border-Farbe des Cards anpassen
const overtimeCard = currentOvertimeEl.closest('.stat-card'); const overtimeCard = currentOvertimeEl.closest('.stat-card');
@@ -548,27 +548,27 @@ function renderWeek() {
// Stunden-Anzeige für halben Tag Urlaub berechnen // Stunden-Anzeige für halben Tag Urlaub berechnen
let hoursDisplay = ''; let hoursDisplay = '';
if (isFullDayVacation) { if (isFullDayVacation) {
hoursDisplay = fullDayHours.toFixed(2) + ' h (Urlaub)'; hoursDisplay = formatHoursMin(fullDayHours) + ' (Urlaub)';
} else if (isHalfDayVacation) { } else if (isHalfDayVacation) {
const halfHours = fullDayHours / 2; const halfHours = fullDayHours / 2;
const workHours = hours || 0; // Das sind die gearbeiteten Stunden (ohne Urlaub) const workHours = hours || 0; // Das sind die gearbeiteten Stunden (ohne Urlaub)
const totalHours = halfHours + workHours; const totalHours = halfHours + workHours;
if (workHours > 0.01) { if (workHours > 0.01) {
hoursDisplay = totalHours.toFixed(2) + ' h (' + halfHours.toFixed(2) + ' h Urlaub + ' + workHours.toFixed(2) + ' h)'; hoursDisplay = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')';
} else { } else {
hoursDisplay = halfHours.toFixed(2) + ' h (Urlaub)'; hoursDisplay = formatHoursMin(halfHours) + ' (Urlaub)';
} }
} else if (isSick) { } else if (isSick) {
hoursDisplay = fullDayHours.toFixed(2) + ' h (Krank)'; hoursDisplay = formatHoursMin(fullDayHours) + ' (Krank)';
} else if (isHoliday && isWeekend) { } else if (isHoliday && isWeekend) {
// Feiertag am Wochenende: keine Tagesarbeitsstunden // Feiertag am Wochenende: keine Tagesarbeitsstunden
hoursDisplay = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; hoursDisplay = formatHoursMin(hours || 0) + ' (Feiertag)';
} else if (isHoliday && !hours) { } else if (isHoliday && !hours) {
hoursDisplay = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursDisplay = formatHoursMin(fullDayHours) + ' (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {
hoursDisplay = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; hoursDisplay = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)';
} else { } else {
hoursDisplay = hours.toFixed(2) + ' h'; hoursDisplay = formatHoursMin(hours);
} }
const requiredBreak = (startTime && endTime) ? calculateRequiredBreakMinutes(startTime, endTime) : null; const requiredBreak = (startTime && endTime) ? calculateRequiredBreakMinutes(startTime, endTime) : null;
@@ -741,7 +741,7 @@ function renderWeek() {
`; `;
document.getElementById('timesheetTable').innerHTML = html; document.getElementById('timesheetTable').innerHTML = html;
document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h'; document.getElementById('totalHours').textContent = formatHoursMin(totalHours);
// Überstunden-Berechnung (startDate und endDate sind bereits oben deklariert) // Überstunden-Berechnung (startDate und endDate sind bereits oben deklariert)
@@ -1017,14 +1017,14 @@ function updateOvertimeDisplay() {
if (overtimeSummaryItem && overtimeHoursSpan) { if (overtimeSummaryItem && overtimeHoursSpan) {
overtimeSummaryItem.style.display = 'block'; overtimeSummaryItem.style.display = 'block';
const sign = overtimeHours >= 0 ? '+' : ''; const sign = overtimeHours >= 0 ? '+' : '';
overtimeHoursSpan.textContent = `${sign}${overtimeHours.toFixed(2)} h`; overtimeHoursSpan.textContent = (sign === '+' ? '+' : '') + formatHoursMin(overtimeHours);
overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c'; overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c';
} }
// Gesamtstunden-Anzeige aktualisieren // Gesamtstunden-Anzeige aktualisieren
const totalHoursElement = document.getElementById('totalHours'); const totalHoursElement = document.getElementById('totalHours');
if (totalHoursElement) { if (totalHoursElement) {
totalHoursElement.textContent = totalHoursWithVacation.toFixed(2) + ' h'; totalHoursElement.textContent = formatHoursMin(totalHoursWithVacation);
} }
} }
@@ -1272,7 +1272,7 @@ async function saveEntry(input) {
if (hoursElement) { if (hoursElement) {
if (isFullDayVacation) { if (isFullDayVacation) {
// Ganzer Tag Urlaub: Zeige fullDayHours mit "(Urlaub)" Label // Ganzer Tag Urlaub: Zeige fullDayHours mit "(Urlaub)" Label
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Urlaub)';
currentEntries[date].total_hours = fullDayHours; currentEntries[date].total_hours = fullDayHours;
} else if (isHalfDayVacation) { } else if (isHalfDayVacation) {
// Halber Tag Urlaub: Berechne Stunden aus Start/Ende falls vorhanden // Halber Tag Urlaub: Berechne Stunden aus Start/Ende falls vorhanden
@@ -1293,9 +1293,9 @@ async function saveEntry(input) {
const totalHours = halfHours + workHours; const totalHours = halfHours + workHours;
if (workHours > 0) { if (workHours > 0) {
hoursElement.textContent = totalHours.toFixed(2) + ' h (' + halfHours.toFixed(2) + ' h Urlaub + ' + workHours.toFixed(2) + ' h)'; hoursElement.textContent = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')';
} else { } else {
hoursElement.textContent = halfHours.toFixed(2) + ' h (Urlaub)'; hoursElement.textContent = formatHoursMin(halfHours) + ' (Urlaub)';
} }
currentEntries[date].total_hours = totalHours; currentEntries[date].total_hours = totalHours;
} else { } else {
@@ -1303,15 +1303,15 @@ async function saveEntry(input) {
const d = new Date(date); const d = new Date(date);
const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0);
if (isSick) { if (isSick) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Krank)';
} else if (isWeekendHoliday) { } else if (isWeekendHoliday) {
hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)';
} else if (isHoliday && !hours) { } else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)';
} else { } else {
hoursElement.textContent = hours.toFixed(2) + ' h'; hoursElement.textContent = formatHoursMin(hours);
} }
} }
} }
@@ -1446,10 +1446,10 @@ async function saveEntry(input) {
const isHalfDayVacation = vacationType === 'half'; const isHalfDayVacation = vacationType === 'half';
const fullDayHours = getFullDayHours(); const fullDayHours = getFullDayHours();
let hoursText = result.total_hours.toFixed(2) + ' h'; let hoursText = formatHoursMin(result.total_hours);
if (isFullDayVacation) { if (isFullDayVacation) {
hoursText = fullDayHours.toFixed(2) + ' h (Urlaub)'; hoursText = formatHoursMin(fullDayHours) + ' (Urlaub)';
} else if (isHalfDayVacation) { } else if (isHalfDayVacation) {
// Bei halbem Tag Urlaub: result.total_hours enthält nur die gearbeiteten Stunden // Bei halbem Tag Urlaub: result.total_hours enthält nur die gearbeiteten Stunden
// Die Urlaubsstunden müssen addiert werden // Die Urlaubsstunden müssen addiert werden
@@ -1458,25 +1458,25 @@ async function saveEntry(input) {
const totalHours = halfHours + workHours; // Gesamt = Urlaub + gearbeitet const totalHours = halfHours + workHours; // Gesamt = Urlaub + gearbeitet
if (workHours > 0.01) { if (workHours > 0.01) {
hoursText = totalHours.toFixed(2) + ' h (' + halfHours.toFixed(2) + ' h Urlaub + ' + workHours.toFixed(2) + ' h)'; hoursText = formatHoursMin(totalHours) + ' (' + formatHoursMin(halfHours) + ' Urlaub + ' + formatHoursMin(workHours) + ')';
} else { } else {
hoursText = halfHours.toFixed(2) + ' h (Urlaub)'; hoursText = formatHoursMin(halfHours) + ' (Urlaub)';
} }
// Aktualisiere currentEntries mit den Gesamtstunden // Aktualisiere currentEntries mit den Gesamtstunden
currentEntries[date].total_hours = totalHours; currentEntries[date].total_hours = totalHours;
} else if (isSick) { } else if (isSick) {
hoursText = fullDayHours.toFixed(2) + ' h (Krank)'; hoursText = formatHoursMin(fullDayHours) + ' (Krank)';
} else if (isHoliday) { } else if (isHoliday) {
const d = new Date(date); const d = new Date(date);
const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0); const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0);
if (isWeekendHoliday) { if (isWeekendHoliday) {
hoursText = (result.total_hours || 0).toFixed(2) + ' h (Feiertag)'; hoursText = formatHoursMin(result.total_hours || 0) + ' (Feiertag)';
} else if (result.total_hours <= fullDayHours) { } else if (result.total_hours <= fullDayHours) {
hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursText = formatHoursMin(fullDayHours) + ' (Feiertag)';
} else { } else {
const overtime = result.total_hours - fullDayHours; const overtime = result.total_hours - fullDayHours;
hoursText = fullDayHours.toFixed(2) + ' + ' + overtime.toFixed(2) + ' h (Überst.)'; hoursText = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(overtime) + ' (Überst.)';
} }
} }
@@ -1492,7 +1492,7 @@ async function saveEntry(input) {
Object.values(currentEntries).forEach(e => { Object.values(currentEntries).forEach(e => {
totalHours += e.total_hours || 0; totalHours += e.total_hours || 0;
}); });
document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h'; document.getElementById('totalHours').textContent = formatHoursMin(totalHours);
// Überstunden-Anzeige aktualisieren (bei jeder Änderung) // Überstunden-Anzeige aktualisieren (bei jeder Änderung)
updateOvertimeDisplay(); updateOvertimeDisplay();
@@ -1591,7 +1591,7 @@ function checkWeekComplete() {
if (overtimeValue > fullDayHours) { if (overtimeValue > fullDayHours) {
if (!startTime || !endTime || startTime === '' || endTime === '') { if (!startTime || !endTime || startTime === '' || endTime === '') {
allWeekdaysFilled = false; allWeekdaysFilled = false;
missingFields.push(formatDateDE(dateStr) + ' (bei Überstunden > ' + fullDayHours.toFixed(2) + 'h müssen Start/Ende vorhanden sein)'); missingFields.push(formatDateDE(dateStr) + ' (bei Überstunden > ' + formatHoursMin(fullDayHours) + ' müssen Start/Ende vorhanden sein)');
continue; // Weiter zum nächsten Tag continue; // Weiter zum nächsten Tag
} }
} }
@@ -2077,22 +2077,22 @@ function toggleSickStatus(dateStr) {
if (newStatus) { if (newStatus) {
// Krank: Zeige fullDayHours mit "(Krank)" Label // Krank: Zeige fullDayHours mit "(Krank)" Label
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Krank)';
currentEntries[dateStr].total_hours = fullDayHours; currentEntries[dateStr].total_hours = fullDayHours;
} else { } else {
// Zurück zu normaler Anzeige basierend auf anderen Status // Zurück zu normaler Anzeige basierend auf anderen Status
const d = new Date(dateStr); const d = new Date(dateStr);
const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0);
if (isFullDayVacation) { if (isFullDayVacation) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Urlaub)';
} else if (isWeekendHoliday) { } else if (isWeekendHoliday) {
hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; hoursElement.textContent = formatHoursMin(hours || 0) + ' (Feiertag)';
} else if (isHoliday && !hours) { } else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)'; hoursElement.textContent = formatHoursMin(fullDayHours) + ' + ' + formatHoursMin(hours) + ' (Überst.)';
} else { } else {
hoursElement.textContent = hours.toFixed(2) + ' h'; hoursElement.textContent = formatHoursMin(hours);
} }
} }
} }

19
public/js/format-hours.js Normal file
View File

@@ -0,0 +1,19 @@
// Gleiche Logik wie helpers/utils.js formatHoursMin für Browser (Dashboard, EJS-Seiten).
// Wird global als window.formatHoursMin bereitgestellt.
(function () {
function formatHoursMin(decimalHours) {
if (decimalHours == null || !Number.isFinite(Number(decimalHours))) return '0 h 0 min';
var n = Number(decimalHours);
var sign = n < 0 ? -1 : 1;
var absVal = Math.abs(n);
var h = Math.floor(absVal);
var min = Math.round((absVal - h) * 60);
if (min >= 60) {
h += 1;
min = 0;
}
var prefix = sign < 0 ? '-' : '';
return prefix + h + ' h ' + min + ' min';
}
window.formatHoursMin = formatHoursMin;
})();

View File

@@ -3,7 +3,7 @@
const PDFDocument = require('pdfkit'); const PDFDocument = require('pdfkit');
const QRCode = require('qrcode'); const QRCode = require('qrcode');
const { db } = require('../database'); const { db } = require('../database');
const { formatDate, formatDateTime } = require('../helpers/utils'); const { formatDate, formatDateTime, formatHoursMin } = require('../helpers/utils');
const { getHolidaysWithNamesForDateRange } = require('./feiertage-service'); const { getHolidaysWithNamesForDateRange } = require('./feiertage-service');
// Kalenderwoche berechnen // Kalenderwoche berechnen
@@ -190,7 +190,7 @@ function generatePDF(timesheetId, req, res) {
// Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen
const holidayDay = new Date(row.date + 'T12:00:00').getDay(); const holidayDay = new Date(row.date + 'T12:00:00').getDay();
const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6); const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6);
const holidayHoursStr = isWeekendHoliday ? '0 h (Feiertag)' : fullDayHours.toFixed(2) + ' h (Feiertag)'; const holidayHoursStr = isWeekendHoliday ? '0 h 0 min (Feiertag)' : formatHoursMin(fullDayHours) + ' (Feiertag)';
const rowData = [formatDate(row.date), '-', '-', '-', holidayHoursStr]; const rowData = [formatDate(row.date), '-', '-', '-', holidayHoursStr];
rowData.forEach((data, i) => { rowData.forEach((data, i) => {
doc.text(data, x, y, { width: colWidths[i], align: 'left' }); doc.text(data, x, y, { width: colWidths[i], align: 'left' });
@@ -215,7 +215,7 @@ function generatePDF(timesheetId, req, res) {
entry.start_time || '-', entry.start_time || '-',
entry.end_time || '-', entry.end_time || '-',
entry.break_minutes ? `${entry.break_minutes} min` : '-', entry.break_minutes ? `${entry.break_minutes} min` : '-',
entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' entry.total_hours ? formatHoursMin(entry.total_hours) : '-'
]; ];
rowData.forEach((data, i) => { rowData.forEach((data, i) => {
@@ -250,7 +250,7 @@ function generatePDF(timesheetId, req, res) {
if (activity.projectNumber) { if (activity.projectNumber) {
activityText += ` (Projekt: ${activity.projectNumber})`; activityText += ` (Projekt: ${activity.projectNumber})`;
} }
activityText += ` - ${activity.hours.toFixed(2)} h`; activityText += ` - ${formatHoursMin(activity.hours)}`;
doc.fontSize(9).font('Helvetica'); doc.fontSize(9).font('Helvetica');
doc.text(activityText, 70, doc.y, { width: 360 }); doc.text(activityText, 70, doc.y, { width: 360 });
doc.moveDown(0.2); doc.moveDown(0.2);
@@ -264,7 +264,7 @@ function generatePDF(timesheetId, req, res) {
overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag'));
} }
if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) {
overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); overtimeInfo.push(`Überstunden genommen: ${formatHoursMin(parseFloat(entry.overtime_taken_hours))}`);
} }
if (entry.vacation_type) { if (entry.vacation_type) {
const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag';
@@ -308,7 +308,7 @@ function generatePDF(timesheetId, req, res) {
doc.font('Helvetica-Bold'); doc.font('Helvetica-Bold');
// Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden (8h pro Feiertag) // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden (8h pro Feiertag)
const totalHoursWithVacation = totalHours + vacationHours + holidayHours; const totalHoursWithVacation = totalHours + vacationHours + holidayHours;
doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); doc.text(`Gesamtstunden: ${formatHoursMin(totalHoursWithVacation)}`, 50, doc.y);
// Überstunden berechnen und anzeigen // Überstunden berechnen und anzeigen
const wochenstunden = timesheet.wochenstunden || 0; const wochenstunden = timesheet.wochenstunden || 0;
@@ -318,11 +318,11 @@ function generatePDF(timesheetId, req, res) {
doc.moveDown(0.3); doc.moveDown(0.3);
doc.font('Helvetica-Bold'); doc.font('Helvetica-Bold');
if (overtimeHours > 0) { if (overtimeHours > 0) {
doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); doc.text(`Überstunden: +${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else if (overtimeHours < 0) { } else if (overtimeHours < 0) {
doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else { } else {
doc.text(`Überstunden: 0.00 h`, 50, doc.y); doc.text(`Überstunden: ${formatHoursMin(0)}`, 50, doc.y);
} }
doc.end(); doc.end();
@@ -462,7 +462,7 @@ function generatePDFToBuffer(timesheetId, req) {
// Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen
const holidayDay = new Date(row.date + 'T12:00:00').getDay(); const holidayDay = new Date(row.date + 'T12:00:00').getDay();
const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6); const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6);
const holidayHoursStr = isWeekendHoliday ? '0 h (Feiertag)' : fullDayHoursBuf.toFixed(2) + ' h (Feiertag)'; const holidayHoursStr = isWeekendHoliday ? '0 h 0 min (Feiertag)' : formatHoursMin(fullDayHoursBuf) + ' (Feiertag)';
const rowDataBuf = [formatDate(row.date), '-', '-', '-', holidayHoursStr]; const rowDataBuf = [formatDate(row.date), '-', '-', '-', holidayHoursStr];
rowDataBuf.forEach((data, i) => { rowDataBuf.forEach((data, i) => {
doc.text(data, x, y, { width: colWidths[i], align: 'left' }); doc.text(data, x, y, { width: colWidths[i], align: 'left' });
@@ -487,7 +487,7 @@ function generatePDFToBuffer(timesheetId, req) {
entry.start_time || '-', entry.start_time || '-',
entry.end_time || '-', entry.end_time || '-',
entry.break_minutes ? `${entry.break_minutes} min` : '-', entry.break_minutes ? `${entry.break_minutes} min` : '-',
entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' entry.total_hours ? formatHoursMin(entry.total_hours) : '-'
]; ];
rowData.forEach((data, i) => { rowData.forEach((data, i) => {
@@ -519,7 +519,7 @@ function generatePDFToBuffer(timesheetId, req) {
if (activity.projectNumber) { if (activity.projectNumber) {
activityText += ` (Projekt: ${activity.projectNumber})`; activityText += ` (Projekt: ${activity.projectNumber})`;
} }
activityText += ` - ${activity.hours.toFixed(2)} h`; activityText += ` - ${formatHoursMin(activity.hours)}`;
doc.fontSize(9).font('Helvetica'); doc.fontSize(9).font('Helvetica');
doc.text(activityText, 70, doc.y, { width: 360 }); doc.text(activityText, 70, doc.y, { width: 360 });
doc.moveDown(0.2); doc.moveDown(0.2);
@@ -532,7 +532,7 @@ function generatePDFToBuffer(timesheetId, req) {
overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag'));
} }
if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) {
overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); overtimeInfo.push(`Überstunden genommen: ${formatHoursMin(parseFloat(entry.overtime_taken_hours))}`);
} }
if (entry.vacation_type) { if (entry.vacation_type) {
const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag';
@@ -573,7 +573,7 @@ function generatePDFToBuffer(timesheetId, req) {
doc.moveDown(0.5); doc.moveDown(0.5);
doc.font('Helvetica-Bold'); doc.font('Helvetica-Bold');
const totalHoursWithVacation = totalHours + vacationHours + holidayHours; const totalHoursWithVacation = totalHours + vacationHours + holidayHours;
doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); doc.text(`Gesamtstunden: ${formatHoursMin(totalHoursWithVacation)}`, 50, doc.y);
const wochenstunden = timesheet.wochenstunden || 0; const wochenstunden = timesheet.wochenstunden || 0;
const overtimeHours = totalHoursWithVacation - wochenstunden; const overtimeHours = totalHoursWithVacation - wochenstunden;
@@ -581,11 +581,11 @@ function generatePDFToBuffer(timesheetId, req) {
doc.moveDown(0.3); doc.moveDown(0.3);
doc.font('Helvetica-Bold'); doc.font('Helvetica-Bold');
if (overtimeHours > 0) { if (overtimeHours > 0) {
doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); doc.text(`Überstunden: +${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else if (overtimeHours < 0) { } else if (overtimeHours < 0) {
doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else { } else {
doc.text(`Überstunden: 0.00 h`, 50, doc.y); doc.text(`Überstunden: ${formatHoursMin(0)}`, 50, doc.y);
} }
doc.end(); doc.end();

View File

@@ -52,11 +52,11 @@
<div class="summary"> <div class="summary">
<div class="summary-item"> <div class="summary-item">
<strong>Gesamtstunden diese Woche:</strong> <strong>Gesamtstunden diese Woche:</strong>
<span id="totalHours">0.00 h</span> <span id="totalHours">0 h 0 min</span>
</div> </div>
<div class="summary-item" id="overtimeSummaryItem" style="display: none;"> <div class="summary-item" id="overtimeSummaryItem" style="display: none;">
<strong>Überstunden diese Woche:</strong> <strong>Überstunden diese Woche:</strong>
<span id="overtimeHours">0.00 h</span> <span id="overtimeHours">0 h 0 min</span>
</div> </div>
</div> </div>
@@ -194,6 +194,7 @@
</div> </div>
</div> </div>
<script src="/js/format-hours.js"></script>
<script src="/js/dashboard.js"></script> <script src="/js/dashboard.js"></script>
<script> <script>
// Wochenende-Sektion ein-/ausklappen // Wochenende-Sektion ein-/ausklappen

View File

@@ -187,6 +187,7 @@
</table> </table>
</div> </div>
<script src="/js/format-hours.js"></script>
<script> <script>
// Rollenwechsel // Rollenwechsel
const roleSwitcher = document.getElementById('roleSwitcher'); const roleSwitcher = document.getElementById('roleSwitcher');
@@ -231,15 +232,7 @@
return new Date(s.replace(' ', 'T') + 'Z'); return new Date(s.replace(' ', 'T') + 'Z');
} }
function formatHours(value) { // formatHoursMin aus format-hours.js (window.formatHoursMin)
const n = Number(value);
if (!Number.isFinite(n)) return '';
const sign = n > 0 ? '+' : '';
let s = sign + n.toFixed(2);
s = s.replace(/\.00$/, '');
s = s.replace(/(\.\d)0$/, '$1');
return s;
}
let correctionsExpanded = false; let correctionsExpanded = false;
function toggleCorrectionsSection() { function toggleCorrectionsSection() {
@@ -286,18 +279,18 @@
// Zusammenfassung anzeigen // Zusammenfassung anzeigen
const totalOvertimeEl = document.getElementById('totalOvertime'); const totalOvertimeEl = document.getElementById('totalOvertime');
totalOvertimeEl.textContent = totalOvertimeEl.textContent =
(totalOvertime >= 0 ? '+' : '') + totalOvertime.toFixed(2) + ' h'; (totalOvertime >= 0 ? '+' : '') + formatHoursMin(totalOvertime);
totalOvertimeEl.className = totalOvertimeEl.className =
'summary-value ' + (totalOvertime >= 0 ? 'overtime-positive' : 'overtime-negative'); 'summary-value ' + (totalOvertime >= 0 ? 'overtime-positive' : 'overtime-negative');
const totalOvertimeTakenEl = document.getElementById('totalOvertimeTaken'); const totalOvertimeTakenEl = document.getElementById('totalOvertimeTaken');
totalOvertimeTakenEl.textContent = totalOvertimeTakenEl.textContent =
totalOvertimeTaken.toFixed(2) + ' h'; formatHoursMin(totalOvertimeTaken);
totalOvertimeTakenEl.className = 'summary-value overtime-positive'; totalOvertimeTakenEl.className = 'summary-value overtime-positive';
const remainingOvertimeEl = document.getElementById('remainingOvertime'); const remainingOvertimeEl = document.getElementById('remainingOvertime');
remainingOvertimeEl.textContent = remainingOvertimeEl.textContent =
(remainingOvertime >= 0 ? '+' : '') + remainingOvertime.toFixed(2) + ' h'; (remainingOvertime >= 0 ? '+' : '') + formatHoursMin(remainingOvertime);
remainingOvertimeEl.className = remainingOvertimeEl.className =
'summary-value ' + (remainingOvertime >= 0 ? 'overtime-positive' : 'overtime-negative'); 'summary-value ' + (remainingOvertime >= 0 ? 'overtime-positive' : 'overtime-negative');
@@ -305,7 +298,7 @@
const offsetItem = document.getElementById('offsetItem'); const offsetItem = document.getElementById('offsetItem');
const offsetValue = document.getElementById('overtimeOffset'); const offsetValue = document.getElementById('overtimeOffset');
if (overtimeOffset !== 0) { if (overtimeOffset !== 0) {
offsetValue.textContent = (overtimeOffset >= 0 ? '+' : '') + overtimeOffset.toFixed(2) + ' h'; offsetValue.textContent = (overtimeOffset >= 0 ? '+' : '') + formatHoursMin(overtimeOffset);
offsetValue.className = 'summary-value ' + (overtimeOffset >= 0 ? 'overtime-positive' : 'overtime-negative'); offsetValue.className = 'summary-value ' + (overtimeOffset >= 0 ? 'overtime-positive' : 'overtime-negative');
offsetItem.style.display = 'flex'; offsetItem.style.display = 'flex';
} else { } else {
@@ -327,12 +320,12 @@
corrections.forEach(c => { corrections.forEach(c => {
const dt = parseSqliteDatetime(c.corrected_at); const dt = parseSqliteDatetime(c.corrected_at);
const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
const hoursText = formatHours(c.correction_hours); const hoursText = formatHoursMin(c.correction_hours);
const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = reason li.textContent = reason
? `Korrektur am ${dateText} ${hoursText} h ${reason}` ? `Korrektur am ${dateText} ${hoursText} ${reason}`
: `Korrektur am ${dateText} ${hoursText} h`; : `Korrektur am ${dateText} ${hoursText}`;
correctionsListEl.appendChild(li); correctionsListEl.appendChild(li);
}); });
@@ -361,10 +354,10 @@
row.innerHTML = ` row.innerHTML = `
<td><strong>${week.year} KW${calendarWeekStr}</strong></td> <td><strong>${week.year} KW${calendarWeekStr}</strong></td>
<td>${dateRange}</td> <td>${dateRange}</td>
<td>${week.total_hours.toFixed(2)} h</td> <td>${formatHoursMin(week.total_hours)}</td>
<td>${week.soll_stunden.toFixed(2)} h</td> <td>${formatHoursMin(week.soll_stunden)}</td>
<td class="${overtimeClass}">${overtimeSign}${week.overtime_hours.toFixed(2)} h</td> <td class="${overtimeClass}">${overtimeSign}${formatHoursMin(week.overtime_hours)}</td>
<td>${week.overtime_taken.toFixed(2)} h</td> <td>${formatHoursMin(week.overtime_taken)}</td>
<td>${week.vacation_days > 0 ? week.vacation_days.toFixed(1) : '-'}</td> <td>${week.vacation_days > 0 ? week.vacation_days.toFixed(1) : '-'}</td>
`; `;
tableBodyEl.appendChild(row); tableBodyEl.appendChild(row);

View File

@@ -318,6 +318,7 @@
</div> </div>
</div> </div>
<script src="/js/format-hours.js"></script>
<script> <script>
async function loadStatsForDiv(statsDiv) { async function loadStatsForDiv(statsDiv) {
const userId = statsDiv.dataset.userId; const userId = statsDiv.dataset.userId;
@@ -341,13 +342,13 @@
const weekOvertimeColor = data.weekOvertimeHours < 0 ? '#dc3545' : (data.weekOvertimeHours > 0 ? '#28a745' : '#666'); const weekOvertimeColor = data.weekOvertimeHours < 0 ? '#dc3545' : (data.weekOvertimeHours > 0 ? '#28a745' : '#666');
const sign = data.weekOvertimeHours >= 0 ? '+' : ''; const sign = data.weekOvertimeHours >= 0 ? '+' : '';
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;"> statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
<strong>Überstunden:</strong> <span style="color: ${weekOvertimeColor};">${sign}${data.weekOvertimeHours.toFixed(2)} h</span> <strong>Überstunden:</strong> <span style="color: ${weekOvertimeColor};">${sign}${formatHoursMin(data.weekOvertimeHours)}</span>
</div>`; </div>`;
} }
if (data.overtimeOffsetHours !== undefined && data.overtimeOffsetHours !== 0) { if (data.overtimeOffsetHours !== undefined && data.overtimeOffsetHours !== 0) {
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;"> statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
<strong>Offset:</strong> <span>${Number(data.overtimeOffsetHours).toFixed(2)} h</span> <strong>Offset:</strong> <span>${formatHoursMin(Number(data.overtimeOffsetHours))}</span>
${data.remainingOvertimeWithOffset !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${Number(data.remainingOvertimeWithOffset).toFixed(2)} h)</span>` : ''} ${data.remainingOvertimeWithOffset !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${formatHoursMin(Number(data.remainingOvertimeWithOffset))})</span>` : ''}
</div>`; </div>`;
} }
if (data.totalVacationDays !== undefined || data.vacationDays !== undefined) { if (data.totalVacationDays !== undefined || data.vacationDays !== undefined) {
@@ -513,15 +514,7 @@
return new Date(s.replace(' ', 'T') + 'Z'); return new Date(s.replace(' ', 'T') + 'Z');
} }
function formatHours(value) { // formatHoursMin aus format-hours.js (window.formatHoursMin)
const n = Number(value);
if (!Number.isFinite(n)) return '';
const sign = n > 0 ? '+' : '';
let s = sign + n.toFixed(2);
s = s.replace(/\.00$/, '');
s = s.replace(/(\.\d)0$/, '$1');
return s;
}
function showOvertimeCorrectionReasonModal(opts) { function showOvertimeCorrectionReasonModal(opts) {
const title = opts && opts.title ? String(opts.title) : 'Grund für die Korrektur'; const title = opts && opts.title ? String(opts.title) : 'Grund für die Korrektur';
@@ -652,12 +645,12 @@
corrections.forEach(c => { corrections.forEach(c => {
const dt = parseSqliteDatetime(c.corrected_at); const dt = parseSqliteDatetime(c.corrected_at);
const dateText = dt ? dt.toLocaleDateString('de-DE') : ''; const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
const hoursText = formatHours(c.correction_hours); const hoursText = formatHoursMin(c.correction_hours);
const reason = (c && c.reason != null) ? String(c.reason).trim() : ''; const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = reason li.textContent = reason
? `Korrektur am ${dateText} ${hoursText} h ${reason}` ? `Korrektur am ${dateText} ${hoursText} ${reason}`
: `Korrektur am ${dateText} ${hoursText} h`; : `Korrektur am ${dateText} ${hoursText}`;
if (listEl) listEl.appendChild(li); if (listEl) listEl.appendChild(li);
}); });
} catch (e) { } catch (e) {
@@ -720,7 +713,7 @@
// Modal: Grund ist Pflicht // Modal: Grund ist Pflicht
showOvertimeCorrectionReasonModal({ showOvertimeCorrectionReasonModal({
title: 'Grund für die Überstunden-Korrektur', title: 'Grund für die Überstunden-Korrektur',
prompt: `Korrektur: ${value > 0 ? '+' : ''}${value} h`, prompt: `Korrektur: ${value >= 0 ? '+' : ''}${formatHoursMin(value)}`,
onCancel: () => { onCancel: () => {
delete this.dataset.modalOpen; delete this.dataset.modalOpen;
this.disabled = false; this.disabled = false;